// The registry — a feed whose records are listings of other feeds. No new primitive.
//
// ★ Consent is STRUCTURAL, not conventional. A listing may only be written for a feed that a live,
// PROVEN knock in this same registry named, and the knock proves which identity root wrote it
// (feed.js applyKnock; knock-identity.test.js). So "asking to be listed" and "being listed" are two
// acts by two parties, and apply enforces it rather than a convention someone has to remember. A
// reader then checks the listed feed's founder against the granted root (registry-peer.test.js).
//
// Two honesty limits, both from hyper-social #61 §8 and neither fixable here:
// Delisting stops propagation; it does not retract. The listing already replicated.
// Unlisted is not private. A feed announces on a topic derived from its key, so anyone holding
// the key finds it regardless of any directory. A registry is a phone book, not a gate.
import test from 'brittle'
import { proofsFor } from './helpers.js'
import { applyOp, isRosterKey, KNOCKS, LISTING_ROOT } from '../lib/feed.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)
const OTHER = 'd2'.repeat(32)
const FEED_A = 'aa'.repeat(32)
const FEED_B = 'bb'.repeat(32)
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
}
// One owner's devices. Minted once so every call site stays synchronous.
const PROOF = await proofsFor([OWNER, OTHER, FEED_A, FEED_B])
const addDevice = (d) => ({ t: 'add-device', device: d, proof: PROOF[d] })
// OTHER is the knocking writer core; its proof chains to ROOT, so the knock is keyed by ROOT.
const ROOT = PROOF.__root
const knock = (feed) => ({ t: 'knock', feed, proof: PROOF[OTHER] })
const listFeed = (feed) => ({ t: 'list-feed', root: ROOT, feed, $oid: 'oid-' + feed, size: 10 })
const delist = (feed) => ({ t: 'delist', feed })
test('a knock then a listing — two parties, two acts', async (t) => {
const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)], [OWNER, listFeed(FEED_A)]])
t.alike(await v.get('listing/' + FEED_A), { $oid: 'oid-' + FEED_A, size: 10 },
'the listing is an ordinary pointer, in an ordinary collection')
t.alike(await v.get(LISTING_ROOT + '/' + FEED_A), { root: ROOT }, 'and the root it was granted to sits beside it')
})
test('⛔ a listing with NO knock is refused — consent is not optional', async (t) => {
const v = await run([[OWNER, addDevice(OWNER)], [OWNER, listFeed(FEED_A)]])
t.is(await v.get('listing/' + FEED_A), null, 'the registry cannot list a feed that never asked')
})
test('⛔ a knock alone does not list you', async (t) => {
const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)]])
t.is(await v.get('listing/' + FEED_A), null, 'asking is not being listed')
t.ok(await v.get(KNOCKS + '/' + ROOT), 'the request is waiting, keyed by the identity that asked')
})
test('⛔ a stranger cannot list itself even after knocking', async (t) => {
const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)], [OTHER, listFeed(FEED_A)]])
t.is(await v.get('listing/' + FEED_A), null, 'writing the listing needs an admitted device')
})
test('a dismissed knock cannot be used to list', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[OTHER, knock(FEED_A)],
[OWNER, { t: 'dismiss-knock', root: ROOT }],
[OWNER, listFeed(FEED_A)]
])
t.is(await v.get('listing/' + FEED_A), null, 'consent withdrawn before use is consent refused')
})
test('delisting tombstones the listing', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[OTHER, knock(FEED_A)],
[OWNER, listFeed(FEED_A)],
[OWNER, delist(FEED_A)]
])
t.alike(await v.get('listing/' + FEED_A), { $deleted: true },
'positive evidence — it stops propagating, it does not retract')
})
test('⛔ a stranger cannot delist', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[OTHER, knock(FEED_A)],
[OWNER, listFeed(FEED_A)],
[OTHER, delist(FEED_A)]
])
t.alike(await v.get('listing/' + FEED_A), { $oid: 'oid-' + FEED_A, size: 10 }, 'still listed')
})
test('a registry holds many listings, each independent', async (t) => {
const v = await run([
[OWNER, addDevice(OWNER)],
[OTHER, knock(FEED_A)], [OWNER, listFeed(FEED_A)],
[OTHER, knock(FEED_B)], [OWNER, listFeed(FEED_B)],
[OWNER, delist(FEED_A)]
])
t.alike(await v.get('listing/' + FEED_A), { $deleted: true })
t.alike(await v.get('listing/' + FEED_B), { $oid: 'oid-' + FEED_B, size: 10 })
})
test('a malformed listing writes nothing and does not throw', async (t) => {
const v = await run([[OWNER, addDevice(OWNER)], [OTHER, knock(FEED_A)],
// A valid root on each, so these test the missing pointer / feed they were written for.
[OWNER, { t: 'list-feed', root: ROOT, feed: FEED_A }],
[OWNER, { t: 'list-feed', root: ROOT, $oid: 'x', size: 1 }],
[OWNER, { t: 'list-feed', root: ROOT, feed: 'not/a/segment', $oid: 'x', size: 1 }]])
t.is(await v.get('listing/' + FEED_A), null, 'a listing without a pointer is not a listing')
})