57 lines
2.6 KiB
TypeScript
57 lines
2.6 KiB
TypeScript
/**
|
|
* 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 <BASE_DOMAIN> [--sub <accountId>]
|
|
*
|
|
* 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<void> {
|
|
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 <BASE_DOMAIN> 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)
|
|
})
|