// The WIRING of isForeign — which is where the defect actually lived.
//
// test/appview-no-clobber.test.mjs already pins the appview's CONTRACT: for a DID we own, an absent
// record is not authored; for a foreign DID it is. That contract was right the whole time. What was
// wrong was the predicate bin/favela-web.js handed it: `(did) => !ROSTER.includes(did)`, which answers
// "is this DID on our roster" when the guard needs "does the local hpp hold it".
//
// Those two sets are identical on tabertron and different on ashatron, so the bug was invisible on the
// node it was developed on and total on the other: ashatron's appview refused every wj62 record as
// "hpp owns these" when its hpp hosts only e2jev, and froze at 2026-07-31T18:49 until 0.3.37.
//
// Run: node test/hosted-dids.test.mjs
import { parseHostedDids, createIsForeign } from '../lib/hosted-dids.js'
const assert = (cond, msg) => { if (!cond) throw new Error('FAIL: ' + msg); console.log(' ok -', msg) }
const WJ62 = 'did:plc:wj62ebbusln6gxo5azo6ogzo'
const E2JEV = 'did:plc:e2jevhuurrhlm36ufk7fklpg'
const WQNW = 'did:plc:wqnwulql7ndcz6xmtvaovl6u'
// The fleet's real roster default (bin/favela-web.js) — the same on BOTH nodes, which is the point.
const ROSTER = [WJ62, E2JEV]
console.log('# parsing hpp config.json')
// Shape taken verbatim from tabertron's ~/.config/hyper-proto-peer/config.json (secrets elided).
const TABERTRON_CFG = JSON.stringify({
swarm: true,
storage: '/home/fleeky/.config/hyper-proto-peer/data',
dids: [{ did: WJ62, primary: true, deviceMnemonic: 'ELIDED' }, { did: WQNW, primary: null, deviceMnemonic: 'ELIDED' }]
})
const ASHATRON_CFG = JSON.stringify({ swarm: true, dids: [{ did: E2JEV, primary: true, deviceMnemonic: 'ELIDED' }] })
assert(JSON.stringify(parseHostedDids(TABERTRON_CFG)) === JSON.stringify([WJ62, WQNW]), 'tabertron hosts wj62 + wqnwulql')
assert(JSON.stringify(parseHostedDids(ASHATRON_CFG)) === JSON.stringify([E2JEV]), 'ashatron hosts e2jev only')
assert(parseHostedDids(TABERTRON_CFG).every((d) => typeof d === 'string' && d.startsWith('did:')), 'only DID strings are taken — never the mnemonics beside them')
console.log('\n# unknown is not the same as empty')
// If "unreadable" collapsed to "hosts nothing", EVERY did would become foreign and the appview would be
// free to author bytes over hpp's pointers — precisely the clobber d78f2e4 closed. It must stay null.
assert(parseHostedDids('') === null, 'empty text -> unknown')
assert(parseHostedDids('not json at all') === null, 'malformed JSON -> unknown')
assert(parseHostedDids('{"swarm":true}') === null, 'no dids array -> unknown')
assert(parseHostedDids('{"dids":[]}') === null, 'an empty dids array is malformed, not an empty fleet')
assert(parseHostedDids('{"dids":[{"primary":true}]}') === null, 'entries without a did string -> unknown')
assert(parseHostedDids(null) === null, 'a non-string (unreadable file) -> unknown')
console.log('\n# ⛔ THE REGRESSION: ashatron rosters a DID it does not host')
const ashaForeign = createIsForeign({ hosted: parseHostedDids(ASHATRON_CFG), roster: ROSTER })
assert(ashaForeign(WJ62) === true, 'wj62 is FOREIGN on ashatron — no local hpp holds it, so we may author its bytes')
assert(ashaForeign(E2JEV) === false, 'e2jev is OURS on ashatron — its hpp hosts it, so hpp authors those bytes')
// The old expression, kept as the control: it is what froze the node.
const oldPredicate = (did) => !ROSTER.includes(did)
assert(oldPredicate(WJ62) === false, 'CONTROL: the pre-0.3.37 predicate called wj62 ours on ashatron — the freeze')
assert(ashaForeign(WJ62) !== oldPredicate(WJ62), 'and the fix inverts exactly that answer')
console.log('\n# tabertron is unchanged for the DID it actually hosts')
const tabForeign = createIsForeign({ hosted: parseHostedDids(TABERTRON_CFG), roster: ROSTER })
assert(tabForeign(WJ62) === false, 'wj62 stays OURS on tabertron — hpp hosts it (no behaviour change where it worked)')
assert(tabForeign(WQNW) === false, 'wqnwulql is OURS on tabertron too, though it is NOT on the roster')
assert(tabForeign(E2JEV) === true, 'e2jev becomes foreign on tabertron — the same bug, mirrored; ashatron holds it')
assert(tabForeign('did:plc:stranger') === true, 'an unrelated peer is foreign, as before')
console.log('\n# UNKNOWN falls back to the roster — the conservative direction')
// Roster ⊇ hosted, so the fallback can only refuse MORE writes. It must never permit one the old code
// refused: that is the difference between a stale index and an unresolvable pointer.
const unknown = createIsForeign({ hosted: null, roster: ROSTER })
for (const did of [WJ62, E2JEV, 'did:plc:stranger']) {
assert(unknown(did) === oldPredicate(did), 'unknown reproduces the old answer exactly for ' + did.slice(0, 16))
}
const hostedUnknown = createIsForeign({ hosted: parseHostedDids('{"dids":[]}'), roster: ROSTER })
assert(hostedUnknown(WJ62) === false, 'a malformed dids array does NOT widen write permission')
console.log('\n# degenerate callers cannot silently unlock authorship')
assert(createIsForeign()('did:plc:anyone') === true, 'no config at all: everything foreign, matching the appview default')
assert(createIsForeign({ hosted: [WJ62] })(WJ62) === false, 'hosted alone is enough — roster is only the fallback')
console.log('\nall hosted-dids assertions passed')