confluence_web/frontend/dev/mock-server.js
Artur Mukhamadiev a9908a533f frontend: web UI track handoff (contract revision 1)
Static frontend with vendored marked/DOMPurify, bounded Markdown
pipeline, same-origin mock server with scenario selection, unit,
contract and CDP end-to-end tests under frontend/**.
2026-09-14 21:57:54 +03:00

824 lines
29 KiB
JavaScript

/**
* 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:',
'<script>alert("xss-script")</script>',
'<img src="https://attacker.example.com/beacon.png" onerror="alert(\'xss-img\')">',
'<style>body { display: none !important; }</style>',
'<iframe src="https://attacker.example.com"></iframe>',
'<form action="https://attacker.example.com"><input type="submit" value="Phish"></form>',
'',
'Harmful links:',
'- [JavaScript link](javascript:alert("xss-link"))',
'- [Data URI link](data:text/html,<script>alert(1)</script>)',
'- [Safe citation link](https://approved.example.com/safe/page)',
'',
'```html',
'<script>console.log("safe inside code fence");</script>',
'```'
].join('\n'),
pages_accessed: [
{
page_id: '777',
title: '<script>alert("xss-title")</script> 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: '<script>alert("xss-param")</script>' },
parameters_truncated: false,
started_at: nowIso,
completed_at: nowIso,
status: 'success',
cache_hit: false,
result: {
raw_payload: '<img src=x onerror=alert(1)>',
nested: { malicious: '<script>evil()</script>' }
},
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 = `
<!-- Mock Server Dev Toolbar (Dev Only) -->
<link rel="stylesheet" href="/dev/scenario-toolbar.css">
<aside id="dev-scenario-bar">
<span id="dev-scenario-label">Mock Scenario:</span>
<select id="dev-scenario-select">
${SCENARIOS.map((s) => `<option value="${s}" ${s === activeScenario ? 'selected' : ''}>${s}</option>`).join('')}
</select>
</aside>
<script type="module" src="/dev/scenario-toolbar.js"></script>
`;
const modifiedHtml = htmlContent.replace('</body>', `${devBar}</body>`);
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}`);
});
}