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).
67 lines
2.1 KiB
Markdown
67 lines
2.1 KiB
Markdown
---
|
|
method: POST
|
|
path: /api/devices/register
|
|
auth: none
|
|
area: devices
|
|
---
|
|
<!-- source: internal/handlers/devices.go:123 DeviceHandler.InitiateRegistration -->
|
|
|
|
# Register Device
|
|
|
|
Register a new device for sync.
|
|
|
|
**Endpoint**: `POST /api/devices/register`
|
|
**Auth**: Not required (device registration flow)
|
|
**Content-Type**: `application/json`
|
|
|
|
## Request Body
|
|
|
|
| Field | Type | Required | Description |
|
|
| ----------------- | ------ | -------- | ---------------------------------------- |
|
|
| device_name | string | Yes | Device name |
|
|
| device_type | string | Yes | Device type: kobo, koreader, web, mobile |
|
|
| device_identifier | string | Yes | Hardware-specific ID |
|
|
|
|
### Example Request
|
|
|
|
```json example-request
|
|
{
|
|
"device_name": "My Kobo Clara",
|
|
"device_type": "kobo",
|
|
"device_identifier": "hardware-specific-id"
|
|
}
|
|
```
|
|
|
|
## Response (201 Created)
|
|
|
|
```json example-response
|
|
{
|
|
"device_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"registration_id": "registration-uuid",
|
|
"auth_url": "https://bookhoard.com/devices/approve/abc123",
|
|
"qr_code": "data:image/png;base64,iVBORw0KG...",
|
|
"pairing_code": "583912",
|
|
"expires_in": 300,
|
|
"poll_interval": 3,
|
|
"setup_instructions": {
|
|
"koreader": "Calibre URL: https://bookhoard.com/api/sync/koreader"
|
|
}
|
|
}
|
|
```
|
|
|
|
`pairing_code` is the claim credential: the registering device displays it, and the
|
|
only person who can pair the device is whoever reads it (they type it into the
|
|
Devices page of THEIR account — `POST /api/devices/claim`). Unclaimed registrations
|
|
appear in no account's pending list, so there is no first-approver race.
|
|
Registrations expire after 5 minutes; re-registering the same `device_identifier`
|
|
retires the earlier attempt. Poll `POST /api/devices/register/status` at
|
|
`poll_interval` seconds until `status` is `approved`, at which point the response
|
|
includes the device's `auth_token`, `device_id`, and `sync_endpoints`.
|
|
|
|
## Error Responses
|
|
|
|
| Code | Description |
|
|
| ---- | ------------------------- |
|
|
| 400 | Invalid device data |
|
|
| 409 | Device already registered |
|