style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -3,7 +3,7 @@ phase: 01-foundation-broker-spike
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["01-01"]
|
||||
depends_on: ['01-01']
|
||||
files_modified:
|
||||
- apps/api/src/broker/crypto.ts
|
||||
- apps/api/src/broker/client.ts
|
||||
@@ -19,40 +19,40 @@ requirements: [CAL-01]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "App passwords are encrypted at rest with AES-256-GCM (key from env) and decrypt losslessly; never exposed to the frontend"
|
||||
- "The broker creates a Fastmail CalDAV client (Basic auth, app password) and fetches calendars via PROPFIND"
|
||||
- "syncCalendar parses VEVENTs with ical.js and upserts them into calendar_events, storing all-day events as DATE (dtstart_date) never DATETIME"
|
||||
- 'App passwords are encrypted at rest with AES-256-GCM (key from env) and decrypt losslessly; never exposed to the frontend'
|
||||
- 'The broker creates a Fastmail CalDAV client (Basic auth, app password) and fetches calendars via PROPFIND'
|
||||
- 'syncCalendar parses VEVENTs with ical.js and upserts them into calendar_events, storing all-day events as DATE (dtstart_date) never DATETIME'
|
||||
- "The poller skips DB writes when a calendar's ctag is unchanged (sync-token with ctag fallback)"
|
||||
- "GET /api/events returns cached events from MariaDB (never a live Fastmail call per request)"
|
||||
- 'GET /api/events returns cached events from MariaDB (never a live Fastmail call per request)'
|
||||
artifacts:
|
||||
- path: "apps/api/src/broker/crypto.ts"
|
||||
provides: "encryptPassword/decryptPassword (AES-256-GCM, key from APP_PASSWORD_ENCRYPTION_KEY)"
|
||||
exports: ["encryptPassword", "decryptPassword"]
|
||||
- path: "apps/api/src/broker/client.ts"
|
||||
provides: "createFastmailClient(email, appPassword) → tsdav DAVClient"
|
||||
exports: ["createFastmailClient"]
|
||||
- path: "apps/api/src/broker/sync.ts"
|
||||
provides: "syncCalendar: REPORT → ical.js → calendar_events upsert"
|
||||
exports: ["syncCalendar"]
|
||||
- path: "apps/api/src/broker/poller.ts"
|
||||
provides: "startBrokerPoller (node-cron 5-min) with ctag change detection"
|
||||
exports: ["startBrokerPoller"]
|
||||
- path: "apps/api/src/routes/events.ts"
|
||||
provides: "GET /api/events → cached events from DB"
|
||||
exports: ["eventsRouter"]
|
||||
- path: 'apps/api/src/broker/crypto.ts'
|
||||
provides: 'encryptPassword/decryptPassword (AES-256-GCM, key from APP_PASSWORD_ENCRYPTION_KEY)'
|
||||
exports: ['encryptPassword', 'decryptPassword']
|
||||
- path: 'apps/api/src/broker/client.ts'
|
||||
provides: 'createFastmailClient(email, appPassword) → tsdav DAVClient'
|
||||
exports: ['createFastmailClient']
|
||||
- path: 'apps/api/src/broker/sync.ts'
|
||||
provides: 'syncCalendar: REPORT → ical.js → calendar_events upsert'
|
||||
exports: ['syncCalendar']
|
||||
- path: 'apps/api/src/broker/poller.ts'
|
||||
provides: 'startBrokerPoller (node-cron 5-min) with ctag change detection'
|
||||
exports: ['startBrokerPoller']
|
||||
- path: 'apps/api/src/routes/events.ts'
|
||||
provides: 'GET /api/events → cached events from DB'
|
||||
exports: ['eventsRouter']
|
||||
key_links:
|
||||
- from: "apps/api/src/broker/poller.ts"
|
||||
to: "apps/api/src/broker/crypto.ts"
|
||||
via: "decryptPassword before client creation"
|
||||
- from: 'apps/api/src/broker/poller.ts'
|
||||
to: 'apps/api/src/broker/crypto.ts'
|
||||
via: 'decryptPassword before client creation'
|
||||
pattern: "decryptPassword\\("
|
||||
- from: "apps/api/src/broker/sync.ts"
|
||||
to: "apps/api/src/db/client.ts"
|
||||
via: "calendarEvents upsert"
|
||||
pattern: "calendarEvents"
|
||||
- from: "apps/api/src/routes/events.ts"
|
||||
to: "apps/api/src/db/client.ts"
|
||||
via: "cache read (no live CalDAV)"
|
||||
pattern: "from ['\"].*db/client"
|
||||
- from: 'apps/api/src/broker/sync.ts'
|
||||
to: 'apps/api/src/db/client.ts'
|
||||
via: 'calendarEvents upsert'
|
||||
pattern: 'calendarEvents'
|
||||
- from: 'apps/api/src/routes/events.ts'
|
||||
to: 'apps/api/src/db/client.ts'
|
||||
via: 'cache read (no live CalDAV)'
|
||||
pattern: 'from [''"].*db/client'
|
||||
---
|
||||
|
||||
<objective>
|
||||
@@ -80,6 +80,7 @@ Output: crypto helper, broker client/sync/poller, /api/events router, all unit-t
|
||||
</context>
|
||||
|
||||
<artifacts_produced>
|
||||
|
||||
## Artifacts this phase produces (Plan 03)
|
||||
|
||||
New files: `apps/api/src/broker/crypto.ts`, `apps/api/src/broker/client.ts`, `apps/api/src/broker/sync.ts`, `apps/api/src/broker/poller.ts`, `apps/api/src/routes/events.ts`.
|
||||
@@ -113,6 +114,7 @@ New env vars: `APP_PASSWORD_ENCRYPTION_KEY` (64-char hex = 32 bytes).
|
||||
Add `APP_PASSWORD_ENCRYPTION_KEY` to `.env.example` with a comment showing the generator: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`.
|
||||
|
||||
Fill `tests/broker/crypto.test.ts` GREEN: set a fixed test key in the test, assert (a) roundtrip lossless; (b) two encrypts of same plaintext differ; (c) tampering authTag causes decrypt to throw.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && pnpm vitest run tests/broker/crypto.test.ts --reporter=verbose</automated>
|
||||
@@ -149,6 +151,7 @@ New env vars: `APP_PASSWORD_ENCRYPTION_KEY` (64-char hex = 32 bytes).
|
||||
Create `src/routes/events.ts` exporting `eventsRouter` (Hono): GET / reads from `calendarEvents` via `db` (cache only — NEVER call Fastmail per request, ARCHITECTURE anti-pattern), returns the rows (id, uid, allDay, dtstart_utc, dtstart_date, raw_vevent or a minimal shape). This router is mounted in Plan 04.
|
||||
|
||||
Fill `tests/broker/sync.test.ts` GREEN with a mocked tsdav client returning captured raw VEVENT strings (timed + all-day fixtures — Wave 0 fixture requirement). Assert the dtstart_utc vs dtstart_date split, all_day flag, and UID-upsert idempotency.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && pnpm vitest run tests/broker/sync.test.ts --reporter=verbose && pnpm exec tsc --noEmit</automated>
|
||||
@@ -183,6 +186,7 @@ New env vars: `APP_PASSWORD_ENCRYPTION_KEY` (64-char hex = 32 bytes).
|
||||
Create `src/broker/poller.ts` exporting `startBrokerPoller()` (and an internal `runPoll()` exported for tests). Per RESEARCH poller pattern: `schedule('*/5 * * * *', runPoll)` using node-cron v4 (Pitfall 4 — basic 5-field cron API is stable). `runPoll`: select all `memberCredentials`; for each, `decryptPassword`, `createFastmailClient`, `fetchCalendars`; for each davCal, compare `davCal.ctag ?? davCal.syncToken ?? null` to the stored calendars row ctag — if equal and non-null, `continue` (skip); else `syncCalendar`. Make `runPoll` injectable/testable (accept the db + client factory or use module mocks) so the unit test can assert skip-on-unchanged without hitting Fastmail.
|
||||
|
||||
Fill `tests/broker/poller.test.ts` GREEN: mock fetchCalendars to return a calendar with a known ctag matching a stored row → assert syncCalendar spy NOT called; then a changed ctag → assert syncCalendar IS called.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && pnpm vitest run tests/broker/poller.test.ts --reporter=verbose</automated>
|
||||
@@ -199,24 +203,26 @@ New env vars: `APP_PASSWORD_ENCRYPTION_KEY` (64-char hex = 32 bytes).
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Boundary | Description |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| member_credentials (DB) → broker | App passwords stored encrypted; only broker/crypto.ts decrypts; never leaves backend |
|
||||
| Broker → Fastmail CalDAV | Outbound Basic auth over TLS; sole holder of Fastmail I/O |
|
||||
| Hono /api/events → browser | Returns only cached event data; never credentials or raw app passwords |
|
||||
| Broker → Fastmail CalDAV | Outbound Basic auth over TLS; sole holder of Fastmail I/O |
|
||||
| Hono /api/events → browser | Returns only cached event data; never credentials or raw app passwords |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-03-01 | Information Disclosure | Fastmail app password at rest | mitigate | AES-256-GCM with 96-bit IV + auth tag (crypto.ts); key from APP_PASSWORD_ENCRYPTION_KEY env, never committed/logged (ASVS V6) |
|
||||
| T-03-02 | Information Disclosure | App password leaking via /api/events | mitigate | events route reads only calendar_events (event data); never joins/returns member_credentials; broker is the only credential reader (D-04) |
|
||||
| T-03-03 | Tampering | Encrypted-credential integrity | mitigate | GCM auth tag verified on decrypt; tampered ciphertext throws, never silently used |
|
||||
| T-03-04 | Information Disclosure | Credentials in logs | mitigate | No console logging of decrypted passwords or the encryption key in client.ts / poller.ts |
|
||||
| T-03-05 | Tampering | Caching client-side event versions | mitigate | Only server-returned objects cached (raw VEVENT verbatim — D-13, Pitfall 14); no write-back in Phase 1 |
|
||||
| T-03-SC | Tampering | tsdav / ical.js / node-cron installs | accept | All [OK] in RESEARCH § Package Legitimacy Audit (tsdav 3+ yrs official repo, ical.js Mozilla-maintained, node-cron 8+ yrs); no [ASSUMED]/[SUS]/[SLOP] |
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | ---------------------- | ------------------------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-03-01 | Information Disclosure | Fastmail app password at rest | mitigate | AES-256-GCM with 96-bit IV + auth tag (crypto.ts); key from APP_PASSWORD_ENCRYPTION_KEY env, never committed/logged (ASVS V6) |
|
||||
| T-03-02 | Information Disclosure | App password leaking via /api/events | mitigate | events route reads only calendar_events (event data); never joins/returns member_credentials; broker is the only credential reader (D-04) |
|
||||
| T-03-03 | Tampering | Encrypted-credential integrity | mitigate | GCM auth tag verified on decrypt; tampered ciphertext throws, never silently used |
|
||||
| T-03-04 | Information Disclosure | Credentials in logs | mitigate | No console logging of decrypted passwords or the encryption key in client.ts / poller.ts |
|
||||
| T-03-05 | Tampering | Caching client-side event versions | mitigate | Only server-returned objects cached (raw VEVENT verbatim — D-13, Pitfall 14); no write-back in Phase 1 |
|
||||
| T-03-SC | Tampering | tsdav / ical.js / node-cron installs | accept | All [OK] in RESEARCH § Package Legitimacy Audit (tsdav 3+ yrs official repo, ical.js Mozilla-maintained, node-cron 8+ yrs); no [ASSUMED]/[SUS]/[SLOP] |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
@@ -227,12 +233,13 @@ New env vars: `APP_PASSWORD_ENCRYPTION_KEY` (64-char hex = 32 bytes).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- App passwords encrypted at rest (AES-256-GCM), lossless roundtrip, tamper-detecting
|
||||
- Broker discovers calendars and syncs VEVENTs into the cache with correct all-day DATE handling
|
||||
- Poller skips unchanged calendars (ctag detection)
|
||||
- /api/events serves cached events without a live Fastmail call
|
||||
- All three unit test files green
|
||||
</success_criteria>
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/01-foundation-broker-spike/01-03-SUMMARY.md` when done.
|
||||
|
||||
Reference in New Issue
Block a user