/**
* Same-origin Mock HTTP Server for the Confluence Research Web UI.
* Implements exact HTTP contracts, security headers, cookie sessions, and deterministic scenarios.
* Strictly binds loopback (default 5173). Zero external dependencies.
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const FRONTEND_ROOT = path.resolve(__dirname, '..');
const DEFAULT_PORT = 5173;
const HOST = '127.0.0.1';
// Active state
let currentGlobalScenario = 'normal';
let isQueryBusy = false;
// Predefined available scenarios
export const SCENARIOS = [
'normal',
'empty_search',
'no_artifacts',
'repeated_cached_view',
'failed_tool',
'warning_truncated_history',
'403_verify',
'409_busy',
'504_timeout',
'delayed_cancellation',
'unknown_expired_download',
'malicious_content',
'large_output',
'queued',
'queued_no_estimate',
'queue_full',
'reservation_lost',
'ticket_lost'
];
// Admission queue scenarios (docs/QUEUE_SPECIFICATION.md §10) that drive a canned join/status
// dance instead of the immediate "ready" every other scenario answers with.
const QUEUE_SCENARIOS = new Set(['queued', 'queued_no_estimate', 'queue_full', 'reservation_lost', 'ticket_lost']);
const QUEUE_RESERVATION_SECONDS = 45;
// Per-session queue simulation state, keyed by cw_session cookie value. Reset whenever the
// scenario active for that session changes, so switching scenarios mid-flow never leaks stale
// poll/join counters into the new scenario.
const queueSessions = new Map();
/**
* Returns (creating or resetting as needed) the per-session queue simulation record.
* @param {string} sessionId
* @param {string} scenario
*/
function getQueueRecord(sessionId, scenario) {
let rec = queueSessions.get(sessionId);
if (!rec || rec.scenario !== scenario) {
rec = { scenario, ticketId: null, joinCount: 0, pollCount: 0, queryAttempt: 0 };
queueSessions.set(sessionId, rec);
}
return rec;
}
/**
* Generates an opaque ticket id in the shape used by the real backend (CONTRACTS §1).
*/
function makeTicketId() {
return `q_${crypto.randomBytes(6).toString('hex')}`;
}
/**
* Standard Security Headers
*/
const SECURITY_HEADERS = {
'Content-Security-Policy': "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; media-src 'none'; font-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
'Referrer-Policy': 'no-referrer',
'X-Content-Type-Options': 'nosniff'
};
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.md': 'text/markdown; charset=utf-8',
'.map': 'application/json'
};
/**
* Parses cookies from request.
*/
function parseCookies(cookieHeader) {
const list = {};
if (!cookieHeader) return list;
cookieHeader.split(';').forEach((cookie) => {
const parts = cookie.split('=');
if (parts.length >= 2) {
list[parts[0].trim()] = decodeURIComponent(parts.slice(1).join('=').trim());
}
});
return list;
}
/**
* Determines the active scenario for a request.
*/
function getScenarioForRequest(req, urlObj) {
// 1. Query parameter
const qScenario = urlObj.searchParams.get('scenario');
if (qScenario && SCENARIOS.includes(qScenario)) return qScenario;
// 2. Custom header
const hScenario = req.headers['x-mock-scenario'];
if (hScenario && SCENARIOS.includes(hScenario)) return hScenario;
// 3. Cookie
const cookies = parseCookies(req.headers.cookie);
if (cookies.mock_scenario && SCENARIOS.includes(cookies.mock_scenario)) {
return cookies.mock_scenario;
}
// 4. Global fallback
return currentGlobalScenario;
}
/**
* Generates fixture data for the requested scenario.
*/
function buildScenarioResponse(scenario, prompt) {
const now = new Date();
const nowIso = now.toISOString();
const expiresAtIso = new Date(now.getTime() + 15 * 60 * 1000).toISOString();
switch (scenario) {
case 'empty_search':
return {
session_id: 'mock-session-empty',
markdown: '# Research Results\n\nNo Confluence documentation matched your query.',
pages_accessed: [],
tool_history: [
{
tool_call_id: 'b_search_01',
tool: 'confluence_search',
parameters: { query: prompt || 'empty query', limit: 10 },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
pages: [],
pagination: { offset: 0, limit: 10, has_more: false }
},
error: null,
result_truncated: false
}
],
artifacts: [],
warnings: [],
duration_seconds: 1.1
};
case 'no_artifacts':
return {
session_id: 'mock-session-no-art',
markdown: '# Research Summary\n\nInformation gathered from [Deployment Guide](https://approved.example.com/pages/viewpage.action?pageId=847291).\n\nNo artifacts produced.',
pages_accessed: [
{
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
accessed_at: nowIso
}
],
tool_history: [
{
tool_call_id: 'b_view_01',
tool: 'confluence_view',
parameters: { page_id: '847291' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
markdown: 'Deploy service X.',
truncated: false
},
error: null,
result_truncated: false
}
],
artifacts: [],
warnings: [],
duration_seconds: 2.3
};
case 'repeated_cached_view':
return {
session_id: 'mock-session-cached',
markdown: '# Deployment Summary\n\nReferenced [Deployment Guide](https://approved.example.com/pages/viewpage.action?pageId=847291) across multiple steps.',
pages_accessed: [
{
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
accessed_at: nowIso
}
],
tool_history: [
{
tool_call_id: 'b_call_01',
tool: 'confluence_view',
parameters: { page_id: '847291' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
markdown: 'Initial view.',
truncated: false
},
error: null,
result_truncated: false
},
{
tool_call_id: 'b_call_02',
tool: 'confluence_view',
parameters: { page_id: '847291' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: true,
result: {
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
markdown: 'Initial view (from cache).',
truncated: false
},
error: null,
result_truncated: false
}
],
artifacts: [],
warnings: [],
duration_seconds: 1.8
};
case 'failed_tool':
return {
session_id: 'mock-session-failed-tool',
markdown: '# Partial Summary\n\nSearch succeeded, but page 999999 could not be accessed due to an upstream error.',
pages_accessed: [],
tool_history: [
{
tool_call_id: 'b_search_01',
tool: 'confluence_search',
parameters: { query: 'archived docs', limit: 5 },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
pages: [{ page_id: '999999', title: 'Archived Page', space: 'ARCH', url: 'https://approved.example.com/pages/viewpage.action?pageId=999999', snippet: 'Missing page' }],
pagination: { offset: 0, limit: 5, has_more: false }
},
error: null,
result_truncated: false
},
{
tool_call_id: 'b_view_02',
tool: 'confluence_view',
parameters: { page_id: '999999' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'error',
cache_hit: false,
result: null,
error: {
code: 'page_not_found',
message: 'Confluence page 999999 was not found or has been deleted.'
},
result_truncated: false
}
],
artifacts: [],
warnings: [
{ code: 'page_not_found', message: 'Page 999999 access failed', tool_call_id: 'b_view_02' }
],
duration_seconds: 2.7
};
case 'warning_truncated_history':
return {
session_id: 'mock-session-warn-trunc',
markdown: '# Bounded Results\n\nExtensive data retrieved with truncated history logs.',
pages_accessed: [
{
page_id: '12345',
title: 'Large Architecture Document',
space: 'ARCH',
url: 'https://approved.example.com/pages/viewpage.action?pageId=12345',
accessed_at: nowIso
}
],
tool_history: [
{
tool_call_id: 'b_call_trunc_01',
tool: 'confluence_view',
parameters: { page_id: '12345', details: 'A'.repeat(500) },
parameters_truncated: true,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
page_id: '12345',
summary: 'Summary preserved while large raw content was truncated.'
},
error: null,
result_truncated: true
}
],
artifacts: [],
warnings: [
{
code: 'history_truncated',
message: 'History result budget exceeded; 1 entry truncated.',
tool_call_id: 'b_call_trunc_01'
},
{
code: 'unknown_custom_warning',
message: 'Custom backend warning code test.'
}
],
duration_seconds: 4.1
};
case 'malicious_content':
return {
session_id: 'mock-session-malicious',
markdown: [
'# Malicious Input Test',
'',
'Attempting XSS and unsafe content:',
'',
'
',
'',
'',
'
',
'',
'Harmful links:',
'- [JavaScript link](javascript:alert("xss-link"))',
'- [Data URI link](data:text/html,)',
'- [Safe citation link](https://approved.example.com/safe/page)',
'',
'```html',
'',
'```'
].join('\n'),
pages_accessed: [
{
page_id: '777',
title: ' Safe Title',
space: 'SEC',
url: 'https://approved.example.com/page/777',
accessed_at: nowIso
}
],
tool_history: [
{
tool_call_id: 'b_xss_01',
tool: 'confluence_view',
parameters: { page_id: '' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
raw_payload: '
',
nested: { malicious: '' }
},
error: null,
result_truncated: false
}
],
artifacts: [],
warnings: [],
duration_seconds: 1.0
};
case 'large_output': {
// Dynamically generate multi-megabyte markdown with sections, tables, and code blocks
const sectionCount = 40;
const mdParts = ['# Large Document Benchmark\n\nGenerated large output to test bounded section rendering and responsive UI.\n'];
for (let i = 1; i <= sectionCount; i++) {
mdParts.push(`\n## Section ${i}: Architectural Components\n`);
mdParts.push(`This is paragraph content for section ${i} detailing deployment topologies, container security, and protocol isolation.\n`);
mdParts.push('```bash\n# Simulated shell commands\necho "Running container isolation check for section ' + i + '"\nfind /work -type f -ls\n```\n');
mdParts.push('| Component | Status | Metric |\n|---|---|---|\n| Bridge | Active | 100% |\n| Storage | Bounded | 50 MiB |\n| Latency | Nominal | 12ms |\n');
// Repeat text to bulk up section size (~50 KiB per section)
mdParts.push('Confluence documentation analysis paragraph '.repeat(200) + '\n');
}
return {
session_id: 'mock-session-large',
markdown: mdParts.join('\n'),
pages_accessed: [
{
page_id: '99991',
title: 'Enterprise Architecture Overview',
space: 'ARCH',
url: 'https://approved.example.com/pages/viewpage.action?pageId=99991',
accessed_at: nowIso
}
],
tool_history: [
{
tool_call_id: 'b_large_01',
tool: 'confluence_view',
parameters: { page_id: '99991' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
page_id: '99991',
title: 'Enterprise Architecture Overview',
space: 'ARCH',
status: 'ok'
},
error: null,
result_truncated: false
}
],
artifacts: [
{
id: 'art-large-export',
name: 'full_architecture.md',
size_bytes: 32,
expires_at: expiresAtIso
}
],
warnings: [],
duration_seconds: 5.4
};
}
case 'normal':
default:
// Shared example scenario from CONTRACTS.md Section 7
return {
session_id: 'a0f2b3c4-1234-5678-9abc-def012345678',
markdown: '# Deployment Guide for Service X\n\nTo deploy **Service X**, follow the steps outlined in the [Deployment Guide](https://approved.example.com/pages/viewpage.action?pageId=847291).\n\n### Key Steps:\n1. Verify container runtime prerequisites.\n2. Review the checklist exported to `checklist.md`.\n3. Execute staged rollout.\n',
pages_accessed: [
{
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
accessed_at: nowIso
}
],
tool_history: [
{
tool_call_id: 'b_call_01',
tool: 'confluence_search',
parameters: { query: 'deploy service X', limit: 10 },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
pages: [
{
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
snippet: 'Deployment steps for service X'
}
],
pagination: { offset: 0, limit: 10, has_more: false }
},
error: null,
result_truncated: false
},
{
tool_call_id: 'b_call_02',
tool: 'confluence_view',
parameters: { page_id: '847291' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
page_id: '847291',
title: 'Deployment Guide',
space: 'OPS',
url: 'https://approved.example.com/pages/viewpage.action?pageId=847291',
markdown: 'Deploy service X using the release checklist.',
truncated: false
},
error: null,
result_truncated: false
}
],
artifacts: [
{
id: 'art-checklist-01',
name: 'checklist.md',
size_bytes: 32,
expires_at: expiresAtIso
}
],
warnings: [],
duration_seconds: 3.2
};
}
}
/**
* Validates that credentials in mock mode are synthetic/dummy credentials.
*/
function validateMockCredentials(url, pat) {
if (!url || !pat) return false;
// Prevent common production token patterns
if (pat.startsWith('ghp_') || pat.startsWith('glpat-') || pat.startsWith('xoxb-')) {
return false;
}
return true;
}
/**
* Creates the HTTP server.
*/
export function createMockServer() {
const server = http.createServer(async (req, res) => {
const urlObj = new URL(req.url, `http://${req.headers.host || `${HOST}:${DEFAULT_PORT}`}`);
const pathname = urlObj.pathname;
const method = req.method.toUpperCase();
// Attach security headers to all responses
Object.entries(SECURITY_HEADERS).forEach(([key, val]) => {
res.setHeader(key, val);
});
// Session cookie: cw_session
const cookies = parseCookies(req.headers.cookie);
let sessionId = cookies.cw_session;
if (!sessionId) {
sessionId = crypto.randomBytes(16).toString('hex');
res.setHeader('Set-Cookie', `cw_session=${sessionId}; Path=/; HttpOnly; SameSite=Strict`);
}
// Origin check for mutation requests
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
const origin = req.headers.origin;
if (origin) {
try {
const origUrl = new URL(origin);
if (!['127.0.0.1', 'localhost'].includes(origUrl.hostname)) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { code: 'origin_denied', message: 'Cross-origin request denied.' } }));
return;
}
} catch {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { code: 'origin_denied', message: 'Malformed origin.' } }));
return;
}
}
}
// Dev Scenario API (development tooling outside user flow)
if (pathname === '/dev/scenario') {
if (method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ scenario: currentGlobalScenario, available: SCENARIOS }));
return;
}
if (method === 'POST') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
const data = JSON.parse(body);
if (data.scenario && SCENARIOS.includes(data.scenario)) {
currentGlobalScenario = data.scenario;
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Set-Cookie': `mock_scenario=${data.scenario}; Path=/; SameSite=Strict`
});
res.end(JSON.stringify({ ok: true, scenario: currentGlobalScenario }));
} else {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Unknown scenario' } }));
}
} catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Invalid JSON' } }));
}
});
return;
}
}
// Admission Queue Endpoint: POST /api/v1/queue/join
if (pathname === '/api/v1/queue/join' && method === 'POST') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
let data;
try {
data = body ? JSON.parse(body) : {};
} catch {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Invalid JSON body' } }));
return;
}
if (data && typeof data === 'object' && ('credentials' in data || 'pat' in data)) {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Queue endpoints do not accept credentials' } }));
return;
}
const scenario = getScenarioForRequest(req, urlObj);
if (scenario === 'queue_full') {
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'queue_full', message: 'The queue is full, please try later.' } }));
return;
}
if (!QUEUE_SCENARIOS.has(scenario)) {
// Every other scenario answers join with an immediate reservation (spec §10).
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({
ticket_id: makeTicketId(),
status: 'ready',
position: 0,
ahead: 0,
eta_seconds: null,
reservation_expires_in_seconds: QUEUE_RESERVATION_SECONDS,
runner: 'reserved'
}));
return;
}
const rec = getQueueRecord(sessionId, scenario);
rec.joinCount += 1;
if (scenario === 'reservation_lost') {
rec.ticketId = rec.ticketId || makeTicketId();
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({
ticket_id: rec.ticketId,
status: 'ready',
position: 0,
ahead: 0,
eta_seconds: null,
reservation_expires_in_seconds: QUEUE_RESERVATION_SECONDS,
runner: 'reserved'
}));
return;
}
if (scenario === 'ticket_lost') {
rec.ticketId = makeTicketId();
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
if (rec.joinCount === 1) {
rec.pollCount = 0;
res.end(JSON.stringify({
ticket_id: rec.ticketId,
status: 'queued',
position: 2,
ahead: 1,
eta_seconds: 60,
reservation_expires_in_seconds: null,
runner: 'running'
}));
} else {
// The automatic rejoin after the ticket was lost (spec §10, §7.1 step 6).
res.end(JSON.stringify({
ticket_id: rec.ticketId,
status: 'ready',
position: 0,
ahead: 0,
eta_seconds: null,
reservation_expires_in_seconds: QUEUE_RESERVATION_SECONDS,
runner: 'reserved'
}));
}
return;
}
// 'queued' / 'queued_no_estimate': idempotent join, position decrements only on
// status polls (see GET /api/v1/queue/status below).
const noEstimate = scenario === 'queued_no_estimate';
rec.ticketId = rec.ticketId || makeTicketId();
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({
ticket_id: rec.ticketId,
status: 'queued',
position: 3,
ahead: 2,
eta_seconds: noEstimate ? null : 150,
reservation_expires_in_seconds: null,
runner: 'running'
}));
});
return;
}
// Admission Queue Endpoint: GET /api/v1/queue/status
if (pathname === '/api/v1/queue/status' && method === 'GET') {
const scenario = getScenarioForRequest(req, urlObj);
if (!QUEUE_SCENARIOS.has(scenario) || scenario === 'queue_full' || scenario === 'reservation_lost') {
// These scenarios never leave a session polling: non-queue scenarios resolve at join,
// queue_full never creates a ticket, and reservation_lost resolves at join too.
res.writeHead(404, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'ticket_not_found', message: 'No ticket for this session.' } }));
return;
}
const rec = queueSessions.get(sessionId);
if (!rec || rec.scenario !== scenario || !rec.ticketId) {
res.writeHead(404, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'ticket_not_found', message: 'No ticket for this session.' } }));
return;
}
rec.pollCount += 1;
// Build the response body first so exactly one writeHead/end pair is ever sent,
// regardless of which branch (200 queued/ready vs. 404 lost) fires.
let statusCode = 200;
let payload;
if (scenario === 'ticket_lost') {
if (rec.pollCount === 1) {
payload = {
ticket_id: rec.ticketId,
status: 'queued',
position: 1,
ahead: 0,
eta_seconds: 30,
reservation_expires_in_seconds: null,
runner: 'running'
};
} else {
// Second poll: the ticket has expired server-side (spec §10).
rec.ticketId = null;
statusCode = 404;
payload = { error: { code: 'ticket_not_found', message: 'Ticket expired.' } };
}
} else {
// 'queued' / 'queued_no_estimate': decrement position on each poll; ready on the third.
const noEstimate = scenario === 'queued_no_estimate';
if (rec.pollCount === 1) {
payload = {
ticket_id: rec.ticketId,
status: 'queued',
position: 2,
ahead: 1,
eta_seconds: noEstimate ? null : 95,
reservation_expires_in_seconds: null,
runner: 'running'
};
} else if (rec.pollCount === 2) {
payload = {
ticket_id: rec.ticketId,
status: 'queued',
position: 1,
ahead: 0,
eta_seconds: noEstimate ? null : 40,
reservation_expires_in_seconds: null,
runner: 'running'
};
} else {
payload = {
ticket_id: rec.ticketId,
status: 'ready',
position: 0,
ahead: 0,
eta_seconds: null,
reservation_expires_in_seconds: QUEUE_RESERVATION_SECONDS,
runner: 'reserved'
};
}
}
res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify(payload));
return;
}
// Admission Queue Endpoint: DELETE /api/v1/queue/ticket
if (pathname === '/api/v1/queue/ticket' && method === 'DELETE') {
const rec = queueSessions.get(sessionId);
if (rec) {
rec.ticketId = null;
}
res.writeHead(204, { 'Cache-Control': 'no-store' });
res.end();
return;
}
// API Endpoint 1: POST /api/v1/auth/verify
// Non-secret deployment facts: the approved Confluence origins offered as a fixed choice.
if (pathname === '/api/v1/config' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ approved_origins: ['https://approved.example.com'] }));
return;
}
if (pathname === '/api/v1/auth/verify' && method === 'POST') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
const data = JSON.parse(body);
if (!data.url || !data.pat) {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Missing URL or PAT' } }));
return;
}
if (!validateMockCredentials(data.url, data.pat)) {
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'confluence_auth_failed', message: 'Invalid mock credentials' } }));
return;
}
const scenario = getScenarioForRequest(req, urlObj);
if (scenario === '403_verify') {
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'confluence_auth_failed', message: 'Confluence authentication failed: invalid PAT.' } }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ valid: true }));
} catch {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Invalid JSON body' } }));
}
});
return;
}
// API Endpoint 2: POST /api/v1/query
if (pathname === '/api/v1/query' && method === 'POST') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', async () => {
try {
const data = JSON.parse(body);
if (!data.prompt || !data.credentials || !data.credentials.url || !data.credentials.pat) {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Missing required query parameters' } }));
return;
}
if (!validateMockCredentials(data.credentials.url, data.credentials.pat)) {
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'confluence_auth_failed', message: 'Invalid mock credentials' } }));
return;
}
const scenario = getScenarioForRequest(req, urlObj);
// Admission queue scenario: the first query after "ready" loses the reservation
// (simulating network trouble); the client's automatic rejoin-then-retry succeeds
// on the second attempt (spec §10).
if (scenario === 'reservation_lost') {
const rec = getQueueRecord(sessionId, scenario);
rec.queryAttempt += 1;
if (rec.queryAttempt === 1) {
res.writeHead(409, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'busy', message: 'Reservation lost; rejoin the queue.' } }));
return;
}
}
// 409 Busy check
if (scenario === '409_busy' || isQueryBusy) {
res.writeHead(409, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'busy', message: 'A query is currently executing or prior cleanup is in progress' } }));
return;
}
// 504 Timeout check
if (scenario === '504_timeout') {
res.writeHead(504, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'query_timeout', message: 'The query deadline of 180 seconds was exceeded' } }));
return;
}
// Delayed cancellation test scenario
if (scenario === 'delayed_cancellation') {
isQueryBusy = true;
let aborted = false;
const cancelTimeout = setTimeout(() => {
isQueryBusy = false;
if (!aborted && !res.writableEnded) {
const responseData = buildScenarioResponse('normal', data.prompt);
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify(responseData));
}
}, 30000); // 30s delay
req.on('close', () => {
if (!res.writableEnded) {
aborted = true;
clearTimeout(cancelTimeout);
// Simulate backend cleanup budget
setTimeout(() => {
isQueryBusy = false;
}, 400);
}
});
return;
}
// Normal response
const responseData = buildScenarioResponse(scenario, data.prompt);
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify(responseData));
} catch {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'invalid_input', message: 'Malformed JSON query body' } }));
}
});
return;
}
// API Endpoint 3: GET /api/v1/artifacts/{id}
if (pathname.startsWith('/api/v1/artifacts/') && method === 'GET') {
const scenario = getScenarioForRequest(req, urlObj);
const artifactId = pathname.slice('/api/v1/artifacts/'.length);
if (scenario === 'unknown_expired_download' || artifactId === 'expired' || artifactId === 'non-existent') {
res.writeHead(404, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: { code: 'artifact_not_found', message: 'Artifact has expired or does not exist.' } }));
return;
}
// Exact 32 bytes from CONTRACTS.md Section 7
const artifactContent = '# Checklist\n\n- Deploy service X\n';
const buffer = Buffer.from(artifactContent, 'utf-8');
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Length': buffer.length,
'Content-Disposition': 'attachment; filename="checklist.md"',
'Cache-Control': 'no-store'
});
res.end(buffer);
return;
}
// Static File Serving
if (method === 'GET' || method === 'HEAD') {
let relativePath = pathname === '/' ? 'index.html' : pathname.slice(1);
const safePath = path.normalize(relativePath).replace(/^(\.\.[/\\])+/, '');
const filePath = path.join(FRONTEND_ROOT, safePath);
// Security check: ensure path is within FRONTEND_ROOT
if (!filePath.startsWith(FRONTEND_ROOT)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
// For index.html in dev mode, inject a dev scenario selector bar
if (ext === '.html') {
fs.readFile(filePath, 'utf8', (readErr, htmlContent) => {
if (readErr) {
res.writeHead(500);
res.end('Server Error');
return;
}
const activeScenario = getScenarioForRequest(req, urlObj);
// Dev scenario selector toolbar (external CSS and JS to satisfy CSP script-src 'self' style-src 'self')
const devBar = `
`;
const modifiedHtml = htmlContent.replace('