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.
9.3 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-shared-lists-live-sync | 02 | tdd | 2 |
|
|
true |
|
|
This is a dedicated TDD plan because it is pure, testable business logic (expect(deliveredEvents).toEqual([...])) and it is the single highest-correctness-risk seam in the phase (private-list leakage). The SSE endpoint (Plan 06) and the route fan-out triggers (Plans 03–06) consume these two functions.
Purpose: Get scoped fan-out provably correct in isolation before any SSE wiring, with the negative test ("private-list events NOT delivered to a non-owner") proven green.
Output: publishListEvent/subscribeListEvents (in-memory EventEmitter singleton) and getAccessibleListIds(userId), both fully unit-tested.
Fan-out mechanism justification (D-18): In-memory EventEmitter, not Redis. The API runs as a single Node process (no replicas), so Redis pub/sub adds a network hop, an ioredis dependency, and operational overhead for zero benefit. D-18 (N-member / multi-process-agnostic design) is satisfied by the abstraction boundary: callers use publishListEvent/subscribeListEvents and never touch the EventEmitter directly, so a future Redis swap is mechanical inside listEmitter.ts. ioredis is intentionally NOT installed in Phase 4.
listAccess (DB-backed, uses the test DB harness from Plan 01):
- Test 5: getAccessibleListIds returns ids of lists the user OWNS.
- Test 6: getAccessibleListIds returns ids of lists shared to the user via list_shares.
- Test 7 (D-04): getAccessibleListIds does NOT return another user's private (non-shared, non-owned) list id.
- Test 8: result has no duplicates when a list is both owned and (erroneously) shared.
listAccess.ts: `getAccessibleListIds(userId: number): Promise<number[]>` — select lists.id where lists.ownerId = userId, union select listShares.listId where listShares.userId = userId, dedupe into a number[]. Use drizzle eq from the events.ts pattern. (Implementation choice: either two selects merged in JS per RESEARCH Finding 3, or a single OR query joined to list_shares — either is acceptable; the tests assert behavior, not query shape.)
Follow RED → GREEN → REFACTOR: write the failing tests first (convert the Plan 01 stub), confirm they fail, implement minimally to green, refactor only if obvious.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| publisher (route handler) → subscriber (SSE stream) | A leak here exposes one member's private list to another |
| API → MariaDB | access-scope query must not over-return list ids |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-04-02 | Information Disclosure | scoped fan-out leak (D-04) — load-bearing | mitigate | Per-list channel keying (list:${listId}) + getAccessibleListIds scoped to owner_id OR list_shares; proven by Test 2 (cross-list isolation) and Test 7 (private list excluded) |
| T-04-03 | Information Disclosure | getAccessibleListIds over-returning ids | mitigate | Test 7 asserts a non-owned, non-shared list id is absent; Test 8 asserts dedupe |
| T-04-04 | Denial of Service | EventEmitter max-listeners warning under many SSE connections | accept | setMaxListeners(200) headroom (100 members × 2 devices); single-process scale is bounded for a household app |
</threat_model>
pnpm --filter @familysync/api exec vitest run tests/lib/listEmitter.test.ts src/lib/listAccess.test.ts - Test 2 (cross-list isolation) and Test 7 (private list excluded) MUST be present and green.<success_criteria>
- RED commit: failing listEmitter/listAccess tests (incl. the D-04 negative).
- GREEN commit: implementation passes all tests.
- REFACTOR commit (if any): tests still green.
- ioredis NOT introduced. </success_criteria>
<artifacts_produced> Symbols/files this plan creates (exclude from drift verification):
apps/api/src/lib/listEmitter.tsexportingpublishListEvent(listId, event),subscribeListEvents(listId, handler): () => void, typeListEventapps/api/src/lib/listAccess.tsexportinggetAccessibleListIds(userId): Promise<number[]>- Tests:
apps/api/tests/lib/listEmitter.test.ts,apps/api/src/lib/listAccess.test.ts</artifacts_produced>