// ⛔ A knock proves WHO is knocking: an identity root, never a bare feed key.
//
// THE DEFECT (hyper-social #77 §1, 2026-10-12). `applyKnock` recorded `__knocks__/<op.feed>` for
// whatever feed the op named, and `applyListFeed` read that row as the named feed's CONSENT to be
// listed. Nothing tied the device that wrote the knock to the feed it named, so anyone holding a
// registry's log could knock "as" any feed and get it listed without its owner ever asking. The
// comment on applyListFeed claimed the opposite ("the knock is written by the knocker's own device").
//
// Why the fix names a ROOT and not a feed: inside apply, this log cannot read another feed's log, so
// "this device belongs to feed X" cannot be proven here. "This device chains to identity root R" can,
// from the proof's own bytes, exactly as add-device does. A feed key is a locator; the identity root is
// who (favela-markdown `uri.js`: the authority is the identity root). So the knock is keyed by the
// proven root, a listing records that root, and a READER checks the listed feed's founder against it
// (Peer.directory).
import test from 'brittle'
import { identity } from './helpers.js'
import { applyOp, isRosterKey, KNOCKS, LISTING, LISTING_ROOT } from '../lib/feed.js'
import { proofsFor } from './helpers.js'
const kv = () => {
const m = new Map()
return {
m,
async set (k, v) { m.set(k, v) },
async get (k) { return m.has(k) ? m.get(k) : null },
entries () { return [...m.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) }
}
}
const OWNER = 'd1'.repeat(32) // the registry's own device
const STRANGER = 'ff'.repeat(32) // the knocking WRITER core, on the registry's log
const OTHER_WRITER = 'ee'.repeat(32)
const THEIR_FEED = 'ab'.repeat(32)
const VICTIM_FEED = 'cd'.repeat(32)
const PROOF = await proofsFor([OWNER])
const STRANGER_ID = await identity()
const STRANGER_DEVICE = await STRANGER_ID.device()
const STRANGER_ROOT = STRANGER_ID.rootHex
const addDevice = (device) => ({ t: 'add-device', device, proof: PROOF[device] })
const run = async (ops, view = kv()) => {
for (const [device, op] of ops) {
const genesis = !view.entries().some(([k]) => isRosterKey(k))
await applyOp(view, op, { device, genesis })
}
return view
}
const knocksIn = (v) => v.entries().filter(([k]) => k.startsWith(KNOCKS))
test('⛔ an unproven knock records nothing', async (t) => {
const v = await run([[OWNER, addDevice(OWNER)], [STRANGER, { t: 'knock', feed: VICTIM_FEED }]])
t.alike(knocksIn(v), [], 'no proof, no knock — a bare feed key is not consent from that feed')
})
test('a proven knock is keyed by the knocker\'s identity root and names its feed', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[STRANGER, { t: 'knock', feed: THEIR_FEED, proof: STRANGER_DEVICE.bind(STRANGER) }]
])
t.alike(await v.get(KNOCKS + '/' + STRANGER_ROOT), { device: STRANGER, feed: THEIR_FEED },
'the row is the proven root: who knocked, with the feed they asked about')
})
test('⛔ a proof bound to a different writer is refused', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[STRANGER, { t: 'knock', feed: THEIR_FEED, proof: STRANGER_DEVICE.bind(OTHER_WRITER) }]
])
t.alike(knocksIn(v), [], 'a proof is never a bearer token for another core')
})
test('knocking twice is one row per identity, the latest feed named', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[STRANGER, { t: 'knock', feed: THEIR_FEED, proof: STRANGER_DEVICE.bind(STRANGER) }],
[STRANGER, { t: 'knock', feed: THEIR_FEED, proof: STRANGER_DEVICE.bind(STRANGER) }]
])
t.is(knocksIn(v).length, 1, 'keyed by root, so a flood is one row')
})
test('⛔ a listing needs a proven knock from that root naming that same feed', async (t) => {
const knock = [STRANGER, { t: 'knock', feed: THEIR_FEED, proof: STRANGER_DEVICE.bind(STRANGER) }]
const list = (feed) => [OWNER, { t: 'list-feed', root: STRANGER_ROOT, feed, $oid: 'o', size: 1 }]
const wrongFeed = await run([[OWNER, addDevice(OWNER)], knock, list(VICTIM_FEED)])
t.is(await wrongFeed.get(LISTING + '/' + VICTIM_FEED), null, 'the root asked about ITS feed, not this one')
const ok = await run([[OWNER, addDevice(OWNER)], knock, list(THEIR_FEED)])
t.alike(await ok.get(LISTING + '/' + THEIR_FEED), { $oid: 'o', size: 1 }, 'listed: the pointer stays exactly { $oid, size }')
t.alike(await ok.get(LISTING_ROOT + '/' + THEIR_FEED), { root: STRANGER_ROOT },
'and the root is recorded beside it, so a reader can check the feed\'s founder')
})
test('⛔ the old spoof: a feed listed on a knock it never wrote', async (t) => {
// The exact attack: knock naming the victim's feed, then a registry lists it as consent.
const v = await run([
[OWNER, addDevice(OWNER)],
[STRANGER, { t: 'knock', feed: VICTIM_FEED }],
[OWNER, { t: 'list-feed', feed: VICTIM_FEED, $oid: 'o', size: 1 }]
])
t.is(await v.get(LISTING + '/' + VICTIM_FEED), null, 'refused: nobody proved they own the victim\'s feed')
})
test('dismissing is by root, and needs admission', async (t) => {
const knock = [STRANGER, { t: 'knock', feed: THEIR_FEED, proof: STRANGER_DEVICE.bind(STRANGER) }]
const v = await run([[OWNER, addDevice(OWNER)], knock, [STRANGER, { t: 'dismiss-knock', root: STRANGER_ROOT }]])
t.ok(await v.get(KNOCKS + '/' + STRANGER_ROOT), 'a stranger cannot clear a knock')
await applyOp(v, { t: 'dismiss-knock', root: STRANGER_ROOT }, { device: OWNER, genesis: false })
t.alike(await v.get(KNOCKS + '/' + STRANGER_ROOT), { $deleted: true }, 'the owner can')
})