M2: Setup, auth, devices and pairing (ENG-250) #14

Merged demi.dev
targeting main from stoopkidddd/eng-250-m2-setup-auth-devices-and-pairing
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-250-m2-setup-auth-devices-and-pairing

Linear: https://linear.app/engineereddev/issue/ENG-250

What's in this PR:

  • instance/: loads meta/owner.json at boot (health.initialized); the setup token is PNEUMO_SETUP_TOKEN, or one is generated and the setup URL logged once while uninitialized; an in-process setup mutex; conditional create with ifNoneMatch: "*".
  • auth/: POST /api/setup (scrypt verifier, first device + token), GET /api/auth/params, POST /api/auth/login with per-IP and global rate limits (TRUST_PROXY-aware, Retry-After), bearer middleware over SHA-256 token hashes.
  • devices/: a registry loaded from devices/*.json; GET /api/me; GET/PATCH/DELETE /api/devices[/:id]; PUT/DELETE /api/devices/self/webpush (stores the subscription only). DTOs never include tokenHash or webPush.
  • pairing/: POST /api/pair (16-byte single-use code, 10-minute expiry) and POST /api/pair/claim (410 when expired or used).
  • events.ts: a typed bus for device_added/device_updated/device_removed, plus a revoke signal for the WebSocket hub.
  • webpush/vapid.ts: loads meta/vapid.json, or generates and persists a P-256 VAPID keypair on first boot; /api/me returns 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 0
  • mise run check:server → exit 0 (Tests 138 passed (138))
  • pnpm --filter @pneumo/server test:e2e → exit 0 (2 files, 9 tests on MinIO). The e2e:server mise 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 MeResponse requires a 65-byte vapidPublicKey, and ENG-255 is blocked by ENG-251. A null would break the Android and web clients on /api/me until then. ENG-255 keeps the sending.
  • The built-server startup test moved from check:server into the MinIO e2e suite, because boot now reads the bucket.

🤖 Generated with Claude Code

Commits 0