| 01-foundation-broker-spike |
03 |
api |
| caldav |
| ical.js |
| tsdav |
| node-cron |
| aes-256-gcm |
| drizzle |
| mariadb |
| vitest |
|
| phase |
provides |
| 01-foundation-broker-spike/01-01 |
db singleton, Drizzle schema (users, memberCredentials, calendars, calendarEvents) |
|
|
| AES-256-GCM encryptPassword / decryptPassword helpers (broker/crypto.ts) |
| tsdav DAVClient factory createFastmailClient (broker/client.ts) |
| syncCalendar |
| REPORT → ical.js → MariaDB upsert with D-13 all-day DATE handling (broker/sync.ts) |
|
| startBrokerPoller / runPoll |
| 5-min node-cron ctag change-detection poller (broker/poller.ts) |
|
| GET /api/events router reading the calendarEvents cache (routes/events.ts) |
| 24 unit tests green across 5 test files |
|
| 01-04 (spike — mounts events route, calls startBrokerPoller from index.ts, live Fastmail test) |
|
| added |
patterns |
| ical.js@2.2.1 (VEVENT parsing, isDate all-day detection) |
| tsdav@2.2.2 (CalDAV PROPFIND + REPORT via createDAVClient) |
| node-cron@4.2.1 (5-min background schedule) |
| node:crypto (built-in AES-256-GCM, no additional package) |
|
| AES-256-GCM with 96-bit random IV + auth tag for credential encryption at rest; key from env (T-03-01) |
| D-13 all-day split |
| isDate=true → dtstartDate (Date@00:00Z), dtstartUtc=null; timed → dtstartUtc, dtstartDate=null |
|
| ctag/syncToken null-defensive |
| davCal.ctag ?? davCal.syncToken ?? null (Pitfall 6) |
|
| runPoll exported for Vitest injection via vi.mock; startBrokerPoller wraps in cron schedule |
| broker module is the sole importer of tsdav and credentials (hard boundary per D-09) |
|
|
| created |
modified |
| apps/api/src/broker/crypto.ts (encryptPassword / decryptPassword, AES-256-GCM, node:crypto) |
| apps/api/src/broker/client.ts (createFastmailClient, FastmailClient type alias) |
| apps/api/src/broker/sync.ts (syncCalendar |
| upsert calendars + calendarEvents, ical.js parse) |
|
| apps/api/src/broker/poller.ts (startBrokerPoller / runPoll, node-cron, ctag detection) |
| apps/api/src/routes/events.ts (eventsRouter |
| GET / reads calendarEvents cache) |
|
| apps/api/tests/broker/crypto.test.ts (roundtrip, IV-uniqueness, tamper-detection) |
| apps/api/tests/broker/sync.test.ts (timed/all-day split, UID idempotency, db mock) |
| apps/api/tests/broker/poller.test.ts (ctag skip, ctag change, first sync, decrypt failure, multi-credential) |
|
| .env.example (APP_PASSWORD_ENCRYPTION_KEY with generator comment) |
|
|
| Store dtstartDate as JS Date at T00:00:00Z (not raw string): Drizzle date column expects a Date or null; ical.js toString().slice(0,10) gives the YYYY-MM-DD, appending T00:00:00Z avoids TZ ambiguity |
| Export runPoll separately from startBrokerPoller: lets tests invoke one poll cycle synchronously with vi.mock injected deps, avoiding real cron schedule in tests |
| ctag skip condition: null ctag means first sync (no row stored) → must always sync; only skip when both sides have a non-null matching ctag |
| Per-credential try/catch in runPoll: one corrupted or expired credential must not block other members' calendars from syncing |
| events route imports db but never tsdav or crypto: enforces broker hard boundary (T-03-02) |
|
| Pattern: broker boundary — tsdav and credentials are imported exclusively under apps/api/src/broker/; routes never touch Fastmail I/O |
| Pattern: D-13 dtstart split — use ical.js ICAL.Time.isDate to route all-day vs timed into separate nullable columns |
| Pattern: ctag null-defence — always use davCal.ctag ?? davCal.syncToken ?? null; Fastmail may return either field |
|
|
~multi-session |
2026-06-04 |