Files
safetynet/apps/server/public/audit-integrity-admin.js
T

24 lines
4.0 KiB
JavaScript

// Verification is explicit: overview refreshes must not repeatedly scan the audit database.
(function(root){
function createAuditIntegrityController({principal,request,render,clear}){
let generation=0,identity=null;
const allowed=()=>principal()?.permissions?.includes('audit.read')===true;
const actor=()=>{const p=principal();return p?JSON.stringify([p.sub,p.role]):null;};
function reset(){generation++;identity=null;clear();}
function permissions(){if(!allowed()||identity!==null&&identity!==actor())reset();}
async function verify(){if(!allowed()){reset();return;}const g=++generation,who=identity=actor();render('loading');try{const data=await request('/api/admin/audit/integrity');if(g!==generation||who!==actor()||!allowed())return;if(typeof data?.valid!=='boolean'||typeof data.authenticated!=='boolean'||typeof data.externallyAnchored!=='boolean')throw Error('Invalid result');render('result',data);}catch{if(g===generation&&who===actor()&&allowed())render('error');}}
return {verify,reset,permissions};
}
if(typeof module==='object'&&module.exports)module.exports={createAuditIntegrityController};
root.createAuditIntegrityController=createAuditIntegrityController;
if(typeof document==='undefined')return;
const card=document.getElementById('audit-integrity'),button=document.getElementById('audit-integrity-verify'),message=document.getElementById('audit-integrity-status'),details=document.getElementById('audit-integrity-details');
let verifying=false;
const controller=createAuditIntegrityController({principal:()=>currentPrincipal,request:path=>api(path),clear:()=>{verifying=false;button.disabled=!currentPrincipal?.permissions?.includes('audit.read');details.replaceChildren();message.className='muted';message.textContent='Not verified in this session.';},render:(kind,data)=>{verifying=kind==='loading';button.disabled=verifying||!currentPrincipal?.permissions?.includes('audit.read');details.replaceChildren();message.className=kind==='error'||kind==='result'&&data.valid!==true?'error':'muted';if(kind==='loading'){message.textContent='Verifying stored audit records…';return;}if(kind==='error'){message.textContent='Audit verification unavailable. No integrity conclusion can be drawn.';return;}message.textContent=data.valid?'Stored audit records are internally consistent.':'Audit integrity verification failed. Investigate before relying on these records.';const lines=[`Signatures authenticated: ${data.authenticated?'yes':'no'}`,`Independent external anchor verified: ${data.externallyAnchored?'yes':'no'}`,`Records: ${Number.isSafeInteger(data.records)?data.records:'unknown'} · Legacy baseline events: ${Number.isSafeInteger(data.legacyEvents)?data.legacyEvents:'unknown'}`,`Head sequence: ${Number.isSafeInteger(data.head?.sequence)?data.head.sequence:'unknown'}`];if(/^[a-f0-9]{64}$/.test(data.head?.hash||''))lines.push(`Head hash: ${data.head.hash}`);if(!data.authenticated)lines.push('An internally consistent unsigned chain does not prove who created or changed it.');if(!data.externallyAnchored)lines.push('Database replacement or removal of the end of the chain cannot be excluded without an independently retained trusted key and head.');for(const limitation of (Array.isArray(data.limitations)?data.limitations:[]).slice(0,8))if(typeof limitation==='string')lines.push(limitation.slice(0,500));for(const value of lines){const p=document.createElement('p');p.textContent=value;details.append(p);}}});
const before=applyOperatorPermissions;applyOperatorPermissions=function(principal){before(principal);controller.permissions();const allowed=principal?.permissions?.includes('audit.read')===true;card.hidden=!allowed;button.disabled=!allowed||verifying;};
button.addEventListener('click',()=>controller.verify());
document.getElementById('admin-logout').addEventListener('click',()=>controller.reset(),true);
root.addEventListener('pagehide',()=>controller.reset());
card.hidden=!currentPrincipal?.permissions?.includes('audit.read');
})(typeof window==='undefined'?globalThis:window);