#!/usr/bin/env node import fs from 'node:fs/promises'; import path from 'node:path'; import mysql from 'mysql2/promise'; import { createLocalGateStack } from '../release-gate/local-stack.mjs'; const root = path.resolve(new URL('..', import.meta.url).pathname); const runId = `gate-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`; const stack = await createLocalGateStack({ root, runId }); const checks = []; function record(id, passed, detail) { checks.push({ id, passed, detail }); console.log(`${passed ? 'PASS' : 'FAIL'} ${id} ${detail}`); } async function register(username, password) { const response = await fetch(`${stack.baseUrl}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, displayName: `Gate ${username}`, email: `${username}@example.invalid`, }), }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(`register ${username}: ${response.status} ${JSON.stringify(body)}`); return body; } async function login(username, password) { const response = await fetch(`${stack.baseUrl}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const body = await response.json().catch(() => ({})); const cookie = response.headers.get('set-cookie')?.split(';')[0] ?? ''; return { response, body, cookie }; } let failed = false; try { const userA = `${runId.replaceAll('-', '').slice(-12)}a`; const userB = `${runId.replaceAll('-', '').slice(-12)}b`; const rateUser = `${runId.replaceAll('-', '').slice(-12)}r`; const password = 'Gate-Local-Only-2026!'; const registeredA = await register(userA, password); const registeredB = await register(userB, password); await register(rateUser, password); const userAId = registeredA.user?.id; const userBId = registeredB.user?.id; if (!userAId || !userBId) throw new Error('registered local users have no stable ids'); const rejectedStatuses = []; for (let attempt = 0; attempt < 5; attempt += 1) { const rejected = await login(rateUser, `wrong-password-${attempt}`); rejectedStatuses.push(rejected.response.status); } const rateLimited = await login(rateUser, password); const retryAfter = Number(rateLimited.response.headers.get('retry-after') ?? 0); record( 'AUTH-02', rejectedStatuses.every((status) => status === 401) && rateLimited.response.status === 429 && retryAfter > 0, `wrong=${rejectedStatuses.join(',')} limited=${rateLimited.response.status} retry_after=${retryAfter}`, ); const authA = await login(userA, password); const authB = await login(userB, password); const authenticatedStatus = await fetch(`${stack.baseUrl}/auth/status`, { headers: { Cookie: authA.cookie }, }); const authenticatedBody = await authenticatedStatus.json().catch(() => ({})); record( 'AUTH-01', authA.response.ok && authA.body.authenticated && Boolean(authA.cookie) && authenticatedStatus.ok && authenticatedBody.authenticated, `login=${authA.response.status} cookie=${Boolean(authA.cookie)} status_api=${authenticatedStatus.status}`, ); await stack.restartPortal(); const statusAfterRestart = await fetch(`${stack.baseUrl}/auth/status`, { headers: { Cookie: authA.cookie }, }); const restartBody = await statusAfterRestart.json().catch(() => ({})); const ghostStatus = await fetch(`${stack.baseUrl}/auth/status`, { headers: { Cookie: 'h5_user_session=synthetic-invalid-session' }, }); const ghostBody = await ghostStatus.json().catch(() => ({})); record( 'AUTH-03', statusAfterRestart.ok && restartBody.authenticated === true && ghostBody.authenticated === false, `valid_after_restart=${Boolean(restartBody.authenticated)} invalid_after_restart=${Boolean(ghostBody.authenticated)}`, ); const syntheticSessionId = `gate-session-${runId}`; const sessionConnection = await mysql.createConnection(stack.mysqlUrl); try { await sessionConnection.execute( `INSERT INTO h5_user_sessions (agent_session_id, user_id, goosed_node, goosed_target, created_at) VALUES (?, ?, 0, NULL, ?)`, [syntheticSessionId, userAId, Date.now()], ); } finally { await sessionConnection.end(); } const crossUserSession = await fetch( `${stack.baseUrl}/api/sessions/${encodeURIComponent(syntheticSessionId)}`, { headers: { Cookie: authB.cookie } }, ); const createPage = await fetch(`${stack.baseUrl}/api/mindspace/v1/pages`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: authA.cookie, }, body: JSON.stringify({ title: 'Gate isolation page', summary: 'Synthetic local-only fixture', content: '

release gate

', template_id: 'article', page_type: 'article', }), }); const pageBody = await createPage.json().catch(() => ({})); const pageId = pageBody?.data?.id; const crossUser = pageId ? await fetch(`${stack.baseUrl}/api/mindspace/v1/pages/${encodeURIComponent(pageId)}`, { headers: { Cookie: authB.cookie }, }) : null; const categoriesResponse = await fetch(`${stack.baseUrl}/api/mindspace/v1/space/categories`, { headers: { Cookie: authA.cookie }, }); const categoriesBody = await categoriesResponse.json().catch(() => ({})); const uploadCategory = (categoriesBody.data ?? []).find( (category) => category.code === 'oa' || category.categoryCode === 'oa', ) ?? (categoriesBody.data ?? []).find( (category) => ['oa', 'public'].includes(category.category_code), ); const uploadContent = Buffer.from('local release gate attachment'); const createUpload = uploadCategory ? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: authA.cookie, }, body: JSON.stringify({ category_id: uploadCategory.id, filename: 'gate-note.txt', size_bytes: uploadContent.length, declared_mime_type: 'text/plain', session_id: syntheticSessionId, }), }) : null; const uploadBody = await createUpload?.json().catch(() => ({})); const uploadId = uploadBody?.data?.id; const writeUpload = uploadId ? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads/${encodeURIComponent(uploadId)}/content`, { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream', Cookie: authA.cookie, }, body: uploadContent, }) : null; const completeUpload = uploadId && writeUpload?.ok ? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads/${encodeURIComponent(uploadId)}/complete`, { method: 'POST', headers: { Cookie: authA.cookie }, }) : null; const assetBody = await completeUpload?.json().catch(() => ({})); const assetId = assetBody?.data?.id; const crossUserAsset = assetId ? await fetch(`${stack.baseUrl}/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`, { headers: { Cookie: authB.cookie }, }) : null; record( 'AUTH-05', crossUserSession.status === 403 && createPage.ok && Boolean(pageId) && [403, 404].includes(crossUser?.status) && completeUpload?.status === 201 && Boolean(assetId) && [403, 404].includes(crossUserAsset?.status), `session=${crossUserSession.status} page_create=${createPage.status} page_cross_user=${crossUser?.status ?? 'not-run'} asset_create=${completeUpload?.status ?? 'not-run'} asset_cross_user=${crossUserAsset?.status ?? 'not-run'}`, ); const logout = await fetch(`${stack.baseUrl}/auth/logout`, { method: 'POST', headers: { Cookie: authA.cookie }, }); const afterLogout = await fetch(`${stack.baseUrl}/api/me`, { headers: { Cookie: authA.cookie }, }); record( 'AUTH-04', logout.ok && [401, 403].includes(afterLogout.status), `logout=${logout.status} old_cookie=${afterLogout.status}`, ); failed = checks.some((check) => !check.passed); await fs.writeFile( path.join(stack.runRoot, 'smoke.json'), `${JSON.stringify({ run_id: runId, checks }, null, 2)}\n`, ); } finally { await stack.cleanup(); } if (failed) process.exit(1);