docs(03): finalize phase plan (8 plans, verified)
This commit is contained in:
@@ -18,10 +18,11 @@ must_haves:
|
||||
- "A member cannot enqueue a write to a calendar they do not own (403) — D-03 / V4 access control"
|
||||
- "GET /api/events/sync-status?uid= returns the outbox status for that member's UID"
|
||||
- "Edit that changes the target calendar enqueues a linked delete+create pair in one transaction (D-04)"
|
||||
- "GET /api/events/writable-calendars returns the member's writable set per D-03 — own personal + shared Family (read-write); never the other member's read-only personal"
|
||||
artifacts:
|
||||
- path: "apps/api/src/routes/events.ts"
|
||||
provides: "create/edit/delete write endpoints + sync-status, all enqueue-only (broker boundary)"
|
||||
contains: "/sync-status"
|
||||
provides: "create/edit/delete write endpoints + sync-status + writable-calendars, all enqueue-only (broker boundary)"
|
||||
contains: "/writable-calendars"
|
||||
key_links:
|
||||
- from: "apps/api/src/routes/events.ts"
|
||||
to: "calendarOutbox"
|
||||
@@ -35,17 +36,21 @@ must_haves:
|
||||
|
||||
<objective>
|
||||
Add the write API surface to the events router: `POST /create`, `PATCH /:uid/edit`,
|
||||
`DELETE /:uid`, and `GET /sync-status`. Every write endpoint validates with zod,
|
||||
asserts the target calendar belongs to the current member (D-03), and ENQUEUES an
|
||||
outbox row — it never calls Fastmail (broker boundary, D-12). The endpoints return 202
|
||||
immediately so the UI can optimistically accept (D-05). sync-status exposes the outbox
|
||||
state for the polled toast (D-09).
|
||||
`DELETE /:uid`, `GET /sync-status`, and `GET /writable-calendars`. Every write endpoint
|
||||
validates with zod, asserts the target calendar belongs to the current member (D-03), and
|
||||
ENQUEUES an outbox row — it never calls Fastmail (broker boundary, D-12). The endpoints
|
||||
return 202 immediately so the UI can optimistically accept (D-05). sync-status exposes the
|
||||
outbox state for the polled toast (D-09). writable-calendars exposes the member's authorized
|
||||
write target set (D-03) so the client picker (Plan 05) renders only legal targets and honors
|
||||
the D-02 single-calendar hide rule.
|
||||
|
||||
Purpose: this is the backend half of the create/edit/delete vertical slices. It depends
|
||||
only on the outbox schema (Plan 01); it does not import the worker or write.ts (those
|
||||
drain the queue the endpoints fill).
|
||||
drain the queue the endpoints fill). The writable-calendars endpoint is the authoritative
|
||||
owner of the D-03 writable-set authorization — the client never derives it.
|
||||
|
||||
Output: extended events.ts, GREEN against the create/edit/delete/sync-status tests from Plan 01.
|
||||
Output: extended events.ts, GREEN against the create/edit/delete/sync-status/writable-calendars
|
||||
tests from Plan 01.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -117,6 +122,36 @@ Output: extended events.ts, GREEN against the create/edit/delete/sync-status tes
|
||||
<done>GET /api/events/sync-status returns the member-scoped outbox status; tests GREEN.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: GREEN — GET /api/events/writable-calendars (D-03 writable set, authoritative)</name>
|
||||
<files>apps/api/src/routes/events.ts, apps/api/tests/routes/events.test.ts</files>
|
||||
<read_first>
|
||||
- apps/api/tests/routes/events.test.ts (extend — add a `GET /api/events/writable-calendars` describe block alongside the create/edit/delete/sync-status stubs)
|
||||
- apps/api/src/routes/events.ts (existing GET / handler — mirror its auth + db.select + try/catch shape)
|
||||
- apps/api/src/db/schema.ts (`calendars` table — `url`, `displayName`, `color`, `userId`, `isShared` columns)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Open Questions Q3 — writable-set resolution query; §Security Domain V4 — D-03 access control)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md (D-02 picker-visibility, D-03 writable set)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md (§events.ts, §Auth guard in write route handlers)
|
||||
</read_first>
|
||||
<action>
|
||||
Add `eventsRouter.get('/writable-calendars', ...)`. Resolve the current member id with the same dev-bypass + `getAuth(c)` pattern as the write endpoints (401 if neither). This endpoint is the AUTHORITATIVE owner of the D-03 writable-set authorization — the client (Plan 05) consumes it verbatim and never derives the set itself.
|
||||
|
||||
Per RESEARCH.md Open Q3: select the writable set = rows in `calendars WHERE userId = currentUser.id` (the member's own personal calendar(s)) UNION rows WHERE `isShared = 1` (the shared Family calendar, when read-write to the household). Express this as a single Drizzle query with `WHERE eq(calendars.userId, currentUser.id) OR eq(calendars.isShared, true)`. The other member's personal calendar (a row with a different `userId` and `isShared = 0/false`) MUST NOT appear — it is a read-only overlay only (D-03), never a write target.
|
||||
|
||||
Map each row to the response shape `{ calendars: [{ url, displayName, color, isShared }] }` (exactly the `WritableCalendar` shape Plan 05's `fetchWritableCalendars` consumes). Wrap the db work in try/catch returning 503 per the existing GET handler pattern. Do NOT include any Fastmail call (broker boundary).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- routes/events && grep -q "/writable-calendars" apps/api/src/routes/events.ts && pnpm --filter @familysync/api exec tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- writable-calendars test GREEN: returns only the member's own personal calendar(s) plus the shared (`isShared=1`) calendar.
|
||||
- The test asserts another member's personal calendar (different userId, isShared=false) is NEVER returned (D-03 / V4).
|
||||
- Response items expose `url`, `displayName`, `color`, `isShared` (the picker's `WritableCalendar` shape).
|
||||
- `grep -c "/writable-calendars" apps/api/src/routes/events.ts` ≥1.
|
||||
</acceptance_criteria>
|
||||
<done>GET /api/events/writable-calendars returns the D-03 writable set (own personal + shared Family), never another member's read-only personal; response matches the Plan 05 WritableCalendar shape; tests GREEN.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
@@ -125,7 +160,7 @@ Output: extended events.ts, GREEN against the create/edit/delete/sync-status tes
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| client → write API | Untrusted member input (event fields, target calendar, uid) crosses here |
|
||||
| member A → member B data | A member must never write to or read another member's outbox/calendar |
|
||||
| member A → member B data | A member must never write to, treat-as-writable, or read another member's outbox/calendar |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
@@ -136,17 +171,18 @@ Output: extended events.ts, GREEN against the create/edit/delete/sync-status tes
|
||||
| T-03-08 | Tampering | XSS/oversized payload via title/location/description | mitigate | zod length bounds (title 255, location/description 2000); plain-text storage; rendered as JSX children downstream |
|
||||
| T-03-09 | Tampering | SQL injection via uid/calendarUrl | mitigate | Drizzle parameterized queries; no string interpolation |
|
||||
| T-03-10 | Spoofing | client-supplied etag bypassing conflict detection | mitigate | etag read from calendarEvents server-side at enqueue; client never supplies it |
|
||||
| T-03-11 | Elevation of Privilege | writable-calendars surfacing another member's personal calendar as a write target | mitigate | Query restricted to `userId = currentUser.id OR isShared = true`; another member's `isShared=false` personal row is never returned; client treats the response as authoritative and the write endpoints re-enforce D-03 on enqueue |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/api test -- routes/events` GREEN (create, edit, delete, sync-status, 403 ownership).
|
||||
- `pnpm --filter @familysync/api test -- routes/events` GREEN (create, edit, delete, sync-status, writable-calendars, 403 ownership).
|
||||
- `pnpm --filter @familysync/api exec tsc --noEmit` passes.
|
||||
- No tsdav import in events.ts (broker boundary): `grep -c "tsdav\|createFastmailClient" apps/api/src/routes/events.ts` returns 0.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All four write/status endpoints enqueue-only and member-scoped.
|
||||
- D-03 ownership enforced; D-04 edit-as-move pair transactional; D-09 polling endpoint live.
|
||||
- All five write/status/writable-calendars endpoints enqueue-only and member-scoped.
|
||||
- D-03 ownership enforced on both the write path and the writable-calendars listing; D-04 edit-as-move pair transactional; D-09 polling endpoint live.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user