import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile, readdir, stat } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import path from 'node:path'; import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts'; import { collectOne, selectReadonlyOperations, validateReadonlyOperation, DENY_REASONS, } from '../scripts/collector.ts'; import { redact } from '../src/redactor.ts'; import { payloadShape, validateShapeNode, validateSensitivePayload, validateStatusContract, } from '../src/verifier.ts'; const root = path.resolve(import.meta.dirname, '..'); const fixtureRoot = path.join(root, 'src/simadmin'); const allowedEnvelope = [ 'schemaVersion', 'upstreamBaseline', 'capturedAt', 'sourceInstanceAlias', 'operationId', 'method', 'pathTemplate', 'statusCategory', 'httpStatus', 'contentType', 'latencyBucket', 'redacted', 'payload', ]; // Preserve business-schema keys such as `url` and channel `headers`; sensitive values // are typed-redacted. The exact top-level envelope already excludes transport metadata. const forbiddenKeys = /^(raw(response)?|instance(id|name))$/i; const DENIED_PATHS = new Set(Object.keys(DENY_REASONS)); const leakPatterns = [ /https?:\/\//i, /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, /\b(?:\d{1,3}\.){3}\d{1,3}\b/, /(?:[0-9a-f]{2}:){5}[0-9a-f]{2}/i, /\+?\d(?:[ ()-]*\d){9,14}/, /\b\d{14,22}\b/, /\b(?:bearer\s+)?[A-Za-z0-9_-]{32,}\b/i, ]; async function jsonFiles(dir: string): Promise { const out: string[] = []; for (const e of await readdir(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isDirectory()) out.push(...(await jsonFiles(p))); else if (e.name.endsWith('.json')) out.push(p); } return out; } function walkKeys(v: any, cb: (k: string) => void) { if (Array.isArray(v)) return v.forEach((x) => walkKeys(x, cb)); if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) { cb(k); walkKeys(x, cb); } } function stringLeaves(v: any, out: string[] = []) { if (Array.isArray(v)) v.forEach((x) => stringLeaves(x, out)); else if (v && typeof v === 'object') Object.values(v).forEach((x) => stringLeaves(x, out)); else if (typeof v === 'string' && v.length >= 3) out.push(v); return out; } test('selector is registry-derived passive GET R0 only with explicit denials', () => { const { selected, denied } = selectReadonlyOperations(upstream58e2204Operations); assert.ok(selected.length > 20); assert.ok( denied.some((x: any) => x.pathTemplate === '/api/network/operators/scan' && x.denyReason), ); for (const op of selected) { assert.equal(op.method, 'GET'); assert.equal(op.riskLevel, 'R0'); assert.doesNotMatch(op.pathTemplate, /[{}]/); assert.notEqual(op.pathTemplate, '/api/network/operators/scan'); } }); test('collector source has no write-method or authentication escape hatch', async () => { const source = await readFile(path.join(root, 'scripts/collect-readonly-fixtures.ts'), 'utf8'); assert.doesNotMatch(source, /ensureAuthenticated|login\s*\(|--method|--path|--origin/i); assert.doesNotMatch(source, /['"](?:POST|PUT|PATCH|DELETE)['"]/); }); test('transport can only receive credential-free GET, manual redirect and timeout', async () => { let init: any; const fake = async (_url: any, i: any) => { init = i; return new Response( JSON.stringify({ phone: '+15551234567', message: 'private', ip: '10.1.2.3' }), { status: 200, headers: { 'content-type': 'application/json', 'set-cookie': 'bad=1' } }, ); }; const op = upstream58e2204Operations.find((x) => x.operationId === 'getHealth')!; const f = await collectOne( { origin: 'http://192.168.1.2', alias: 'instance-1' }, op, fake as any, ); assert.deepEqual( { method: init.method, body: init.body, redirect: init.redirect, credentials: init.credentials, }, { method: 'GET', body: undefined, redirect: 'manual', credentials: 'omit' }, ); assert.deepEqual(init.headers, { accept: 'application/json' }); assert.ok(init.signal); assert.equal(f.payload.phone, '[REDACTED]'); assert.equal(f.payload.message, 'private'); assert.equal(f.payload.ip, '[REDACTED]'); }); test('redactor covers sensitive keys and value patterns deterministically', () => { const x = redact({ password: 'p', TOKEN: 't', content: 'body', phone: 'x', other: [ '+155****4567', '+155****4567', 'a@b.example', 'https://private.example/x', 'aa:bb:cc:dd:ee:ff', '2001:db8::1', ], }); assert.equal(x.password, '[REDACTED]'); assert.equal(x.TOKEN, '[REDACTED]'); assert.equal(x.content, '[REDACTED]'); assert.equal(x.phone, '[REDACTED]'); for (const p of leakPatterns) assert.doesNotMatch(JSON.stringify(x), p); }); test('Registry operation identity also rejects in-place structural mutation', () => { const op: any = upstream58e2204Operations.find((x) => x.operationId === 'getHealth')!; const original = structuredClone(op); const mutations = [ (x: any) => x.sensitiveFields.push({ direction: 'response', path: '$.response.data.secret' }), (x: any) => { delete x.pathTemplate; }, (x: any) => { x.method = 'POST'; }, (x: any) => { x.sensitiveFields = 'changed'; }, ]; for (const mutate of mutations) { try { mutate(op); assert.throws(() => validateReadonlyOperation(op), /Registry integrity/); } finally { for (const key of Object.keys(op)) delete op[key]; Object.assign(op, structuredClone(original)); } } validateReadonlyOperation(op); }); test('collector categorizes status and JSON parse outcomes consistently', async () => { const op = upstream58e2204Operations.find((x) => x.operationId === 'getHealth')!; const run = (status: number, type: string, body: string | null) => collectOne( { origin: 'http://192.168.1.2', alias: 'instance-1' }, op, async () => new Response(body, { status, headers: { 'content-type': type } }), ); for (const [status, type, body, category] of [ [200, 'application/problem+json', '{}', 'success'], [200, 'application/json', 'bad', 'non-json'], [204, 'text/plain', null, 'non-json'], [401, 'application/json', '{}', 'auth-required'], [405, 'application/json', '{}', 'unsupported'], [500, 'application/json', '{}', 'http-error'], ] as const) { const f = await run(status, type, body); assert.equal(f.statusCategory, category); validateStatusContract(f); } const redirect = await run(302, 'application/json', '{}'); assert.equal(redirect.statusCategory, 'http-error'); validateStatusContract(redirect); assert.throws(() => validateStatusContract({ statusCategory: 'success', httpStatus: 401, contentType: 'application/json', payload: {}, latencyBucket: '<250ms', }), ); }); test('real fixtures satisfy envelope, registry, leak scan, and manifest integrity', async () => { const manifest = JSON.parse(await readFile(path.join(root, 'src/manifest.json'), 'utf8')); assert.deepEqual(Object.keys(manifest).sort(), [ 'domainCoverage', 'files', 'realFixtureCount', 'schemaVersion', 'shapeBaseline', 'syntheticFixtureCount', 'upstreamBaseline', ]); const files = (await jsonFiles(fixtureRoot)).filter((x) => !x.includes('/synthetic-errors/')); const srcEntries = await readdir(path.join(root, 'src')); assert.ok(!srcEntries.includes('synthetic-errors')); const config = JSON.parse(await readFile(path.resolve(root, '../../config.json'), 'utf8')); const privateValues = stringLeaves(config); const expected = selectReadonlyOperations(upstream58e2204Operations).selected; assert.equal(files.length, expected.length * 2); assert.equal(manifest.realFixtureCount, files.length); assert.equal(manifest.syntheticFixtureCount, 0); assert.equal(manifest.schemaVersion, 1); assert.equal(manifest.upstreamBaseline, '58e2204'); const shapePath = path.join(root, 'src/response-shapes-58e2204.json'); const shapeText = await readFile(shapePath, 'utf8'); const shapeBaseline = JSON.parse(shapeText); assert.deepEqual(Object.keys(shapeBaseline).sort(), [ 'schemaVersion', 'shapes', 'upstreamBaseline', ]); assert.equal(shapeBaseline.schemaVersion, 1); assert.equal(shapeBaseline.upstreamBaseline, '58e2204'); assert.equal(Object.keys(shapeBaseline.shapes).length, 78); assert.deepEqual(Object.keys(manifest.shapeBaseline).sort(), ['path', 'sha256', 'size']); assert.equal(manifest.shapeBaseline.path, 'src/response-shapes-58e2204.json'); assert.equal(manifest.shapeBaseline.size, (await stat(shapePath)).size); assert.equal(manifest.shapeBaseline.sha256, createHash('sha256').update(shapeText).digest('hex')); assert.ok(!manifest.files.some((x: any) => x.path === manifest.shapeBaseline.path)); for (const node of Object.values(shapeBaseline.shapes)) validateShapeNode(node); const registry = new Map(upstream58e2204Operations.map((x) => [x.operationId, x])); const domains = new Map(); const pairs = new Set(); const disk = files.map((file) => path.relative(root, file)).sort(); const listed = manifest.files.map((x: any) => x.path).sort(); assert.deepEqual(listed, disk); assert.equal(new Set(listed).size, listed.length); for (const file of files) { const text = await readFile(file, 'utf8'); const f = JSON.parse(text); assert.deepEqual(Object.keys(f).sort(), [...allowedEnvelope].sort()); assert.equal(f.schemaVersion, 1); assert.equal(f.upstreamBaseline, '58e2204'); assert.match(f.capturedAt, /^20(?:2[4-9]|[3-9]\d)-\d{2}-\d{2}$/); assert.ok(!Number.isNaN(Date.parse(`${f.capturedAt}T00:00:00Z`))); assert.ok(['instance-1', 'instance-2'].includes(f.sourceInstanceAlias)); assert.equal(f.redacted, true); assert.equal(f.method, 'GET'); assert.ok(!('synthetic' in f)); assert.ok( [ 'success', 'non-json', 'auth-required', 'unsupported', 'http-error', 'timeout', 'network-error', ].includes(f.statusCategory), ); if (['timeout', 'network-error'].includes(f.statusCategory)) assert.equal(f.httpStatus, null); else assert.ok(Number.isInteger(f.httpStatus)); const op: any = registry.get(f.operationId); assert.ok(op); validateStatusContract(f); validateSensitivePayload(f.payload, op); const shapeKey = `${f.sourceInstanceAlias}::${f.operationId}`; assert.deepEqual(payloadShape(f.payload), shapeBaseline.shapes[shapeKey]); assert.equal(op?.riskLevel, 'R0'); assert.equal(op?.method, 'GET'); assert.equal(op?.pathTemplate, f.pathTemplate); assert.ok(!DENIED_PATHS.has(f.pathTemplate)); domains.set(op.upstreamDomain, (domains.get(op.upstreamDomain) || 0) + 1); const pair = `${f.sourceInstanceAlias}:${f.operationId}`; assert.ok(!pairs.has(pair)); pairs.add(pair); walkKeys(f, (k) => assert.doesNotMatch(k, forbiddenKeys)); for (const p of leakPatterns) assert.doesNotMatch(text, p); for (const value of privateValues) assert.ok(!text.includes(value), 'fixture contains configured value'); const rel = path.relative(root, file); const m = manifest.files.find((x: any) => x.path === rel); assert.ok(m); assert.equal(m.size, (await stat(file)).size); assert.match(m.sha256, /^[a-f0-9]{64}$/); assert.equal(m.sha256, createHash('sha256').update(text).digest('hex')); } assert.deepEqual(Object.fromEntries([...domains].sort()), manifest.domainCoverage); assert.deepEqual( Object.keys(shapeBaseline.shapes).sort(), [...pairs].map((x) => x.replace(':', '::')).sort(), ); assert.deepEqual( [...pairs].sort(), expected .flatMap((op: any) => ['instance-1', 'instance-2'].map((alias) => `${alias}:${op.operationId}`), ) .sort(), ); });