| … | … | @@ -93,3 +93,42 @@ export function buildRelease ({ nonce, username, root, cred }) { |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | 95 | export default { connectBridge, buildClaim, buildRelease, PROTOCOL } |
| 96 | ||
| 97 | // ── which bridges carry this node ──────────────────────────────────────────────────────────────── | |
| 98 | // | |
| 99 | // ⛔ RELEASING NEEDS THE KEY AGAIN, and the name alone does not contain it. `name@host` says which | |
| 100 | // bridge in human terms; connecting needs the 64-hex key, and asking a person to paste it a second | |
| 101 | // time — to perform the one irreversible act in this flow — is how the wrong bridge gets asked. | |
| 102 | // | |
| 103 | // ⚠ LOCAL, not in `reach`. `reach` says where this identity PUBLISHES, which is a statement about | |
| 104 | // the identity and replicates with it. Which key this node dialled to arrange that is operational | |
| 105 | // detail: another device holding the same identity would resolve the same bridge by its own record. | |
| 106 | ||
| 107 | import { join } from '#node:path' | |
| 108 | import fs from '#node:fs' | |
| 109 | ||
| 110 | const bridgesPath = (storage) => join(storage, 'bridges.json') | |
| 111 | ||
| 112 | const readBridges = (storage) => { | |
| 113 | try { return JSON.parse(fs.readFileSync(bridgesPath(storage), 'utf8')) } catch { return {} } | |
| 114 | } | |
| 115 | ||
| 116 | /** Remember that `acct` is carried by `bridge`. Best-effort: losing this costs a re-paste, not a name. */ | |
| 117 | export function rememberBridge (storage, acct, bridge) { | |
| 118 | if (!acct || !bridge) return | |
| 119 | const all = readBridges(storage) | |
| 120 | all[acct] = String(bridge).replace(/^bridge:/, '').toLowerCase() | |
| 121 | try { fs.writeFileSync(bridgesPath(storage), JSON.stringify(all, null, 2) + '\n') } catch {} | |
| 122 | } | |
| 123 | ||
| 124 | /** Which bridge carries `acct`, or null. */ | |
| 125 | export function bridgeFor (storage, acct) { | |
| 126 | return (acct && readBridges(storage)[acct]) || null | |
| 127 | } | |
| 128 | ||
| 129 | export function forgetBridge (storage, acct) { | |
| 130 | const all = readBridges(storage) | |
| 131 | if (!(acct in all)) return | |
| 132 | delete all[acct] | |
| 133 | try { fs.writeFileSync(bridgesPath(storage), JSON.stringify(all, null, 2) + '\n') } catch {} | |
| 134 | } |
| … | … | @@ -410,17 +410,32 @@ export async function dispatch (node, cmd, a) { |
| 410 | 410 | const r = await rpc.request('claim', req) |
| 411 | 411 | // The bridge answers in HTTP's vocabulary on purpose — it is already the contract a peer reads. |
| 412 | 412 | if (r.status !== 200 && r.status !== 202) throw new Error((r.body && r.body.error) || ('bridge refused: ' + r.status)) |
| 413 | // ⛔ Remember WHICH bridge. Releasing needs the key again and `name@host` does not contain it; | |
| 414 | // asking a person to paste it a second time, for the one irreversible act here, is how the | |
| 415 | // wrong bridge gets asked. | |
| 416 | const { rememberBridge } = await import('./bridge.js') | |
| 417 | rememberBridge(node.storage, r.body && r.body.acct, a.bridge) | |
| 413 | 418 | return { ...r.body, pending: r.status === 202 ? (r.body && r.body.pending) || true : false } |
| 414 | 419 | } |
| 415 | 420 | case 'bridge-release': { |
| 416 | const { connectBridge, buildRelease } = await import('./bridge.js') | |
| 417 | const { rpc } = await connectBridge(node.swarm, a.bridge) | |
| 421 | const { connectBridge, buildRelease, bridgeFor, forgetBridge } = await import('./bridge.js') | |
| 422 | // ⚠ The remembered key wins over one supplied by the caller: the node knows which bridge it | |
| 423 | // actually claimed on, and a release sent to the wrong bridge is a request to burn a name | |
| 424 | // somewhere it was never held. | |
| 425 | const key = bridgeFor(node.storage, a.acct) || a.bridge | |
| 426 | const { rpc } = await connectBridge(node.swarm, key) | |
| 418 | 427 | const { nonce } = await rpc.request('challenge', {}) |
| 419 | 428 | const r = await rpc.request('release', buildRelease({ nonce, username: a.name, root: node.rootHex(), cred: node.cred })) |
| 420 | 429 | if (r.status !== 200) throw new Error((r.body && r.body.error) || ('bridge refused: ' + r.status)) |
| 430 | forgetBridge(node.storage, a.acct) | |
| 421 | 431 | return r.body |
| 422 | 432 | } |
| 423 | 433 | |
| 434 | case 'bridge-which': { | |
| 435 | const { bridgeFor } = await import('./bridge.js') | |
| 436 | return { bridge: bridgeFor(node.storage, a.acct) } | |
| 437 | } | |
| 438 | ||
| 424 | 439 | case 'reach': return peer.reach() |
| 425 | 440 | case 'set-reach': { const r = await peer.setReach(a); await peer.update(); return r } |
| 426 | 441 | // Bytes cross the socket as base64 — it is a local UNIX socket carrying a small image, and a |