#!/usr/bin/env bun import * as fs from 'node:fs'; import { connect } from 'node:net'; import { HttpAssertion, VerificationObservation, object } from './contracts'; interface Config { phase:'before'|'after'; port:number; legitimate:HttpAssertion[]; security:HttpAssertion } function matches(status:number,body:string,oracle:HttpAssertion['expected']):boolean{return status===oracle.status&&(oracle.includes===undefined||body.includes(oracle.includes))&&(oracle.excludes===undefined||!body.includes(oracle.excludes));} export async function boundedResponseBody(response:Response,limit=65536):Promise{ if(!Number.isSafeInteger(limit)||limit<1)throw new Error('invalid response limit'); const declared=response.headers.get('content-length'); if(declared!==null&&(/^\d+$/.test(declared)?Number(declared)>limit:true)){await response.body?.cancel();throw new Error('response too large');} if(!response.body)return''; const reader=response.body.getReader(),chunks:Uint8Array[]=[];let total=0; try{ for(;;){const next=await reader.read();if(next.done)break;if(!next.value)continue;total+=next.value.byteLength;if(total>limit){await reader.cancel();throw new Error('response too large');}chunks.push(next.value);} }finally{reader.releaseLock();} const bytes=new Uint8Array(total);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.byteLength;}return new TextDecoder().decode(bytes); } async function request(a:HttpAssertion,port:number):Promise<{status:number;body:string}>{ const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),5000); try{const response=await fetch(`http://127.0.0.1:${port}${a.path}`,{method:a.method,headers:a.headers,body:['GET'].includes(a.method)?undefined:a.body,redirect:'manual',signal:controller.signal});return{status:response.status,body:await boundedResponseBody(response)};}finally{clearTimeout(timer);} } async function ready(port:number):Promise{return await new Promise(resolve=>{const socket=connect({host:'127.0.0.1',port}),done=(value:boolean)=>{socket.removeAllListeners();socket.destroy();resolve(value);},timer=setTimeout(()=>done(false),500);socket.once('connect',()=>{clearTimeout(timer);done(true);});socket.once('error',()=>{clearTimeout(timer);done(false);});});} async function main(){ const file=process.argv[2];if(!file||!file.startsWith('/policy/'))throw new Error('trusted policy path required');const raw=fs.readFileSync(file,'utf8');if(Buffer.byteLength(raw)>1024*1024)throw new Error('policy too large');const v=object(JSON.parse(raw),'verifier policy') as any; if(!['before','after'].includes(v.phase)||!Number.isInteger(v.port)||v.port<1024||v.port>65535||!Array.isArray(v.legitimate)||!v.security)throw new Error('invalid verifier policy');const config=v as Config; let booted=false;for(let attempt=0;attempt<60;attempt++){if(await ready(config.port)){booted=true;break;}await Bun.sleep(250);} let legitimate=false,security:VerificationObservation['security']='inconclusive',summary='application did not answer a legitimate control'; if(booted){try{legitimate=(await Promise.all(config.legitimate.map(async a=>{const r=await request(a,config.port);return matches(r.status,r.body,a.expected);}))).every(Boolean);const r=await request(config.security,config.port),fixed=matches(r.status,r.body,config.security.expected),vulnerable=matches(r.status,r.body,config.security.vulnerable!);security=config.phase==='before'?(vulnerable&&!fixed?'intended_failure':fixed&&!vulnerable?'pass':'inconclusive'):(fixed&&!vulnerable?'pass':'inconclusive');summary=`boot=true legitimate=${legitimate} security=${security}`;}catch{summary='bounded verifier request failed';}} process.stdout.write(JSON.stringify({booted,legitimate,security,existingTests:false,output:summary,inputHash:''})+'\n'); } if(import.meta.main)main().catch(()=>{process.stdout.write(JSON.stringify({booted:false,legitimate:false,security:'inconclusive',existingTests:false,output:'verifier setup failed',inputHash:''})+'\n');process.exitCode=1;});