/**
* TrustMap — pubkey → permissions store backed by Hyperbee.
*
* Two modes of access checking:
*
* 1. DIRECT: a peer's pubkey is directly registered with permissions.
* Used by services, CI runners, single-keypair clients.
*
* tm.grant(pubkey, 'repo-x', ['read', 'write'])
* tm.check(pubkey, 'repo-x', 'write') // → true
*
* 2. CHAIN: a peer presents a hyperident-style attestation chain (signed
* by their master identity via keet-identity-key). Trust-map verifies
* the chain, extracts the master (identityPublicKey), and looks it up.
* Devices inherit permissions from their master.
*
* tm.grant(masterPubkey, 'repo-y', ['admin'])
* tm.checkChain({ proof, data, resource: 'repo-y', perm: 'admin' })
*
* The check() functions accept either Buffer or hex string pubkeys.
*
* Storage layout (Hyperbee, JSON values):
* trust:{pubkey-hex}:{resource} → { perms: [...] }
*
* Wildcard resource '*' grants apply to ALL resources.
*/
import ReadyResource from 'ready-resource'
import Hyperbee from 'hyperbee'
import b4a from 'b4a'
export class TrustMap extends ReadyResource {
/**
* @param {object} opts
* @param {Hypercore|Hyperbee} opts.core - hypercore to wrap (or pass `bee`)
* @param {Hyperbee} [opts.bee] - existing hyperbee
* @param {object} [opts.identityKey] - keet-identity-key module for chain verify
*/
constructor (opts = {}) {
super()
this._bee = opts.bee || null
this._core = opts.core || null
this._identityKey = opts.identityKey || null
}
async _open () {
if (!this._bee && this._core) {
this._bee = new Hyperbee(this._core, {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
}
if (!this._bee) throw new Error('TrustMap requires `core` or `bee` opt')
await this._bee.ready()
}
async _close () {
// Don't close the bee/core — caller owns them
}
// ── Helpers ──────────────────────────────────────────────────
static _toHex (pubkey) {
if (typeof pubkey === 'string') return pubkey
return b4a.toString(pubkey, 'hex')
}
_key (pubkey, resource) {
return `trust:${TrustMap._toHex(pubkey)}:${resource}`
}
// ── Direct grants ────────────────────────────────────────────
/**
* Grant permissions to a pubkey for a resource.
* @param {string|Buffer} pubkey
* @param {string} resource - resource name, or '*' for all resources
* @param {string[]} perms - e.g. ['read', 'write', 'admin']
*/
async grant (pubkey, resource, perms) {
if (!Array.isArray(perms) || perms.length === 0) {
throw new Error('perms must be a non-empty array')
}
const key = this._key(pubkey, resource)
const existing = await this._bee.get(key)
const merged = existing
? Array.from(new Set([...existing.value.perms, ...perms]))
: [...perms]
await this._bee.put(key, { perms: merged })
}
/**
* Revoke specific permissions (or all if perms omitted) for a pubkey/resource.
* @param {string|Buffer} pubkey
* @param {string} resource
* @param {string[]} [perms] - if omitted, removes all perms for this resource
*/
async revoke (pubkey, resource, perms) {
const key = this._key(pubkey, resource)
const existing = await this._bee.get(key)
if (!existing) return
if (!perms || perms.length === 0) {
await this._bee.del(key)
return
}
const remaining = existing.value.perms.filter(p => !perms.includes(p))
if (remaining.length === 0) {
await this._bee.del(key)
} else {
await this._bee.put(key, { perms: remaining })
}
}
/**
* Check if a pubkey has a specific permission on a resource.
* Falls back to wildcard '*' resource if exact match has no entry or
* lacks the requested permission.
* @param {string|Buffer} pubkey
* @param {string} resource
* @param {string} perm
* @returns {Promise<boolean>}
*/
async check (pubkey, resource, perm) {
const exact = await this._bee.get(this._key(pubkey, resource))
if (exact && exact.value.perms.includes(perm)) return true
const wildcard = await this._bee.get(this._key(pubkey, '*'))
if (wildcard && wildcard.value.perms.includes(perm)) return true
return false
}
/**
* List all entries for a pubkey across resources.
* @param {string|Buffer} pubkey
* @returns {Promise<Array<{ resource: string, perms: string[] }>>}
*/
async listForPubkey (pubkey) {
const hex = TrustMap._toHex(pubkey)
const prefix = `trust:${hex}:`
const out = []
for await (const entry of this._bee.createReadStream({
gte: prefix,
lt: prefix + '\xff'
})) {
out.push({
resource: entry.key.slice(prefix.length),
perms: entry.value.perms
})
}
return out
}
/**
* List all trust map entries.
* @returns {Promise<Array<{ pubkey: string, resource: string, perms: string[] }>>}
*/
async list () {
const out = []
for await (const entry of this._bee.createReadStream({
gte: 'trust:',
lt: 'trust:\xff'
})) {
const parts = entry.key.split(':')
// key format: trust:{pubkey-hex}:{resource}
// resource may contain colons, so rejoin from index 2
const pubkey = parts[1]
const resource = parts.slice(2).join(':')
out.push({ pubkey, resource, perms: entry.value.perms })
}
return out
}
// ── Chain verification (via keet-identity-key) ───────────────
/**
* Verify a hyperident attestation chain and check permissions for the
* MASTER identity (root of the chain).
*
* Caller passes the proof bytes and the data that was signed. We use
* keet-identity-key to verify the proof and extract the identityPublicKey
* (master). We then look up the master in the trust map.
*
* @param {object} args
* @param {Buffer} args.proof - the attestation proof from hyperident
* @param {Buffer} args.data - the data that was signed
* @param {string} args.resource - the resource being accessed
* @param {string} args.perm - the permission being checked
* @returns {Promise<{ ok: boolean, masterPubkey?: string, devicePubkey?: string, reason?: string }>}
*/
async checkChain ({ proof, data, resource, perm }) {
if (!this._identityKey) {
return { ok: false, reason: 'no identityKey module configured' }
}
if (!proof || !data) {
return { ok: false, reason: 'proof and data required' }
}
let verification
try {
verification = this._identityKey.verify(proof, data)
} catch (err) {
return { ok: false, reason: 'verify threw: ' + err.message }
}
if (!verification) {
return { ok: false, reason: 'invalid attestation' }
}
const masterPubkey = TrustMap._toHex(verification.identityPublicKey)
const devicePubkey = TrustMap._toHex(verification.devicePublicKey)
// Look up the master in the trust map (devices inherit master's permissions)
const allowed = await this.check(masterPubkey, resource, perm)
if (!allowed) {
return { ok: false, masterPubkey, devicePubkey, reason: 'master not authorized for ' + perm + ' on ' + resource }
}
return { ok: true, masterPubkey, devicePubkey }
}
}