OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / lib / admin-control.js

64 lines · javascript ref:
// The operator's LOCAL control surface — recovery that cannot be locked out.
//
// ⛔ A UNIX SOCKET, BECAUSE THE FILESYSTEM IS THE GATE. This is the fleet's pattern, not a new one:
// pgv3 carries `ctx.transport` through hyper-svc and its handlers self-gate on it ("Block HTTP
// (anonymous callers). UDS (local operator) … allowed"), and hyper-proto-peer's own UDS surface says
// why — "a UDS connection is owner-gated by the filesystem (only a local process with access to the
// socket path can connect), so the local surface is TRUSTED WITHOUT the writer-allowlist".
//
// ⚠ AND IT IS STRICTLY STRONGER THAN THE LOOPBACK HTTP ROUTE IT REPLACES. That route gated on the
// socket's remote address, which is the right intent and a weaker gate: loopback is reachable by ANY
// process or user on the machine, while a unix socket is reachable only by one that can open its path.
// Both mean "you are on this box"; only one means "you are the operator".
//
// ⛔ WHAT IT EXISTS FOR. Losing every operator browser — cleared storage, a dead laptop — otherwise
// shuts the bridge for good: the bootstrap token gates the FIRST claim only, and browsers.js never
// reopens the instance, not even when the claimant is revoked. Without this the recovery is deleting
// the identity store, which throws away every operator grant in order to get one back.
//
// ⛔ IT ISSUES AN INVITE, NEVER RE-ARMS TOFU. Re-arming would reopen the instance to whoever reached
// the port first, on a surface bound to 0.0.0.0 — the hole the bootstrap token exists to close.

import { Service } from 'hyper-svc'

export function adminControlHandlers ({ browsers, log = () => {} }) {
  return {
    /** Is anyone operating this bridge yet, and who? The read a CLI needs before it acts. */
    'admin-status': async () => ({
      unclaimed: await browsers.unclaimed(),
      operators: await browsers.list()
    }),

    /**
     * Mint a single-use invite for another browser.
     *
     * ⚠ LOUD. An invite minted by a local process with nobody watching is the one action here worth an
     * audit line, precisely because it needs no session.
     */
    'admin-invite': async () => {
      const { token, expiresAt } = await browsers.invite()
      log({ msg: 'ADMIN RECOVERY — invite issued over the local control socket', expiresAt })
      return { token, expiresAt }
    },

    /** Retire an operator browser. The counterpart to invite, for a key that should no longer hold. */
    'admin-revoke': async (ctx, { publicKey } = {}) => {
      if (!publicKey) throw new Error('admin-revoke: publicKey required')
      await browsers.revoke(publicKey)
      log({ msg: 'ADMIN RECOVERY — operator revoked over the local control socket', publicKey })
      return { revoked: publicKey }
    }
  }
}

export async function serveAdminControl ({ browsers, uds, log = () => {} }) {
  if (!uds) throw new Error('serveAdminControl: a uds path is required')
  const svc = new Service({ name: 'ap-bridge-admin', uds })
  const handlers = adminControlHandlers({ browsers, log })
  for (const [name, fn] of Object.entries(handlers)) svc.handle(name, fn)
  await svc.listen()
  return { uds, close: async () => { try { await svc.close() } catch {} } }
}

export default { serveAdminControl, adminControlHandlers }