'use strict'; const fs=require('node:fs'); const path=require('node:path'); const crypto=require('node:crypto'); const {SCOPES}=require('./agent-control.cjs'); const validId=id=>typeof id==='string'&&/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(id); const live=(value,now)=>Number.isFinite(Date.parse(value))&&Date.parse(value)>now; function activeGrants(policy,now=Date.now()){ if(policy?.agents?.identityMode!=='individual'||!live(policy.expiresAt,now))return []; return (policy.agents.grants||[]).slice(0,32).filter(g=>validId(g.id)&&g.enabled===true&&live(g.expiresAt,now)&&Number.isSafeInteger(g.generation)&&g.generation>=1).map(g=>structuredClone(g)); } function authorizeGrant({policy,ready,principalId,generation,action,url,hostMatches,now=Date.now()}){ const denied={allowed:false,role:'agent',scopes:[]}; const agents=policy?.agents,scope=SCOPES[action]; if(!ready||agents?.enabled!==true||!live(policy.expiresAt,now)||!scope||!agents.scopes?.includes(action))return denied; let grant; if(agents.identityMode==='individual'){ grant=activeGrants(policy,now).find(g=>g.id===principalId&&g.generation===generation); if(!grant||!grant.scopes.includes(action))return denied; }else if(agents.identityMode!=='legacy'||principalId!==undefined||generation!==undefined)return denied; if(url!==undefined){ let address;try{address=new URL(url);}catch{return denied;} if(!['http:','https:'].includes(address.protocol)||address.username||address.password)return denied; for(const patterns of [agents.allowedHosts,grant?.allowedHosts]){ if(patterns?.length&&(!hostMatches||!patterns.some(pattern=>hostMatches(address.hostname,pattern))))return denied; } } return {allowed:true,role:'agent',scopes:[scope]}; } function reconcileGenerationFloor(grants,floor={}){ const next=Object.create(null); for(const [id,value]of Object.entries(floor))if(validId(id)&&Number.isSafeInteger(value)&&value>=1)next[id]=value; const result=grants.map(g=>{ if(!validId(g.id)||!Number.isSafeInteger(g.generation)||g.generation<1)throw Error('Invalid agent generation'); const prior=next[g.id]||0;next[g.id]=Math.max(prior,g.generation); return {...structuredClone(g),enabled:g.enabled===true&&g.generation>=prior}; }); return {grants:result,floor:{...next}}; } function descriptorPath(directory,id){ if(!validId(id))throw Error('Invalid agent identifier'); return path.join(directory,'agent-'+crypto.createHash('sha256').update(id).digest('hex')+'.json'); } function guard(filePath){ const absolute=path.resolve(filePath),parent=path.dirname(absolute); let current=parent; while(true){const stat=fs.lstatSync(current);if(!stat.isDirectory()||stat.isSymbolicLink()||fs.realpathSync(current).toLowerCase()!==path.resolve(current).toLowerCase())throw Error('Unsafe credential directory');const next=path.dirname(current);if(next===current)break;current=next;} try{const stat=fs.lstatSync(absolute);if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)throw Error('Unsafe credential file');}catch(error){if(error.code!=='ENOENT')throw error;} return absolute; } async function persistConnection({filePath,connection,protect,isCurrent}){ if(typeof protect!=='function'||typeof isCurrent!=='function'||!isCurrent())throw Error('Agent credential context changed'); const target=guard(filePath); const encrypted=await protect(JSON.stringify(connection)); if(!isCurrent())throw Error('Agent credential context changed'); const bytes=Buffer.from(JSON.stringify({format:'windows-dpapi-v1',protectedConnection:encrypted})); const temporary=target+'.'+crypto.randomUUID()+'.tmp';let fd; try{ guard(target);fd=fs.openSync(temporary,'wx',0o600);fs.writeFileSync(fd,bytes);fs.fsyncSync(fd);fs.closeSync(fd);fd=undefined; if(!isCurrent())throw Error('Agent credential context changed');guard(target);fs.renameSync(temporary,target); return target; }finally{bytes.fill(0);if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(temporary);}catch(error){if(error.code!=='ENOENT')throw error;}} } module.exports={activeGrants,authorizeGrant,reconcileGenerationFloor,descriptorPath,persistConnection};