OverviewFilesCommitsIssuesDocsReleasesPeers

hypercompute / lib / enroll.js

72 lines · javascript ref:
// 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 }