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:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
@@ -20,36 +20,36 @@ requirements: [NOTIF-01, NOTIF-02, NOTIF-03]
must_haves:
truths:
- "A member can tap 'Enable Notifications' in the post-install prompt; the browser subscribes via pushManager.subscribe and POST /api/push/subscription persists a row scoped to their userId (D-08)"
- "The custom service worker shows a visible notification for EVERY push (including malformed payloads) via event.waitUntil(showNotification) — no silent pushes (D-11)"
- "notificationclick opens the deep-link URL from the payload (focus existing window or openWindow) (D-14)"
- "The /callback, /api/, /health navigation denylist is preserved after the generateSW to injectManifest migration (T-03-20)"
- "GET /api/push/vapid-public-key serves the public key; subscription POST/DELETE are scoped to the authenticated user (V4 access control)"
- 'The custom service worker shows a visible notification for EVERY push (including malformed payloads) via event.waitUntil(showNotification) — no silent pushes (D-11)'
- 'notificationclick opens the deep-link URL from the payload (focus existing window or openWindow) (D-14)'
- 'The /callback, /api/, /health navigation denylist is preserved after the generateSW to injectManifest migration (T-03-20)'
- 'GET /api/push/vapid-public-key serves the public key; subscription POST/DELETE are scoped to the authenticated user (V4 access control)'
artifacts:
- path: "apps/api/src/routes/push.ts"
provides: "pushRouter — GET /vapid-public-key, POST /subscription, DELETE /subscription"
exports: ["pushRouter"]
- path: "apps/pwa/src/sw.ts"
provides: "custom injectManifest SW: precache + push + notificationclick + nav denylist"
contains: "showNotification"
- path: "apps/pwa/src/hooks/usePushSubscription.ts"
provides: "subscribe/unsubscribe lifecycle (subscribe in tap handler only)"
exports: ["usePushSubscription"]
- path: "apps/pwa/src/components/PushPermissionPrompt.tsx"
provides: "post-install permission bottom sheet (D-08)"
exports: ["PushPermissionPrompt"]
- path: 'apps/api/src/routes/push.ts'
provides: 'pushRouter — GET /vapid-public-key, POST /subscription, DELETE /subscription'
exports: ['pushRouter']
- path: 'apps/pwa/src/sw.ts'
provides: 'custom injectManifest SW: precache + push + notificationclick + nav denylist'
contains: 'showNotification'
- path: 'apps/pwa/src/hooks/usePushSubscription.ts'
provides: 'subscribe/unsubscribe lifecycle (subscribe in tap handler only)'
exports: ['usePushSubscription']
- path: 'apps/pwa/src/components/PushPermissionPrompt.tsx'
provides: 'post-install permission bottom sheet (D-08)'
exports: ['PushPermissionPrompt']
key_links:
- from: "apps/pwa/src/hooks/usePushSubscription.ts"
to: "/api/push/subscription"
via: "fetch POST sub.toJSON() inside tap handler"
pattern: "api/push/subscription"
- from: "apps/api/src/index.ts"
to: "webpush.setVapidDetails"
via: "isMainModule startup before serve"
pattern: "setVapidDetails"
- from: "apps/pwa/src/sw.ts"
to: "showNotification"
via: "event.waitUntil in push handler"
pattern: "waitUntil"
- from: 'apps/pwa/src/hooks/usePushSubscription.ts'
to: '/api/push/subscription'
via: 'fetch POST sub.toJSON() inside tap handler'
pattern: 'api/push/subscription'
- from: 'apps/api/src/index.ts'
to: 'webpush.setVapidDetails'
via: 'isMainModule startup before serve'
pattern: 'setVapidDetails'
- from: 'apps/pwa/src/sw.ts'
to: 'showNotification'
via: 'event.waitUntil in push handler'
pattern: 'waitUntil'
---
<objective>
@@ -169,23 +169,25 @@ Output: pushRouter (subscribe/unsubscribe/vapid-public-key) wired in index.ts wi
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| browser → POST /api/push/subscription | untrusted subscription body crosses into the API |
| SW → push payload | push payload from the service is untrusted input parsed in the SW |
| SW → /callback navigation | OIDC callback must reach the server, never the SW cache |
| Boundary | Description |
| ------------------------------------- | ----------------------------------------------------------------- |
| browser → POST /api/push/subscription | untrusted subscription body crosses into the API |
| SW → push payload | push payload from the service is untrusted input parsed in the SW |
| SW → /callback navigation | OIDC callback must reach the server, never the SW cache |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-09 | Spoofing | POST /subscription (user A subscribing as user B) | mitigate | userId comes from the OIDC session via resolveUserId, never from the body |
| T-05-10 | Input Validation | subscription body | mitigate | zod subscribeSchema (endpoint url, p256dh/auth bounded) before insert |
| T-05-11 | Tampering | SW serving /callback from cache | mitigate | NavigationRoute denylist /^\/callback/, /^\/api\//, /^\/health/ re-implemented in sw.ts (T-03-20 / Pitfall 4) |
| T-05-12 | Denial of Service | malformed push payload in SW | mitigate | try/catch in push handler; ALWAYS showNotification (generic fallback) so iOS never sees a silent push |
| T-05-13 | Access Control | DELETE /subscription | mitigate | scoped WHERE userId = caller; cannot delete another member's subscription |
| Threat ID | Category | Component | Disposition | Mitigation Plan |
| --------- | ----------------- | ------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------- |
| T-05-09 | Spoofing | POST /subscription (user A subscribing as user B) | mitigate | userId comes from the OIDC session via resolveUserId, never from the body |
| T-05-10 | Input Validation | subscription body | mitigate | zod subscribeSchema (endpoint url, p256dh/auth bounded) before insert |
| T-05-11 | Tampering | SW serving /callback from cache | mitigate | NavigationRoute denylist /^\/callback/, /^\/api\//, /^\/health/ re-implemented in sw.ts (T-03-20 / Pitfall 4) |
| T-05-12 | Denial of Service | malformed push payload in SW | mitigate | try/catch in push handler; ALWAYS showNotification (generic fallback) so iOS never sees a silent push |
| T-05-13 | Access Control | DELETE /subscription | mitigate | scoped WHERE userId = caller; cannot delete another member's subscription |
</threat_model>
<verification>
@@ -195,11 +197,12 @@ Output: pushRouter (subscribe/unsubscribe/vapid-public-key) wired in index.ts wi
</verification>
<success_criteria>
- Subscribe/unsubscribe/vapid-public-key API live and user-scoped.
- generateSW to injectManifest migration complete with denylist preserved.
- Every push shows a visible notification (incl. malformed); notificationclick deep-links.
- Post-install permission prompt matches UI-SPEC Surface 1 and subscribes on tap.
</success_criteria>
</success_criteria>
<output>
Create `.planning/phases/05-web-push-notifications/05-04-SUMMARY.md` when done.