2026-10-13 10:33:31 +02:00
import express from 'express' ;
2026-10-13 16:58:15 +02:00
import { installRequestTelemetry , traceOperation } from './telemetry.mjs' ;
2026-10-13 10:33:31 +02:00
import transferFacts from '../../packages/core/transfer-facts.cjs' ;
2026-10-13 16:58:15 +02:00
import requestTransferFacts from '../../packages/core/request-transfer-facts.cjs' ;
import agentAudit from '../../packages/core/agent-audit.cjs' ;
2026-10-13 18:04:02 +02:00
import interactionAudit from '../../packages/core/interaction-facts.cjs' ;
2026-10-13 10:33:31 +02:00
import { installEvidence } from './evidence.mjs' ;
import { installIncidents } from './incidents.mjs' ;
import { installBookmarkRoutes , globalBookmarksValid } from './bookmarks.mjs' ;
2026-10-13 16:58:15 +02:00
import { installLibraryRoutes } from './library.mjs' ;
import { runApprovedAI } from './ai-gateway.mjs' ;
2026-10-13 10:33:31 +02:00
import { z } from 'zod' ;
import { randomUUID , timingSafeEqual } from 'node:crypto' ;
import { fileURLToPath } from 'node:url' ;
2026-10-13 23:19:22 +02:00
import { PolicySchema , inspect } from '../../packages/core/policy.mjs' ;
2026-10-13 10:33:31 +02:00
import { digest , token } from './store.mjs' ;
import { policyPublisher } from './policy-review.mjs' ;
import { installPolicyHistory } from './policy-history.mjs' ;
import { installUserProfiles } from './user-profiles.mjs' ;
import { installPolicyGroups } from './policy-groups.mjs' ;
import { adminMiddleware , authRoutes , cookie } from './auth.mjs' ;
import { bootstrapAllowed , normalizePrincipal } from './admin-access.mjs' ;
import { toECS , toCEF } from '../../packages/core/siem.mjs' ;
2026-10-13 16:58:15 +02:00
import { installSIEMRoutes } from './siem.mjs' ;
import { createCompanyNewsReader } from './company-news.mjs' ;
2026-10-13 10:33:31 +02:00
import { inspector } from './inspection.mjs' ;
import { SecuritySchema , VisualBrandSchema , VisualOriginSchema } from '../../packages/core/security.mjs' ;
import { defaultSemantic } from '../../packages/core/semantic.mjs' ;
import { installPolicyChannel } from './policy-channel.mjs' ;
import enterpriseInspection from '../browser/enterprise-inspection.cjs' ;
import icapClient from '../browser/icap-client.cjs' ;
import { EnterpriseSchema } from '../../packages/core/enterprise.mjs' ;
import { policyPresets , previewPreset } from '../../packages/core/presets.mjs' ;
import { enterpriseReadiness } from '../../packages/core/enterprise.mjs' ;
export function createApp ( store , config ) {
2026-10-13 16:58:15 +02:00
const app = express (); app . disable ( 'x-powered-by' ); installRequestTelemetry ( app );
const readCompanyNews = createCompanyNewsReader ({ allowedHosts : config . companyNewsAllowedHosts || []});
2026-10-13 10:33:31 +02:00
const limits = new Map ();
app . use (( req , res , next )=>{
res . set ({ 'X-Content-Type-Options' : 'nosniff' , 'Referrer-Policy' : 'no-referrer' , 'Cache-Control' : 'no-store' ,
2026-10-13 16:58:15 +02:00
'Content-Security-Policy' : "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'" });
2026-10-13 10:33:31 +02:00
if ( ! [ 'GET' , 'HEAD' , 'OPTIONS' ]. includes ( req . method )) {
if ( req . headers . origin && req . headers . origin !== config . publicUrl ) return res . status ( 403 ). json ({ error : 'Origin rejected' });
if ( req . headers [ 'sec-fetch-site' ] === 'cross-site' ) return res . status ( 403 ). json ({ error : 'Cross-site request rejected' });
}
const now = Date . now (), auth = ( req . headers . authorization || '' ). replace ( /^Bearer / , '' );
// Corporate NATs share an IP. Only an authenticated, active device gets its
// own bucket; arbitrary bearer strings cannot evade the IP limit.
const enrolled = auth . length >= 32 && auth . length <= 200 ? store . db . prepare ( 'SELECT id FROM devices WHERE token_hash=? AND revoked=0' ). get ( digest ( auth )) : null ;
const bootstrap = bootstrapAllowed ( config ) && auth && config . adminToken && timingSafeEqual ( Buffer . from ( digest ( auth )), Buffer . from ( digest ( config . adminToken )));
const sessionCredential = cookie ( req , 'sn_session' );
const sessionIdentity =! enrolled &&! bootstrap && sessionCredential . length >= 32 && sessionCredential . length <= 200 ? store . db . prepare ( 'SELECT hash FROM sessions WHERE hash=? AND expires>?' ). get ( digest ( sessionCredential ), now ) : null ;
const key = enrolled ? `device: ${ enrolled . id } ` : bootstrap ? 'admin:bootstrap' : sessionIdentity ? `admin: ${ sessionIdentity . hash } ` : `ip: ${ req . socket . remoteAddress || 'unknown' } ` ;
let count = limits . get ( key ); if ( ! count || count . until < now ) { count = { until : now + 60000 , n : 0 }; limits . set ( key , count );}
if ( ++ count . n > 300 ) return res . status ( 429 ). json ({ error : 'Rate limit exceeded' });
if ( limits . size > 10000 ) for ( const [ k , v ] of limits ) if ( v . until < now ) limits . delete ( k );
next ();
});
app . use ( express . json ({ limit : '2mb' }));
app . get ( '/health' ,( _req , res )=> res . json ({ status : 'ok' , version : '0.1.0' }));
app . get ( '/api/auth/status' ,( _req , res )=> res . json ({ sso :!! config . oidcIssuer , publicUrl : config . publicUrl , bootstrapAllowed : bootstrapAllowed ( config )}));
authRoutes ( app , store , config );
const admin = adminMiddleware ( store , config );
const device = ( req , res , next )=>{
const auth = ( req . headers . authorization || '' ). replace ( /^Bearer / , '' );
const d = store . db . prepare ( 'SELECT * FROM devices WHERE token_hash=? AND revoked=0' ). get ( digest ( auth ));
if ( ! d ) return res . status ( 401 ). json ({ error : 'Device is unenrolled or revoked' });
req . device = d ; next ();
};
installBookmarkRoutes ( app , store , device , admin );
2026-10-13 16:58:15 +02:00
installLibraryRoutes ( app , store , config , device );
2026-10-13 10:33:31 +02:00
installEvidence ( app , store , config , device , admin );
const managementChannel = installPolicyChannel ( app , store , device );
app . get ( '/api/admin/management-channel' , admin ,( _req , res )=> res . json ({ version : managementChannel . version , signatureKey : managementChannel . signatureKey , requirePQManagement :!! config . requirePQManagement }));
function enroll ( user , body , protectedBootstrap = false , verified = false ) {
const b = z . object ({ name : z . string (). min ( 1 ). max ( 100 ), platform : z . enum ([ 'win32' , 'darwin' , 'linux' ])}). parse ( body );
const id = randomUUID (), credential = token ();
store . db . prepare ( 'INSERT INTO devices(id,name,platform,user,token_hash,last_seen) VALUES(?,?,?,?,?,?)' ). run ( id , b . name , b . platform , user , digest ( credential ), new Date (). toISOString ());
if ( verified ) store . db . prepare ( 'UPDATE devices SET verified_subject=? WHERE id=?' ). run ( user , id );
store . event ( id , 'device.enrolled' ,{ platform : b . platform , actor : user });
return { id , token : credential , publicKey : store . publicKey (),...( ! config . requirePQManagement &&! protectedBootstrap ? { policy : store . envelope ( id )} : {}), managementChannel };
}
app . post ( '/api/device/enroll' ,( req , res )=>{
const code = z . string (). min ( 20 ). max ( 200 ). parse ( req . body . code );
const e = store . db . prepare ( 'SELECT * FROM enrollments WHERE hash=? AND expires>?' ). get ( digest ( code ), Date . now ());
if ( ! e ) return res . status ( 401 ). json ({ error : 'Enrollment code invalid or expired' });
// Validate before consuming the one-use code.
z . object ({ name : z . string (). min ( 1 ). max ( 100 ), platform : z . enum ([ 'win32' , 'darwin' , 'linux' ])}). parse ( req . body );
store . db . prepare ( 'DELETE FROM enrollments WHERE hash=?' ). run ( e . hash ); res . json ( enroll ( e . user , req . body , req . headers [ 'x-safetynet-pq-bootstrap' ] === '1' ));
});
app . post ( '/api/device/sso/start' ,( req , res )=>{
if ( ! config . oidcIssuer ) return res . status ( 409 ). json ({ error : 'SSO not configured' });
const id = token (), secret = token (); store . db . prepare ( 'INSERT INTO tickets VALUES(?,?,?,?)' ). run ( id , digest ( secret ), Date . now () + 300000 , null );
res . json ({ id , secret , url : ` ${ config . publicUrl } /auth/login?ticket= ${ id } ` });
});
app . post ( '/api/device/sso/poll' ,( req , res )=>{
const b = z . object ({ id : z . string (). max ( 100 ), secret : z . string (). max ( 100 ), name : z . string (). min ( 1 ). max ( 100 ), platform : z . enum ([ 'win32' , 'darwin' , 'linux' ])}). parse ( req . body );
const t = store . db . prepare ( 'SELECT * FROM tickets WHERE id=? AND secret_hash=? AND expires>?' ). get ( b . id , digest ( b . secret ), Date . now ());
if ( ! t ) return res . status ( 401 ). json ({ error : 'Device login expired' });
if ( ! t . credential ) return res . status ( 202 ). json ({ pending : true });
store . db . prepare ( 'DELETE FROM tickets WHERE id=?' ). run ( b . id ); res . json ( enroll ( JSON . parse ( t . credential ). sub , b , req . headers [ 'x-safetynet-pq-bootstrap' ] === '1' , true ));
});
app . get ( '/api/device/policy' , device ,( req , res )=> config . requirePQManagement ? res . status ( 403 ). json ({ error : 'Protected management channel required' }) : res . json ( store . envelope ( req . device . id )));
app . post ( '/api/device/heartbeat' , device ,( req , res )=>{
const b = z . object ({ policyVersion : z . number (). int (). nonnegative ()}). parse ( req . body );
store . db . prepare ( 'UPDATE devices SET last_seen=?,policy_version=? WHERE id=?' ). run ( new Date (). toISOString (), b . policyVersion , req . device . id ); res . json ({ ok : true });
});
app . post ( '/api/device/events' , device ,( req , res )=>{
2026-10-13 21:00:41 +02:00
const b = z . object ({ eventId : z . string (). uuid (), kind : z . enum ([ 'browser.interaction' , 'browser.interaction.dropped' , 'dlp.block' , 'navigation.block' , 'download.block' , 'policy.applied' , 'browser.started' , 'agent.action' , 'agent.denied' , 'library.ai.proposed' , 'library.ai.applied' , 'library.ai.reconcile' , 'devtools.open-requested' , 'devtools.opened' , 'devtools.closed' , 'devtools.denied' , 'download.scan' , 'download.released' , 'upload.authorized' , 'upload.block' , 'dns.block' , 'content.block' , 'vault.submission-completed' , 'vault.submission.failure' , 'vault.submission-prepared' , 'vault.submission-authorized' , 'vault.submission-denied' , 'vault.created' , 'vault.unlocked' , 'vault.locked' , 'vault.saved' , 'vault.deleted' , 'vault.filled' , 'vault.fill-requested' , 'phishing.block' , 'phishing.error' , 'phishing.scanned' , 'clipboard.block' , 'clipboard.copy' , 'clipboard.cut' , 'clipboard.paste' ]), ruleId : z . string (). max ( 100 ). optional (), host : z . string (). max ( 253 ). optional (), policyVersion : z . number (). int (). positive (), traceId : z . string (). regex ( /^[a-f0-9]{32}$/ ). optional (), spanId : z . string (). regex ( /^[a-f0-9]{16}$/ ). optional (), file : transferFacts . FileFactsSchema . optional (), request : requestTransferFacts . RequestFactsSchema . optional (), agent : agentAudit . AgentAuditSchema . optional (), interaction : interactionAudit . InteractionFactsSchema . optional ()}). strict (). parse ( req . body );
2026-10-13 18:24:44 +02:00
if ( b . interaction &&! interactionAudit . interactionFactsForEvent ( b . kind , b . interaction )) return res . status ( 400 ). json ({ error : 'Interaction facts require a matching lifecycle event' });
2026-10-13 16:58:15 +02:00
if ( b . agent &&! agentAudit . agentFactsForEvent ( b . kind , b . agent )) return res . status ( 400 ). json ({ error : 'Agent facts require a matching agent lifecycle event' });
2026-10-13 10:33:31 +02:00
if ( b . file &&! b . kind . startsWith ( 'download.' ) &&! b . kind . startsWith ( 'upload.' )) return res . status ( 400 ). json ({ error : 'File facts require a transfer event' });
2026-10-13 16:58:15 +02:00
if ( b . request &&! b . kind . startsWith ( 'upload.' )) return res . status ( 400 ). json ({ error : 'Request facts require an upload event' });
// Older clients reported body hashes as file facts. Never persist those hashes.
if ( b . kind . startsWith ( 'upload.' ) && b . file ){ b . request = requestTransferFacts . requestTransferFacts ({ bytes : b . file . bytes , detectedType : b . file . detectedType }); delete b . file ;}
2026-10-13 10:33:31 +02:00
if ( b . kind . startsWith ( 'vault.' ) && ( b . host !== undefined || b . ruleId !== undefined )) return res . status ( 400 ). json ({ error : 'Vault audit events accept lifecycle metadata only' });
store . db . exec ( 'BEGIN IMMEDIATE' ); try {
const inserted = store . db . prepare ( 'INSERT OR IGNORE INTO device_events VALUES(?,?)' ). run ( req . device . id , b . eventId );
if ( inserted . changes ) store . event ( req . device . id , b . kind , b ); store . db . exec ( 'COMMIT' ); res . json ({ ok : true });
} catch ( e ){ store . db . exec ( 'ROLLBACK' ); throw e ;}
});
2026-10-13 16:58:15 +02:00
app . get ( '/api/device/start-page/news' , device , async ( req , res )=>{ try { const p = store . effectivePolicy ( req . device . id ); if ( Date . parse ( p . expiresAt ) <= Date . now ()) return res . status ( 403 ). json ({ error : 'Policy expired' }); const news = await readCompanyNews ( p ); res . json ({ policyVersion : p . version , news });} catch { res . status ( 502 ). json ({ error : 'Company news unavailable' });}});
2026-10-13 10:33:31 +02:00
app . post ( '/api/device/ai' , device , async ( req , res , next )=>{
2026-10-13 23:19:22 +02:00
const controller = new AbortController (), cancel = ()=>{ if ( ! res . writableFinished ) controller . abort ();};
req . once ( 'aborted' , cancel ); res . once ( 'close' , cancel );
2026-10-13 16:58:15 +02:00
try { const b = z . object ({ prompt : z . string (). min ( 1 ). max ( 100000 )}). strict (). parse ( req . body ), p = store . effectivePolicy ( req . device . id );
2026-10-13 23:19:22 +02:00
const answer = await runApprovedAI ({ store , config , deviceId : req . device . id , policy : p , prompt : b . prompt , signal : controller . signal }); if ( ! controller . signal . aborted ) res . json ({ answer });
2026-10-13 16:58:15 +02:00
} catch ( e ){ if ([ 403 , 409 , 502 ]. includes ( e . status )) return res . status ( e . status ). json ({ error : e . message ,...( e . ruleId ? { ruleId : e . ruleId } : {})}); next ( e );}
2026-10-13 23:19:22 +02:00
finally { req . removeListener ( 'aborted' , cancel ); res . removeListener ( 'close' , cancel );}
2026-10-13 10:33:31 +02:00
});
app . use ( '/api/admin' , admin );
2026-10-13 16:58:15 +02:00
installSIEMRoutes ( app , store , config );
2026-10-13 10:33:31 +02:00
installIncidents ( app , store );
app . get ( '/api/admin/session' ,( req , res )=> res . json ({ principal : req . principal }));
app . get ( '/api/admin/sessions' ,( req , res )=>{
const rows = store . db . prepare ( 'SELECT hash,expires,principal FROM sessions WHERE expires>? ORDER BY expires DESC LIMIT 501' ). all ( Date . now ());
const sessions = []; store . db . exec ( 'BEGIN IMMEDIATE' );
try {
for ( const row of rows . slice ( 0 , 500 )){
const principal = normalizePrincipal ( JSON . parse ( row . principal )); if ( ! principal ) continue ;
let handle = store . db . prepare ( 'SELECT handle FROM management_session_handles WHERE session_hash=?' ). get ( row . hash ) ? . handle ;
if ( ! handle ){ handle = randomUUID (); store . db . prepare ( 'INSERT INTO management_session_handles VALUES(?,?)' ). run ( handle , row . hash );}
sessions . push ({ id : handle , subject : principal . sub , name : principal . name || principal . sub , role : principal . role , expiresAt : new Date ( row . expires ). toISOString ()});
}
store . event ( null , 'management.sessions-reviewed' ,{ actor : req . principal . sub , count : sessions . length }); store . db . exec ( 'COMMIT' );
} catch ( error ){ store . db . exec ( 'ROLLBACK' ); throw error ;}
res . json ({ sessions , truncated : rows . length > 500 });
});
app . post ( '/api/admin/sessions/:id/revoke' ,( req , res )=>{
const id = z . string (). uuid (). parse ( req . params . id ); z . object ({}). strict (). parse ( req . body );
const target = store . db . prepare ( 'SELECT session_hash FROM management_session_handles WHERE handle=?' ). get ( id );
if ( ! target ) return res . status ( 404 ). json ({ error : 'Management session not found' });
store . db . exec ( 'BEGIN IMMEDIATE' );
try { const result = store . db . prepare ( 'DELETE FROM sessions WHERE hash=?' ). run ( target . session_hash ); if ( ! result . changes ) throw Error ( 'Session revocation conflict' ); store . event ( null , 'management.session-revoked' ,{ actor : req . principal . sub , count : 1 }); store . db . exec ( 'COMMIT' );}
catch ( error ){ store . db . exec ( 'ROLLBACK' ); throw error ;}
res . json ({ ok : true , revoked : 1 });
});
2026-10-13 23:19:22 +02:00
app . post ( '/api/admin/integrations/icap/probe' , async ( req , res , _next )=>{
2026-10-13 10:33:31 +02:00
try { const settings = EnterpriseSchema . parse ({ icap : req . body }). icap ; const result = await icapClient . probeIcap ( settings , enterpriseInspection . deploymentTLS ()); store . event ( null , 'integration.tested' ,{ actor : req . principal . sub , integration : 'icap' , ready : result . ready }); res . json ({... result , scope : 'management-server' , check : 'options-only' , contentSent : false });}
catch { res . status ( 400 ). json ({ error : 'ICAP configuration or deployment certificates are unavailable' });}
});
app . get ( '/api/admin/presets' ,( _req , res )=> res . json ({ presets : policyPresets }));
app . get ( '/api/admin/integrations' ,( _req , res )=> res . json ({ readiness : enterpriseReadiness ( store . get ( 'policy' ))}));
app . post ( '/api/admin/presets/preview' ,( req , res )=>{ const b = z . object ({ id : z . string (). max ( 80 ), policy : PolicySchema }). strict (). parse ( req . body ); if ( ! policyPresets . some ( p => p . id === b . id )) return res . status ( 400 ). json ({ error : 'Unknown enterprise preset' }); res . json ( previewPreset ( b . policy , b . id ));});
const publisher = policyPublisher ( app , store , config );
installPolicyGroups ( app , store , publisher );
installUserProfiles ( app , store , publisher );
installPolicyHistory ( app , store , publisher );
app . put ( '/api/admin/security/blocklists' ,( req , res )=>{
const b = z . object ({ expectedVersion : z . number (). int (). positive (), blockedHosts : z . array ( z . string ()). max ( 10000 ), blockedIPs : z . array ( z . string ()). max ( 10000 )}). strict (). parse ( req . body );
SecuritySchema . parse ({ dns : { blockedHosts : b . blockedHosts , blockedIPs : b . blockedIPs }});
const p = PolicySchema . parse ( store . get ( 'policy' )); if ( p . version !== b . expectedVersion ) return res . status ( 409 ). json ({ error : 'Policy version conflict' });
p . security . dns . blockedHosts = b . blockedHosts ; p . security . dns . blockedIPs = b . blockedIPs ; p . version ++ ;
publisher . submit ( req , res , p , 'security.blocklists-published' );
});
app . post ( '/api/admin/security/visual-reference' , async ( req , res , next )=>{
try {
const b = z . object ({ expectedVersion : z . number (). int (). positive (), brandId : z . string (). min ( 1 ). max ( 80 ), label : z . string (). min ( 1 ). max ( 100 ), allowedOrigins : z . array ( VisualOriginSchema ). min ( 1 ). max ( 50 ), data : z . string (). max ( 1800000 ). regex ( /^[A-Za-z0-9+/]*={0,2}$/ ), scope : z . enum ([ 'viewport' , 'login-form' ])}). strict (). parse ( req . body );
if ( store . get ( 'policy' ). version !== b . expectedVersion ) return res . status ( 409 ). json ({ error : 'Policy version conflict' });
const bytes = Buffer . from ( b . data , 'base64' ); if ( ! bytes . length || bytes . length > 1024 * 1024 || bytes . toString ( 'base64' ) !== b . data ) return res . status ( 400 ). json ({ error : 'Reference must be canonical base64 PNG up to 1 MB' });
const { fingerprintPNG } = await import ( '../../packages/core/visual-image.cjs' ); const result = fingerprintPNG ( bytes );
if ( result . usable !== true ) return res . status ( 400 ). json ({ error : 'Reference has insufficient visual detail; use a trusted nonblank login screenshot' });
const reference = { hash : result . hash , algorithm : 'phash-dct32-v1' , scope : b . scope , viewport : { width : result . width , height : result . height }};
const brand = VisualBrandSchema . parse ({ id : b . brandId , label : b . label , allowedOrigins : b . allowedOrigins , references : [ reference ]});
store . event ( null , 'phishing.reference-derived' ,{ actor : req . principal . sub , ruleId : b . brandId , version : b . expectedVersion });
const quality = z . object ({ contrast : z . number (). finite (), stddev : z . number (). finite (), dynamicRange : z . number (). finite (), entropy : z . number (). finite (), detail : z . number (). finite ()}). parse ( result . quality );
res . json ({ brand , quality : {... quality , width : result . width , height : result . height }, warning : 'Draft only. Explicitly publish signed policy to enable this reference. Perceptual similarity is not proof of a phishing page; use trusted reference images and validate false positives.' });
} catch ( e ){ next ( e );}
});
app . get ( '/api/admin/teaching' ,( _req , res )=> res . json ({ drafts : store . db . prepare ( 'SELECT * FROM teaching ORDER BY time DESC LIMIT 100' ). all (). map ( r =>({... r , reference : JSON . parse ( r . reference )}))}));
app . post ( '/api/admin/teaching' , async ( req , res , next )=>{
try { const b = z . object ({ label : z . string (). min ( 1 ). max ( 100 ), data : z . string (). max ( 1400000 ). regex ( /^[A-Za-z0-9+/]*={0,2}$/ ), filename : z . string (). max ( 200 )}). strict (). parse ( req . body ); const bytes = Buffer . from ( b . data , 'base64' ); if ( ! bytes . length || bytes . length > 1024 * 1024 ) return res . status ( 413 ). json ({ error : 'Reference document limit is 1 MB' });
2026-10-13 16:58:15 +02:00
const reference = await inspector ( config , '/reference-document' ,{ data : b . data , filename : b . filename , policy : store . get ( 'policy' )});
2026-10-13 10:33:31 +02:00
const id = randomUUID (); store . db . prepare ( 'INSERT INTO teaching VALUES(?,?,?,?,?,?,?,?)' ). run ( id , new Date (). toISOString (), req . principal . sub , b . label , 'draft' , JSON . stringify ( reference ), null , null ); store . event ( null , 'teaching.drafted' ,{ actor : req . principal . sub , referenceId : id }); res . json ({ id , reference , status : 'draft' });
} catch ( e ){ next ( e );}
});
app . post ( '/api/admin/teaching/:id/publish' ,( req , res )=>{
const b = z . object ({ expectedVersion : z . number (). int (). positive (), approve : z . literal ( true ), mode : z . enum ([ 'complete' , 'available-fingerprints' ]). default ( 'complete' )}). strict (). parse ( req . body ), draft = store . db . prepare ( "SELECT * FROM teaching WHERE id=? AND status='draft'" ). get ( req . params . id );
if ( ! draft ) return res . status ( 404 ). json ({ error : 'Draft not found' }); const p = PolicySchema . parse ( store . get ( 'policy' )); if ( p . version !== b . expectedVersion ) return res . status ( 409 ). json ({ error : 'Policy version conflict' });
const reference = JSON . parse ( draft . reference ); if ( ! reference . registrationReady && b . mode === 'complete' ) return res . status ( 409 ). json ({ error : 'Complete local inspection required before publication; available-fingerprints mode can explicitly approve partial coverage' }); p . dlp . rules . push ({ id : draft . id , label : draft . label , kind : 'sha256' , value : reference . sha256 , action : 'block' });
for ( const [ index , value ] of reference . fragmentHashes . slice ( 0 , 1000 ). entries ()) p . dlp . rules . push ({ id : ` ${ draft . id } : ${ index } ` , label : draft . label , kind : 'fragment-sha256' , value , action : 'block' }); p . version ++ ; PolicySchema . parse ( p );
const coverage = reference . registrationReady ? 'complete' : 'partial' ; publisher . submit ( req , res , p , 'teaching.published' ,{ type : 'teaching' , id : draft . id , coverage , mode : b . mode },{ coverage });
});
app . get ( '/api/admin/overview' ,( req , res )=>{
let policy , migrationRequired = false ; try { policy = store . get ( 'policy' );} catch ( error ){ if ( ! ( error instanceof z . ZodError )) throw error ; const raw = store . getRaw ( 'policy' ); policy = PolicySchema . parse ({... raw , dlp : {... raw . dlp , semantic : defaultSemantic ()}}); migrationRequired = true ;}
const devices = store . db . prepare ( 'SELECT id,name,platform,user,revoked,last_seen,policy_version,document_analysis_enabled FROM devices ORDER BY last_seen DESC' ). all (). map ( d =>({... d , documentAnalysisEnabled : d . document_analysis_enabled === null ? policy . inspection . documentAnalysisEnabled :!! d . document_analysis_enabled }));
const alerts = store . db . prepare ( 'SELECT * FROM alerts ORDER BY id DESC LIMIT 100' ). all ();
const events = store . db . prepare ( 'SELECT * FROM events ORDER BY id DESC LIMIT 100' ). all (). map ( e =>({... e , metadata : JSON . parse ( e . metadata )}));
res . json ({ policy , migrationRequired , publicKey : store . publicKey (), devices , events , alerts , principal : req . principal , siem : config . siemStatus ? .() || null , auditRetention : config . auditRetentionStatus ? .() || { enabled : false }, governance : { requireIndependentPolicyApproval :!! config . requireIndependentPolicyApproval }});
});
2026-10-13 16:58:15 +02:00
app . get ( '/api/admin/audit/integrity' , async ( _req , res )=> res . json ( await traceOperation ( 'audit.verify' ,{},()=> store . auditIntegrity . verify ())));
app . get ( '/api/admin/audit/entries' ,( req , res )=>{
const q = z . object ({ after : z . coerce . number (). int (). nonnegative (). default ( 0 ), limit : z . coerce . number (). int (). min ( 1 ). max ( 1000 ). default ( 100 )}). strict (). parse ( req . query );
res . json ( store . auditIntegrity . entries ( q ));
});
2026-10-13 10:33:31 +02:00
app . get ( '/api/admin/events/export' ,( req , res )=>{
const query = z . object ({ after : z . coerce . number (). int (). nonnegative (). default ( 0 ), limit : z . coerce . number (). int (). min ( 1 ). max ( 1000 ). default ( 100 ), format : z . enum ([ 'ecs' , 'cef' ]). default ( 'ecs' )}). parse ( req . query );
2026-10-13 16:58:15 +02:00
const rows = store . db . prepare ( 'SELECT * FROM events WHERE id>? ORDER BY id LIMIT ?' ). all ( query . after , query . limit ). map ( e =>({... e , metadata : JSON . parse ( e . metadata ), audit : store . auditIntegrity . linkage ( e . id )}));
2026-10-13 10:33:31 +02:00
res . setHeader ( 'X-SafetyNet-Next-Cursor' , String ( rows . at ( - 1 ) ? . id ?? query . after ));
res . type ( query . format === 'ecs' ? 'application/x-ndjson' : 'text/plain' ). send ( rows . map ( e => query . format === 'ecs' ? JSON . stringify ( toECS ( e , store . get ( 'installationId' ))) : toCEF ( e , store . get ( 'installationId' ))). join ( '\n' ) + ( rows . length ? '\n' : '' ));
});
app . put ( '/api/admin/policy' ,( req , res )=>{
const p = PolicySchema . parse ( req . body ), previous = store . getRaw ( 'policy' );
if ( ! globalBookmarksValid ( store , p )) return res . status ( 400 ). json ({ error : 'Global policies require empty personal bookmarks and distinct company IDs' });
if ( p . version !== previous . version + 1 ) return res . status ( 409 ). json ({ error : 'Refresh the console: version must increase by exactly one' });
const expires = Date . parse ( p . expiresAt ); if ( expires <= Date . now () || expires > Date . now () + 30 * 86400000 ) return res . status ( 400 ). json ({ error : 'Policy expiry must be within the next 30 days' });
publisher . submit ( req , res , p );
});
app . post ( '/api/admin/enrollments' ,( req , res )=>{
const user = z . string (). min ( 1 ). max ( 200 ). parse ( req . body . user ), code = token ();
store . db . prepare ( 'INSERT INTO enrollments VALUES(?,?,?)' ). run ( digest ( code ), Date . now () + 600000 , user );
store . event ( null , 'enrollment.created' ,{ actor : req . principal . sub , user }); res . json ({ code , expiresInSeconds : 600 });
});
app . post ( '/api/admin/devices/:id/revoke' ,( req , res )=>{
const r = store . db . prepare ( 'UPDATE devices SET revoked=1 WHERE id=? AND revoked=0' ). run ( req . params . id );
if ( ! r . changes ) return res . status ( 404 ). json ({ error : 'Active device not found' });
store . event ( req . params . id , 'device.revoked' ,{ actor : req . principal . sub }); res . json ({ ok : true });
});
app . patch ( '/api/admin/devices/:id/inspection' ,( req , res )=>{
const b = z . object ({ expectedVersion : z . number (). int (). positive (), documentAnalysisEnabled : z . boolean ()}). strict (). parse ( req . body ), p = store . get ( 'policy' );
if ( p . version !== b . expectedVersion ) return res . status ( 409 ). json ({ error : 'Policy version conflict' });
if ( ! store . db . prepare ( 'SELECT id FROM devices WHERE id=? AND revoked=0' ). get ( req . params . id )) return res . status ( 404 ). json ({ error : 'Active device not found' });
p . version ++ ; publisher . submit ( req , res , p , 'inspection.policy-published' ,{ type : 'inspection' , deviceId : req . params . id , enabled : b . documentAnalysisEnabled },{ documentAnalysisEnabled : b . documentAnalysisEnabled });
});
app . post ( '/api/admin/policy/test' , async ( req , res , next )=>{
try { const b = z . object ({ text : z . string (). max ( 100000 ), policy : PolicySchema }). parse ( req . body ); let result = inspect ( Buffer . from ( b . text ), b . policy );
if ( result . decision === 'allow' && b . policy . dlp . semantic . enabled ) result = await inspector ( config , '/inspect' ,{ data : Buffer . from ( b . text ). toString ( 'base64' ), policy : b . policy }); res . json ( result );
} catch ( e ){ next ( e );}
});
app . post ( '/api/admin/semantic/reference' , async ( req , res , next )=>{
try {
const b = z . object ({ description : z . string (). min ( 10 ). max ( 4000 )}). strict (). parse ( req . body );
2026-10-13 16:58:15 +02:00
const result = await inspector ( config , '/embed' ,{ data : Buffer . from ( b . description ). toString ( 'base64' ), policy : store . get ( 'policy' )});
2026-10-13 10:33:31 +02:00
if ( ! result . coverage . complete ||! result . coverage . extractionComplete ) return res . status ( 409 ). json ({ error : 'Complete local text inspection is required to create a semantic reference' });
store . event ( null , 'semantic.reference-created' ,{ actor : req . principal . sub , model : result . model , revision : result . revision , dimensions : result . dimensions }); res . json ( result );
} catch ( e ){ next ( e );}
});
app . post ( '/api/admin/semantic/document-reference' , async ( req , res , next )=>{
try {
const b = z . object ({ data : z . string (). max ( 1400000 ). regex ( /^[A-Za-z0-9+/]*={0,2}$/ ), filename : z . string (). min ( 1 ). max ( 200 )}). strict (). parse ( req . body ), bytes = Buffer . from ( b . data , 'base64' );
if ( ! bytes . length || bytes . length > 1048576 || bytes . toString ( 'base64' ) !== b . data ) return res . status ( 413 ). json ({ error : 'Semantic reference document must be canonical base64 up to 1 MiB' });
2026-10-13 16:58:15 +02:00
const result = await inspector ( config , '/embed' ,{ data : b . data , policy : store . get ( 'policy' )});
2026-10-13 10:33:31 +02:00
if ( ! result . coverage . complete ||! result . coverage . extractionComplete ) return res . status ( 409 ). json ({ error : 'Complete extraction and chunk coverage are required for a semantic document reference' });
store . event ( null , 'semantic.document-reference-created' ,{ actor : req . principal . sub , model : result . model , revision : result . revision , dimensions : result . dimensions }); res . json ( result );
} catch ( e ){ next ( e );}
});
app . use ( express . static ( fileURLToPath ( new URL ( './public' , import . meta . url )),{ index : 'index.html' }));
app . use (( error , _req , res , _next )=>{
config . onError ? .( error );
const invalid = error instanceof z . ZodError || error . type === 'entity.parse.failed' ;
res . status ( invalid ? 400 : error . type === 'entity.too.large' ? 413 : [ 400 , 403 , 404 , 409 ]. includes ( error . status ) ? error . status : 500 ). json ({ error : invalid ? 'Request validation failed' : 'Request failed' ,...( error instanceof z . ZodError ? { issues : error . issues . map ( v =>({ path : v . path . join ( '.' ), message : v . message }))} : {})});
});
return app ;
}