// lib/enroll.js — enrollment ceremony (SPEC §4-enroll). SCAFFOLD (P1).
//
// The one-time "secret handshake" that mints a peer into the membership view.
// Built on blind-pairing (API VERIFIED against shipped source this session):
// BlindPairing.createInvite(key, opts) -> { id, invite, publicKey: invitePub }
// blind.addMember({ discoveryKey, onadd }) // owner; request.open()/confirm()/deny()
// blind.addCandidate({ invite, userData, onadd }) // joiner; onadd(paired)
//
// The ceremony's ONLY durable output is a `grant` (+ add-writer) in the pool —
// after that the hot path authenticates by membership lookup (membership.js).
//
// NOT wired into P0's test (which uses direct grant/add-writer). Kept as the
// real shape to build out in P1.
const BlindPairing = require('blind-pairing')
const crypto = require('hypercore-crypto')
const b4a = require('b4a')
// OWNER: accept candidates against an invite, enroll them into the pool.
// `verify(claim)` returns a role string to grant, or null to deny.
function acceptMembers (swarm, node, { verify } = {}) {
const blind = new BlindPairing(swarm)
const inviteStore = new Map()
blind.addMember({
discoveryKey: crypto.discoveryKey(node.base.key),
onadd: async (request) => {
const rec = inviteStore.get(b4a.toString(request.inviteId, 'hex'))
if (!rec || rec.used) return request.deny({ status: 2 }) // INVITE_USED
const userData = request.open(rec.invitePub) // verifies invite-holding
const claim = JSON.parse(b4a.toString(userData)) // P0: JSON; P1: compact-encoding
const role = verify ? await verify(claim) : 'worker'
if (!role) return request.deny()
// CONTROL-PLANE WRITE = the enrollment
await node.grant(claim.identityPubKey, role)
if (claim.writerKey) await node.addWriter(claim.writerKey)
rec.used = true
request.confirm({ key: node.base.key, encryptionKey: node.base.encryptionKey })
}
})
return {
blind,
mintInvite () {
const { id, invite, publicKey } = BlindPairing.createInvite(node.base.key)
inviteStore.set(b4a.toString(id, 'hex'), { invitePub: publicKey, used: false })
return b4a.toString(invite, 'hex')
}
}
}
// JOINER: present a claim, receive the pool key.
function join (swarm, node, inviteHex) {
const blind = new BlindPairing(swarm)
const userData = b4a.from(JSON.stringify({
identityPubKey: node.idHex,
writerKey: b4a.toString(node.writerKey, 'hex')
}))
return new Promise((resolve, reject) => {
blind.addCandidate({
invite: b4a.from(inviteHex, 'hex'),
userData,
onadd: async (paired) => resolve(paired) // { key, encryptionKey, data }
}).pairing.catch(reject)
})
}
module.exports = { acceptMembers, join }