Repository Public
1import { afterAll, beforeAll, describe, expect, it } from 'vitest';2import { PASSWORD, createHarness, type Harness } from './testkit/harness.js';34const ORIGIN = 'https://claude.example.org';5let h: Harness;67beforeAll(async () => {8 h = await createHarness({ env: { NODE_ENV: 'production', CR_PUBLIC_ORIGIN: ORIGIN }, tweak: (c) => (c.rateLimit = { ...c.rateLimit, api: 100_000, preAuth: 5 }) });9});10afterAll(async () => {11 await h.close();12});1314describe('production mode', () => {15 it('session cookie is __Host-, Secure, HttpOnly, SameSite=Strict, Path=/ with no Domain', async () => {16 const res = await h.server.app.inject({17 method: 'POST', url: '/api/v1/auth/setup', headers: { origin: ORIGIN, 'content-type': 'application/json' },18 payload: JSON.stringify({ setupToken: h.setupToken(), username: 'admin', password: PASSWORD }),19 });20 expect(res.statusCode).toBe(200);21 const raw = [res.headers['set-cookie']].flat().join('\n');22 expect(raw).toMatch(/^__Host-cr_sid=[A-Za-z0-9_-]{43};/);23 expect(raw).toContain('; Secure');24 expect(raw).toContain('; HttpOnly');25 expect(raw).toContain('; SameSite=Strict');26 expect(raw).toContain('; Path=/');27 expect(raw).not.toMatch(/domain=/i);28 // the dev cookie name is not honoured in production29 const token = /__Host-cr_sid=([^;]+)/.exec(raw)?.[1] as string;30 const dev = await h.server.app.inject({ method: 'GET', url: '/api/v1/auth/status', headers: { cookie: `cr_sid=${token}` } });31 expect(dev.json().user).toBeNull();32 const prod = await h.server.app.inject({ method: 'GET', url: '/api/v1/auth/status', headers: { cookie: `__Host-cr_sid=${token}` } });33 expect(prod.json().user).toMatchObject({ username: 'admin' });34 });35 it('the pre-auth passkey challenge cookie has the same flags', async () => {36 const res = await h.server.app.inject({ method: 'POST', url: '/api/v1/auth/passkey/login/options', headers: { origin: ORIGIN, 'content-type': 'application/json' }, payload: '{}' });37 expect(res.statusCode).toBe(200);38 const raw = [res.headers['set-cookie']].flat().join('\n');39 expect(raw).toMatch(/^__Host-cr_pre=/);40 for (const flag of ['; Secure', '; HttpOnly', '; SameSite=Strict', '; Path=/']) expect(raw).toContain(flag);41 expect(res.json().rpId).toBe('claude.example.org');42 expect(res.json().userVerification).toBe('required');43 });44 it('strict rate limit on the credential-checking endpoints (shared bucket per client IP)', async () => {45 const post = (url: string, remoteAddress: string, payload = '{}') =>46 h.server.app.inject({ method: 'POST', url: `/api/v1${url}`, remoteAddress, headers: { origin: ORIGIN, 'content-type': 'application/json' }, payload });47 let limited = 0;48 for (let i = 0; i < 8; i++) {49 const res = await post('/auth/passkey/login/options', '198.51.100.20');50 if (res.statusCode === 429) {51 limited++;52 expect(res.json().error.code).toBe('rate_limited');53 expect(res.headers['content-security-policy']).toBeDefined();54 }55 }56 expect(limited).toBe(3);57 // One budget for all of them: login, setup and passkey verification are refused too.58 for (const url of ['/auth/login', '/auth/setup', '/auth/passkey/login/verify']) {59 const res = await post(url, '198.51.100.20', JSON.stringify({ username: 'admin', password: PASSWORD }));60 expect([url, res.statusCode, res.json().error.code]).toEqual([url, 429, 'rate_limited']);61 }62 // another client is unaffected63 expect((await post('/auth/passkey/login/options', '198.51.100.21')).statusCode).toBe(200);64 });65});66