OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 52256e1

the node remembers which bridge carries it
Releasing needs the key again and `name@host` does not contain it. Asking a person to paste 64 hex characters a second time — to perform the one irreversible act in this flow — is how the wrong bridge gets asked to burn a name. Local, not in `reach`: reach says where this identity PUBLISHES, which is a statement about the identity and replicates with it. Which key this node dialled is operational detail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52256e1c60c5e549d62561c43bcfd0f3ec49db7e
by plankatron <fleeky@gmail.com> · 2026-09-11T16:25:05.000Z

Changed files (2) +56 -2

lib/bridge.js +39 -0
……@@ -93,3 +93,42 @@ export function buildRelease ({ nonce, username, root, cred }) {
9393}
9494
9595export 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
107import { join } from '#node:path'
108import fs from '#node:fs'
109
110const bridgesPath = (storage) => join(storage, 'bridges.json')
111
112const 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. */
117export 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. */
125export function bridgeFor (storage, acct) {
126 return (acct && readBridges(storage)[acct]) || null
127}
128
129export 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}
lib/rpc.js +17 -2
……@@ -410,17 +410,32 @@ export async function dispatch (node, cmd, a) {
410410 const r = await rpc.request('claim', req)
411411 // The bridge answers in HTTP's vocabulary on purpose — it is already the contract a peer reads.
412412 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)
413418 return { ...r.body, pending: r.status === 202 ? (r.body && r.body.pending) || true : false }
414419 }
415420 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)
418427 const { nonce } = await rpc.request('challenge', {})
419428 const r = await rpc.request('release', buildRelease({ nonce, username: a.name, root: node.rootHex(), cred: node.cred }))
420429 if (r.status !== 200) throw new Error((r.body && r.body.error) || ('bridge refused: ' + r.status))
430 forgetBridge(node.storage, a.acct)
421431 return r.body
422432 }
423433
434 case 'bridge-which': {
435 const { bridgeFor } = await import('./bridge.js')
436 return { bridge: bridgeFor(node.storage, a.acct) }
437 }
438
424439 case 'reach': return peer.reach()
425440 case 'set-reach': { const r = await peer.setReach(a); await peer.update(); return r }
426441 // Bytes cross the socket as base64 — it is a local UNIX socket carrying a small image, and a