import { afterEach, describe, expect, it, vi } from 'vitest'; import { discoverAgentAuth, exchangeAgentToken, getAgentIdentity, registerAgent, } from '../src/agent-auth.js'; import { agentRequest, AgentApiError } from '../src/agent-http.js'; const issuer = 'https://auth.example.com'; const discovery = { issuer, token_endpoint: `${issuer}/oauth2/token`, agent_auth: { identity_endpoint: `${issuer}/agent/identity`, claim_endpoint: `${issuer}/agent/identity/claim`, identity_types_supported: ['anonymous'], }, }; const credentials = { registrationId: 'reg_1', issuer, assertion: 'assertion-secret', claimToken: 'claim-secret', }; function respond(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status }); } afterEach(() => vi.unstubAllGlobals()); describe('agent authentication', () => { it('preserves caller cancellation alongside the request timeout', async () => { const controller = new AbortController(); let requestSignal: AbortSignal | undefined; vi.stubGlobal( 'fetch', vi.fn(async (_url, init: RequestInit) => { requestSignal = init.signal ?? undefined; return respond({}); }) ); await agentRequest('https://api.example.com', { signal: controller.signal }); expect(requestSignal?.aborted).toBe(false); controller.abort(); expect(requestSignal?.aborted).toBe(true); }); it('discovers endpoints from Mintlify and validates their origin', async () => { const request = vi .fn() .mockResolvedValueOnce(respond({ authorization_servers: [issuer] })) .mockResolvedValueOnce(respond(discovery)); vi.stubGlobal('fetch', request); expect(await discoverAgentAuth('https://api.example.com')).toEqual(discovery); expect(request.mock.calls[0]?.[0]).toBe( 'https://api.example.com/api/agent/.well-known/oauth-protected-resource' ); }); it('does not send credentials to endpoints on a different origin', async () => { vi.stubGlobal( 'fetch', vi .fn() .mockResolvedValueOnce(respond({ authorization_servers: [issuer] })) .mockResolvedValueOnce( respond({ ...discovery, token_endpoint: 'https://other.example.com/token' }) ) ); await expect(discoverAgentAuth('https://api.example.com')).rejects.toThrow('do not match'); }); it('parses the hosted registration response and exchanges without resource', async () => { const request = vi .fn() .mockResolvedValueOnce( respond({ id: 'reg_1', identity: { assertion: credentials.assertion }, claim: { token: credentials.claimToken }, }) ) .mockResolvedValueOnce( respond({ access_token: 'access', token_type: 'Bearer', expires_in: 300 }) ); vi.stubGlobal('fetch', request); expect(await registerAgent(discovery)).toEqual(credentials); expect((await exchangeAgentToken(discovery, credentials)).accessToken).toBe('access'); const body = request.mock.calls[1]?.[1].body; expect(body.get('assertion')).toBe(credentials.assertion); expect(body.has('resource')).toBe(false); }); it('sends the agent token without reading human credentials', async () => { const request = vi .fn() .mockResolvedValue(respond({ registrationId: 'reg_1', identityType: 'anonymous' })); vi.stubGlobal('fetch', request); await getAgentIdentity('https://api.example.com', 'agent-token'); expect(request.mock.calls[0]?.[1].headers.Authorization).toBe('Bearer agent-token'); }); it('does not register again on outages or print upstream secrets', async () => { const request = vi .fn() .mockResolvedValue(respond({ error: 'unavailable', message: credentials.assertion }, 503)); vi.stubGlobal('fetch', request); await expect(exchangeAgentToken(discovery, credentials)).rejects.toThrow(AgentApiError); expect(request).toHaveBeenCalledTimes(1); }); it('rejects malformed successful responses without exposing them', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue(respond({ assertion: credentials.assertion })) ); await expect(registerAgent(discovery)).rejects.toThrow('unexpected response'); }); });