'use strict'; const channel=require('../../packages/core/policy-channel.cjs'); const crypto=require('node:crypto'); function authorization(token,route,body='{}',principal){const time=Date.now(),nonce=crypto.randomBytes(32).toString('base64url');const mac=crypto.createHmac('sha384',token).update(principal?JSON.stringify(['safetynet-agent-auth-v1',route,principal.principalId,principal.generation,time,nonce,crypto.createHash('sha384').update(body).digest('base64url')]):`${route}:${time}:${nonce}:${crypto.createHash('sha384').update(body).digest('base64url')}`).digest('base64url');return principal?`SafetyNet-Agent-PQ ${principal.principalId}.${principal.generation}.${time}.${nonce}.${mac}`:`SafetyNet-PQ ${time}.${nonce}.${mac}`;} async function boundedJson(response,max){const reader=response.body.getReader();const chunks=[];let size=0;try{for(;;){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>max)throw Error('Protected agent response too large');chunks.push(Buffer.from(value));}return JSON.parse(Buffer.concat(chunks).toString('utf8'));}finally{await reader.cancel();}} async function requestAttempt(credential, action, options={}) { const url=new URL(credential.address); if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password||url.pathname!=='/'||url.search||url.hash||credential.protocol!==channel.VERSION)throw Error('Protected agent credential required'); const individual=credential.principalId!==undefined;if(individual&&(!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(credential.principalId)||!Number.isSafeInteger(credential.generation)||credential.generation<1||!credential.principalBinding?.startsWith(`agent:${credential.principalId}:${credential.generation}:`)||!/^.{1,120}:[0-9a-f-]{36}$/.test(credential.principalBinding)||!Number.isFinite(Date.parse(credential.expiresAt))||Date.parse(credential.expiresAt)<=Date.now()))throw Error('Invalid principal credential'); const headers=(route,body='{}')=>({'Content-Type':'application/json',Authorization:authorization(credential.token,route,body,individual?credential:undefined),...options.headers}); const challenge=await fetch(`${url.origin}/v1/challenge`,{method:'POST',headers:headers('/v1/challenge'),body:'{}',redirect:'error',signal:AbortSignal.timeout(10000)}); if(!challenge.ok){await boundedJson(challenge,32768);return {ok:false,status:challenge.status,json:async()=>({error:'Protected agent challenge refused'})};} const bundle=await boundedJson(challenge,32768); channel.verifyChallenge(bundle,individual?credential.principalBinding:'local-agent',credential.edPublic,credential.pqPublic); const context=channel.prepare(bundle); try{ const payload=channel.response({kind:'agent-request',action:action.action,args:action.args??{}},bundle,context); const body=JSON.stringify({handshake:context.request,payload});if(Buffer.byteLength(body)>131072)throw Error('Protected agent request too large'); const response=await fetch(`${url.origin}/v1/actions`,{method:'POST',headers:headers('/v1/actions',body),body,redirect:'error',signal:AbortSignal.timeout(65000)}); const value=await boundedJson(response,4*1024*1024+65536); if(!value.encrypted&&response.ok)throw Error('Protected agent response required'); const data=value.encrypted?channel.finish(value,bundle,context):value; if(value.encrypted&&(data.kind!=='agent-response'||!Number.isInteger(data.status)||data.status<200||data.status>599))throw Error('Protected agent response refused'); const status=value.encrypted?data.status:response.status,ok=value.encrypted&&status>=200&&status<300; const busy=value.encrypted&&status===409&&action.action==='bookmarks:mutate'&&data.code==='browser-management-busy'&&data.error==='Management update in progress. Try again.'; const clean=data.error?{error:data.error,...(busy?{code:data.code}:{})}:{result:data.result}; return {ok,status,json:async()=>clean,...(busy?{retryManagementBusy:true}:{})}; }finally{context.responseKey.fill(0);} } async function requestAgent(credential, action, options={}) { // Snapshot validated JSON arguments: retries cannot update CAS or signed-policy inputs. const fixed=JSON.parse(JSON.stringify(action)); for(let retry=0;;retry++){ const response=await requestAttempt(credential,fixed,options); if(!response.retryManagementBusy||retry>=3){delete response.retryManagementBusy;return response;} // Each attempt creates a fresh authenticated proof and consumes a new signed challenge. await new Promise(resolve=>setTimeout(resolve,200*(retry+1))); } } module.exports={requestAgent,authorization};