Add policy-bound credential submission and native DLP audit proofs

This commit is contained in:
Karl committed 2026-10-13 21:00:41 +02:00
1 parent b991ee9da6
commit c8b6ff9cdd
45 files changed
+777 -98

No files matched your search

+13 -9
View File
@@ -1,10 +1,10 @@
'use strict';
const {randomUUID}=require('node:crypto');
const {fillScript}=require('./password-fill.cjs');
const {fillScript,prepareFillScript}=require('./password-fill.cjs');
const refused=()=>{throw Error('Managed vault operation refused');};
const idPattern=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const hostMatch=(name,pattern)=>pattern.startsWith('*.')?name.endsWith(pattern.slice(1))&&name!==pattern.slice(2):name===pattern;
function createVaultAgentControls({getContext,contextCurrent,getVault,checkVisual,inspect,audit,traceOperation=async(_name,fn)=>fn(),now=Date.now}={}) {
function createVaultAgentControls({getContext,contextCurrent,getVault,checkVisual,inspect,audit,beforeFill=async()=>{},afterFill=()=>{},prepareSubmission,traceOperation=async(_name,fn)=>fn(),now=Date.now}={}) {
if([getContext,contextCurrent,getVault,checkVisual,inspect,audit,traceOperation,now].some(fn=>typeof fn!=='function'))throw TypeError('Vault boundary callbacks required');
const references=new Map();
const allowed=(origin,c)=>{
@@ -13,7 +13,7 @@ function createVaultAgentControls({getContext,contextCurrent,getVault,checkVisua
return u.protocol==='https:'&&!u.username&&!u.password&&u.origin===origin&&Array.isArray(hosts)&&(!hosts.length||hosts.some(p=>typeof p==='string'&&hostMatch(u.hostname.toLowerCase(),p.toLowerCase())))&&c.destinationAllowed(origin,c.policy)===true;
};
function snapshot(args,signal,action){
const fields=action==='vault:fill'?['sessionId','credentialRef']:['sessionId'];
const fields=['vault:fill','vault:prepare-submit'].includes(action)?['sessionId','credentialRef']:['sessionId'];
if(!args||typeof args!=='object'||Array.isArray(args)||Object.keys(args).some(key=>!fields.includes(key))||(args.sessionId!==undefined&&!idPattern.test(args.sessionId)))refused();
const c=getContext(args.sessionId);
if(signal?.aborted||!c?.ready||!c.token||(!c.actor||typeof c.actor.agentId!=='string'||!c.actor.agentId||!Number.isSafeInteger(c.actor.generation))||!c.policy?.agents?.enabled||!c.policy.agents.scopes?.includes(action)||!c.policy.passwordManager?.enabled||!Number.isFinite(Date.parse(c.policy.expiresAt))||Date.parse(c.policy.expiresAt)<=now()||typeof c.destinationAllowed!=='function')refused();
@@ -45,10 +45,11 @@ function createVaultAgentControls({getContext,contextCurrent,getVault,checkVisua
if(references.size+Math.min(safe.length,32)>32)refused();
return safe.slice(0,32).map(row=>{const credentialRef=randomUUID();references.set(credentialRef,{...row,sessionId:c.sessionId,webContents:c.webContents,actor:{...c.actor},generation:vault.accessGeneration(),policy:c.policy,token:c.token,vault,expires:now()+60000});return {credentialRef,origin:row.origin};});
};
const fill=async(args={}, {signal}={})=>{
const fill=async(args={}, {signal}={},action='vault:fill')=>{
let credential;
try{
const c=snapshot(args,signal,'vault:fill'),vault=unlocked(c),ref=references.get(args.credentialRef);
const c=snapshot(args,signal,action),vault=unlocked(c),ref=references.get(args.credentialRef);
const controlled=action==='vault:prepare-submit';if(controlled&&(!c.policy.passwordManager.controlledSubmission?.enabled||typeof prepareSubmission!=='function'))refused();
if(!idPattern.test(args.credentialRef||'')||!ref||ref.sessionId!==c.sessionId||ref.webContents!==c.webContents||ref.expires<=now()||(ref.actor.agentId!==c.actor.agentId||ref.actor.generation!==c.actor.generation)||ref.generation!==vault.accessGeneration()||ref.policy!==c.policy||ref.vault!==vault||!contextCurrent(ref.token)||c.foreground!==true||c.interactive===false||!c.policy.passwordManager.allowAutofill||!c.webContents||c.webContents.isDestroyed?.())refused();
const url=c.webContents.getURL(),u=new URL(url);
if(u.username||u.password||u.protocol!=='https:'||u.origin!==ref.origin||!allowed(ref.origin,c))refused();
@@ -58,13 +59,16 @@ function createVaultAgentControls({getContext,contextCurrent,getVault,checkVisua
// Consume once before the native effect: failures never authorize an automatic retry.
references.delete(args.credentialRef);
credential={...vault.credential(ref.id,ref.origin),origin:ref.origin};recheck();
c.webContents.focus();recheck();
const result=await traceOperation('vault.fill.native',()=>c.webContents.executeJavaScriptInIsolatedWorld(999,[{code:fillScript(url,credential,Date.parse(c.policy.expiresAt))}],false));
await beforeFill(c);recheck();c.webContents.focus();recheck();
const code=controlled?prepareFillScript(url,credential,c.policy.passwordManager.controlledSubmission.forms,Date.parse(c.policy.expiresAt)):fillScript(url,credential,Date.parse(c.policy.expiresAt));
const result=await traceOperation('vault.fill.native',()=>c.webContents.executeJavaScriptInIsolatedWorld(999,[{code}],false));
recheck();if(result?.completed!==true)refused();
await audit({action:'vault.filled',outcome:'completed',sessionId:c.sessionId});recheck();return {completed:true};
await audit({action:'vault.filled',outcome:'completed',sessionId:c.sessionId});recheck();
if(controlled){const resultLease=await prepareSubmission({context:c,formId:result.formId,credential,vault,vaultGeneration:ref.generation});try{recheck();if(resultLease?.prepared!==true)refused();afterFill();return {completed:true,prepared:true};}catch(error){resultLease?.cancel?.();throw error;}}
afterFill();return {completed:true};
}catch{refused();}finally{if(credential){credential.username='';credential.password='';credential.origin='';}}
};
const safe=fn=>async(...args)=>{try{return await fn(...args);}catch{refused();}};
return {status:safe(status),list:safe(list),fill:safe(fill),clear:()=>references.clear()};
return {status:safe(status),list:safe(list),fill:safe(fill),prepare:safe((args,options)=>fill(args,options,'vault:prepare-submit')),clear:()=>references.clear()};
}
module.exports={createVaultAgentControls};