import test from 'brittle'
import Corestore from 'corestore'
import IdentityKey from 'keet-identity-key'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import { TrustMap } from '../index.js'
async function makeTrustMap (t, opts = {}) {
const store = new Corestore('/tmp/trust-map-test-' + Math.random().toString(36).slice(2))
await store.ready()
const core = store.get({ name: 'trust-map' })
await core.ready()
const tm = new TrustMap({ core, identityKey: opts.identityKey })
await tm.ready()
t.teardown(async () => {
await tm.close()
await store.close()
})
return tm
}
// ── Direct grants (no chain) ─────────────────────────────────
test('grant + check + revoke (direct pubkey)', async (t) => {
const tm = await makeTrustMap(t)
const pk = b4a.toString(crypto.randomBytes(32), 'hex')
await tm.grant(pk, 'repo-x', ['read', 'write'])
t.ok(await tm.check(pk, 'repo-x', 'read'), 'has read')
t.ok(await tm.check(pk, 'repo-x', 'write'), 'has write')
t.absent(await tm.check(pk, 'repo-x', 'admin'), 'no admin')
await tm.revoke(pk, 'repo-x', ['write'])
t.ok(await tm.check(pk, 'repo-x', 'read'), 'still has read')
t.absent(await tm.check(pk, 'repo-x', 'write'), 'write revoked')
await tm.revoke(pk, 'repo-x')
t.absent(await tm.check(pk, 'repo-x', 'read'), 'all perms revoked')
})
test('grant accepts Buffer pubkey too', async (t) => {
const tm = await makeTrustMap(t)
const pkBuf = crypto.randomBytes(32)
const pkHex = b4a.toString(pkBuf, 'hex')
await tm.grant(pkBuf, 'repo-y', ['read'])
t.ok(await tm.check(pkBuf, 'repo-y', 'read'), 'check by Buffer')
t.ok(await tm.check(pkHex, 'repo-y', 'read'), 'check by hex string')
})
test('multiple grants merge perms (no duplicates)', async (t) => {
const tm = await makeTrustMap(t)
const pk = b4a.toString(crypto.randomBytes(32), 'hex')
await tm.grant(pk, 'repo-x', ['read'])
await tm.grant(pk, 'repo-x', ['write'])
await tm.grant(pk, 'repo-x', ['read', 'admin']) // re-adds 'read'
const entries = await tm.listForPubkey(pk)
t.is(entries.length, 1)
t.alike(entries[0].perms.sort(), ['admin', 'read', 'write'])
})
test('wildcard resource grants apply to all resources', async (t) => {
const tm = await makeTrustMap(t)
const pk = b4a.toString(crypto.randomBytes(32), 'hex')
await tm.grant(pk, '*', ['read'])
t.ok(await tm.check(pk, 'repo-a', 'read'), 'wildcard grants read on repo-a')
t.ok(await tm.check(pk, 'repo-b', 'read'), 'wildcard grants read on repo-b')
t.absent(await tm.check(pk, 'repo-a', 'write'), 'wildcard does not grant write')
// Specific grant overlays wildcard
await tm.grant(pk, 'repo-a', ['write'])
t.ok(await tm.check(pk, 'repo-a', 'write'), 'specific resource grant works')
t.ok(await tm.check(pk, 'repo-a', 'read'), 'wildcard still grants read')
})
test('listForPubkey + list', async (t) => {
const tm = await makeTrustMap(t)
const pk1 = b4a.toString(crypto.randomBytes(32), 'hex')
const pk2 = b4a.toString(crypto.randomBytes(32), 'hex')
await tm.grant(pk1, 'repo-a', ['read', 'write'])
await tm.grant(pk1, 'repo-b', ['read'])
await tm.grant(pk2, 'repo-a', ['admin'])
const pk1Entries = await tm.listForPubkey(pk1)
t.is(pk1Entries.length, 2)
const all = await tm.list()
t.is(all.length, 3)
})
test('persistence across reopen', async (t) => {
const dir = '/tmp/trust-map-persist-' + Math.random().toString(36).slice(2)
const store1 = new Corestore(dir)
await store1.ready()
const core1 = store1.get({ name: 'tm' })
await core1.ready()
const tm1 = new TrustMap({ core: core1 })
await tm1.ready()
const pk = b4a.toString(crypto.randomBytes(32), 'hex')
await tm1.grant(pk, 'repo-x', ['read'])
await tm1.close()
await store1.close()
// Reopen
const store2 = new Corestore(dir)
await store2.ready()
const core2 = store2.get({ name: 'tm' })
await core2.ready()
const tm2 = new TrustMap({ core: core2 })
await tm2.ready()
t.ok(await tm2.check(pk, 'repo-x', 'read'), 'state persisted across reopen')
await tm2.close()
await store2.close()
})
// ── Chain verification (via keet-identity-key) ───────────────
test('checkChain: verifies a real hyperident-style attestation chain', async (t) => {
const tm = await makeTrustMap(t, { identityKey: IdentityKey })
// Create a master identity from a mnemonic
const mnemonic = IdentityKey.generateMnemonic()
const masterIdentity = await IdentityKey.from({ mnemonic })
// Create a device keypair
const deviceKeyPair = crypto.keyPair()
// Master attests the device (this is what hyperident does internally)
const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)
// Sign some data with the device chain
const data = b4a.from('hello, trust-map')
const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)
// Get the master pubkey (this is what trust-map looks up)
const masterPubkey = b4a.toString(masterIdentity.identityKeyPair.publicKey, 'hex')
// Grant permissions to the master, NOT the device
await tm.grant(masterPubkey, 'repo-x', ['read', 'write'])
// The peer presents the proof + data — trust-map should accept
const result = await tm.checkChain({
proof,
data,
resource: 'repo-x',
perm: 'write'
})
t.ok(result.ok, 'chain accepted')
t.is(result.masterPubkey, masterPubkey, 'extracted correct master')
t.is(result.devicePubkey, b4a.toString(deviceKeyPair.publicKey, 'hex'), 'extracted device pubkey')
})
test('checkChain: rejects when master is not in trust map', async (t) => {
const tm = await makeTrustMap(t, { identityKey: IdentityKey })
const masterIdentity = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
const deviceKeyPair = crypto.keyPair()
const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)
const data = b4a.from('hello')
const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)
// No grant for the master — should be rejected
const result = await tm.checkChain({
proof, data, resource: 'repo-x', perm: 'read'
})
t.absent(result.ok)
t.ok(result.reason.includes('not authorized'), 'reason explains failure')
})
test('checkChain: rejects tampered data', async (t) => {
const tm = await makeTrustMap(t, { identityKey: IdentityKey })
const masterIdentity = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
const deviceKeyPair = crypto.keyPair()
const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)
const data = b4a.from('original data')
const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)
const masterPubkey = b4a.toString(masterIdentity.identityKeyPair.publicKey, 'hex')
await tm.grant(masterPubkey, 'repo-x', ['write'])
// Try to verify with TAMPERED data
const result = await tm.checkChain({
proof,
data: b4a.from('tampered data'),
resource: 'repo-x',
perm: 'write'
})
t.absent(result.ok)
t.ok(result.reason.includes('invalid attestation'), 'rejected as invalid')
})
test('checkChain: device key NOT in trust map (only master matters)', async (t) => {
const tm = await makeTrustMap(t, { identityKey: IdentityKey })
const masterIdentity = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
const deviceKeyPair = crypto.keyPair()
const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)
const data = b4a.from('test')
const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)
const masterPubkey = b4a.toString(masterIdentity.identityKeyPair.publicKey, 'hex')
const devicePubkeyHex = b4a.toString(deviceKeyPair.publicKey, 'hex')
// Only grant to master
await tm.grant(masterPubkey, 'repo-x', ['admin'])
// Direct check on the device pubkey should FAIL (device not in map)
t.absent(await tm.check(devicePubkeyHex, 'repo-x', 'admin'), 'device not directly authorized')
// Chain check should SUCCEED (device inherits from master)
const result = await tm.checkChain({ proof, data, resource: 'repo-x', perm: 'admin' })
t.ok(result.ok, 'chain check inherits from master')
})
test('checkChain: returns useful error when identityKey not configured', async (t) => {
const tm = await makeTrustMap(t) // no identityKey
const result = await tm.checkChain({
proof: b4a.from('fake'),
data: b4a.from('fake'),
resource: 'r',
perm: 'p'
})
t.absent(result.ok)
t.ok(result.reason.includes('no identityKey'))
})