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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user