A pending device registration used to be announced to EVERY account's
Devices page, and whoever clicked Approve first bound the device — and
its freshly minted sync token — to their account. Worse, the pending
card displayed the registration UUID, which is also the bearer
credential on the unauthenticated /api/devices/register/status poll
channel: any family user who could see the list already held everything
needed to take the device. On a family server that is a
wrong-account-binding bug (accidental or deliberate); this commit
removes the class of problem rather than patching the race.
The new model is the Netflix/YouTube TV device flow: the registering
device displays a 6-digit pairing code, and the only person who can
read it — whoever holds the device — types it into THEIR account on the
Devices page. Possession of the device screen becomes the credential;
nothing is broadcast; the reject-on-someone-else's-behalf hole dies
with the list.
Server changes:
- POST /api/devices/register now mints pairing_code (crypto/rand,
rejection-sampled for uniformity, collision-checked against live
registrations) and retires prior unclaimed registrations with the
same device_identifier, so re-registering leaves exactly one live
attempt.
- POST /api/devices/claim (JWT) binds the registration behind a code to
the caller's account, reusing the rotate-or-create approval logic
factored out of ApproveDevice (shared bindRegistrationToUser helper;
the Android self-approval route keeps working unchanged).
- POST /api/devices/claim/lookup (JWT) returns the confirm-card data
without binding. Lookup and claim share a per-account budget (12
attempts/min, DeviceRateLimiter gained a claim case) — the 1M-code
space over a 5-minute TTL makes guessing infeasible without it.
- POST /api/devices/register/cancel lets the registering device end its
own attempt (registration UUID bearer, idempotent, never touches a
claimed registration).
- ListPendingRegistrations / GetPendingRegistrationsData are
deprecated-by-emptiness: unclaimed registrations return to nobody.
- RejectDevice and its route are deleted — the broadcast list was its
only consumer, and there is nothing left for anyone to reject.
Additive-only for pinned surfaces: the register response gains
pairing_code (Android DTO untouched), the four KOReader sync surfaces
and approve keep byte-identical shapes. contract_pins.md changelog
records the widening and the reject removal. Docs, schema registry, and
bruno mirrors move together per the drift gate; bruno_gen.go now keeps
hand-authored bruno/podcasts/ (API docs never existed for them, so the
next -update run would have swept them — latent landmine, defused).
Unit tests: code shape, live-collision avoidance, typed-code
normalization, and the resolve scopes (live/expired/claimed/miss).
14 endpoints backfilled: devices pending/approve/reject; users
password/profile/profile-delete self-service (admin variants in prose);
admin list_users + update_user_max_devices; progress history; book
matching; new password-rules doc.
Gate fix: authLabel missed JWT middleware written inline as
createJWTMiddleware(cfg) (uppercase 'JWT') — reported auth 'none' for
/api/auth self-service routes and would have for any wrapper-inlined
group. Labeled 'JWTMiddleware' + fallthrough 'jwt' case.
Claude registry: 12 entries. Drift fixed by gate: list_users on-device —
handler returns raw []database.Users (bare array, full model incl.
password_hash — real leak the API emits; pinned + documented; earlier
draft had invented {users,total,limit,offset} wrapper). max-devices
response is {message} only. books/match is GET /api/books/match
{matches, action}. progress history sessions = ReadingHistory rows
(pgtype fields). make test 9/9.
Clean up API documentation files by removing Phase X references:
Remove 'API Explorer will be inserted here in Phase X' placeholders from:
- 70+ API endpoint documentation files
- Authentication endpoints (login, logout, register, refresh)
- User endpoints (profile, settings, password)
- Device endpoints (registration, sync, shelves)
- Library endpoints (CRUD, folders, visibility)
- Media endpoints (items, progress, highlights, notes)
- Admin endpoints (users, analytics)
- Sync endpoints (Kobo, KOReader)
- OPDS endpoints
- Scanner endpoints
- Queue endpoints
These placeholders were from planning documents and have no meaning
to API consumers. The documentation is now clean and ready for use.