import { createServer } from 'node:net'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const capabilities = 'asset_search,asset_valuation'; const safeEnv = { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: process.env.HOME ?? '/tmp', TMPDIR: process.env.TMPDIR ?? '/tmp' }; async function freePort(): Promise { return await new Promise((resolve, reject) => { const probe = createServer(); probe.once('error', reject); probe.listen(0, '127.0.0.1', () => { const address = probe.address(); const port = typeof address === 'object' && address ? address.port : 0; probe.close((error) => error ? reject(error) : resolve(port)); }); }); } const port = await freePort(); const base = `http://127.0.0.1:${port}`; const releaseId = `gate-${crypto.randomUUID()}`; const referenceDir = await mkdtemp(join(tmpdir(), 'eden-gate-reference-')); let server: Bun.Subprocess | undefined; let trap: ReturnType | undefined; async function run(args: string[], env: Record): Promise { const child = Bun.spawn(args, { env: { ...safeEnv, ...env }, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' }); if (await child.exited !== 0) throw new Error(`Command failed: ${args.join(' ')}`); } async function waitForServer(baseUrl: string, expectedReleaseId: string): Promise { const deadline = Date.now() + 30_000; while (Date.now() < deadline) { try { const response = await fetch(baseUrl); if (response.status === 200 && response.headers.get('x-application-release') === expectedReleaseId) return; } catch { /* startup race */ } await Bun.sleep(250); } throw new Error('Rollout-gate smoke server did not become ready'); } async function assertStatus(response: Response, expected: number, label: string): Promise { if (response.status !== expected) throw new Error(`${label}: expected ${expected}, got ${response.status}; ${await response.clone().text()}`); } try { await run(['bun', 'workers/reference.ts', '--fixture', '--directory', referenceDir], { APP_MODE: 'demo', REFERENCE_DIR: referenceDir }); server = Bun.spawn(['bun', 'build/index.js'], { env: { ...safeEnv, APP_MODE: 'demo', APP_ORIGIN: base, ORIGIN: base, HOST: '127.0.0.1', PORT: String(port), REFERENCE_DIR: referenceDir, APP_RELEASE_ID: releaseId, APP_CAPABILITY_ALLOWLIST: capabilities, TYPESAFE_API_KEY: 'sentinel-not-used-when-assistant-is-disabled', }, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore', }); await waitForServer(base, releaseId); const headers = { origin: base, 'content-type': 'application/json' }; const chat = await fetch(`${base}/api/chat`, { method: 'POST', headers, body: JSON.stringify({ message: '/value', requestId: crypto.randomUUID() }) }); await assertStatus(chat, 503, 'assistant dispatch'); if (!(await chat.text()).includes('assistant_dispatch')) throw new Error('assistant gate reason missing'); const sync = await fetch(`${base}/api/sync`, { method: 'POST', headers: { origin: base } }); await assertStatus(sync, 503, 'asset sync'); const token = await fetch(`${base}/api/session/token`); await assertStatus(token, 503, 'session token'); const characters = await fetch(`${base}/characters`); await assertStatus(characters, 200, 'character page'); if (!(await characters.text()).includes('identity')) throw new Error('identity gate reason missing'); const oauth = await fetch(`${base}/auth/eve`, { method: 'POST', headers: { origin: base, 'content-type': 'application/x-www-form-urlencoded' }, redirect: 'manual' }); await assertStatus(oauth, 503, 'OAuth initiation'); const callback = await fetch(`${base}/auth/eve/callback?state=synthetic`, { redirect: 'manual' }); await assertStatus(callback, 303, 'OAuth callback'); if (!callback.headers.get('location')?.includes('auth=disabled')) throw new Error('OAuth callback gate reason missing'); server.kill('SIGTERM'); await server.exited; server = undefined; // Live-mode trap: a synthetic session cookie makes any accidental session() // call observable without contacting Convex or a provider. let trapCalls = 0; trap = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => { trapCalls++; return new Response('null', { headers: { 'content-type': 'application/json' } }); } }); const livePort = await freePort(); const liveBase = `http://127.0.0.1:${livePort}`; const liveRelease = `gate-live-${crypto.randomUUID()}`; server = Bun.spawn(['bun', 'build/index.js'], { env: { ...safeEnv, APP_MODE: 'live', APP_ORIGIN: liveBase, ORIGIN: liveBase, HOST: '127.0.0.1', PORT: String(livePort), REFERENCE_DIR: referenceDir, APP_RELEASE_ID: liveRelease, APP_CAPABILITY_ALLOWLIST: '', CONVEX_URL: `http://127.0.0.1:${trap.port}`, PUBLIC_CONVEX_URL: `http://127.0.0.1:${trap.port}`, CONVEX_BRIDGE_SECRET: 'synthetic-bridge-secret', TYPESAFE_API_KEY: 'sentinel-not-used', }, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore', }); await waitForServer(liveBase, liveRelease); const cookie = { cookie: 'eve_session=synthetic-session' }; const liveRoot = await fetch(liveBase, { headers: cookie }); await assertStatus(liveRoot, 200, 'authenticated fully-disabled root'); const liveChat = await fetch(`${liveBase}/api/chat`, { method: 'POST', headers: { ...headers, origin: liveBase, ...cookie }, body: JSON.stringify({ message: '/value', requestId: crypto.randomUUID() }) }); await assertStatus(liveChat, 503, 'authenticated chat gate'); const liveSync = await fetch(`${liveBase}/api/sync`, { method: 'POST', headers: { origin: liveBase, ...cookie } }); await assertStatus(liveSync, 503, 'authenticated sync gate'); const liveCharacters = await fetch(`${liveBase}/characters`, { headers: cookie }); await assertStatus(liveCharacters, 200, 'authenticated character gate'); const liveAssets = await fetch(`${liveBase}/assets`, { headers: cookie }); await assertStatus(liveAssets, 200, 'authenticated asset gate'); if (trapCalls !== 0) throw new Error(`disabled route performed ${trapCalls} Convex session calls`); console.log(`Rollout-gate HTTP smoke passed: anonymous and authenticated disabled paths avoided provider/session calls`); } finally { if (server) { server.kill('SIGTERM'); await server.exited; } trap?.stop(); await rm(referenceDir, { recursive: true, force: true }); }