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
@@ -2,10 +2,18 @@
phase: 05-web-push-notifications
plan: 04
subsystem: api/push-routes, pwa/sw, pwa/hooks, pwa/components
tags: [web-push, vapid, injectManifest, service-worker, push-subscription, permission-prompt, tdd-green]
tags:
[web-push, vapid, injectManifest, service-worker, push-subscription, permission-prompt, tdd-green]
dependency_graph:
requires: [05-01, 05-02]
provides: [pushRouter (GET/POST/DELETE), setVapidDetails at startup, custom sw.ts with push+notificationclick+denylist, usePushSubscription hook, PushPermissionPrompt component]
provides:
[
pushRouter (GET/POST/DELETE),
setVapidDetails at startup,
custom sw.ts with push+notificationclick+denylist,
usePushSubscription hook,
PushPermissionPrompt component,
]
affects:
- apps/api/src/routes/push.ts
- apps/api/src/index.ts
@@ -36,13 +44,13 @@ key_files:
- apps/pwa/src/components/InstallPrompt.tsx
- apps/pwa/src/App.tsx
decisions:
- "setVapidDetails wrapped in try/catch — prevents startup crash on malformed VAPID key in .env"
- "rolldownOptions.output.format=iife added to force sw.js output (not sw.mjs) matching registerSW.js"
- "PushPermissionPrompt mounted in both App.tsx (installed-PWA path) and InstallPrompt.tsx (justInstalled Android path)"
- "urlBase64ToUint8Array uses new ArrayBuffer() explicitly to satisfy Uint8Array<ArrayBuffer> TS constraint"
- 'setVapidDetails wrapped in try/catch — prevents startup crash on malformed VAPID key in .env'
- 'rolldownOptions.output.format=iife added to force sw.js output (not sw.mjs) matching registerSW.js'
- 'PushPermissionPrompt mounted in both App.tsx (installed-PWA path) and InstallPrompt.tsx (justInstalled Android path)'
- 'urlBase64ToUint8Array uses new ArrayBuffer() explicitly to satisfy Uint8Array<ArrayBuffer> TS constraint'
metrics:
duration: 11
completed_date: "2026-06-10"
completed_date: '2026-06-10'
tasks_completed: 3
files_changed: 9
---
@@ -54,14 +62,17 @@ End-to-end push vertical slice: pushRouter (GET/POST/DELETE) wired with VAPID at
## Tasks Executed
### Task 1: Push subscription API + startup VAPID wiring
**Status:** Completed. Commit: `f6f1374`, `d816f79`
Created `apps/api/src/routes/push.ts` exporting `pushRouter`:
- `GET /vapid-public-key` — returns `{publicKey: process.env.VAPID_PUBLIC_KEY}` (public only; never private key)
- `POST /subscription` — zod-validated (`subscribeSchema`), `resolveUserId` guard (T-05-09), upserts on endpoint unique constraint, returns 201
- `DELETE /subscription` — user-scoped WHERE userId=caller (T-05-13), returns 200
`apps/api/src/index.ts` changes:
- Import `pushRouter` + `import webpush from 'web-push'`
- Mount `app.route('/api/push', pushRouter)` alongside other API routes
- In `isMainModule()` guard, BEFORE `serve()`: call `webpush.setVapidDetails(...)` wrapped in try/catch (non-fatal — server still starts with a warning on bad VAPID key)
@@ -71,15 +82,18 @@ Created `apps/api/src/routes/push.ts` exporting `pushRouter`:
Auto-fixed bug (Rule 1): The RED scaffold's `vi.mocked(vi.getMockImplementation).mockImplementation?.(() => undefined)` was calling a non-function and crashing the 401 test. Removed that broken line; kept the `vi.doMock` + fresh import pattern intact.
### Task 2: Service-worker migration to injectManifest
**Status:** Completed. Commit: `e5953eb`
`apps/pwa/vite.config.ts` migrated from `generateSW` to `injectManifest`:
- `strategies: 'injectManifest'`, `srcDir: 'src'`, `filename: 'sw.ts'`
- `rolldownOptions.output.format: 'iife'` to produce `sw.js` (not `sw.mjs`) matching `registerSW.js` registration
- `injectManifest.globIgnores: ['**/node_modules/**', '**/callback**']`
- `registerType: 'autoUpdate'` and `manifest` block preserved byte-identical
Created `apps/pwa/src/sw.ts`:
- `self.skipWaiting()` + `clientsClaim()` — reproduces autoUpdate behavior
- `precacheAndRoute(self.__WB_MANIFEST)` — app shell precache
- `NavigationRoute` with denylist `[/^\/callback/, /^\/api\//, /^\/health/]` (T-03-20, T-05-11 preserved)
@@ -89,9 +103,11 @@ Created `apps/pwa/src/sw.ts`:
Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotification`, `waitUntil`, `callback` denylist, `notificationclick` all present.
### Task 3: usePushSubscription hook + PushPermissionPrompt + desktop verification
**Status:** Completed. Commit: `bf8f63b`
**`apps/pwa/src/hooks/usePushSubscription.ts`:**
- `usePushSubscription()` returns `{subscribe, unsubscribe, permission}`
- `subscribe(registration)` — fetches VAPID key (cached in sessionStorage), calls `pushManager.subscribe({userVisibleOnly:true, applicationServerKey})`, POSTs `sub.toJSON()` to `/api/push/subscription`
- `unsubscribe()``getSubscription()`, `sub.unsubscribe()`, `DELETE /api/push/subscription`
@@ -100,6 +116,7 @@ Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotif
- `urlBase64ToUint8Array` uses explicit `new ArrayBuffer()` to satisfy TS `Uint8Array<ArrayBuffer>` constraint
**`apps/pwa/src/components/PushPermissionPrompt.tsx`:**
- Bottom sheet: `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, no backdrop-dismiss (UI-SPEC Surface 1)
- Bell icon, "Stay in the loop" heading, body copy per UI-SPEC
- Primary CTA: "Enable Notifications", 48px, `var(--color-member-0, #4A90D9)`
@@ -108,17 +125,20 @@ Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotif
- `prefetchVapidKey()` called in `useEffect` while visible
**Mount points:**
- `App.tsx`: `<PushPermissionPrompt />` as sibling of `<BottomTabBar>` — covers installed-PWA path
- `InstallPrompt.tsx`: `justInstalled` flag (from `appinstalled` event) renders `<PushPermissionPrompt>` immediately post-Android-install
**Desktop playwright-cli verification results:**
- `GET /api/push/vapid-public-key``{publicKey: "BJiOYmT4HC3Ik..."}` (87-char base64url P-256 key)
- `GET /api/push/vapid-public-key``{publicKey: "BJiOYmT4HC3Ik..."}` (87-char base64url P-256 key)
- `POST /api/push/subscription` (simulated body) → 201 Created
- `DELETE /api/push/subscription` → 200 OK
- Notification.permission granted via `page.context().grantPermissions(['notifications'])`
- Browser console: only favicon 404 (non-issue), no app errors
**iOS-only items (deferred to Phase 5 human gate — device-only):**
- iOS Safari standalone-mode install (Home Screen required, per CLAUDE.md)
- iOS push delivery round-trip (APNs-specific)
- iOS pushManager.subscribe user-gesture validation (requires real device tap)
@@ -128,6 +148,7 @@ Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotif
### Auto-fixed issues
**1. [Rule 1 - Bug] Broken vi.getMockImplementation call in push.test.ts scaffold**
- **Found during:** Task 1 test run
- **Issue:** RED scaffold line 107 `vi.mocked(vi.getMockImplementation).mockImplementation?.(() => undefined)` called `vi.getMockImplementation` which is not a function — TypeError crash on the 401 test
- **Fix:** Removed the broken defensive line; the actual 401 test mechanism (vi.doMock + fresh import with `?v=unauth` cache buster) remained intact
@@ -135,6 +156,7 @@ Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotif
- **Commit:** `f6f1374`
**2. [Rule 1 - Bug] setVapidDetails crashes server when VAPID_PRIVATE_KEY is malformed**
- **Found during:** Task 1 playwright-cli verification startup
- **Issue:** The `.env` VAPID_PRIVATE_KEY is truncated (41 chars vs expected 43) due to a multiline formatting issue. The startup guard passed the truthiness check but `web-push` threw "Vapid private key should be 32 bytes long when decoded" crashing the process.
- **Fix:** Wrapped `webpush.setVapidDetails(...)` in try/catch — logs a warning but server starts; push dispatch will fail on actual sends but other routes are unaffected
@@ -142,6 +164,7 @@ Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotif
- **Commit:** `d816f79`
**3. [Rule 1 - Bug] vite-plugin-pwa 1.3.x + Vite 8 outputs sw.mjs instead of sw.js**
- **Found during:** Task 2 build verification
- **Issue:** With TypeScript source (`sw.ts`) + Vite 8, vite-plugin-pwa 1.3.x defaults to ES module output format, producing `sw.mjs`. But `registerSW.js` always registers `/sw.js` — the service worker would fail to register.
- **Fix:** Added `rolldownOptions: { output: { format: 'iife' } }` to vite.config.ts to force IIFE format, which produces `sw.js`
@@ -149,6 +172,7 @@ Build: `dist/sw.js` produced with 7-entry precache manifest; verified `showNotif
- **Commit:** `e5953eb`
**4. [Rule 1 - Bug] TypeScript Uint8Array<ArrayBufferLike> incompatible with PushSubscriptionOptionsInit.applicationServerKey**
- **Found during:** Task 3 PWA build
- **Issue:** TypeScript 5.x strict: `new Uint8Array(rawData.length)` produces `Uint8Array<ArrayBufferLike>` but `applicationServerKey` expects `ArrayBufferView<ArrayBuffer>` — SharedArrayBuffer not assignable to ArrayBuffer
- **Fix:** Changed to `const buffer = new ArrayBuffer(rawData.length); const outputArray = new Uint8Array(buffer)` which types as `Uint8Array<ArrayBuffer>`
@@ -174,23 +198,25 @@ These are tracked as the Phase 5 human gate (device-only verification, Phase 5 G
No new threat surface beyond the plan's threat model. All five threats mitigated:
| Threat | Status |
|--------|--------|
| T-05-09: Spoofing (userId from body) | Mitigated — resolveUserId from OIDC session only |
| T-05-10: Input validation | Mitigated — zod subscribeSchema (endpoint URL, p256dh/auth bounded) |
| T-05-11: SW serving /callback | Mitigated — NavigationRoute denylist in sw.ts |
| T-05-12: Malformed push payload | Mitigated — try/catch fallback; always showNotification |
| T-05-13: DELETE another member's subscription | Mitigated — WHERE userId=caller only |
| Threat | Status |
| --------------------------------------------- | ------------------------------------------------------------------- |
| T-05-09: Spoofing (userId from body) | Mitigated — resolveUserId from OIDC session only |
| T-05-10: Input validation | Mitigated — zod subscribeSchema (endpoint URL, p256dh/auth bounded) |
| T-05-11: SW serving /callback | Mitigated — NavigationRoute denylist in sw.ts |
| T-05-12: Malformed push payload | Mitigated — try/catch fallback; always showNotification |
| T-05-13: DELETE another member's subscription | Mitigated — WHERE userId=caller only |
## Self-Check
**Files created/verified:**
- [x] apps/api/src/routes/push.ts — exists
- [x] apps/pwa/src/sw.ts — exists
- [x] apps/pwa/src/hooks/usePushSubscription.ts — exists
- [x] apps/pwa/src/components/PushPermissionPrompt.tsx — exists
**Commits verified:**
- f6f1374: feat(05-04): push subscription API + VAPID startup wiring
- e5953eb: feat(05-04): SW migration to injectManifest with push + notificationclick + denylist
- bf8f63b: feat(05-04): usePushSubscription hook + PushPermissionPrompt + App mount