import express from 'express'; import type { AddressInfo } from 'node:net'; import { afterEach, describe, expect, it } from 'vitest'; import { createDashboardAuth, createDashboardSessionRouter } from '../src/web/auth.js'; import { WebServer } from '../src/web/server.js'; import { ConfigSchema } from '../src/config/index.js'; const servers: Array> = []; async function request( app: express.Express, path: string, init: RequestInit = {}, ): Promise { const server = app.listen(0); servers.push(server); await new Promise((resolve) => server.once('listening', resolve)); const { port } = server.address() as AddressInfo; return fetch(`http://127.0.0.1:${port}${path}`, init); } afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve())))); }); describe('dashboard authentication', () => { it('rejects protected dashboard and API requests without a token', async () => { const app = express(); app.use(createDashboardAuth('dashboard-secret')); app.get('/api/stats', (_req, res) => res.json({ ok: true })); const response = await request(app, '/api/stats'); expect(response.status).toBe(401); expect(await response.json()).toEqual({ error: 'Dashboard authentication required' }); }); it('accepts a bearer token without exposing it in the URL', async () => { const app = express(); app.use(createDashboardAuth('dashboard-secret')); app.get('/api/stats', (_req, res) => res.json({ ok: true })); const response = await request(app, '/api/stats', { headers: { Authorization: 'Bearer dashboard-secret' }, }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ ok: true }); }); it('limits agent bearer tokens to their declared access', async () => { const app = express(); app.use(express.json()); app.use(createDashboardAuth('dashboard-secret', { readToken: 'agent-read-secret', controlToken: 'agent-control-secret', })); app.get('/api/v1/overview', (_req, res) => res.json({ ok: true })); app.post('/api/v1/control/start', (_req, res) => res.json({ ok: true })); app.post('/api/conversations/start', (_req, res) => res.json({ ok: true })); const read = await request(app, '/api/v1/overview', { headers: { Authorization: 'Bearer agent-read-secret' } }); const readMutation = await request(app, '/api/v1/control/start', { method: 'POST', headers: { Authorization: 'Bearer agent-read-secret' } }); const control = await request(app, '/api/v1/control/start', { method: 'POST', headers: { Authorization: 'Bearer agent-control-secret' } }); const legacyMutation = await request(app, '/api/conversations/start', { method: 'POST', headers: { Authorization: 'Bearer agent-control-secret' } }); expect(read.status).toBe(200); expect(readMutation.status).toBe(403); expect(control.status).toBe(200); expect(legacyMutation.status).toBe(403); }); it('issues an HttpOnly session only for a schema-valid token payload', async () => { const app = express(); app.use(express.json()); app.use(createDashboardSessionRouter('dashboard-secret')); const invalid = await request(app, '/auth/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: 123 }), }); const valid = await request(app, '/auth/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: 'dashboard-secret' }), }); expect(invalid.status).toBe(400); expect(valid.status).toBe(204); expect(valid.headers.get('set-cookie')).toMatch(/HttpOnly/); expect(valid.headers.get('set-cookie')).toMatch(/SameSite=Strict/); }); it('allows the login page to create a same-origin session under its CSP', async () => { const app = express(); app.use(createDashboardSessionRouter('dashboard-secret')); const response = await request(app, '/auth/login'); expect(response.headers.get('content-security-policy')).toContain("connect-src 'self'"); }); it('does not mount debug endpoints unless explicitly enabled', async () => { const config = ConfigSchema.parse({ reddit: { subreddits: ['test'] }, openrouter: { apiKey: 'key', imageAnalysisPrompt: 'test', messageGenerationPrompt: 'test' }, web: { enabled: true, port: 0, authToken: 'dashboard-secret-token' }, }); const activityRepo = { getRecent: () => [], insert: () => undefined, prune: () => undefined, countSentSkipped: () => 0 }; const server = new WebServer( 0, config, {} as never, {} as never, {} as never, activityRepo as never, { info: () => undefined } as never, ); server.start(); const port = server.getListeningPort(); expect(port).not.toBeNull(); const response = await fetch(`http://127.0.0.1:${port}/api/debug/cookies`, { headers: { Authorization: 'Bearer dashboard-secret-token' }, }); expect(response.status).toBe(404); expect(await response.json()).toEqual({ error: 'API endpoint not found' }); server.stop(); }); });