import test from 'node:test'; import assert from 'node:assert/strict'; import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts'; import { collectOne, selectReadonlyOperations, validateReadonlyOperation, validateInstanceOrigin } from '../scripts/collector.ts'; import { loadInstances } from '../scripts/collect-readonly-fixtures.ts'; import { redact, redactOperationPayload, parseResponseSensitivePath } from '../src/redactor.ts'; const health=upstream58e2204Operations.find((x:any)=>x.operationId==='getHealth')!; const fakeResponse=async()=>new Response('{"ok":true}',{headers:{'content-type':'application/json'}}); test('operation validator rejects path and registry identity bypasses', async()=>{ const attacks=['https://example.invalid/api/health','//example.invalid/api/health','/api/health?scan=1','/api/health#x','/api/%73can','/api/%252e%252e/health','/api/a/../health','/api\\health','/api//health']; for(const pathTemplate of attacks) assert.throws(()=>validateReadonlyOperation({...health,pathTemplate})); assert.throws(()=>validateReadonlyOperation({...health})); assert.throws(()=>validateReadonlyOperation({operationId:health.operationId,method:'GET',riskLevel:'R0',pathTemplate:health.pathTemplate})); await assert.rejects(()=>collectOne({origin:'http://192.168.1.2',alias:'instance-1'},{...health},fakeResponse)); }); test('all path bypass classes fail before transport',async()=>{ let calls=0;const transport=async()=>{calls++;return new Response('{}',{headers:{'content-type':'application/json'}})}; const attacks=['https://evil.invalid/api/health','//evil.invalid/api/health','/api/health?q=1','/api/health#x','/api/%68ealth','/api/%252e%252e/x','/api/a/../x','/api\\health','/api//health']; for(const pathTemplate of attacks) await assert.rejects(()=>collectOne({origin:'http://192.168.1.2',alias:'instance-1'},{...health,pathTemplate},transport)); assert.equal(calls,0); assert.throws(()=>selectReadonlyOperations([{...health}])); }); test('collectOne validates RFC1918 origin before transport',()=>{ for(const origin of ['http://127.0.0.1','http://169.254.1.1','http://100.100.100.200','http://8.8.8.8','http://router.local','http://u:p@192.168.1.2','http://192.168.1.2/x','http://192.168.1.2?q=1','ftp://192.168.1.2']) assert.throws(()=>validateInstanceOrigin(origin)); assert.equal(validateInstanceOrigin('https://172.16.2.3:8443'),'https://172.16.2.3:8443'); }); test('passive selection explicitly denies connectivity and active scan',()=>{ const {selected,denied}=selectReadonlyOperations(upstream58e2204Operations); assert.ok(denied.some((x:any)=>x.pathTemplate==='/api/connectivity')); assert.ok(denied.some((x:any)=>x.pathTemplate==='/api/network/operators/scan')); assert.ok(!selected.some((x:any)=>x.pathTemplate==='/api/connectivity')); }); test('config uses formal validation plus collector LAN/auth/origin constraints',()=>{ const good={instances:[{id:'a',url:'http://192.168.1.2',auth:{mode:'none'}},{id:'b',url:'https://10.0.0.2',auth:{mode:'none'}}]}; assert.deepEqual(loadInstances(good).map((x:any)=>x.alias),['instance-1','instance-2']); const bad=['http://127.0.0.1','http://169.254.1.2','http://100.100.100.200','http://8.8.8.8','http://router.local','http://u:***@192.168.1.2','http://192.168.1.2/path','http://192.168.1.2?q=1','http://192.168.1.2#x','http://[::1]']; for(const url of bad) assert.throws(()=>loadInstances({instances:[{id:'a',url},{id:'b',url:'http://10.0.0.2'}]})); assert.throws(()=>loadInstances({instances:[{id:'a',url:'http://192.168.1.2',auth:{password:'x'}},{id:'b',url:'http://10.0.0.2'}]})); assert.throws(()=>loadInstances({instances:[{id:'a',url:'http://192.168.1.2'}]})); assert.throws(()=>loadInstances({instances:[{id:'a',url:'http://192.168.1.2'},{id:'b',url:'http://10.0.0.2'},{id:'c',url:'http://10.0.0.3'}]})); }); test('directional response JSONPaths redact every declared path and preserve shape/types',()=>{ const ops=upstream58e2204Operations.filter((op:any)=>op.sensitiveFields.some((f:any)=>f.direction==='response')); assert.ok(ops.length>0); for(const op of ops) for(const field of op.sensitiveFields.filter((f:any)=>f.direction==='response')){ const parts=parseResponseSensitivePath(field.path); assert.ok(parts.length>0,`${op.operationId} unsupported response path`); let leaf:any={s:'private',n:42,b:true,z:null,a:['x',{q:9}]}; let payload:any=leaf; for(let i=parts.length-1;i>=0;i--){const p=parts[i]; payload=p==='*'?[payload]:{[p]:payload};} const out:any=redactOperationPayload(payload,op); let hit=out; for(const p of parts) hit=p==='*'?hit[0]:hit[p]; assert.deepEqual(hit,{s:'[REDACTED]',n:0,b:false,z:null,a:['[REDACTED]',{q:0}]}); } }); test('deep sanitizer handles reviewed key/value bypasses without schema destruction',()=>{ const input:any={credential:'x',api_key:'x',access_key:'x',device_key:'x',private_key:'x',pin:12,puk:34,username:'x',user:'x',account:'x',serial:'x',revision:'x',path:'/private/a',template:'user=x token=abc123 path=/private/a',config:{access_token:'tiny'},phone_number_is_manual:true,sms_center_is_manual:false,message:'API is healthy',nested:'{"username":"x","enabled":true}',host:'private.internal:8080',short_number:'12345678',split_id:'12-34-56-78-90-12-34',ipv6:'::1%lo0',latitude:1.234567,lon:2.345678,cell_id:99,lac:2,tac:3,pci:4,enb:5,gnb:6,uptime:777,traffic:888,timestamp:'2026-07-16T12:34:56Z',unknown:'AKIAIOSFODNN7EXAMPLE'}; const x:any=redact(input); for(const k of ['credential','api_key','access_key','device_key','private_key','username','user','account','serial','revision','path','template','host','short_number','split_id','ipv6','timestamp','unknown']) assert.equal(x[k],'[REDACTED]'); for(const k of ['pin','puk','latitude','lon','cell_id','lac','tac','pci','enb','gnb','uptime','traffic']) assert.equal(x[k],0); assert.equal(typeof x.nested,'string'); assert.deepEqual(JSON.parse(x.nested),{username:'[REDACTED]',enabled:true}); assert.deepEqual(x.config,{access_token:'[REDACTED]'}); assert.equal(x.phone_number_is_manual,true); assert.equal(x.sms_center_is_manual,false); assert.equal(x.message,'API is healthy'); }); test('schema shapes and primitive types survive conservative redaction',()=>{ const x:any=redact({phone_numbers:['12345678','87654321'],ip_addresses:[{v4:'192.168.1.9',active:true}],urls:['private.example/x'],webhook:{url:'//private.example/h',enabled:true},config:{s:'secret',n:42,b:true,z:null,a:['x',{n:9}]}}); assert.ok(Array.isArray(x.phone_numbers));assert.equal(x.phone_numbers.length,2); assert.ok(Array.isArray(x.ip_addresses));assert.equal(typeof x.ip_addresses[0],'object'); assert.ok(Array.isArray(x.urls));assert.equal(typeof x.webhook,'object'); assert.deepEqual(x.config,{s:'[REDACTED]',n:0,b:false,z:null,a:['[REDACTED]',{n:0}]}); }); test('invalid declared JSON and non-JSON bodies are non-json and discarded',async()=>{ const invalid=await collectOne({origin:'http://192.168.1.2',alias:'instance-1'},health,async()=>new Response('{bad',{status:200,headers:{'content-type':'application/json'}})); assert.equal(invalid.statusCategory,'non-json');assert.deepEqual(invalid.payload,{error:'invalid-json'}); const text=await collectOne({origin:'http://192.168.1.2',alias:'instance-1'},health,async()=>new Response('private body',{status:200,headers:{'content-type':'text/plain'}})); assert.equal(text.statusCategory,'non-json');assert.deepEqual(text.payload,{text:'[REDACTED]'}); }); test('collector errors never echo rejected origin values',async()=>{ const secret='http://user:password@evil.invalid/private?token=x'; try{await collectOne({origin:secret,alias:'instance-1'},health,fakeResponse);assert.fail('expected rejection');}catch(error:any){assert.ok(!String(error.message).includes(secret));assert.ok(!String(error.message).includes('password'));} });