import { describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { encodeBase64UrlBytes } from 'relay-contracts' import { generateIdentity } from '../src/keys/identity.js' import { openKeystore } from '../src/keys/keystore.js' import { DEFAULT_ENROLL_MODE, EnrollError, PairingCodeExpiredError, PairingCodeSpentError, redeemPairingCode, } from '../src/enroll/pair.js' const ENROLL_URL = 'https://example.com/enroll' const VALID_UUID = '11111111-1111-4111-8111-111111111111' function freshKs() { const dir = mkdtempSync(join(tmpdir(), 'wta-pair-')) return { dir, ks: openKeystore(dir) } } function jsonResponse(status: number, body: unknown): Response { return { ok: status >= 200 && status < 300, status, json: async () => body, } as unknown as Response } function okBody(wrappedSecret: Uint8Array) { return { hostId: VALID_UUID, subdomain: 'host-42', cert: 'CERTPEM', caChain: 'CAPEM', hostContentSecret: encodeBase64UrlBytes(wrappedSecret), } } describe('redeemPairingCode (§4.5, T4)', () => { it('defaults to ed25519 mode', () => { expect(DEFAULT_ENROLL_MODE).toBe('ed25519') }) it('sends only pubkey + csr, never the private key (INV4)', async () => { const { dir, ks } = freshKs() const id = generateIdentity() const fetchImpl = vi.fn(async (_u: string | URL | Request, init?: RequestInit) => { const body = JSON.parse(String(init!.body)) as Record expect(body).toHaveProperty('agentPubkey') expect(body).toHaveProperty('csr') expect(JSON.stringify(body)).not.toContain('PRIVATE KEY') expect(body).not.toHaveProperty('privateKey') return jsonResponse(200, okBody(new Uint8Array([9, 9, 9]))) }) await redeemPairingCode(ENROLL_URL, 'ABCD-1234', id, ks, { fetchImpl: fetchImpl as unknown as typeof fetch }) rmSync(dir, { recursive: true, force: true }) }) it('stores cert + unwrapped content secret; wrapped bytes not persisted (FIX 3)', async () => { const { dir, ks } = freshKs() const id = generateIdentity() const wrapped = new Uint8Array([1, 2, 3]) const unwrapped = new Uint8Array([7, 7, 7]) const fetchImpl = async () => jsonResponse(200, okBody(wrapped)) await redeemPairingCode(ENROLL_URL, 'ABCD-1234', id, ks, { fetchImpl: fetchImpl as unknown as typeof fetch, unwrapContentSecret: () => unwrapped, }) expect(ks.loadCert()).toEqual({ certPem: 'CERTPEM', caChainPem: 'CAPEM' }) const stored = ks.loadContentSecret()! expect(Buffer.from(stored).equals(Buffer.from(unwrapped))).toBe(true) expect(Buffer.from(stored).equals(Buffer.from(wrapped))).toBe(false) rmSync(dir, { recursive: true, force: true }) }) it('maps 409 → PairingCodeSpentError (single-use)', async () => { const { dir, ks } = freshKs() const fetchImpl = async () => jsonResponse(409, {}) await expect( redeemPairingCode(ENROLL_URL, 'X', generateIdentity(), ks, { fetchImpl: fetchImpl as unknown as typeof fetch }), ).rejects.toBeInstanceOf(PairingCodeSpentError) rmSync(dir, { recursive: true, force: true }) }) it('maps 410 → PairingCodeExpiredError', async () => { const { dir, ks } = freshKs() const fetchImpl = async () => jsonResponse(410, {}) await expect( redeemPairingCode(ENROLL_URL, 'X', generateIdentity(), ks, { fetchImpl: fetchImpl as unknown as typeof fetch }), ).rejects.toBeInstanceOf(PairingCodeExpiredError) rmSync(dir, { recursive: true, force: true }) }) it('rejects a schema-mismatched response (boundary validation)', async () => { const { dir, ks } = freshKs() const fetchImpl = async () => jsonResponse(200, { hostId: 'not-a-uuid', subdomain: 'x' }) await expect( redeemPairingCode(ENROLL_URL, 'X', generateIdentity(), ks, { fetchImpl: fetchImpl as unknown as typeof fetch }), ).rejects.toBeInstanceOf(EnrollError) rmSync(dir, { recursive: true, force: true }) }) })