OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / lib / admin-cli.js

86 lines · javascript ref:
// The operator's CLI, reached as `ap-bridge admin …`.
//
// ⛔ A SUBCOMMAND, NOT A SECOND BINARY. pgh installs exactly one launcher per app — `pgh.bin` is a
// single name, and schema.js rejects anything else — and that launcher is `exec … index.js "$@"`, so
// arguments already pass through. Shipping a second bin/ would have been a file nobody could install:
// it exists in the tree, is absent from PATH, and the documented command silently is not there.
//
// ⛔ OVER THE UNIX SOCKET, which is the gate. Only a local process that can open the socket path may
// call this, which is the fleet's pattern for an operator surface — pgv3's handlers gate on
// `ctx.transport`, and hyper-proto-peer's UDS surface is "TRUSTED WITHOUT the writer-allowlist" for
// the same reason.
//
// ⛔ WHY IT EXISTS. Lose every operator browser — cleared storage, a dead laptop — and the web surface
// is shut for good: the bootstrap token gates the FIRST claim only, and the instance never reopens,
// 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.

import { Client } from 'hyper-svc'

export const USAGE = `ap-bridge admin <command>

  status            who operates this bridge
  invite            mint a single-use invite for another browser
  revoke <pubkey>   retire an operator browser

The bridge must be running with AP_ADMIN_PORT set — the control socket comes up beside the
admin surface. Only a process on this machine that can open the socket may call it.`

/**
 * @returns {Promise<number>} the process exit code — this function never exits on its own, so it
 * stays testable and a caller keeps control of the process.
 */
export async function runAdminCli (argv, { uds, out = console.log, err = console.error, connect = null } = {}) {
  const cmd = argv[0]
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') { out(USAGE); return 0 }

  const client = connect
    ? await connect(uds).catch(() => null)
    : await Client.discover({ uds, prefer: 'uds' }).catch(() => null)

  if (!client) {
    err('no control socket at ' + uds)
    err('Is the bridge running, and is AP_ADMIN_PORT set? The control socket comes up with the admin surface.')
    return 1
  }

  try {
    if (cmd === 'status') {
      const s = await client.call('admin-status', {})
      out(s.unclaimed
        ? 'UNCLAIMED — claim it with the bootstrap token printed in the service log'
        : 'claimed')
      if (!s.operators.length) out('  no operator browsers')
      for (const o of s.operators) out('  ' + o.publicKey + (o.revoked ? '  (revoked)' : ''))
      return 0
    }

    if (cmd === 'invite') {
      const { token, expiresAt } = await client.call('admin-invite', {})
      // ⚠ The token alone on stdout, everything else on stderr, so `… invite | pbcopy` pipes a token
      // and not a paragraph.
      out(token)
      err('single-use' + (expiresAt ? ', expires ' + new Date(expiresAt).toISOString() : '') +
        ' — paste it into the Invite field on the sign-in page')
      return 0
    }

    if (cmd === 'revoke') {
      if (!argv[1]) { err('usage: ap-bridge admin revoke <publicKey>'); return 1 }
      await client.call('admin-revoke', { publicKey: argv[1] })
      out('revoked ' + argv[1])
      return 0
    }

    err(USAGE)
    return 1
  } catch (e) {
    err((e && e.message) || String(e))
    return 1
  } finally {
    try { await client.close() } catch {}
  }
}

export default { runAdminCli, USAGE }