Harden sync and security; add IQ Bible, templates, password reset, backups
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Sync and data safety - Autosave flushes on leaving a study or the tab; storage-full errors are reported - Unsynced studies, deletes and ink are queued and retried when back online - Deletes propagate across devices (with a "Keep it" undo) and no longer resurrect - Server assigns study versions (no client-clock conflicts); "Combine both" merges a conflicted study chunk by chunk - Reading-plan progress made offline is merged, not overwritten; plans, ink and marks are cleared from the device on sign-out Security - Block SSRF through the ntfy reminder URL; failure-only rate limiting per IP and account; constant-time login; TOTP replay protection; input validation; SESSION_SECRET required in production; admin user delete fixed - Forgot password: one-time code sent to the account's ntfy topic Features - IQ Bible proxy (route allow-list, text-only caching per its terms): more commentaries, lexicons, study aids, daily verse and devotional, plan calendar export, narrations, podcast background - Study templates, reading-plan catch-up, account export, daily database snapshots Tests and tooling - ESLint config; App tests split into src/test; tests for sync queues, library hook, merge, templates, IQ Bible UI and reset form Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6bb392933a
commit
f274bc9c8a
67 files changed
+4734
-1569
No files matched your search
@@ -1,6 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import http from 'node:http';
|
||||
import { test, expect, afterEach } from 'vitest';
|
||||
process.env.ALLOW_PRIVATE_NTFY = '1'; // the tests talk to a local server
|
||||
import { isDue, isValidTimeZone, reminderBody, sendNtfy, sendTestReminder, zonedNow } from './reminders.js';
|
||||
|
||||
const at = (iso) => new Date(iso);
|
||||
@@ -84,3 +85,17 @@ test('an ntfy error response becomes a readable failure', async () => {
|
||||
test('an unreachable server says so instead of hanging', async () => {
|
||||
await expect(sendNtfy({ ntfyUrl: 'http://127.0.0.1:9', ntfyTopic: 'abc' }, { title: 't', body: 'b' })).rejects.toThrow(/Could not reach/);
|
||||
});
|
||||
|
||||
test('private and loopback addresses are rejected as reminder servers', async () => {
|
||||
const { isPrivateAddress, assertPublicUrl, readToday } = await import('./reminders.js');
|
||||
for (const a of ['127.0.0.1', '10.1.2.3', '192.168.0.5', '172.16.0.1', '169.254.169.254', '::1', 'fd00::1', '::ffff:10.0.0.1']) expect(isPrivateAddress(a)).toBe(true);
|
||||
expect(isPrivateAddress('8.8.8.8')).toBe(false);
|
||||
const prev = process.env.ALLOW_PRIVATE_NTFY;
|
||||
delete process.env.ALLOW_PRIVATE_NTFY;
|
||||
await expect(assertPublicUrl('http://localhost:3001')).rejects.toThrow();
|
||||
await expect(assertPublicUrl('http://169.254.169.254/latest')).rejects.toThrow();
|
||||
await expect(assertPublicUrl('ftp://example.com')).rejects.toThrow();
|
||||
process.env.ALLOW_PRIVATE_NTFY = prev;
|
||||
expect(readToday({ activity: ['2026-09-29'] }, '2026-09-29')).toBe(true);
|
||||
expect(readToday({ activity: [] }, '2026-09-29')).toBe(false);
|
||||
});
|
||||
Reference in new issue
Block a user