summaryrefslogtreecommitdiffstats
path: root/docs/desktop-client-v1.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-18 03:24:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-18 03:24:55 +0200
commit768e07046368819b8a8f15c8b21e5a8bbfcdf282 (patch)
treefba4fa5f85e3963b2281004b503be05f552aff2c /docs/desktop-client-v1.md
parente9d5e979fdab9a1cc3c729d602e6f27207b9480c (diff)
downloadmeshbay-768e07046368819b8a8f15c8b21e5a8bbfcdf282.tar.gz
feat: device linking, and signing in to the hub with a device key
Stage C. Identity keys are per node, so a browser and a desktop client are two keys on one account there — and the node refused the second where it accepted the first. Without this, an account created natively could never be opened in a browser without an operator code per node, and "a native client must not prevent web use" would have been dead on arrival. Device linking (node) --------------------- `identities` is keyed by `(user_id, pk_ed25519)` instead of `user_id` alone. The old shape did `INSERT OR REPLACE`, so a second device overwrote the first silently; SQLite cannot change a primary key in place, so the table is rebuilt. Existing pins are carried over — verified against a live roster with 10 of them, nobody re-pairs. A new device files a request bound by `sha256(code ‖ its own keys)`, and a key the node **already pinned** countersigns it. The hub cannot: it has stored no user keys since 2026-08-14, which is what makes this safe to do without an operator in the loop. **The code never reaches the node.** It lists this account's pending requests with their stored hashes; the approver recomputes and keeps the match. A node offering fabricated keys would have to produce a hash over a code it has never seen. Nothing rests on a human comparing digits — that ritual was dropped in 12.1 as "correct, unusable as the default" and must not return by the back door. The design document had the approver look a request up *by* its hash, which is circular: computing it needs the keys being asked about. Corrected in both. Revocation marks rather than deletes, because a deleted row is a key the node would happily pin again — which is the laptop somebody just reported lost. Your last device cannot be revoked: coming back would need an operator's code. Hub — the only change in the whole plan --------------------------------------- `POST /v1/users/auth` signs in with a device Ed25519 key, on the same pattern as `/v1/nodes/auth`, plus `/v1/users/devices` to register, list and retire. New `user_devices` table with an Alembic migration, because `create_all()` is not one. This is **not** the key directory that was H3, and the tests say so: nothing reads it but the hub, no group key is ever wrapped for one, and it is a different key from the per-node identities. What it does cost is metadata — the hub now knows how many devices an account has and when each last signed in. Also `client.minimum` / `client.recommended` in `GET /v1/hub/version`: an installed client meets a newer hub the day the interface ships in a package, and that is cheap now and awkward to retrofit. Browser ------- The `key_changed` refusal becomes `unknown_device` and offers a linking code instead of telling someone to find their operator. The Members panel lists this account's devices here, approves one by code, and retires one. 773 tests pass. `e2e.py` gained a step that links a device end to end against the live deployment — file, list, recompute, countersign, then open the group with the new keys and no code — and it also gained `recv_type`, because a step that assumes the next message is its own answer reads an ack left by the step before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/desktop-client-v1.md')
-rw-r--r--docs/desktop-client-v1.md16
1 files changed, 12 insertions, 4 deletions
diff --git a/docs/desktop-client-v1.md b/docs/desktop-client-v1.md
index b167525..491af65 100644
--- a/docs/desktop-client-v1.md
+++ b/docs/desktop-client-v1.md
@@ -1000,10 +1000,18 @@ refactoring the same six commands twice.
| # | Component | Prio |
|---|---|---|
-| C1 | **Device linking** — `identities` schema, `device_requests`, the two transcripts, operator surface (§4) | 1 |
-| C2 | **`POST /v1/users/auth`** — device Ed25519 authentication (§5). *The only hub change in the whole plan* | 1 |
-| C3 | **Minimum client version** in `GET /v1/hub/version` (§2.6, O8) | 1 |
-| C4 | Device management in the SPA — list, approve, revoke | 1 |
+| C1 | ✅ **DONE 2026-08-18** — `identities` keyed by (user_id, pk_ed25519) with a rebuild migration that preserves existing pins, `device_requests`, both transcripts in `meshbay_common/device.py`, handlers and audit events | 1 |
+| C2 | ✅ **DONE 2026-08-18** — `POST /v1/users/auth`, plus `/v1/users/devices` to register, list and retire. New `user_devices` table with an Alembic migration. *The only hub change in the whole plan* | 1 |
+| C3 | ✅ **DONE 2026-08-18** — `client.minimum` and `client.recommended` in `GET /v1/hub/version` | 1 |
+| C4 | ✅ **DONE 2026-08-18** — the `unknown_device` refusal offers a linking code; the Members panel lists devices, approves by code and retires one | 1 |
+
+**One correction the implementation forced.** §4.3 has the approver look a request
+up by its hash — which is circular, because computing that hash needs the keys
+being asked about. What shipped: the node lists this account's pending requests
+**with their stored hashes**, and the client recomputes `sha256(code ‖ keys)` for
+each and keeps the match. The code never reaches the node, which is what makes
+substitution impossible: a node offering fabricated keys would have to produce a
+hash over a code it has never seen.
C1 lands in the SPA first, where both ends of a link can be exercised without a desktop
build existing.