/** * 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' ]; /** * Standard Security Headers */ const SECURITY_HEADERS = { 'Content-Security-Policy': "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'none'; 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); if (!cookies.cw_session) { const newSession = crypto.randomBytes(16).toString('hex'); res.setHeader('Set-Cookie', `cw_session=${newSession}; 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; } } // API Endpoint 1: POST /api/v1/auth/verify 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); // 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('', `${devBar}`); res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': Buffer.byteLength(modifiedHtml) }); res.end(modifiedHtml); }); return; } // Other static files res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stats.size }); if (method === 'HEAD') { res.end(); return; } const stream = fs.createReadStream(filePath); stream.pipe(res); }); return; } res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('Method Not Allowed'); }); return server; } // If run directly from CLI if (process.argv[1] === fileURLToPath(import.meta.url)) { const portArgIdx = process.argv.indexOf('--port'); const port = portArgIdx !== -1 ? parseInt(process.argv[portArgIdx + 1], 10) : (parseInt(process.env.PORT, 10) || DEFAULT_PORT); const server = createMockServer(); server.listen(port, HOST, () => { console.log(`Mock server running at http://${HOST}:${port}/`); console.log(`Current default scenario: ${currentGlobalScenario}`); }); }