25 lines
5.3 KiB
JavaScript
25 lines
5.3 KiB
JavaScript
'use strict';
|
|||
|
|
const {randomUUID}=require('node:crypto');
|
||
|
|
const denied=()=>{const error=Error('Native agent session ownership refused');error.status=403;throw error;};
|
||
|
|
function identity(input){if(!input||typeof input!=='object'||Object.keys(input).sort().join(',')!=='deviceId,generation,principalId'||typeof input.deviceId!=='string'||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(input.deviceId)||typeof input.principalId!=='string'||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(input.principalId)||!Number.isSafeInteger(input.generation)||input.generation<1)denied();return Object.freeze({deviceId:input.deviceId.toLowerCase(),principalId:input.principalId,generation:input.generation});}
|
||
|
|
function createAgentSessionOwnership({createSession,installGuards,isLive,quarantineSession=()=>{},disposeSession=async session=>{await session.closeAllConnections();await session.clearStorageData();},maxSessions=32}={}){
|
||
|
|
if([createSession,installGuards,isLive,quarantineSession,disposeSession].some(fn=>typeof fn!=='function')||!Number.isInteger(maxSessions)||maxSessions<1||maxSessions>32)throw TypeError('Native ownership callbacks required');
|
||
|
|
const entries=new Map(),nativeEntries=new WeakMap(),revoked=new Set();let closed=false;
|
||
|
|
const live=(owner,options)=>{try{return isLive(owner,options)===true;}catch{return false;}};
|
||
|
|
const key=owner=>JSON.stringify([owner.deviceId,owner.principalId,owner.generation]);
|
||
|
|
const assertLive=(entry,options={})=>{if(closed||entry.active!==true||revoked.has(entry.key)||(!entry.ready&&!['acquire','install','installed'].includes(options.phase))||!live(entry.owner,{phase:options.phase||'network',url:options.url,background:options.background===true}))denied();};
|
||
|
|
const invalidate=entry=>{if(!entry.active)return;entry.active=false;entry.epoch++;revoked.add(entry.key);try{if(entry.session)quarantineSession(entry.session,entry.owner);}catch{/* Native release guards are already revoked. */}};
|
||
|
|
const cleanup=async entry=>{if(entry.cleanup)return entry.cleanup;entry.cleanup=(async()=>{if(entry.session&&!entry.disposed){await disposeSession(entry.session,entry.owner);entry.disposed=true;}if(entries.get(entry.key)===entry)entries.delete(entry.key);})();try{await entry.cleanup;}finally{entry.cleanup=null;}};
|
||
|
|
async function acquire(input,{signal}={}){const owner=identity(input),ownerKey=key(owner);if(closed||signal?.aborted||revoked.has(ownerKey)||revoked.size>=4096||!live(owner,{phase:'acquire',background:false}))denied();let entry=entries.get(ownerKey);if(!entry){if(entries.size>=maxSessions)denied();entry={owner,key:ownerKey,partition:'safetynet-agent-'+randomUUID(),active:true,ready:false,epoch:1,session:null,disposed:false};entries.set(ownerKey,entry);entry.promise=(async()=>{try{assertLive(entry,{phase:'acquire'});const session=await createSession(entry.partition,{cache:false});if(!session||typeof session!=='object'||nativeEntries.has(session))denied();entry.session=session;nativeEntries.set(session,entry);assertLive(entry,{phase:'install'});await installGuards(session,owner,options=>assertLive(entry,options));assertLive(entry,{phase:'installed'});entry.ready=true;return session;}catch(error){invalidate(entry);try{await cleanup(entry);}catch{}throw error;}})();}const aborted=()=>invalidate(entry);signal?.addEventListener('abort',aborted,{once:true});try{const session=await entry.promise;if(signal?.aborted)denied();assertLive(entry,{phase:'acquire'});if(!entry.ready)denied();return session;}finally{signal?.removeEventListener('abort',aborted);if(!entry.active)try{await cleanup(entry);}catch{}}}
|
||
|
|
function getOwner(session){const entry=nativeEntries.get(session);if(!entry||!entry.ready)denied();assertLive(entry,{phase:'owner'});return entry.owner;}
|
||
|
|
function guard(session,options={}){options=Object.freeze({phase:options.phase,url:options.url,background:options.background===true});const entry=nativeEntries.get(session);if(!entry||!entry.ready)denied();assertLive(entry,options);const epoch=entry.epoch;return Object.freeze({owner:entry.owner,assertCurrent:()=>{if(nativeEntries.get(session)!==entry||entry.epoch!==epoch)denied();assertLive(entry,options);}});}
|
||
|
|
async function revoke(input){const owner=identity(input),ownerKey=key(owner);revoked.add(ownerKey);const entry=entries.get(ownerKey);if(!entry)return;invalidate(entry);try{await entry.promise;}catch{}await cleanup(entry);}
|
||
|
|
async function close(){closed=true;const all=[...entries.values()];for(const entry of all)invalidate(entry);await Promise.allSettled(all.map(async entry=>{try{await entry.promise;}catch{}await cleanup(entry);}));}
|
||
|
|
function enumerateEntries(){return Object.freeze([...entries.values()].map(entry=>Object.freeze({owner:entry.owner,session:entry.session,ready:entry.ready,active:entry.active,epoch:entry.epoch,disposed:entry.disposed})));}
|
||
|
|
function liveSessions(){return Object.freeze([...entries.values()].filter(entry=>entry.active&&entry.ready&&live(entry.owner,{phase:'owner',background:false})).map(entry=>entry.session));}
|
||
|
|
async function sweep(){const all=[...entries.values()];for(const entry of all)if(!live(entry.owner,{phase:'reconcile',background:false}))invalidate(entry);return Promise.allSettled(all.filter(entry=>!entry.active).map(async entry=>{try{await entry.promise;}catch{}await cleanup(entry);}));}
|
||
|
|
return {acquire,getOwner,guard,revoke,close,enumerateEntries,liveSessions,sweep};
|
||
|
|
}
|
||
|
|
module.exports={createAgentSessionOwnership};
|
||
|
|
|
||
|
|
|