// Slice 3 (ban guard) — the real networked proof (atproto-base-REVOCATION §3/§6, Keet
// handleRemoveMember + add-guard). Over real StateBaseV3 replication on a testnet: __members__ is
// projected by the CREATOR (lowest-key member = the TOFU bootstrap indexer) so admission is
// actually attestation-gated; a peer is admitted + writing; the creator bans it; afterwards the
// banned device is recorded in the durable __removed__ set, removeWriter'd, and its NEW writes no
// longer land (and it cannot re-admit — the guard rejects a removed identity).
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import { StateBaseV3 } from 'atproto-room/lib/state-base.js'
import { SwarmDevice } from '../lib/swarm-device.js'
import { makeAttest, makeVerifyAttest, attestDeviceChain, deviceKeetKeyPair, rootIdentityPublicKey } from 'atproto-room/lib/membership.js'
import { deriveAtprotoKey } from 'atproto-identity'
const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVA = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const DEVB = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const DID = 'did:plc:swarmban2222222222222222'
const POST = 'app.bsky.feed.post'
const signKey = deriveAtprotoKey(ROOT)
const rec = (text) => ({ $type: POST, text, createdAt: '2026-07-04T00:00:00.000Z' })
const tmp = () => { const d = '/tmp/hpp-ban-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
test('the creator bans a peer: removeWriter + durable __removed__ + post-ban writes do not land', async (t) => {
// Networked 2-device replication test — allow generous headroom over brittle's 30s default for
// the attestation-admission + ban-propagation round trips (the post-ban writes now fail FAST via
// the banned-device guard in requestWriter, so this is headroom, not a hang mask).
t.timeout(90000)
const testnet = await createTestnet(3)
const rootPub = await rootIdentityPublicKey(ROOT)
const devA = await deviceKeetKeyPair(DEVA); const proofA = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: devA.publicKey })
const devB = await deviceKeetKeyPair(DEVB); const proofB = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: devB.publicKey })
const idA = b4a.toString(devA.publicKey, 'hex'); const idB = b4a.toString(devB.publicKey, 'hex')
const members = [{ key: idA, role: 1 }, { key: idB, role: 1 }]
const roomSeed = crypto.randomBytes(32); const roomKey = crypto.randomBytes(32); const encryptionKey = crypto.randomBytes(32)
const mk = (dev, proof) => new StateBaseV3(tmp(), roomKey, {
roomSeed, encryptionKey, swarmOpts: { bootstrap: testnet.bootstrap },
selfKey: dev.publicKey, attest: makeAttest(dev, proof), verifyAttest: makeVerifyAttest(rootPub),
getMembers: async () => members
})
const A = mk(devA, proofA); await A.ready()
const B = mk(devB, proofB); await B.ready()
t.teardown(async () => { try { await A.close(); await B.close() } catch {} ; await testnet.destroy() })
const deviceA = new SwarmDevice(A, { did: DID, privateKey: signKey.privateKey, self: idA })
const deviceB = new SwarmDevice(B, { did: DID, privateKey: signKey.privateKey, self: idB })
// TOFU origin: the lowest-key member is the creator/bootstrap indexer → the banner. Project on
// both (only the creator persists __members__; it replicates to the peer).
const creatorIsA = [idA, idB].sort()[0] === idA
const banner = creatorIsA ? deviceA : deviceB
const banned = creatorIsA ? deviceB : deviceA
const bannedSb = creatorIsA ? B : A
const bannedId = creatorIsA ? idB : idA
let projected = false
for (let i = 0; i < 20 && !projected; i++) {
try { await A.projectMembers(members) } catch {}; try { await B.projectMembers(members) } catch {}
const m = await banner.sb.get('__members__').catch(() => null)
projected = !!(m && m[idA] === 1 && m[idB] === 1)
if (!projected) await sleep(500)
}
t.ok(projected, 'creator projected __members__ (admission is attestation-gated)')
// the peer (banned) gets admitted + writes a record the banner can see.
let admitted = false
for (let i = 0; i < 40 && !admitted; i++) {
try { await banned.putRecord(POST, '3peer', rec('from peer pre-ban')) } catch {}
await bannedSb.set('__ping__', { i }).catch(() => {})
await sleep(1500)
admitted = await bannedSb.effectiveWritable().catch(() => false)
}
t.ok(admitted, 'peer admitted as a writer via attestation')
let seen = null
for (let i = 0; i < 30 && !seen; i++) { await banner.sb.base.update().catch(() => {}); seen = await banner.repo.getRecord(POST, '3peer'); if (!seen) await sleep(1000) }
t.ok(seen && seen.text === 'from peer pre-ban', 'banner sees the peer\'s record (peer can write pre-ban)')
// the creator bans the peer.
await banner.banDevice(bannedId)
let isBanned = false
for (let i = 0; i < 40 && !isBanned; i++) {
await banner.sb.base.update().catch(() => {}); await bannedSb.base.update().catch(() => {})
isBanned = !!(await banner.removed())[bannedId]
if (!isBanned) await sleep(1000)
}
t.ok(isBanned, 'peer recorded in the durable __removed__ set')
// the peer is removeWriter'd → a NEW write no longer lands (and it can't re-admit).
for (let i = 0; i < 8; i++) { try { await banned.putRecord(POST, '3afterban', rec('SHOULD NOT LAND')) } catch {}; await bannedSb.set('__ping__', { z: i }).catch(() => {}); await sleep(1000) }
for (let i = 0; i < 8; i++) { await banner.sb.base.update().catch(() => {}); await sleep(500) }
const leaked = await banner.repo.getRecord(POST, '3afterban')
t.absent(leaked, 'peer\'s post-ban write does NOT land (removeWriter + admission guard)')
})