/** * RELAY-PHASE1 · staging admin-token minter (run from relay-run/ so relay-auth resolves). * * cd relay-run && npx tsx scripts/mint-manage-token.ts --aud [--sub ] * * Mints a §4.3 `manage` capability token signed by the P5 PRIVATE key, for the deny-by-default * control-plane admin API (POST /accounts, /accounts/:id/pairing-codes). `aud` MUST equal the CP's * BASE_DOMAIN (its `expectedAud`). The admin API verifies signature+aud+rights and derives accountId * ONLY from the token (INV3) — it does NOT require a live DPoP proof — but the issuer still stamps a * well-formed `cnf.jkt` (from a throwaway ephemeral key) because issueCapabilityToken mandates it. * TTL is clamped to 60 s, so mint immediately before each admin call. * * Prints ONLY the raw token to stdout; the signing key material is never printed. */ import { readFileSync } from 'node:fs' import { issueCapabilityToken } from 'relay-auth' import { generateEd25519KeyPair, exportEd25519PublicRaw } from 'relay-auth/src/crypto/ed25519.js' import { jwkThumbprint } from 'relay-auth/src/crypto/thumbprint.js' function arg(name: string, fallback?: string): string | undefined { const i = process.argv.indexOf(`--${name}`) return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : fallback } async function main(): Promise { const keyPath = arg('key', '/etc/relay/capability/capability-sign.key.pem') as string const aud = arg('aud') const sub = arg('sub', 'bootstrap') as string const host = arg('host', '_manage_') as string const rights = (arg('rights', 'manage') as string).split(',').map((r) => r.trim()) if (aud === undefined || aud.length === 0) { process.stderr.write('FATAL: --aud is required\n') process.exit(2) } const pem = readFileSync(keyPath, 'utf8') const der = Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''), 'base64') const signingKey = await globalThis.crypto.subtle.importKey('pkcs8', der, { name: 'Ed25519' }, false, ['sign']) const eph = await generateEd25519KeyPair() const cnfJkt = await jwkThumbprint(await exportEd25519PublicRaw(eph.publicKey)) const now = Math.floor(Date.now() / 1000) const token = await issueCapabilityToken( // Runtime only reads principal.accountId; a minimal shape is intentional for this staging tool. { principal: { accountId: sub } as never, aud, host, rights: rights as never, ttlSeconds: 60, cnfJkt }, signingKey, now, ) process.stdout.write(token) } main().catch((e: unknown) => { process.stderr.write(`mint failed: ${e instanceof Error ? e.message : String(e)}\n`) process.exit(1) })