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:
@@ -25,14 +25,14 @@ key_files:
|
||||
- apps/api/src/routes/lists.ts
|
||||
- apps/api/tests/routes/lists.test.ts
|
||||
decisions:
|
||||
- "windowMs exposed as optional 3rd arg on notifyListChange for test-time override (avoids fake-timer/real-I/O race)"
|
||||
- "pollUntil() helper (inline, no test-library deps) replaces @testing-library/waitFor for async DB assertion polling"
|
||||
- "vi.doMock + vi.resetModules in beforeEach — required so each test gets a fresh vi.fn() mock instance for dispatchPush"
|
||||
- "DELETE /:id notifyListChange fires after DB delete — sendListChangePush handles missing list gracefully (early return)"
|
||||
- "List create (POST /) does NOT notify — empty list is not a change worth pinging (D-01 spirit)"
|
||||
- 'windowMs exposed as optional 3rd arg on notifyListChange for test-time override (avoids fake-timer/real-I/O race)'
|
||||
- 'pollUntil() helper (inline, no test-library deps) replaces @testing-library/waitFor for async DB assertion polling'
|
||||
- 'vi.doMock + vi.resetModules in beforeEach — required so each test gets a fresh vi.fn() mock instance for dispatchPush'
|
||||
- 'DELETE /:id notifyListChange fires after DB delete — sendListChangePush handles missing list gracefully (early return)'
|
||||
- 'List create (POST /) does NOT notify — empty list is not a change worth pinging (D-01 spirit)'
|
||||
metrics:
|
||||
duration: 8
|
||||
completed_date: "2026-06-10"
|
||||
completed_date: '2026-06-10'
|
||||
tasks_completed: 2
|
||||
files_changed: 4
|
||||
---
|
||||
@@ -48,12 +48,14 @@ TDD RED→GREEN: `listChangeDispatcher.ts` (notifyListChange) implemented; hooke
|
||||
**Status:** Completed.
|
||||
|
||||
**Commits:**
|
||||
|
||||
- RED: `test(05-05): add failing tests for listChangeDispatcher — RED gate` — 97f7026
|
||||
- GREEN: `feat(05-05): implement listChangeDispatcher — access-scoped, self-suppressed, coalesced push (NOTIF-02)` — 6923104
|
||||
|
||||
Created `apps/api/src/lib/listChangeDispatcher.ts` exporting `notifyListChange(listId, actorId, windowMs?)`:
|
||||
|
||||
**`notifyListChange`** — wraps `coalesceListPush` with a dispatch closure that:
|
||||
|
||||
1. Resolves actor `displayName` and list `name` from DB in parallel
|
||||
2. Builds audience: `{list owner} ∪ {list_shares.userId} MINUS actorId` (D-03)
|
||||
3. Loads `push_subscriptions` for all audience members
|
||||
@@ -61,11 +63,13 @@ Created `apps/api/src/lib/listChangeDispatcher.ts` exporting `notifyListChange(l
|
||||
5. D-02 generic copy: `"{Actor} made {N} changes to {ListName}"` — no item text
|
||||
|
||||
**Threat mitigations:**
|
||||
|
||||
- T-05-14: audience derived from list access (owner + list_shares only) — never all users
|
||||
- T-05-15: notification body carries actor name + count, no item text (D-02)
|
||||
- T-05-16: actorId filtered before audience union → actor's own subscriptions never dispatched (D-03)
|
||||
|
||||
**Tests (5/5 GREEN):**
|
||||
|
||||
- Burst coalescing: 3 rapid calls → 1 `dispatchPush` to non-actor with `count=3`, body contains actor name + "3"
|
||||
- D-03 self-suppression: actor-only list → 0 dispatches
|
||||
- T-05-14 access scoping: unrelated 3rd user (no owner/share) → never dispatched
|
||||
@@ -80,24 +84,26 @@ Created `apps/api/src/lib/listChangeDispatcher.ts` exporting `notifyListChange(l
|
||||
|
||||
`apps/api/src/routes/lists.ts` updated — `notifyListChange` called (fire-and-forget) after each meaningful mutation:
|
||||
|
||||
| Route | Mutation | Push? |
|
||||
|-------|----------|-------|
|
||||
| `POST /api/lists/:id/items` | Item added | YES |
|
||||
| `PATCH /api/list-items/:itemId` | checked/text change | YES |
|
||||
| `PATCH /api/list-items/:itemId` | position change (reorder) | **NO** (D-01) |
|
||||
| `DELETE /api/list-items/:itemId` | Item deleted | YES |
|
||||
| `PATCH /api/lists/:id` | List rename/sharing toggle | YES |
|
||||
| `DELETE /api/lists/:id` | List deleted | YES |
|
||||
| `POST /api/lists` | List created | **NO** (empty list, D-01 spirit) |
|
||||
| Route | Mutation | Push? |
|
||||
| -------------------------------- | -------------------------- | -------------------------------- |
|
||||
| `POST /api/lists/:id/items` | Item added | YES |
|
||||
| `PATCH /api/list-items/:itemId` | checked/text change | YES |
|
||||
| `PATCH /api/list-items/:itemId` | position change (reorder) | **NO** (D-01) |
|
||||
| `DELETE /api/list-items/:itemId` | Item deleted | YES |
|
||||
| `PATCH /api/lists/:id` | List rename/sharing toggle | YES |
|
||||
| `DELETE /api/lists/:id` | List deleted | YES |
|
||||
| `POST /api/lists` | List created | **NO** (empty list, D-01 spirit) |
|
||||
|
||||
Critical D-01 guard in `PATCH /list-items/:itemId`:
|
||||
|
||||
```typescript
|
||||
if (patch.position === undefined) {
|
||||
notifyListChange(item.listId, currentUserId)
|
||||
notifyListChange(item.listId, currentUserId);
|
||||
}
|
||||
```
|
||||
|
||||
**New tests in lists.test.ts (2 tests):**
|
||||
|
||||
- `PATCH { position }` (reorder) does NOT call `notifyListChange` — spy confirms 0 calls
|
||||
- `PATCH { checked: true }` DOES call `notifyListChange(listId, ownerId)` — spy confirms 1 call with correct args
|
||||
|
||||
@@ -145,23 +151,26 @@ None. `notifyListChange` is fully wired end-to-end. Push dispatch will fail with
|
||||
|
||||
No new threat surface beyond what the plan's threat model covers. All three threats mitigated:
|
||||
|
||||
| Threat | Status |
|
||||
|--------|--------|
|
||||
| T-05-14: Info disclosure — push to non-member | Mitigated — audience = owner ∪ list_shares only |
|
||||
| T-05-15: Info disclosure — item text in payload | Mitigated — D-02 generic copy only |
|
||||
| T-05-16: Spoofing — actor notified of own change | Mitigated — D-03 excludeUserId = actorId |
|
||||
| Threat | Status |
|
||||
| ------------------------------------------------ | ----------------------------------------------- |
|
||||
| T-05-14: Info disclosure — push to non-member | Mitigated — audience = owner ∪ list_shares only |
|
||||
| T-05-15: Info disclosure — item text in payload | Mitigated — D-02 generic copy only |
|
||||
| T-05-16: Spoofing — actor notified of own change | Mitigated — D-03 excludeUserId = actorId |
|
||||
|
||||
## Self-Check
|
||||
|
||||
**Files created/verified:**
|
||||
|
||||
- [x] apps/api/src/lib/listChangeDispatcher.ts — exists (min_lines: 25 ✓, ~110 lines)
|
||||
- [x] apps/api/tests/lib/listChangeDispatcher.test.ts — exists
|
||||
|
||||
**Key links verified:**
|
||||
|
||||
- [x] apps/api/src/routes/lists.ts imports and calls `notifyListChange` at 5 mutation sites
|
||||
- [x] apps/api/src/lib/listChangeDispatcher.ts calls `coalesceListPush` from `pushCoalescer.ts`
|
||||
|
||||
**Commits verified:**
|
||||
|
||||
- 97f7026: test(05-05): add failing tests for listChangeDispatcher — RED gate
|
||||
- 6923104: feat(05-05): implement listChangeDispatcher — VAPID send + access-scoped, self-suppressed, coalesced push (NOTIF-02)
|
||||
- d2ce4e0: feat(05-05): hook notifyListChange into list/item mutations (reorder excluded)
|
||||
|
||||
Reference in New Issue
Block a user