consturl=z.string().max(4096).refine(v=>{try{constu=newURL(v);return['https:','http:'].includes(u.protocol)&&!u.username&&!u.password&&!u.search&&!u.hash;}catch{returnfalse;}},'Evidence URLs require HTTP(S), no credentials, query or fragment');
constdigest=z.string().regex(/^[a-f0-9]{64}$/);
// Typed allowlists prevent password, cookie, authorization and arbitrary fields.
constdays=config.evidenceRetentionDays??30;if(!Number.isInteger(days)||days<1||days>365)throwError('Evidence retention must be 1..365 days');
if(config.evidenceKeyFile){
if(!isAbsolute(config.evidenceKeyFile))throwError('Evidence key requires an absolute deployment file');
for(letpath=config.evidenceKeyFile;;path=dirname(path)){if(lstatSync(path).isSymbolicLink())throwError('Linked evidence key paths are forbidden');if(dirname(path)===path)break;}
conststat=lstatSync(config.evidenceKeyFile);if(!stat.isFile()||stat.size!==32)throwError('Evidence key must be a regular file containing exactly 32 random bytes');
if(process.platform!=='win32'&&(stat.mode&0o077))throwError('Evidence key must be restricted to its service owner');
key=readFileSync(config.evidenceKeyFile);if(key.length!==32)throwError('Evidence key changed during loading');keyId=createHash('sha256').update(key).digest('hex').slice(0,32);
}
db.exec(`CREATE TABLE IF NOT EXISTS protected_evidence(id TEXT PRIMARY KEY,source_event_id INTEGER NOT NULL REFERENCES events(id),device_id TEXT NOT NULL REFERENCES devices(id),type TEXT NOT NULL,key_id TEXT NOT NULL,created_at TEXT NOT NULL,expires_at TEXT NOT NULL,nonce BLOB NOT NULL,ciphertext BLOB NOT NULL,tag BLOB NOT NULL,UNIQUE(source_event_id,type)); CREATE INDEX IF NOT EXISTS evidence_source ON protected_evidence(source_event_id);`);
constenabled=(_req,res,next)=>key?next():res.status(503).json({error:'Protected evidence storage is not configured'});
if(Date.parse(policy.expiresAt)<=Date.now()||!policy.evidence.enabled||b.type==='download'&&!policy.evidence.captureDownloads||b.type==='interaction'&&!policy.evidence.captureInteractions)returnres.status(403).json({error:'Evidence collection is disabled by current policy'});
constpayload=(b.type==='interaction'?InteractionEvidenceSchema:payloads[b.type]).parse(b.payload);if(includesCredential(payload))returnres.status(400).json({error:'Recognizable credentials are excluded from evidence'});plain=Buffer.from(JSON.stringify(payload));if(plain.length>65536)returnres.status(413).json({error:'Evidence too large'});
constsource=db.prepare('SELECT id,kind,metadata FROM events WHERE device_id=? AND json_extract(metadata,\'$.eventId\')=?').get(req.device.id,b.eventId);if(!source)returnres.status(404).json({error:'Own source event not found'});
constmatching=b.type==='download'?['download.scan','download.released','download.block']:['clipboard.copy','clipboard.cut','clipboard.paste','agent.action'];if(!matching.includes(source.kind))returnres.status(400).json({error:'Evidence type does not match source event'});
plain.fill(0);if(same)returnres.json({id:prior.id,sourceEventId:source.id,expiresAt:prior.expires_at,replayed:true});returnres.status(409).json({error:'Evidence already recorded with different or unverifiable payload'});
transaction(db,()=>{db.prepare('INSERT INTO protected_evidence VALUES(?,?,?,?,?,?,?,?,?,?)').run(row.id,source.id,row.device_id,row.type,keyId,row.created_at,row.expires_at,nonce,ciphertext,cipher.getAuthTag());store.event(row.device_id,'evidence.recorded',{evidenceId:row.id,sourceEventId:source.id,evidenceType:row.type,keyId});});res.status(201).json({id:row.id,sourceEventId:source.id,expiresAt:row.expires_at});
constid=z.string().uuid().parse(req.params.id),incident=db.prepare('SELECT id,event_id,status FROM incident_cases WHERE id=?').get(id);if(!incident)returnres.status(404).json({error:'Incident not found'});
constrows=db.prepare("SELECT p.id,p.source_event_id,p.type,p.key_id,p.created_at,p.expires_at,json_extract(e.metadata,'$.eventId') event_uuid FROM protected_evidence p JOIN events e ON e.id=p.source_event_id WHERE p.source_event_id=? ORDER BY p.created_at DESC,p.id DESC LIMIT 100").all(incident.event_id);
constid=z.string().uuid().parse(req.params.id),reason=z.enum(['incident-response','investigation','compliance']).parse(req.query.reason),row=db.prepare('SELECT * FROM protected_evidence WHERE id=?').get(id);if(!row)returnres.status(404).json({error:'Evidence not found'});
constincident=db.prepare("SELECT id FROM incident_cases WHERE event_id=? AND status IN ('open','investigating')").get(row.source_event_id);if(row.expires_at<=newDate().toISOString()&&!incident)returnres.status(410).json({error:'Evidence access expired'});if(row.key_id!==keyId)returnres.status(503).json({error:'Evidence key unavailable'});