/** * B5 · unit tests for the STAGING operator token-mint (`POST /auth/mint`) and the P5 capability * signing-key loader. Covers: the DPoP-bound short-lived token happy path (claims + PoP binding), * the deny-by-default gates (bad password / unknown+revoked subdomain / malformed body / wrong * method / oversized body), route claiming semantics, and PEM+base64 key loading round-trips. */ import { describe, it, expect, beforeAll } from 'vitest' import { Readable } from 'node:stream' import type { IncomingMessage, ServerResponse } from 'node:http' import type { HostRecord } from 'control-plane/src/model/records.js' import { verifyPaseto } from 'relay-auth/src/crypto/paseto.js' import { createAuthMintRoute, loadSigningKeyFromEnv, type SubdomainHostLookup, } from '../src/servers/auth-mint.js' const subtle = globalThis.crypto.subtle const NOW = 1_800_000_000 const PASSWORD = 'staging-operator-secret' // A syntactically valid base64url SHA-256 JWK thumbprint (exactly 43 chars). const JKT = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNO12'.padEnd(43, 'X').slice(0, 43) interface Loaded { readonly signingKey: CryptoKey readonly publicKey: CryptoKey } let keys: Loaded beforeAll(async () => { const kp = (await subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])) as { publicKey: CryptoKey privateKey: CryptoKey } keys = { signingKey: kp.privateKey, publicKey: kp.publicKey } }) function mkHost(overrides: Partial = {}): HostRecord { return { hostId: 'host-1', accountId: 'acct-1', subdomain: 'alice', agentPubkey: new Uint8Array(32), enrollFpr: 'fpr-host-1', status: 'online', lastSeen: '2026-01-01T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z', revokedAt: null, ...overrides, } } function fakeHosts(rec: HostRecord | null): SubdomainHostLookup { return { getBySubdomain: async () => rec } } interface CapturedRes { statusCode: number headers: Record body: string headersSent: boolean writeHead(status: number, headers: Record): CapturedRes end(chunk?: string): void } function fakeRes(): { res: ServerResponse; captured: CapturedRes; done: Promise } { let resolveDone!: () => void const done = new Promise((r) => (resolveDone = r)) const captured: CapturedRes = { statusCode: 0, headers: {}, body: '', headersSent: false, writeHead(status, headers) { this.statusCode = status this.headers = headers this.headersSent = true return this }, end(chunk?: string) { if (chunk !== undefined) this.body += chunk resolveDone() }, } return { res: captured as unknown as ServerResponse, captured, done } } function fakeReq(method: string, url: string, body?: string): IncomingMessage { const chunks = body === undefined ? [] : [Buffer.from(body, 'utf8')] const req = Readable.from(chunks) as unknown as IncomingMessage ;(req as { method?: string }).method = method ;(req as { url?: string }).url = url return req } function mkRoute(hosts: SubdomainHostLookup): (req: IncomingMessage, res: ServerResponse) => boolean { return createAuthMintRoute({ signingKey: keys.signingKey, hosts, operatorPassword: PASSWORD, now: () => NOW, onError: () => {}, }) } async function post( hosts: SubdomainHostLookup, bodyObj: unknown, ): Promise { const route = mkRoute(hosts) const { res, captured, done } = fakeRes() const claimed = route(fakeReq('POST', '/auth/mint', JSON.stringify(bodyObj)), res) expect(claimed).toBe(true) await done return captured } describe('createAuthMintRoute — happy path', () => { it('mints a short-lived capability token bound to the client jkt (INV3 identity from store)', async () => { const captured = await post(fakeHosts(mkHost()), { password: PASSWORD, jkt: JKT, subdomain: 'alice' }) expect(captured.statusCode).toBe(200) expect(captured.headers['cache-control']).toBe('no-store') const parsedBody = JSON.parse(captured.body) as { token: string } expect(typeof parsedBody.token).toBe('string') const claims = (await verifyPaseto(parsedBody.token, keys.publicKey)) as { sub: string aud: string host: string rights: string[] iat: number exp: number cnf: { jkt: string } } // Identity is the STORE row's, never the request body's (INV3). expect(claims.sub).toBe('acct-1') expect(claims.host).toBe('host-1') expect(claims.aud).toBe('alice') expect(claims.rights).toContain('attach') // DPoP proof-of-possession binding to the client-provided thumbprint. expect(claims.cnf.jkt).toBe(JKT) // Short-lived (<= 60 s). expect(claims.iat).toBe(NOW) expect(claims.exp - claims.iat).toBeLessThanOrEqual(60) expect(claims.exp - claims.iat).toBeGreaterThan(0) // The token itself must never leak into logs — asserted by construction (no console here). }) }) describe('createAuthMintRoute — deny by default', () => { it('rejects a wrong password with 401 (no token)', async () => { const captured = await post(fakeHosts(mkHost()), { password: 'wrong', jkt: JKT, subdomain: 'alice' }) expect(captured.statusCode).toBe(401) expect(captured.body).not.toContain('token') }) it('rejects an unknown subdomain with 404', async () => { const captured = await post(fakeHosts(null), { password: PASSWORD, jkt: JKT, subdomain: 'ghost' }) expect(captured.statusCode).toBe(404) }) it('rejects a revoked host with 403 (INV12: revoked never mints)', async () => { const captured = await post( fakeHosts(mkHost({ status: 'revoked', revokedAt: '2026-02-01T00:00:00.000Z' })), { password: PASSWORD, jkt: JKT, subdomain: 'alice' }, ) expect(captured.statusCode).toBe(403) }) it('rejects a malformed jkt with 400', async () => { const captured = await post(fakeHosts(mkHost()), { password: PASSWORD, jkt: 'too-short', subdomain: 'alice' }) expect(captured.statusCode).toBe(400) }) it('rejects a malformed subdomain with 400', async () => { const captured = await post(fakeHosts(mkHost()), { password: PASSWORD, jkt: JKT, subdomain: 'Not_Valid!' }) expect(captured.statusCode).toBe(400) }) it('rejects invalid JSON with 400', async () => { const route = mkRoute(fakeHosts(mkHost())) const { res, captured, done } = fakeRes() const claimed = route(fakeReq('POST', '/auth/mint', '{not json'), res) expect(claimed).toBe(true) await done expect(captured.statusCode).toBe(400) }) it('rejects an oversized body with 413', async () => { const big = 'x'.repeat(5000) const captured = await post(fakeHosts(mkHost()), { password: PASSWORD, jkt: JKT, subdomain: 'alice', pad: big }) expect(captured.statusCode).toBe(413) }) }) describe('createAuthMintRoute — routing semantics', () => { it('claims /auth/mint but 405s a non-POST method', () => { const route = mkRoute(fakeHosts(mkHost())) const { res, captured } = fakeRes() const claimed = route(fakeReq('GET', '/auth/mint', undefined), res) expect(claimed).toBe(true) expect(captured.statusCode).toBe(405) }) it('does NOT claim a non-matching path (returns false, response untouched)', () => { const route = mkRoute(fakeHosts(mkHost())) const { res, captured } = fakeRes() const claimed = route(fakeReq('POST', '/index.html', undefined), res) expect(claimed).toBe(false) expect(captured.statusCode).toBe(0) }) it('claims /auth/mint even with a query string', () => { const route = mkRoute(fakeHosts(null)) const { res } = fakeRes() const claimed = route(fakeReq('GET', '/auth/mint?foo=1', undefined), res) expect(claimed).toBe(true) }) }) describe('loadSigningKeyFromEnv', () => { async function exportPkcs8Pem(): Promise<{ pem: string; b64: string; publicKey: CryptoKey }> { const kp = (await subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])) as { publicKey: CryptoKey privateKey: CryptoKey } const der = new Uint8Array(await subtle.exportKey('pkcs8', kp.privateKey)) const b64 = Buffer.from(der).toString('base64') const pem = `-----BEGIN PRIVATE KEY-----\n${b64.match(/.{1,64}/g)!.join('\n')}\n-----END PRIVATE KEY-----\n` return { pem, b64, publicKey: kp.publicKey } } async function canSign(priv: CryptoKey, pub: CryptoKey): Promise { const data = new Uint8Array([1, 2, 3, 4]) const sig = new Uint8Array(await subtle.sign({ name: 'Ed25519' }, priv, data)) return subtle.verify({ name: 'Ed25519' }, pub, sig, data) } it('loads a PKCS#8 PEM into a usable signing key', async () => { const { pem, publicKey } = await exportPkcs8Pem() const key = await loadSigningKeyFromEnv(pem) expect(await canSign(key, publicKey)).toBe(true) }) it('loads a bare base64 PKCS#8 DER into a usable signing key', async () => { const { b64, publicKey } = await exportPkcs8Pem() const key = await loadSigningKeyFromEnv(b64) expect(await canSign(key, publicKey)).toBe(true) }) it('throws on an empty value', async () => { await expect(loadSigningKeyFromEnv(' ')).rejects.toThrow() }) it('throws on a non-key value (never leaking material)', async () => { await expect(loadSigningKeyFromEnv('not-a-real-key')).rejects.toThrow() }) })