// The swarm door onto registration — the same room as the HTTP one.
//
// ⛔ WHY IT EXISTS. favela-peer has no outbound HTTP (measured: no bare-fetch, no bare-https, nothing
// in lib/), so a peer asking a bridge for a name cannot use the HTTP route. Both already speak the
// swarm. What must NOT happen is a second copy of the rules, so these assert that this door reaches
// the same `admit.js` and refuses exactly what the other door refuses.
import test from 'brittle'
import b4a from 'b4a'
import IdentityKey from 'keet-identity-key'
import crypto from 'hypercore-crypto'
import { signAttested } from 'favela-identity/lib/attestation.js'
import { MemoryRegistrations } from '../lib/registrations.js'
import { Admission } from '../lib/admission.js'
import { registerHandlers } from '../lib/register-rpc.js'
const FEED = 'f'.repeat(64)
async function identity () {
const idk = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
const kp = crypto.keyPair()
const proof = await IdentityKey.bootstrap({ identity: idk.identityKeyPair }, kp.publicKey)
return { rootHex: b4a.toString(idk.identityKeyPair.publicKey, 'hex'), cred: { keyPair: kp, proof: b4a.toString(proof, 'base64') } }
}
const deps = (policy = 'open', extra = {}) => ({
registrations: new MemoryRegistrations({ policy }),
admission: new Admission(),
config: { host: 'ap.7rnx.net' },
...extra
})
test('★ a peer asks the policy before trying, rather than guessing from a refusal', async (t) => {
const h = registerHandlers(deps('invite'))
const p = await h.policy()
t.is(p.policy, 'invite')
t.is(p.host, 'ap.7rnx.net')
t.ok(p.proof, 'and that this bridge requires a proof')
})
test('★★ the whole ceremony over the swarm door: challenge, sign, claim', async (t) => {
const d = deps('open')
const h = registerHandlers(d)
const me = await identity()
const { nonce } = await h.challenge()
const proof = signAttested(d.admission.claimBytes({ nonce, username: 'alice', feedKey: FEED }), me.cred)
const r = await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED, nonce, proof })
t.is(r.status, 200)
t.is(r.body.acct, 'alice@ap.7rnx.net', 'the name a person actually types')
t.is(r.body.actor, 'https://ap.7rnx.net/ap/users/alice/actor')
t.is((await d.registrations.get('alice')).root, me.rootHex)
})
test('⛔ it refuses exactly what the HTTP door refuses', async (t) => {
const d = deps('open')
const h = registerHandlers(d)
const me = await identity()
const other = await identity()
t.is((await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED })).status, 403, 'no proof')
const { nonce } = await h.challenge()
const proof = signAttested(d.admission.claimBytes({ nonce, username: 'alice', feedKey: FEED }), other.cred)
const r = await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED, nonce, proof })
t.is(r.status, 403, 'a proof from somebody else')
t.is(r.body.error, 'proof does not attest that identity')
t.is(await d.registrations.get('alice'), null, 'and nothing was registered')
})
test('⛔ the feed is held at claim time, so a name is a served actor not a row in a file', async (t) => {
const held = []
const d = deps('open', { onRegistered: async (u) => { held.push(u) } })
const h = registerHandlers(d)
const me = await identity()
const { nonce } = await h.challenge()
const proof = signAttested(d.admission.claimBytes({ nonce, username: 'alice', feedKey: FEED }), me.cred)
t.is((await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED, nonce, proof })).status, 200)
t.is(held.length, 1)
t.is(held[0].feedKey, FEED, 'the bridge was told which feed to hold')
})
test('⚠ a name claimed but not servable says so, rather than promising an actor that is dead', async (t) => {
const d = deps('open', { onRegistered: async () => { throw new Error('swarm unreachable') } })
const h = registerHandlers(d)
const me = await identity()
const { nonce } = await h.challenge()
const proof = signAttested(d.admission.claimBytes({ nonce, username: 'alice', feedKey: FEED }), me.cred)
const r = await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED, nonce, proof })
t.is(r.status, 202, 'claimed, and honest that it is not live')
t.ok(/swarm unreachable/.test(r.body.pending), r.body.pending)
t.ok(await d.registrations.get('alice'), 'the name IS held — releasing it would lose what the peer was told it had')
})
test('★ releasing over the same door needs the same proof', async (t) => {
const d = deps('open')
const h = registerHandlers(d)
const me = await identity()
let { nonce } = await h.challenge()
let proof = signAttested(d.admission.claimBytes({ nonce, username: 'alice', feedKey: FEED }), me.cred)
await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED, nonce, proof })
t.is((await h.release({ username: 'alice', root: me.rootHex })).status, 403, 'unproven cannot burn it')
t.ok(await d.registrations.get('alice'), 'still held')
;({ nonce } = await h.challenge())
proof = signAttested(d.admission.releaseBytes({ nonce, username: 'alice' }), me.cred)
t.is((await h.release({ username: 'alice', root: me.rootHex, nonce, proof })).status, 200, 'the holder can')
})
// ── the claim reply names the user's observation feed (hyper-social #82, I1 S3) ─────────────────────────────
//
// ⛔ SIGNED BY THE BRIDGE KEY (operator decision O-2 b). The peer pins the observer root it is handed only because
// it arrives signed by the swarm key it claimed against. And a RE-CLAIM returns it too — that is how a peer that
// registered before observation feeds existed (fleeky, ashatron) learns its feed.
test('★★ the claim reply carries a signed observer grant; a re-claim returns it again', async (t) => {
const { verifyObserver } = await import('favela-identity/lib/observer-grant.js')
const bridge = crypto.keyPair()
const OBS = '0b'.repeat(32)
const ROOT = 'e4'.repeat(32)
const d = deps('open', {
onRegistered: async () => {},
observerFor: async ({ username, generation }) => ({ username, generation, observationFeed: OBS, observerRoot: ROOT }),
bridgeKeyPair: bridge
})
const h = registerHandlers(d)
const me = await identity()
const claim = async () => {
const { nonce } = await h.challenge()
const proof = signAttested(d.admission.claimBytes({ nonce, username: 'fleeky', feedKey: FEED }), me.cred)
return h.claim({ username: 'fleeky', root: me.rootHex, feedKey: FEED, nonce, proof })
}
const r = await claim()
t.is(r.status, 200)
const o = r.body.observer
t.ok(o, 'the reply carries an observer grant')
t.alike({ observationFeed: o.observationFeed, observerRoot: o.observerRoot }, { observationFeed: OBS, observerRoot: ROOT })
t.is(verifyObserver({ username: 'fleeky', generation: r.body.generation, observationFeed: o.observationFeed, observerRoot: o.observerRoot }, o.sig, b4a.toString(bridge.publicKey, 'hex')), true, 'verifiable against the bridge key')
const again = await claim()
t.is(again.status, 200, 'a re-claim by the same holder is not "pending"')
t.is(again.body.observer && again.body.observer.observationFeed, OBS, 'and returns the grant again')
})
test('no observer configured: the reply is unchanged (no grant field)', async (t) => {
const d = deps('open', { onRegistered: async () => {} })
const h = registerHandlers(d)
const me = await identity()
const { nonce } = await h.challenge()
const proof = signAttested(d.admission.claimBytes({ nonce, username: 'alice', feedKey: FEED }), me.cred)
const r = await h.claim({ username: 'alice', root: me.rootHex, feedKey: FEED, nonce, proof })
t.is(r.status, 200)
t.absent(r.body.observer)
})
// ⛔ A PEER THAT ASKS LATE ON A LONG-LIVED CONNECTION MUST STILL BE ANSWERED. 2026-10-14: a phone holding fleeky's
// feed stays connected to the bridge for hours (the bridge holds its feeds). The bridge opens its registration
// channel the moment the connection appears; the phone has none yet, so ITS protomux rejects that open
// (_requestSession → _rejectSession) and the bridge's channel closes. When the user later presses "claim", the
// phone's open meets no channel and no pair() handler and is rejected too: "channel closed", every time, until the
// connection drops. A fresh connection that asks at once (every earlier registration) never saw it.
test('★ a client that opens the registration channel LATE, on an already-muxed connection, is answered', async (t) => {
const SecretStream = (await import('@hyperswarm/secret-stream')).default
const Protomux = (await import('protomux')).default
const { attachRegistration, PROTOCOL } = await import('../lib/register-rpc.js')
const ProtomuxRPC = (await import('protomux-rpc')).default
const c = (await import('compact-encoding')).default
const bridge = new SecretStream(true)
const peer = new SecretStream(false)
bridge.rawStream.pipe(peer.rawStream).pipe(bridge.rawStream)
t.teardown(() => { try { bridge.destroy() } catch {} try { peer.destroy() } catch {} })
// As in production: the peer's stream already carries a mux (feed replication), so the bridge's early open is
// processed — and rejected — on arrival.
Protomux.from(peer)
attachRegistration(bridge, registerHandlers(deps()))
await new Promise((resolve) => setTimeout(resolve, 300))
const rpc = new ProtomuxRPC(peer, { protocol: PROTOCOL, valueEncoding: c.json })
const answer = await Promise.race([
rpc.request('policy', {}).then((r) => ({ ok: r }), (e) => ({ err: e.message })),
new Promise((resolve) => setTimeout(() => resolve({ err: 'timeout' }), 3000))
])
t.ok(answer.ok, 'answered (was: ' + (answer.err || 'ok') + ')')
t.is(answer.ok && answer.ok.policy, 'open')
})