feat(auth): add PKCE helpers and OAuth callback server for Anthropic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,3 +77,71 @@ describe('auth/anthropic', () => {
|
||||
expect(mod.getAnthropicAuthToken()).toBe('tok-env');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCodeVerifier and generateCodeChallenge', () => {
|
||||
it('generateCodeVerifier returns a 43-char base64url string with no padding', async () => {
|
||||
vi.resetModules();
|
||||
const { generateCodeVerifier } = await import('./anthropic.js');
|
||||
const v = generateCodeVerifier();
|
||||
expect(v).toMatch(/^[A-Za-z0-9_-]+$/);
|
||||
expect(v.length).toBe(43);
|
||||
expect(v).not.toContain('=');
|
||||
});
|
||||
|
||||
it('generateCodeChallenge returns correct SHA256 base64url of the verifier', async () => {
|
||||
vi.resetModules();
|
||||
const { generateCodeVerifier, generateCodeChallenge } = await import('./anthropic.js');
|
||||
const { createHash } = await import('crypto');
|
||||
const verifier = generateCodeVerifier();
|
||||
const challenge = generateCodeChallenge(verifier);
|
||||
const expected = createHash('sha256').update(verifier).digest('base64url');
|
||||
expect(challenge).toBe(expected);
|
||||
expect(challenge).not.toContain('=');
|
||||
expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/);
|
||||
});
|
||||
|
||||
it('generateCodeVerifier produces unique values', async () => {
|
||||
vi.resetModules();
|
||||
const { generateCodeVerifier } = await import('./anthropic.js');
|
||||
const a = generateCodeVerifier();
|
||||
const b = generateCodeVerifier();
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startCallbackServer', () => {
|
||||
it('resolves with code and state when browser redirects to /callback', async () => {
|
||||
vi.resetModules();
|
||||
const { startCallbackServer } = await import('./anthropic.js');
|
||||
const { port, waitForCode } = await startCallbackServer(5000);
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/callback?code=test-code&state=test-state`);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
expect(text.toLowerCase()).toContain('close this tab');
|
||||
|
||||
const { code, state } = await waitForCode;
|
||||
expect(code).toBe('test-code');
|
||||
expect(state).toBe('test-state');
|
||||
});
|
||||
|
||||
it('rejects waitForCode on timeout', async () => {
|
||||
vi.resetModules();
|
||||
const { startCallbackServer } = await import('./anthropic.js');
|
||||
const { waitForCode } = await startCallbackServer(50);
|
||||
await expect(waitForCode).rejects.toThrow(/timed out/i);
|
||||
});
|
||||
|
||||
it('returns 404 for non-callback paths', async () => {
|
||||
vi.resetModules();
|
||||
const { startCallbackServer } = await import('./anthropic.js');
|
||||
const { port, waitForCode } = await startCallbackServer(5000);
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/other`);
|
||||
expect(res.status).toBe(404);
|
||||
|
||||
// Clean up: hit the real callback to resolve
|
||||
await fetch(`http://127.0.0.1:${port}/callback?code=c&state=s`);
|
||||
await waitForCode;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user