SPB Git

Repository Public

prod.test.ts 3.5 KB 66 lines typescript

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
Files main
Menu

Theme