// The feed's apply — deterministic, total, and the only place authorisation happens.
//
// A feed is ONE identity's log. Devices are plural; the member is singular and is the feed itself.
// So the authorisation rule is small enough to state in a sentence: an op counts if the device that
// wrote it is in this feed's device roster. Nothing else grows the writer set — which is the
// property that keeps a popular feed from turning every interactor into a hypercore.
//
// apply() runs inside autobase, so it is REPLAYED on every boot and every reorg. It must therefore
// be deterministic (no clock, no randomness, no external lookup) and TOTAL (never throw — a throw
// mid-replay would wedge the base). A rejected op is IGNORED, deterministically, by every peer.
import test from 'brittle'
import { proofsFor, identity } from './helpers.js'
import { applyOp, isRosterKey, ROSTER, FOUNDER } 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 D1 = 'd1'.repeat(32)
const D2 = 'd2'.repeat(32)
const D3 = 'd3'.repeat(32)
const ptr = (n) => ({ $oid: 'oid' + n, size: n })
const put = (collection, name, n) => ({ t: 'put', collection, name, ...ptr(n) })
const del = (collection, name) => ({ t: 'del', collection, name })
// One owner's devices. Proofs are minted once, so every call site stays synchronous.
const PROOF = await proofsFor([D1, D2, D3])
const addDevice = (device) => ({ t: 'add-device', device, proof: PROOF[device] })
// Run a list of [device, op] pairs through apply in order, as autobase would.
const run = async (ops, view = kv()) => {
for (const [device, op] of ops) await applyOp(view, op, { device })
return view
}
test('the first device bootstraps the roster', async (t) => {
const v = await run([[D1, addDevice(D1)]])
t.ok(await v.get(ROSTER + '/' + D1), 'the founding device admits itself')
t.alike(await v.get(FOUNDER), { device: D1, identityRoot: PROOF.__root },
'genesis is explicit in the view AND binds the owner — the root is what makes it this feed')
})
test('a rostered device can write', async (t) => {
const v = await run([[D1, addDevice(D1)], [D1, put('post', 'a1', 3)]])
t.alike(await v.get('post/a1'), ptr(3))
})
test('⛔ an op from an unknown device is IGNORED, not thrown', async (t) => {
const v = await run([[D1, addDevice(D1)], [D2, put('post', 'a1', 3)]])
t.is(await v.get('post/a1'), null, 'the write did not land')
t.is(v.m.size, 2, 'only the roster entry and the genesis marker exist')
})
// ⛔ THIS TEST'S PREMISE CHANGED, and the change is the point of attestation.
//
// It used to assert "admission requires already being admitted". That rule is gone: a proof is the
// authorisation, so WHO carries the op is irrelevant. D2 is not on the roster and D3 still gets in,
// because D3's proof says D3 belongs to this feed's owner and nothing else was ever being asked.
//
// Proofs are public — they sit in the log — so anyone can replay one. That is harmless by
// construction: replaying a proof can only re-assert a fact the owner already signed, revocation is
// checked BEFORE the proof so a removed device stays removed, and re-admitting an admitted device
// is a no-op. What a replayed proof can never do is admit a device its owner never attested.
test('a proof is the authorisation — who carries the op does not matter', async (t) => {
const v = await run([[D1, addDevice(D1)], [D2, addDevice(D3)]])
t.ok(await v.get(ROSTER + '/' + D3), 'D2 is not admitted, and D3 still gets in on its own proof')
t.is(await v.get(ROSTER + '/' + D2), null, 'carrying it admitted nothing for D2 itself')
})
test('a rostered device admits another, which can then write', async (t) => {
const v = await run([
[D1, addDevice(D1)],
[D1, addDevice(D2)],
[D2, put('post', 'a1', 5)]
])
t.ok(await v.get(ROSTER + '/' + D2), 'admitted')
t.alike(await v.get('post/a1'), ptr(5), 'and its write counts')
})
test('del writes a tombstone, never a removal', async (t) => {
const v = await run([[D1, addDevice(D1)], [D1, put('post', 'a1', 3)], [D1, del('post', 'a1')]])
t.alike(await v.get('post/a1'), { $deleted: true }, 'absence stays ambiguous; a tombstone does not')
})
test('⛔ an op cannot write into the reserved namespace', async (t) => {
const v = await run([[D1, addDevice(D1)], [D1, put(ROSTER, D3, 1)], [D1, put('__x__', 'y', 1)]])
t.is(await v.get(ROSTER + '/' + D3), null, 'a put cannot forge a roster entry')
t.is(await v.get('__x__/y'), null, 'nor any other control key')
t.ok(isRosterKey(ROSTER + '/' + D1), 'roster keys are recognisable')
})
test('⛔ a malformed op changes nothing and does not throw', async (t) => {
const v = kv()
for (const bad of [null, undefined, {}, { t: 'nope' }, { t: 'put' }, { t: 'put', collection: 'a' },
{ t: 'put', collection: 'a/b', name: 'c', $oid: 'x', size: 1 }, { t: 'add-device' }]) {
await applyOp(v, bad, { device: D1 })
}
t.is(v.m.size, 0, 'apply is total — every malformed op is a no-op')
})
test('⛔ a put with a non-pointer payload is refused', async (t) => {
const v = await run([[D1, addDevice(D1)],
[D1, { t: 'put', collection: 'post', name: 'a1', body: 'inline' }],
[D1, { t: 'put', collection: 'post', name: 'a2', $oid: 'x' }]])
t.is(await v.get('post/a1'), null, 'a record body is not a pointer')
t.is(await v.get('post/a2'), null, 'a size is required')
})
test('★ replay is deterministic — the same ops rebuild the same view', async (t) => {
const ops = [
[D1, addDevice(D1)], [D1, put('post', 'b', 2)], [D1, addDevice(D2)],
[D2, put('post', 'a', 1)], [D2, del('post', 'b')], [D3, put('post', 'c', 9)]
]
t.alike((await run(ops)).entries(), (await run(ops)).entries(), 'replayed from scratch, byte for byte')
})
test('★ order matters and is the ONLY thing that matters', async (t) => {
// D2's write before its admission is dropped; after, it lands. Same ops, different order.
const before = await run([[D1, addDevice(D1)], [D2, put('post', 'a', 1)], [D1, addDevice(D2)]])
const after = await run([[D1, addDevice(D1)], [D1, addDevice(D2)], [D2, put('post', 'a', 1)]])
t.is(await before.get('post/a'), null, 'a write before admission does not retroactively count')
t.alike(await after.get('post/a'), ptr(1), 'the same write after admission does')
})