// Membership: a device attested to the DID's root can prove itself; verification resolves to
// (root, device) and rejects wrong data / wrong root. This is the admission crypto the swarm uses.
import '../lib/bare-globals.js'
import test from 'brittle'
import b4a from 'b4a'
import { makeAttest, makeVerifyAttest, attestDeviceChain, attestDeviceViaDevice, deviceKeetKeyPair, rootIdentityPublicKey } from '../lib/membership.js'
const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVICE = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const OTHER = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
test('attested device: attest admission bytes → verify resolves to root + device', async (t) => {
const dev = await deviceKeetKeyPair(DEVICE)
const deviceProof = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: dev.publicKey })
const rootPub = await rootIdentityPublicKey(ROOT)
const attest = makeAttest(dev, deviceProof)
const verify = makeVerifyAttest(rootPub)
const signData = b4a.from('localKey||baseKey admission bytes')
const proof = await attest(signData)
t.ok(await verify(proof, signData, dev.publicKey), 'valid attestation verifies to root + device')
t.absent(await verify(proof, b4a.from('tampered bytes'), dev.publicKey), 'wrong signData rejected')
// a DIFFERENT root identity must not accept this device's proof
const wrongRootPub = await rootIdentityPublicKey(OTHER)
const verifyWrong = makeVerifyAttest(wrongRootPub)
t.absent(await verifyWrong(proof, signData, dev.publicKey), 'wrong root identity rejected')
})
test('a device NOT attested by the root cannot prove membership', async (t) => {
// device attested under OTHER root, verified against ROOT → must fail
const dev = await deviceKeetKeyPair(DEVICE)
const proofUnderOther = await attestDeviceChain({ rootMnemonic: OTHER, devicePublicKey: dev.publicKey })
const rootPub = await rootIdentityPublicKey(ROOT)
const attest = makeAttest(dev, proofUnderOther)
const verify = makeVerifyAttest(rootPub)
const signData = b4a.from('admission bytes')
const proof = await attest(signData)
t.absent(await verify(proof, signData, dev.publicKey), 'device attested by a different root is not a member')
})
// ── CHAINED ATTESTATION: an existing device admits the next one, with NO root mnemonic ────────────
// Adding a co-owner to a room needs a proof anchored at the room's ROOT identity, and attestDeviceChain
// needs the root MNEMONIC — which is deliberately never stored on a node. Read alone that makes adding a
// co-owner look impossible without recovering a secret nobody kept (measured: no root mnemonic exists on
// any node in the fleet). It is not: keet's attestDevice lets an ALREADY-ATTESTED device extend the chain,
// and the result passes the IDENTICAL admission gate because it resolves to the same root and epoch.
//
// This is the load-bearing check before provisioning a real co-owner: it exercises the actual admission
// crypto (makeAttest/makeVerifyAttest, exactly what StateBaseV3 injects), not just the library call.
test('a device attested BY ANOTHER DEVICE passes the same admission gate (no root mnemonic)', async (t) => {
const rootPub = await rootIdentityPublicKey(ROOT)
// The existing co-owner: root-attested, holds only its own mnemonic + proof (what a node actually has).
const parent = await deviceKeetKeyPair(DEVICE)
const parentProof = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: parent.publicKey })
// The NEW co-owner generates its own keypair; the parent vouches for it. No root secret involved.
const child = await deviceKeetKeyPair(OTHER)
const childProof = attestDeviceViaDevice({ parentKeyPair: parent, parentProof, devicePublicKey: child.publicKey })
const verify = makeVerifyAttest(rootPub)
const signData = b4a.from('localKey||baseKey admission bytes')
const childAttest = await makeAttest(child, childProof)(signData)
const r = await verify(childAttest, signData, child.publicKey)
t.ok(r, 'the chained device is admitted — root -> parent -> child resolves to the room root')
t.alike(b4a.from(r.identityPublicKey), b4a.from(rootPub), 'and resolves to the SAME root identity')
t.alike(b4a.from(r.devicePublicKey), b4a.from(child.publicKey), 'as the child device')
// The parent's own proof must be UNCHANGED — attestDevice mutates a decoded chain, so a wrapper that
// passed the object through would grow the parent's proof on every call and eventually break it.
const parentAttest = await makeAttest(parent, parentProof)(signData)
t.ok(await verify(parentAttest, signData, parent.publicKey), 'the parent still verifies after vouching')
// Identity binding still holds: the chained proof is bound to the CHILD, not reusable as the parent.
t.absent(await verify(childAttest, signData, parent.publicKey), 'the child proof cannot claim to be the parent')
t.absent(await verify(childAttest, b4a.from('tampered'), child.publicKey), 'and is still bound to its signData')
// And a chain under a DIFFERENT root is still rejected — no privilege is created by chaining.
const wrongVerify = makeVerifyAttest(await rootIdentityPublicKey('zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'))
t.absent(await wrongVerify(childAttest, signData, child.publicKey), 'chaining does not launder a foreign root')
})