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

39 lines
4.7 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import net from 'node:net';import {spawn} from 'node:child_process';import assert from 'node:assert/strict';import crypto from 'node:crypto';
const read=p=>JSON.parse(fs.readFileSync(p,'utf8'));
const 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/settlement-page-'+tag),settings=path.join(root,'settings.json'),owned=read(settings),g=read(path.join(root,'settlement-fixture.json'));
if(owned.tag!==tag||owned.cluster!=='sgf_modular_regression_'+state.id)throw Error('Owned cluster mismatch');
const output=path.join(root,'settlement-read-http.json');if(fs.existsSync(output))throw Error('Completed read HTTP proof preserved');
await new Promise((resolve,reject)=>{const p=net.createServer();p.once('error',()=>reject(Error('Proof port unavailable')));p.listen(8047,'127.0.0.1',()=>p.close(resolve));});
const log=fs.openSync(path.join(root,'settlement-read-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/settlement-read-http-router.php')],{cwd:process.cwd(),env:{...process.env,SETTLEMENT_PAGE_QA_SETTINGS:settings},windowsHide:true,stdio:['ignore',log,log]});
let checks=0,auth;const jar=new Map(),check=(ok,why)=>{checks++;assert(ok,why);};
async function request(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(key)headers['X-QA-Read-Proof']=key;
if(body)Object.assign(headers,{'Content-Type':'application/json','X-CSRF-TOKEN':auth?.csrf??'','Idempotency-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,data,text,headers:r.headers};
}
try{
let ready=false;for(let n=0;n<50&&!ready;n++){try{const r=await request('/api/supplier-payments');if(r.status===401)ready=true;}catch{}if(!ready)await new Promise(r=>setTimeout(r,100));}
check(ready,'Unauthenticated request is refused by real middleware');
const entry=await request('/qa/read-enter');check(entry.status===200,'Owned QA entry');auth=entry.data;
const query='payment='+g.payment_id+'&source='+g.source_ids[0];
const focused=await request('/api/supplier-payments?'+query,{key:'focus'});
check(focused.status===200&&focused.data.payments[0].unallocated_minor===41,'HTTP preserves original full-history position');
const facts=read(path.join(root,'read-http-focus.json'));
check(facts.transaction.isolation==='repeatable read'&&facts.transaction.read_only==='on'&&facts.writer_lock_queries===0,'Whole HTTP JSON query is read-only snapshot without writer locks');
const page=await request('/supplier-payments?'+query,{key:'page'});check(page.status===200&&page.text.includes('Conferência do pagamento #'+g.payment_id),'HTML read path works');
const htmlFacts=read(path.join(root,'read-http-page.json'));
check(htmlFacts.transaction.isolation==='repeatable read'&&htmlFacts.transaction.read_only==='on'&&htmlFacts.writer_lock_queries===0,'Whole HTML data query is read only without writer locks');
check((await request('/api/supplier-payments?'+query,{headers:{'X-SGF-Context':'obsolete'}})).status===409,'Current context still required');
check((await request('/api/supplier-payments?payment=9223372036854775807')).status===403,'Payment focus cannot escape scope');
const body={proposal_id:g.pending_proposal_id,decision:'confirmed',reason:'Read-only authority does not authorize confirmation.'};
check((await request('/api/supplier-payments/allocation-decide',{body})).status===403,'Read permission still grants no decision');
check((await request('/api/supplier-payments/allocation-decide',{body,headers:{'X-CSRF-TOKEN':''}})).status===419,'CSRF remains enforced on commands');
check(focused.headers.get('cache-control').includes('no-store'),'Private response retained');
const report={passed:true,checks,real_http_kernel:true,json_query:facts,html_query:htmlFacts,no_browser:true,visual_review:false};fs.writeFileSync(output,JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify({passed:true,checks,json_writer_locks:facts.writer_lock_queries,html_writer_locks:htmlFacts.writer_lock_queries}));
}finally{if(server.exitCode===null){server.kill();await new Promise(resolve=>server.once('exit',resolve));}fs.closeSync(log);}