import { createServer } from 'node:net'; import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; const appRoot = resolve(import.meta.dirname, '..'); const entrypoint = resolve(appRoot, 'build/index.js'); const host = '127.0.0.1'; const basePath = process.env.CARTESIUM_BASE_PATH ?? ''; const routePaths = [`${basePath}/embed`, `${basePath}/embed/v1`]; if (basePath !== '' && (!basePath.startsWith('/') || basePath.endsWith('/'))) { throw new Error('CARTESIUM_BASE_PATH must be empty or a slash-prefixed path without a trailing slash.'); } const port = await findFreePort(); const origin = `http://${host}:${port}`; const output = []; const server = spawn(process.execPath, [entrypoint], { cwd: appRoot, env: { ...process.env, HOST: host, PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] }); for (const stream of [server.stdout, server.stderr]) { stream?.on('data', (chunk) => { output.push(chunk.toString()); if (output.length > 40) output.shift(); }); } try { for (const routePath of routePaths) { const response = await waitForResponse(`${origin}${routePath}`, server); if (response.status !== 200) { throw new Error(`GET ${routePath} returned ${response.status}, expected 200.`); } const contentSecurityPolicy = response.headers.get('content-security-policy') ?? ''; if (!/(?:^|\s|;)frame-ancestors\s+\*/i.test(contentSecurityPolicy)) { throw new Error( `GET ${routePath} did not return frame-ancestors * (received ${JSON.stringify(contentSecurityPolicy)}).` ); } if (response.headers.has('x-frame-options')) { throw new Error(`GET ${routePath} returned a conflicting X-Frame-Options header.`); } process.stdout.write(`adapter-node embed smoke passed: ${response.status} ${routePath}\n`); } const resolverPath = `${basePath}/api/embed/v1/at`; const resolverResponse = await fetch(`${origin}${resolverPath}`); if (resolverResponse.status !== 400) { throw new Error(`GET ${resolverPath} returned ${resolverResponse.status}, expected 400.`); } if (resolverResponse.headers.get('cache-control') !== 'no-store') { throw new Error(`GET ${resolverPath} did not return Cache-Control: no-store.`); } const resolverError = await resolverResponse.json(); if (resolverError?.error !== 'invalid-at-uri') { throw new Error(`GET ${resolverPath} did not return the structured invalid-at-uri error.`); } process.stdout.write(`adapter-node resolver smoke passed: ${resolverResponse.status} ${resolverPath}\n`); const removedRoute = await fetch(`${origin}${basePath}/embed/static`); if (removedRoute.status !== 404) { throw new Error(`GET ${basePath}/embed/static returned ${removedRoute.status}, expected 404.`); } process.stdout.write(`adapter-node removed-route smoke passed: ${removedRoute.status} ${basePath}/embed/static\n`); } catch (error) { if (output.length > 0) process.stderr.write(`\nadapter-node output:\n${output.join('')}`); throw error; } finally { server.kill('SIGTERM'); await new Promise((resolveExit) => { if (server.exitCode !== null) { resolveExit(); return; } server.once('exit', resolveExit); setTimeout(() => { if (server.exitCode === null) server.kill('SIGKILL'); resolveExit(); }, 5000).unref(); }); } async function findFreePort() { const probe = createServer(); await new Promise((resolveListen, reject) => { probe.once('error', reject); probe.listen(0, host, resolveListen); }); const address = probe.address(); if (!address || typeof address === 'string') { probe.close(); throw new Error('Unable to determine a free adapter-node port.'); } const { port } = address; await new Promise((resolveClose, reject) => probe.close((error) => (error ? reject(error) : resolveClose()))); return port; } async function waitForResponse(url, child) { const deadline = Date.now() + 30_000; let lastError; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error(`adapter-node exited with status ${child.exitCode}.`); } try { return await fetch(url); } catch (error) { lastError = error; await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); } } throw new Error(`Timed out waiting for ${url}: ${lastError instanceof Error ? lastError.message : lastError}`); }