Files
SGF/scripts/ledger-http.mjs
2026-11-04 09:37:20 +02:00

33 lines
6.7 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import net from 'node:net';import {spawn,spawnSync} from 'node:child_process';import assert from 'node:assert/strict';import crypto from 'node:crypto';
const read=p=>JSON.parse(fs.readFileSync(p,'utf8')),state=read('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),tag=process.argv[2];
if(state.port!==55439||!/^[a-f0-9]{12}$/.test(tag??''))throw Error('Owned QA tag required');const root=path.join(state.root,'private/ledger-'+tag),settings=path.join(root,'settings.json'),owned=read(settings);if(owned.tag!==tag||owned.cluster!=='sgf_modular_regression_'+state.id)throw Error('Owned cluster mismatch');const output=path.join(root,'ledger-http.json');if(fs.existsSync(output))throw Error('Completed HTTP proof preserved');
if(!fs.existsSync(path.join(root,'ledger-http-input.json'))){const r=spawnSync('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/payable-history-sgf-proof.php',settings,'ledger-http-input'],{encoding:'utf8',windowsHide:true});if(r.status!==0||!fs.existsSync(path.join(root,'ledger-http-input.json')))throw Error('Private recovery preparation failed');}
const input=read(path.join(root,'ledger-http-input.json'));
await new Promise((resolve,reject)=>{const p=net.createServer();p.once('error',()=>reject(Error('Proof port occupied')));p.listen(8047,'127.0.0.1',()=>p.close(resolve));});
const log=fs.openSync(path.join(root,'ledger-http-server.log'),'a'),server=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['-S','127.0.0.1:8047',path.resolve('scripts/ledger-http-router.php')],{cwd:process.cwd(),env:{...process.env,LEDGER_QA_SETTINGS:settings},windowsHide:true,stdio:['ignore',log,log]});
let checks=0;const check=(ok,why)=>{checks++;assert(ok,why);};
const client=()=>{let auth;const jar=new Map();const request=async(url,{body,key,headers:extra={}}={})=>{const headers={Accept:'application/json',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),'X-SGF-Context':auth?.context??'',...extra};if(body)Object.assign(headers,{'Content-Type':'application/json','X-CSRF-TOKEN':auth?.csrf??'','Idempotency-Key':key??crypto.randomUUID()},extra);const r=await fetch('http://127.0.0.1:8047'+url,{method:body?'POST':'GET',headers,body:body?JSON.stringify(body):undefined,redirect:'manual',signal:AbortSignal.timeout(8000)});for(const c of r.headers.getSetCookie()){const p=c.split(';')[0],at=p.indexOf('=');jar.set(p.slice(0,at),p.slice(at+1));}const text=await r.text();let data;try{data=JSON.parse(text);}catch{}return{status:r.status,text,data,headers:r.headers};};return{request,enter:async(role,module='accounting')=>{const r=await request('/qa/ledger-enter/'+role+'?module='+module);if(r.status!==200)throw Error('QA entry failed');auth=r.data;return auth;}};};
try{
const reader=client();let ready=false;for(let n=0;n<50&&!ready;n++){try{ready=(await reader.request('/api/general-ledger')).status===401;}catch{}if(!ready)await new Promise(r=>setTimeout(r,100));}check(ready,'Unauthenticated ledger refused');await reader.enter('read');
const data=await reader.request('/api/general-ledger?period_key=QA-2026-10',{headers:{'X-Ledger-QA-Proof':'read'}});check(data.status===200&&data.data.journals.length===1&&data.data.lines.length===2&&data.data.totals.debit_minor===6000&&data.data.totals.credit_minor===6000,'Journal and period totals reconciled');
const mode=read(path.join(root,'ledger-http-read-mode.json'));check(mode.transaction.isolation==='repeatable read'&&mode.transaction.read_only==='on'&&mode.writer_lock_queries===0,'Ledger reads are coherent and nonlocking');
check((await reader.request('/api/general-ledger/post',{body:input.post.body})).status===403,'Read actor cannot post');
check((await reader.request('/api/general-ledger/validate',{body:input.validate.body})).status===403,'Read actor cannot validate');
check((await reader.request('/api/general-ledger?account_id=9223372036854775807')).status===403,'Foreign or unavailable account refused');
check((await reader.request('/api/general-ledger?period_key=NOT-CONFIGURED')).status===403,'Missing period not presented as zero');
for(const q of ['company_id=999','page=0','account_id%5Bx%5D=1'])check((await reader.request('/api/general-ledger?'+q)).status===422,'Invalid/injected filters refused');
const html=await reader.request('/general-ledger?period_key=QA-2026-10&account_id=');check(html.status===200&&html.text.includes('60,00 MZN')&&html.text.includes('QA-CONTRAPARTIDA')&&html.text.includes('QA-FORNECEDOR'),'HTML presents immutable source/rule and amounts');
check(!html.text.includes('data-action="post"')&&!html.text.includes('data-action="validate"'),'Read HTML grants no actions');
check(data.headers.get('cache-control').includes('no-store'),'Private response');
check((await reader.request('/api/general-ledger',{headers:{'X-SGF-Context':'obsolete'}})).status===409,'Stale context refused');
await reader.enter('read','purchasing');check((await reader.request('/api/general-ledger')).status===409,'Wrong active module refused');await reader.enter('read');
const preparer=client();await preparer.enter('prepare');const reviewer=client();await reviewer.enter('validate');
for(const [action,c] of [['prepare',preparer],['validate',reviewer],['post',preparer]]){const r=await c.request('/api/general-ledger/'+action,input[action]);check(r.status===200&&r.data.replayed,'Actual HTTP recovers original '+action+' receipt');}
const other=await preparer.request('/api/general-ledger/post',{body:input.post.body});check(other.status===200&&other.data.journal_id===input.journal_id,'Different key recovers same journal');
check((await preparer.request('/api/general-ledger/post',{body:{...input.post.body,reason:'Different effect request content.'}})).status===409,'Conflicting posting content refused');
check((await preparer.request('/api/general-ledger/post',{body:input.post.body,headers:{'X-CSRF-TOKEN':''}})).status===419,'Real CSRF middleware enforced');
const home=await reader.request('/');check(home.status===200&&home.text.includes('/build/assets/'),'Private compiled app manifest served without replacing demos');
const assets=read(path.join(owned.sgf.storage,'public/build/manifest.json')),asset=Object.values(assets).find(a=>a.isEntry)?.file;check(Boolean(asset)&&(await reader.request('/build/'+asset)).status===200,'Private compiled module assets reachable');
fs.writeFileSync(output,JSON.stringify({passed:true,checks,actual_http_kernel:true,recovered_all_three_original_command_receipts:true,one_effect_retained:true,read_transaction:mode,private_build_served:true,visual_review:false,browser_interactions_tested:false},null,2)+'\n');console.log(JSON.stringify({passed:true,checks,real_csrf:true,three_command_routes_recovered:true}));
}finally{if(server.exitCode===null){server.kill();await new Promise(resolve=>server.once('exit',resolve));}fs.closeSync(log);}