From 768e07046368819b8a8f15c8b21e5a8bbfcdf282 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 18 Aug 2026 03:24:55 +0200 Subject: feat: device linking, and signing in to the hub with a device key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/meshbay-hub/src/meshbay_hub/static/locales/de.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/en.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/es.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/it.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js | 13 +++++++++++++ packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js | 13 +++++++++++++ .../meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js | 13 +++++++++++++ .../meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js | 13 +++++++++++++ 10 files changed, 130 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/locales') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index aaed0d1..5a946f7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -92,6 +92,19 @@ export default { 'group.upload': 'Hochladen', 'group.mkdir': 'Neuer Ordner', 'group.mkdir_prompt': 'Name des neuen Ordners:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(nicht verfügbar — Laufwerk getrennt)', 'group.view': 'Ansehen', 'group.delete': 'Löschen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 02532d6..fd2d6ec 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -92,6 +92,19 @@ export default { 'group.upload': 'Upload', 'group.mkdir': 'New folder', 'group.mkdir_prompt': 'Name of the new folder:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(unavailable — the drive is disconnected)', 'group.view': 'View', 'group.delete': 'Delete', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 49bfc1c..e564164 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -90,6 +90,19 @@ export default { 'group.upload': 'Subir', 'group.mkdir': 'Nueva carpeta', 'group.mkdir_prompt': 'Nombre de la nueva carpeta:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(no disponible — la unidad está desconectada)', 'group.view': 'Ver', 'group.delete': 'Eliminar', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 1afc5b1..5848f9d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -91,6 +91,19 @@ export default { 'group.upload': 'Envoyer', 'group.mkdir': 'Nouveau dossier', 'group.mkdir_prompt': 'Nom du nouveau dossier :', + 'device.add_title': 'Ce navigateur n’est pas encore lié à ce nœud', + 'device.add_hint': 'Votre compte est connu ici, mais ce navigateur détient une autre clé. Approuvez-le depuis un appareil déjà lié — sans passer par l’opérateur.', + 'device.add_btn': 'Obtenir un code de liaison', + 'device.add_show': 'Saisissez ce code sur un appareil déjà lié à ce nœud, dans l’heure :', + 'device.mine_title': 'Vos appareils sur ce nœud', + 'device.mine_hint': 'Chaque navigateur ou client que vous utilisez ici détient sa propre clé. Ils sont distincts de vos appareils sur les autres nœuds.', + 'device.mine_empty': 'Aucun appareil lié ici pour le moment.', + 'device.this_one': 'celui-ci', + 'device.revoke': 'Retirer', + 'device.revoke_confirm': 'Retirer cet appareil ? Il perdra l’accès à ce nœud jusqu’à une nouvelle liaison.', + 'device.approve_hint': 'Vous liez un nouvel appareil ? Saisissez le code qu’il affiche.', + 'device.approve_btn': 'Approuver', + 'device.approved': 'Appareil lié.', 'group.root_unavailable': '(indisponible — le disque est déconnecté)', 'group.view': 'Afficher', 'group.delete': 'Supprimer', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 44b4353..61913e3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -91,6 +91,19 @@ export default { 'group.upload': 'Carica', 'group.mkdir': 'Nuova cartella', 'group.mkdir_prompt': 'Nome della nuova cartella:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(non disponibile — l’unità è scollegata)', 'group.view': 'Visualizza', 'group.delete': 'Elimina', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 33fdee3..aa656ee 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -89,6 +89,19 @@ export default { 'group.upload': 'アップロード', 'group.mkdir': '新しいフォルダー', 'group.mkdir_prompt': '新しいフォルダーの名前:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(利用不可 — ドライブが切断されています)', 'group.view': '表示', 'group.delete': '削除', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index aa0385e..dae903f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -92,6 +92,19 @@ export default { 'group.upload': 'Uploaden', 'group.mkdir': 'Nieuwe map', 'group.mkdir_prompt': 'Naam van de nieuwe map:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(niet beschikbaar — de schijf is losgekoppeld)', 'group.view': 'Bekijken', 'group.delete': 'Verwijderen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index e66dd12..65c81ca 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -95,6 +95,19 @@ export default { 'group.upload': 'Wyślij', 'group.mkdir': 'Nowy folder', 'group.mkdir_prompt': 'Nazwa nowego folderu:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(niedostępne — dysk jest odłączony)', 'group.view': 'Podgląd', 'group.delete': 'Usuń', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 03de040..729a7fc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -92,6 +92,19 @@ export default { 'group.upload': 'Enviar', 'group.mkdir': 'Nova pasta', 'group.mkdir_prompt': 'Nome da nova pasta:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(indisponível — a unidade está desconectada)', 'group.view': 'Visualizar', 'group.delete': 'Excluir', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 72995dd..0ff5a49 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -88,6 +88,19 @@ export default { 'group.upload': '上传', 'group.mkdir': '新建文件夹', 'group.mkdir_prompt': '新文件夹的名称:', + 'device.add_title': 'This browser is not linked to this node yet', + 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', + 'device.add_btn': 'Get a linking code', + 'device.add_show': 'Type this code into a device already linked to this node, within the hour:', + 'device.mine_title': 'Your devices on this node', + 'device.mine_hint': 'Each browser or client you use here holds its own key. They are separate from your devices on other nodes.', + 'device.mine_empty': 'No device is linked here yet.', + 'device.this_one': 'this one', + 'device.revoke': 'Remove', + 'device.revoke_confirm': 'Remove this device? It will lose access to this node until it is linked again.', + 'device.approve_hint': 'Linking a new device? Enter the code it is showing.', + 'device.approve_btn': 'Approve', + 'device.approved': 'Device linked.', 'group.root_unavailable': '(不可用 — 驱动器已断开连接)', 'group.view': '查看', 'group.delete': '删除', -- cgit v1.2.3