import { mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { generateKeyPairSync } from 'node:crypto'; import { assertJwksConfig } from './check-jwks-config'; const tmpDir = mkdtempSync(join(tmpdir(), 'apf-jwks-spec-')); function writeKey( filename: string, type: 'rsa-3072' | 'rsa-1024' | 'ec-p256' | 'ec-p384' | 'ed25519' | 'garbage', ): string { const path = join(tmpDir, filename); let pem: string; switch (type) { case 'rsa-3072': pem = generateKeyPairSync('rsa', { modulusLength: 3072 }).privateKey.export({ type: 'pkcs8', format: 'pem', }) as string; break; case 'rsa-1024': pem = generateKeyPairSync('rsa', { modulusLength: 1024 }).privateKey.export({ type: 'pkcs8', format: 'pem', }) as string; break; case 'ec-p256': pem = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }).privateKey.export({ type: 'pkcs8', format: 'pem', }) as string; break; case 'ec-p384': pem = generateKeyPairSync('ec', { namedCurve: 'secp384r1' }).privateKey.export({ type: 'pkcs8', format: 'pem', }) as string; break; case 'ed25519': pem = generateKeyPairSync('ed25519').privateKey.export({ type: 'pkcs8', format: 'pem', }) as string; break; case 'garbage': pem = '-----BEGIN PRIVATE KEY-----\nbm90LWEta2V5\n-----END PRIVATE KEY-----\n'; break; } writeFileSync(path, pem); return path; } describe('assertJwksConfig', () => { const originalPath = process.env['BFF_JWKS_PRIVATE_KEY_PATH']; const originalKid = process.env['BFF_JWKS_KID']; beforeEach(() => { process.env['BFF_JWKS_KID'] = 'bff-2026-05'; }); afterEach(() => { restore('BFF_JWKS_PRIVATE_KEY_PATH', originalPath); restore('BFF_JWKS_KID', originalKid); }); it('returns the parsed key + kid + alg=RS256 for a well-formed RSA-3072 key', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072'); const config = assertJwksConfig(); expect(config.kid).toBe('bff-2026-05'); expect(config.alg).toBe('RS256'); expect(config.privateKey.asymmetricKeyType).toBe('rsa'); }); it('returns alg=ES256 for an EC P-256 key', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('ec256.pem', 'ec-p256'); expect(assertJwksConfig().alg).toBe('ES256'); }); it('returns alg=ES384 for an EC P-384 key', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('ec384.pem', 'ec-p384'); expect(assertJwksConfig().alg).toBe('ES384'); }); it('throws when BFF_JWKS_PRIVATE_KEY_PATH is unset', () => { delete process.env['BFF_JWKS_PRIVATE_KEY_PATH']; expect(() => assertJwksConfig()).toThrow(/BFF_JWKS_PRIVATE_KEY_PATH is not set/); }); it('throws when the file does not exist', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = join(tmpDir, 'missing.pem'); expect(() => assertJwksConfig()).toThrow(/could not be read/); }); it('throws when the file is not a valid PEM private key', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('garbage.pem', 'garbage'); expect(() => assertJwksConfig()).toThrow(/not a valid PEM private key/); }); it('refuses RSA keys weaker than 2048 bits', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('weak.pem', 'rsa-1024'); expect(() => assertJwksConfig()).toThrow(/unsupported key type/); }); it('refuses Ed25519 keys (v1 supports RSA + EC P-256/P-384 only)', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('ed25519.pem', 'ed25519'); expect(() => assertJwksConfig()).toThrow(/unsupported key type/); }); it('throws when BFF_JWKS_KID is unset', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072'); delete process.env['BFF_JWKS_KID']; expect(() => assertJwksConfig()).toThrow(/BFF_JWKS_KID is not set/); }); it('throws when BFF_JWKS_KID has illegal characters', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072'); process.env['BFF_JWKS_KID'] = 'has spaces and !'; expect(() => assertJwksConfig()).toThrow(/must match/); }); it('throws when BFF_JWKS_KID is too short', () => { process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072'); process.env['BFF_JWKS_KID'] = 'ab'; expect(() => assertJwksConfig()).toThrow(/must match/); }); }); function restore(name: string, original: string | undefined): void { if (original === undefined) { delete process.env[name]; } else { process.env[name] = original; } }