import * as jose from 'jose' import { defineChain } from 'viem' import { tempoMainnet } from 'viem/tempo/chains' import * as VercelOidc from './VercelOidc.js' const chainId = 421_700_001 const issuer = 'https://oidc.vercel.com/privy' const kid = 'vercel-test-key' const zone = defineChain({ ...tempoMainnet, id: chainId, name: 'privy', rpcUrls: { default: { http: ['https://example.com'] } }, sourceId: tempoMainnet.id, }) const options = { enforce: true, environments: ['preview', 'production'], issuer, orgIds: ['org_privy', 'org_privy_secondary'], projectIds: ['prj_privy', 'prj_privy_secondary'], teamId: 'team_privy', } satisfies VercelOidc.create.Options describe('create', () => { test('defaults to shadow evaluation', async () => { const policy = VercelOidc.create({ environments: options.environments, issuer: options.issuer, orgIds: options.orgIds, projectIds: options.projectIds, teamId: options.teamId, }) await expect(policy.verify(request(), zone)).resolves.toMatchObject({ enforce: false, outcome: 'denied', reason: 'missing', }) }) test('verifies the bearer identity independently from the API key', async () => { const { policy, privateKey } = await setup() const credential = await sign(privateKey, { project_id: 'prj_privy_secondary' }) await expect(policy.verify(request(credential), zone)).resolves.toMatchObject({ chainId, environment: 'production', enforce: true, outcome: 'authorized', reason: 'authorized', token: { header: { alg: 'RS256', kid, typ: 'JWT' }, payload: { aud: String(chainId), environment: 'production', exp: expect.any(Number), iat: expect.any(Number), iss: issuer, owner_id: 'team_privy', project_id: 'prj_privy_secondary', }, }, }) const preview = await sign(privateKey, { environment: 'preview' }) await expect(policy.verify(request(preview), zone)).resolves.toMatchObject({ chainId, environment: 'preview', outcome: 'authorized', }) }) test('rejects a token minted for another Zone', async () => { const { policy, privateKey } = await setup() const credential = await sign(privateKey) await expect( policy.verify(request(credential), { ...zone, id: chainId + 1 }), ).resolves.toMatchObject({ chainId: chainId + 1, outcome: 'denied', reason: 'invalid', }) }) test('reports missing, invalid, and forbidden credentials', async () => { const { policy, privateKey } = await setup() const forbidden = await sign(privateKey, { project_id: 'prj_other' }) await expect(policy.verify(request(), zone)).resolves.toMatchObject({ outcome: 'denied', reason: 'missing', }) await expect(policy.verify(request('invalid'), zone)).resolves.toMatchObject({ outcome: 'denied', reason: 'invalid', }) await expect(policy.verify(request(forbidden), zone)).resolves.toMatchObject({ outcome: 'denied', reason: 'forbidden', }) }) test('reports signing-key service failures without exposing the cause', async () => { const { privateKey } = await jose.generateKeyPair('RS256') const policy = VercelOidc.create(options, { jwks: () => async () => { throw new Error('sensitive upstream detail') }, }) await expect(policy.verify(request(await sign(privateKey)), zone)).resolves.toMatchObject({ outcome: 'error', reason: 'verification_unavailable', }) }) }) describe('authorizationError', () => { test.each([ ['missing', 401, 'Authentication required.'], ['invalid', 401, 'Authentication required.'], ['forbidden', 403, 'Access denied.'], ['verification_unavailable', 500, 'Internal server error.'], ] as const)('maps %s to %s', (reason, status, message) => { expect( VercelOidc.authorizationError({ chainId, enforce: true, outcome: 'denied', reason }), ).toMatchObject({ message, status }) }) test('accepts an authorized identity', () => { expect( VercelOidc.authorizationError({ chainId, enforce: true, outcome: 'authorized', reason: 'authorized', }), ).toBeUndefined() }) test('accepts failed shadow evaluations', () => { expect( VercelOidc.authorizationError({ chainId, enforce: false, outcome: 'denied', reason: 'missing', }), ).toBeUndefined() }) }) async function setup() { const { privateKey, publicKey } = await jose.generateKeyPair('RS256', { extractable: true }) const jwks = jose.createLocalJWKSet({ keys: [{ ...(await jose.exportJWK(publicKey)), kid }], }) return { policy: VercelOidc.create(options, { jwks: () => jwks }), privateKey } } type Claims = { environment?: string | undefined project_id?: string | undefined } async function sign(privateKey: jose.CryptoKey, claims: Claims = {}) { return new jose.SignJWT({ environment: claims.environment ?? 'production', owner_id: 'team_privy', project_id: 'prj_privy', ...claims, }) .setProtectedHeader({ alg: 'RS256', kid, typ: 'JWT' }) .setIssuer(issuer) .setAudience(String(chainId)) .setIssuedAt() .setExpirationTime('1h') .sign(privateKey) } function request(credential?: string) { const headers = new Headers({ 'tempo-api-key': 'tempo:sk:existing' }) if (credential) headers.set('authorization', `Bearer ${credential}`) return new Request('https://api.tempo.xyz', { headers, }) }