feat(devices): pairing codes replace the registration broadcast
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).
This commit is contained in:
1 parent
ab5c52c709
commit
8fbacd9f46
47 files changed
+935
-273
No files matched your search
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Bulk Add Books
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Bulk Assign Books
|
||||
type: http
|
||||
seq: 3
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Bulk Remove Books
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Create Collection
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Create Shelf Mapping
|
||||
type: http
|
||||
seq: 6
|
||||
seq: 5
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Delete Collection
|
||||
type: http
|
||||
seq: 7
|
||||
seq: 6
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Delete Shelf Mapping
|
||||
type: http
|
||||
seq: 8
|
||||
seq: 7
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Collection
|
||||
type: http
|
||||
seq: 9
|
||||
seq: 8
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Book Collections
|
||||
type: http
|
||||
seq: 10
|
||||
seq: 9
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: List Collections
|
||||
type: http
|
||||
seq: 11
|
||||
seq: 10
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Preview Collection
|
||||
type: http
|
||||
seq: 12
|
||||
seq: 11
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Reorder Collection Items
|
||||
type: http
|
||||
seq: 14
|
||||
seq: 12
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Update Collection
|
||||
type: http
|
||||
seq: 16
|
||||
seq: 14
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
|
||||
+12
-6
@@ -1,12 +1,18 @@
|
||||
info:
|
||||
name: Reject Device Registration
|
||||
name: Cancel Device Registration
|
||||
type: http
|
||||
seq: 16
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/reject/{{registration_id}}'
|
||||
url: '{{base_url}}/api/devices/register/cancel'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"registration_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
@@ -23,9 +29,9 @@ runtime:
|
||||
|
||||
docs:
|
||||
content: |-
|
||||
# Reject Device Registration
|
||||
# Cancel Device Registration
|
||||
|
||||
`POST /api/devices/reject/:registration_id` — auth: jwt.
|
||||
`POST /api/devices/register/cancel` — auth: none.
|
||||
|
||||
Canonical reference: docs/developer/api/devices/reject_device_registration.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||
Canonical reference: docs/developer/api/devices/cancel_device_registration.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||
type: text/markdown
|
||||
@@ -0,0 +1,37 @@
|
||||
info:
|
||||
name: Claim Device (Pairing Code)
|
||||
type: http
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/claim'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"pairing_code": "583912"
|
||||
}
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
|
||||
runtime:
|
||||
scripts:
|
||||
- type: tests
|
||||
code: |-
|
||||
test("status is 200", function() {
|
||||
expect(res.status).to.eql(200);
|
||||
});
|
||||
|
||||
docs:
|
||||
content: |-
|
||||
# Claim Device (Pairing Code)
|
||||
|
||||
`POST /api/devices/claim` — auth: jwt.
|
||||
|
||||
Canonical reference: docs/developer/api/devices/claim_device.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||
type: text/markdown
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Clear Shelf
|
||||
type: http
|
||||
seq: 3
|
||||
seq: 5
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Delete Device
|
||||
type: http
|
||||
seq: 4
|
||||
seq: 6
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Download Sidecar Config
|
||||
type: http
|
||||
seq: 5
|
||||
seq: 7
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Device
|
||||
type: http
|
||||
seq: 6
|
||||
seq: 8
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Device Collection Mappings
|
||||
type: http
|
||||
seq: 7
|
||||
seq: 9
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Device / Check Registration Status
|
||||
type: http
|
||||
seq: 8
|
||||
seq: 10
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Shelf
|
||||
type: http
|
||||
seq: 9
|
||||
seq: 11
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Get Device Sidecar Config
|
||||
type: http
|
||||
seq: 10
|
||||
seq: 12
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Link Book
|
||||
type: http
|
||||
seq: 11
|
||||
seq: 13
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: List User Devices
|
||||
type: http
|
||||
seq: 12
|
||||
seq: 14
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: List Pending Device Registrations:<|end|><|start|><|start|>assistant to=edit<|channel|>analysis <|constrain|>xml<|message|><rfl:parameter name="path">
|
||||
type: http
|
||||
seq: 13
|
||||
seq: 15
|
||||
|
||||
http:
|
||||
method: GET
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
info:
|
||||
name: Look Up Pairing Code
|
||||
type: http
|
||||
seq: 16
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/claim/lookup'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"pairing_code": "583912"
|
||||
}
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
|
||||
runtime:
|
||||
scripts:
|
||||
- type: tests
|
||||
code: |-
|
||||
test("status is 200", function() {
|
||||
expect(res.status).to.eql(200);
|
||||
});
|
||||
|
||||
docs:
|
||||
content: |-
|
||||
# Look Up Pairing Code
|
||||
|
||||
`POST /api/devices/claim/lookup` — auth: jwt.
|
||||
|
||||
Canonical reference: docs/developer/api/devices/lookup_pairing_code.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||
type: text/markdown
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Regenerate Device Token
|
||||
type: http
|
||||
seq: 14
|
||||
seq: 17
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Register Device
|
||||
type: http
|
||||
seq: 15
|
||||
seq: 18
|
||||
|
||||
http:
|
||||
method: POST
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Remove from Shelf
|
||||
type: http
|
||||
seq: 17
|
||||
seq: 19
|
||||
|
||||
http:
|
||||
method: DELETE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Update Device
|
||||
type: http
|
||||
seq: 18
|
||||
seq: 20
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
info:
|
||||
name: Update Device Collection Mapping
|
||||
type: http
|
||||
seq: 19
|
||||
seq: 21
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
|
||||
Reference in new issue
Block a user