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:
William Valentin
2026-02-26 10:32:13 -08:00
parent 6e63e00b84
commit 82f09422d6
2 changed files with 151 additions and 0 deletions
+68
View File
@@ -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;
});
});