2026-10-13 23:19:22 +02:00
import test from 'node:test' ;
import assert from 'node:assert/strict' ;
import net from 'node:net' ;
import { createServer } from 'node:http' ;
import { mkdtempSync , rmSync } from 'node:fs' ;
import { tmpdir } from 'node:os' ;
import { join } from 'node:path' ;
import { randomUUID } from 'node:crypto' ;
import { openStore } from '../apps/server/store.mjs' ;
import { runApprovedAI } from '../apps/server/ai-gateway.mjs' ;
for ( const change of [ 'revoke' , 'same-version-policy' , 'output-revoke' , 'client-abort' ]) test ( `gateway suppresses release after ${ change } during actual ICAP scan` , async t =>{
const directory = mkdtempSync ( join ( tmpdir (), 'safetynet-ai-race-' )), store = openStore ( directory ), deviceId = randomUUID ();
store . db . prepare ( 'INSERT INTO devices(id,name,platform,user,token_hash) VALUES(?,?,?,?,?)' ). run ( deviceId , 'Fixture' , 'win32' , 'Fixture' , randomUUID ());
let deliveries = 0 , scans = 0 ; const sockets = new Set (), controller = new AbortController ();
const provider = createServer (( req , res )=>{ deliveries ++ ; req . resume (); res . end ( JSON . stringify ({ choices : [{ message : { content : 'Public answer' }}]}));}). listen ( 0 , '127.0.0.1' );
await new Promise ( r => provider . once ( 'listening' , r ));
const scanner = net . createServer ( socket =>{ sockets . add ( socket ); socket . on ( 'error' ,()=>{}); socket . once ( 'close' ,()=> sockets . delete ( socket )); let bytes = Buffer . alloc ( 0 ), done = false ;
socket . on ( 'data' , chunk =>{ bytes = Buffer . concat ([ bytes , chunk ]); if ( done ||! bytes . subarray ( - 5 ). equals ( Buffer . from ( '0\r\n\r\n' ))) return ; done = true ;
if ( bytes . subarray ( 0 , 7 ). toString () === 'OPTIONS' ){ socket . end ( 'ICAP/1.0 200 OK\r\nMethods: RESPMOD\r\nAllow: 204\r\nISTag: "fixture"\r\nEncapsulated: null-body=0\r\n\r\n' ); return ;}
scans ++ ;
if ( change === 'client-abort' ) controller . abort ();
if ( change === 'revoke' || change === 'output-revoke' && scans === 2 ) store . db . prepare ( 'UPDATE devices SET revoked=1 WHERE id=?' ). run ( deviceId );
else if ( change === 'same-version-policy' ){ const p = store . get ( 'policy' ); p . ai . enabled = false ; store . put ( 'policy' , p );}
socket . end ( 'ICAP/1.0 204 No Content\r\nISTag: "fixture"\r\n\r\n' );
});
}). listen ( 0 , '127.0.0.1' ); await new Promise ( r => scanner . once ( 'listening' , r ));
t . after ( async ()=>{ for ( const socket of sockets ) socket . destroy (); provider . closeAllConnections (); await Promise . all ([ provider , scanner ]. map ( server => new Promise ( r => server . close ( r )))); store . db . close (); rmSync ( directory ,{ recursive : true , force : true });});
const p = store . get ( 'policy' ); p . ai = {... p . ai , enabled : true , trustMode : 'local' , model : 'fixture' }; p . enterprise . icap = {... p . enterprise . icap , enabled : true , url : `icap://127.0.0.1: ${ scanner . address (). port } /scan` , timeoutMs : 1000 }; store . put ( 'policy' , p );
const policy = store . effectivePolicy ( deviceId ), config = { aiBaseUrl : `http://127.0.0.1: ${ provider . address (). port } ` , aiModel : 'fixture' , aiEndpointId : 'default' };
await assert . rejects ( runApprovedAI ({ store , config , deviceId , policy , prompt : 'Public fixture prompt' , signal : controller . signal }), error => error . status === ( change === 'same-version-policy' ? 409 : 403 ) && error . ruleId === 'ai-authorization-changed' );
assert . equal ( deliveries , change === 'output-revoke' ? 1 : 0 );
});
2026-10-14 00:37:32 +02:00
test ( 'caller cancellation retains fixed authorization status while native fetch is pending' , async t =>{
const directory = mkdtempSync ( join ( tmpdir (), 'safetynet-ai-abort-' )), store = openStore ( directory ), deviceId = randomUUID (), controller = new AbortController ();
store . db . prepare ( 'INSERT INTO devices(id,name,platform,user,token_hash) VALUES(?,?,?,?,?)' ). run ( deviceId , 'Fixture' , 'win32' , 'Fixture' , randomUUID ());
let received ; const incoming = new Promise ( resolve =>{ received = resolve ;}); const provider = createServer (( req , _res )=>{ req . resume (); received ();}). listen ( 0 , '127.0.0.1' ); await new Promise ( resolve => provider . once ( 'listening' , resolve ));
t . after ( async ()=>{ provider . closeAllConnections (); await new Promise ( resolve => provider . close ( resolve )); store . db . close (); rmSync ( directory ,{ recursive : true , force : true });});
const p = store . get ( 'policy' ); p . ai = {... p . ai , enabled : true , trustMode : 'local' , model : 'fixture' }; store . put ( 'policy' , p );
const pending = runApprovedAI ({ store , config : { aiBaseUrl : `http://127.0.0.1: ${ provider . address (). port } ` , aiModel : 'fixture' }, deviceId , policy : store . effectivePolicy ( deviceId ), prompt : 'Public fixture' , signal : controller . signal });
const rejected = assert . rejects ( pending , error => error . status === 403 && error . ruleId === 'ai-authorization-changed' ); await incoming ; controller . abort (); await rejected ;
});