Linear: https://linear.app/engineereddev/issue/ENG-250
What's in this PR:
instance/: loadsmeta/owner.jsonat boot (health.initialized); the setup token isPNEUMO_SETUP_TOKEN, or one is generated and the setup URL logged once while uninitialized; an in-process setup mutex; conditional create withifNoneMatch: "*".auth/:POST /api/setup(scrypt verifier, first device + token),GET /api/auth/params,POST /api/auth/loginwith per-IP and global rate limits (TRUST_PROXY-aware,Retry-After), bearer middleware over SHA-256 token hashes.devices/: a registry loaded fromdevices/*.json;GET /api/me;GET/PATCH/DELETE /api/devices[/:id];PUT/DELETE /api/devices/self/webpush(stores the subscription only). DTOs never includetokenHashorwebPush.pairing/:POST /api/pair(16-byte single-use code, 10-minute expiry) andPOST /api/pair/claim(410 when expired or used).events.ts: a typed bus fordevice_added/device_updated/device_removed, plus a revoke signal for the WebSocket hub.webpush/vapid.ts: loadsmeta/vapid.json, or generates and persists a P-256 VAPID keypair on first boot;/api/mereturns the public key.- Log redaction for the new sensitive fields (Web Push keys, the VAPID private key), with assertions.
Verify (lead, rebased on 3c116f5):
pnpm install --frozen-lockfile→ exit 0mise run check:server→ exit 0 (Tests 138 passed (138))pnpm --filter @pneumo/server test:e2e→ exit 0 (2 files, 9 tests on MinIO). Thee2e:servermise task arrives with PR #13.- No
pneumo-e2e-*containers left.
Deviations:
- Lead decision: the VAPID keypair bootstrap (ENG-255's first bullet) moved into this issue. The frozen
MeResponserequires a 65-bytevapidPublicKey, and ENG-255 is blocked by ENG-251. Anullwould break the Android and web clients on/api/meuntil then. ENG-255 keeps the sending. - The built-server startup test moved from
check:serverinto the MinIO e2e suite, because boot now reads the bucket.
🤖 Generated with Claude Code