import { generateKeyPairSync } from 'node:crypto';
import { SignJWT, importPKCS8 } from 'jose';
import { vi, describe, it, expect, beforeAll } from 'vitest';
import { JwtVerifier } from './verifier.js';
const KEY_ID = 'test-key';
const createSigningMaterial = (kid = KEY_ID) => {
    const { publicKey, privateKey } = generateKeyPairSync('rsa', {
        modulusLength: 2048
    });
    const jwk = {
        ...publicKey.export({ format: 'jwk' }),
        kid,
        alg: 'RS256',
        use: 'sig'
    };
    return {
        jwk,
        privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
    };
};
const buildJwtVerifier = (jwk) => {
    const getKeysInterceptor = vi.fn(async () => [jwk]);
    const options = {
        jwksUri: 'https://example.com/.well-known/jwks.json',
        cache: false,
        getKeysInterceptor
    };
    return { verifier: new JwtVerifier(options), getKeysInterceptor };
};
describe('JwtVerifier', () => {
    let signingMaterial;
    beforeAll(() => {
        signingMaterial = createSigningMaterial();
    });
    it('verifies a token with a matching JWKS entry', async () => {
        const { verifier, getKeysInterceptor } = buildJwtVerifier(signingMaterial.jwk);
        const payload = {
            sub: 'user-123',
            scope: 'read:all'
        };
        const key = await importPKCS8(signingMaterial.privateKeyPem, 'RS256');
        const token = await new SignJWT(payload).setProtectedHeader({ alg: 'RS256', kid: signingMaterial.jwk.kid }).sign(key);
        const result = await verifier.verify(token, { algorithms: ['RS256'] });
        expect(getKeysInterceptor).toHaveBeenCalledTimes(1);
        expect(result.sub).toBe(payload.sub);
        expect(result.scope).toBe(payload.scope);
    });
    it('throws UnauthorizedError when the signature does not match the JWKS entry', async () => {
        const { verifier } = buildJwtVerifier(signingMaterial.jwk);
        const mismatchedKey = createSigningMaterial();
        const invalidKey = await importPKCS8(mismatchedKey.privateKeyPem, 'RS256');
        const invalidToken = await new SignJWT({ sub: 'user-456' })
            .setProtectedHeader({ alg: 'RS256', kid: signingMaterial.jwk.kid })
            .sign(invalidKey);
        await expect(verifier.verify(invalidToken, { algorithms: ['RS256'] })).rejects.toMatchObject({
            status: 401,
            message: expect.stringContaining('signature verification failed')
        });
    });
    it('honors JwtVerifyOptions when audience and issuer match', async () => {
        const { verifier, getKeysInterceptor } = buildJwtVerifier(signingMaterial.jwk);
        const audience = 'target-app';
        const issuer = 'https://issuer.example.com';
        const key = await importPKCS8(signingMaterial.privateKeyPem, 'RS256');
        const token = await new SignJWT({ sub: 'user-789' })
            .setProtectedHeader({ alg: 'RS256', kid: signingMaterial.jwk.kid })
            .setAudience(audience)
            .setIssuer(issuer)
            .sign(key);
        const result = await verifier.verify(token, {
            algorithms: ['RS256'],
            audience,
            issuer
        });
        expect(getKeysInterceptor).toHaveBeenCalledTimes(1);
        expect(result.sub).toBe('user-789');
        expect(result.aud).toBe(audience);
        expect(result.iss).toBe(issuer);
    });
    it('rejects when VerifyOptions constraints fail', async () => {
        const { verifier } = buildJwtVerifier(signingMaterial.jwk);
        const audience = 'target-app';
        const issuer = 'https://issuer.example.com';
        const key = await importPKCS8(signingMaterial.privateKeyPem, 'RS256');
        const token = await new SignJWT({ sub: 'user-987' })
            .setProtectedHeader({ alg: 'RS256', kid: signingMaterial.jwk.kid })
            .setAudience(audience)
            .setIssuer(issuer)
            .sign(key);
        await expect(verifier.verify(token, {
            algorithms: ['RS256'],
            audience: `${audience}-mismatch`,
            issuer
        })).rejects.toMatchObject({
            status: 401,
            message: expect.stringContaining('unexpected "aud" claim value')
        });
    });
    it('rejects malformed tokens before attempting JWKS lookup', async () => {
        const { verifier, getKeysInterceptor } = buildJwtVerifier(signingMaterial.jwk);
        await expect(verifier.verify('definitely-not-a-jwt')).rejects.toThrow('Invalid token specified');
        expect(getKeysInterceptor).not.toHaveBeenCalled();
    });
    it('rejects verification when no algorithms allowlist is provided', async () => {
        const { verifier, getKeysInterceptor } = buildJwtVerifier(signingMaterial.jwk);
        const key = await importPKCS8(signingMaterial.privateKeyPem, 'RS256');
        const token = await new SignJWT({ sub: 'user-noalg' }).setProtectedHeader({ alg: 'RS256', kid: signingMaterial.jwk.kid }).sign(key);
        await expect(verifier.verify(token)).rejects.toMatchObject({ status: 401 });
        // The allowlist guard must trip before any signing key is resolved.
        expect(getKeysInterceptor).not.toHaveBeenCalled();
    });
    it('rejects a token whose algorithm is outside the pinned allowlist before any key lookup', async () => {
        const { verifier, getKeysInterceptor } = buildJwtVerifier(signingMaterial.jwk);
        const key = await importPKCS8(signingMaterial.privateKeyPem, 'RS256');
        const token = await new SignJWT({ sub: 'user-rs256' }).setProtectedHeader({ alg: 'RS256', kid: signingMaterial.jwk.kid }).sign(key);
        // The token is validly signed, but the caller pins ES256 only.
        await expect(verifier.verify(token, { algorithms: ['ES256'] })).rejects.toMatchObject({ status: 401 });
        expect(getKeysInterceptor).not.toHaveBeenCalled();
    });
});
//# sourceMappingURL=verifier.test.js.map