563 lines
47 KiB
JavaScript
563 lines
47 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
// AERE Quantum Security Control Plane - JUDECATA ORGANIZATIEI (B2, roadmap master punctele 41 si 43; 2026-10-20). Planul de control
|
|
// "toate domeniile intr-un loc": o organizatie aduce un PACHET DE DOVEZI (prezentari de identitate, plicuri AIP-23, lanturi de incredere AI,
|
|
// chitante de plata, jurnale de masini, starea Shield, raportul de conformitate PQ), iar aici fiecare artefact e judecat de VERIFICATORUL
|
|
// PUBLICAT al felului lui, si se scoate un verdict pe domeniu si un rezumat care nu e mai tare decat cel mai slab domeniu cerut.
|
|
//
|
|
// DE CE UN MODUL VECIN, NU O COMANDA NOUA IN consola.mjs: consola are un contract publicat (iesirea `aere-control-plane-console` forma 2,
|
|
// steagurile si codurile ei) pe care se sprijina proba ei, vizorul web si poarta publicarii; judecata organizatiei are alta intrare (un
|
|
// pachet + contextul celui care se bazeaza pe el), alta iesire, si cheama alte programe. Domeniul Shield e judecat CHIAR de consola.mjs
|
|
// (evalueaza), deci regulile ei traiesc intr-un singur loc.
|
|
//
|
|
// DE CE PRIN ADAPTORUL SUITEI DE CONFORMITATE: tools/aere-conformance/implementare-aere.mjs (publicat in aere-quantum/conformance/) pune
|
|
// deja cele saptesprezece verificatoare publicate in aceeasi forma de verdict, fara nicio regula a lui; un al doilea adaptor scris aici ar
|
|
// diverge in tacere de primul. Il chemam ca PROGRAM (--batch), deci "verificatorul nu porneste" se vede ca atare si iese NEMASURAT.
|
|
//
|
|
// PRINCIPIILE, si sunt valoarea modulului:
|
|
// 1. niciun verdict din pachet nu e crezut: se recalculeaza; un verdict scris in pachet care difera de masuratoare e numit ca suspect;
|
|
// 2. intrarile VERIFICATORULUI (ceasul, emitentii si producatorii de incredere, audienta, nonce-ul, cheia emitentului aleasa, citirile
|
|
// proprii ale lantului) sunt ale celui care se bazeaza pe pachet, nu ale organizatiei: vin din CONTEXT; una luata din pachet nu mai
|
|
// poate da VALID (organizatia si-ar alege singura increderea); ceasul nu se ia NICIODATA din pachet;
|
|
// 3. un domeniu fara artefacte e NOT_PRESENTED, nu verde; un verificator care nu porneste e NOT_MEASURED, nu INVALID si nu VALID;
|
|
// 4. reteaua (finalitatea pe lant) se citeste numai cu --online; fara el, artefactele care o cer sunt numite si verdictul spune ca nu a masurat.
|
|
//
|
|
// node organizatie.mjs --pack pack.json [--context context.json] [--require shield,identity,...] [--at <RFC 3339>] [--online] [--json]
|
|
// iesire 0 = rezumat VALID; 1 = INVALID; 2 = NOT_MEASURED (sau nu s-a putut rula); 3 = PARTIAL. Iesirea e in engleza.
|
|
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { spawn } from 'node:child_process';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const RAD = path.resolve(AICI, '..', '..');
|
|
const sha256 = (s) => '0x' + crypto.createHash('sha256').update(s).digest('hex');
|
|
const obiect = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
|
|
|
|
export const VERSIUNE = 'aere-organization-judgement/1 (2026-10-20)';
|
|
export const PACK_KIND = 'aere-organization-evidence-pack';
|
|
export const CONTEXT_KIND = 'aere-organization-context';
|
|
export const OUT_KIND = 'aere-organization-judgement';
|
|
|
|
/** Domeniile, in ordinea in care se arata. "cloud" nu e un domeniu al lui: ce intorc produsele Cloud se judeca in domeniul artefactului. */
|
|
export const DOMENII = [
|
|
{ id: 'shield', title: 'Security and post-quantum migration (Shield)' },
|
|
{ id: 'identity', title: 'Identity' },
|
|
{ id: 'compliance', title: 'Compliance' },
|
|
{ id: 'ai', title: 'AI and agents' },
|
|
{ id: 'payments', title: 'Payments and settlement' },
|
|
{ id: 'devices', title: 'Machines' },
|
|
{ id: 'audit', title: 'Audit logs' },
|
|
{ id: 'proofs', title: 'Proofs and data' },
|
|
];
|
|
const ID_DOMENII = DOMENII.map((d) => d.id);
|
|
|
|
/** felul artefactului -> domeniul. Felurile suitei de conformitate trebuie sa fie TOATE aici (proba o deriva din reguli.mjs); doua sunt ale
|
|
* planului de control insusi: starea Shield (judecata de consola.mjs) si raportul de conformitate PQ (raport-conformitate.mjs + plicul lui). */
|
|
export const DOMENIUL = {
|
|
'shield-console': 'shield',
|
|
'identity-presentation': 'identity', 'sdjwt-presentation': 'identity', 'sdjwt-vc-presentation': 'identity',
|
|
'compliance-presentation': 'compliance', 'travel-rule-binding': 'compliance', 'travel-rule-message': 'compliance', 'travel-rule-receipt': 'compliance',
|
|
'pq-compliance-report': 'compliance',
|
|
'ai-trust-bundle': 'ai', 'agent-ledger': 'ai',
|
|
'payment-risk': 'payments', 'payment-receipt': 'payments', 'market-timeline': 'payments',
|
|
'device-bundle': 'devices', 'device-operation-log': 'devices', 'device-reputation': 'devices',
|
|
'audit-log': 'audit', 'audit-inclusion': 'audit', 'audit-consistency': 'audit',
|
|
envelope: 'proofs', 'dataset-record': 'proofs', 'dataset-answer': 'proofs', 'dataset-consistency': 'proofs', 'dataset-sample': 'proofs',
|
|
};
|
|
export const FELURI_PROPRII = ['shield-console', 'pq-compliance-report'];
|
|
|
|
/** Cine aduce fiecare intrare a verificatorului, pe fel. Cheile sunt EXACT cele pe care le citeste adaptorul suitei (proba le deriva din
|
|
* sursa lui si din cei 791 de vectori, si cade pe o cheie neclasificata):
|
|
* artifact numai din pachet (artefactul organizatiei);
|
|
* stapled semnat de un tert (o lista de stare, o revocare, o politica de firmware): din pachet SI din context, unite (listele
|
|
* CONTEXTULUI intai, apoi ale pachetului: un verificator judeca numai primele N); o valoare care nu e lista: contextul castiga;
|
|
* verifier alegerea celui care verifica (increderea, provocarea, asteptarile, citirile lui proprii): din context; daca lipseste
|
|
* acolo si o aduce pachetul, se foloseste, dar artefactul nu mai poate fi VALID (`self-supplied`);
|
|
* verifier-only numai din context; din pachet se arunca, numit (lantul de citit, cheile KEM private ale beneficiarului, plafonul de timp);
|
|
* clock ceasul: contextul pe artefact sau pe fel, apoi --at, apoi `now` global al contextului, apoi ceasul masinii; din pachet NICIODATA.
|
|
* Revizia B2 (2026-10-21): nu mai exista fel "tot inputul e artefactul". Cronologia de piata si reputatia erau date intregi verificatorului,
|
|
* deci organizatia isi alegea singura ARBITRUL pietei (constants.arbiter: o rezolvare a disputei de catre ea insasi iesea VALID, iar constantele
|
|
* aduse de cel care verifica erau ignorate) si FAPTELE din care se calculeaza reputatia (fapte inventate, fara jurnal, iesea VALID). Cheile pe
|
|
* care le citeste modulul insusi se deriva acum din sursa lui (proba) si se clasifica pe nume, la fel ca toate celelalte; '*': A ramane numai
|
|
* pentru o cheie pe care modulul NU o citeste pe nume (un membru in plus), ca modulul care cere forma exacta sa o refuze in continuare. */
|
|
const A = 'artifact', S = 'stapled', V = 'verifier', O = 'verifier-only', C = 'clock';
|
|
const AUDIT = { log: A, head: A, proof: A, newer: A, older: V, signer: V };
|
|
const SDJWT = { sdJwt: A, now: C, issuerKey: V, issuerAlg: V, classicalHalf: V, audience: V, nonce: V, expectedIssuer: V, requireKeyBinding: V, requireExp: V,
|
|
maxAgeS: V, expectedTyp: V, expectedVct: V, statusListJwt: S, statusListKey: V, statusListAlg: V, requireStatus: V };
|
|
const TRAVEL = { binding: A, presentation: A, message: A, receipt: A, now: C, registries: V, originatorId: V, nonce: V, me: V, seen: V, beneficiaryId: V,
|
|
maxAgeS: V, statusLists: S, kem: O };
|
|
const DATASET = { record: A, commitment: A, answer: A, proof: A, newer: A, served: A, older: V, k: V, seed: V };
|
|
export const INTRARI = {
|
|
envelope: { envelope: A, raw: A, chain: O },
|
|
'payment-risk': { policy: A, inputs: A },
|
|
'payment-receipt': { receipt: A, policy: A, inputs: A, tx: V, txReceipt: V, chain: O },
|
|
// constants: ARBITER si ARBITRATION_TIMEOUT citite de cel care verifica din contractul pietei in care are incredere; chainId: lantul pe care se
|
|
// citeste plicul executiei (ca `chain` la plic); certified: citirea de sub ancora, pe care adaptorul o face el insusi
|
|
'market-timeline': { chainId: O, market: A, jobId: A, constants: V, job: A, events: A, result: A, spec: A, envelope: A, certified: O, '*': A },
|
|
'ai-trust-bundle': { bundle: A, chain: O },
|
|
'audit-log': AUDIT, 'audit-inclusion': AUDIT, 'audit-consistency': AUDIT,
|
|
'agent-ledger': { ledger: A, policy: A, policyHash: V, anchors: V, notBefore: V, revocations: S, maxTime: O },
|
|
'identity-presentation': { presentation: A, now: C, audience: V, nonce: V, trustedIssuers: V, maxAgeS: V, request: V, statusLists: S, revocations: S },
|
|
'device-bundle': { bundle: A, softwareAttestation: A, now: C, nonce: V, audience: V, trustedManufacturers: V, anchors: V, maxAgeS: V,
|
|
statusLists: S, firmwarePolicies: S, statusRecords: S },
|
|
'sdjwt-presentation': SDJWT, 'sdjwt-vc-presentation': SDJWT,
|
|
'travel-rule-binding': TRAVEL, 'travel-rule-message': TRAVEL, 'travel-rule-receipt': TRAVEL,
|
|
'compliance-presentation': { presentation: A, now: C, policy: V, audience: V, nonce: V, request: V, statusLists: S, revocations: S },
|
|
'device-operation-log': { log: A, now: C, clockSkewS: V, trustedManufacturers: V, readings: V, beacons: V, otherRecords: V, statusLists: S, statusRecords: S },
|
|
// facts: ce a DERIVAT verificatorul de operare din jurnal (citirea celui care verifica); params si claimed sunt afirmatia (fereastra ei si cifrele)
|
|
'device-reputation': { facts: V, params: A, claimed: A, '*': A },
|
|
'dataset-record': DATASET, 'dataset-answer': DATASET, 'dataset-consistency': DATASET, 'dataset-sample': DATASET,
|
|
'shield-console': { bundle: A, plan: A, execution: A },
|
|
'pq-compliance-report': { report: A, envelope: A },
|
|
};
|
|
|
|
/** Regulile planului de control pentru felurile lui proprii (restul regulilor sunt ale verificatoarelor, numite de suita). */
|
|
export const REGULI_PROPRII = {
|
|
'shield.bundle-form': ['integrity', 'the Shield state carries a sidecar bundle (kind aere-verify-layer-bundle) with at least one entry'],
|
|
'shield.audit-chain': ['integrity', 'the audit chain recomputed from its entries is intact (the console does not read chainOk from the bundle)'],
|
|
'shield.envelope-integrity': ['integrity', 'every AIP-23 envelope in the bundle: sha256 of its statement is its statementHash'],
|
|
'shield.self-report': ['integrity', 'what the bundle declares about its own chain (chainOk) is what the measurement gives'],
|
|
'shield.execution-chain': ['integrity', 'the migration execution chain (execution.json) recomputed is intact'],
|
|
'shield.plan-form': ['integrity', 'the migration plan has the planner\'s list of actions'],
|
|
'pqreport.binding': ['integrity', 'the compliance report is the one bound to the AIP-23 compliance envelope (canonical sha256 == evidenceHash)'],
|
|
// revizia B2, 2026-10-21: plicul nu e semnat, deci legarea singura lasa un rezumat rescris peste constatarile lui; si o executie rupta de planul ei
|
|
'pqreport.derivation': ['integrity', 'the report is what the report generator gives for its own findings, date, risk and CNSA choice, and the envelope\'s statement is the one it writes'],
|
|
'shield.execution-plan': ['integrity', 'the execution presented with a plan is the executor\'s run of that plan: one record per automatic action, in the plan\'s order, between its start and end records, and the end record counts them'],
|
|
};
|
|
/** Felurile pe care adaptorul le judeca CITIND LANTUL cu --online (plicul AIP-23 al lor); proba le deriva din sursa adaptorului. Ceilalti (o
|
|
* masina, un jurnal de operare) isi judeca notarizarea pe CITIRILE celui care verifica (anchors, readings), aduse in context: --online nu le ajuta. */
|
|
export const CITESC_LANTUL = ['envelope', 'payment-receipt', 'market-timeline', 'ai-trust-bundle', 'pq-compliance-report'];
|
|
|
|
// ---- unde stau celelalte piese: langa planul de control intr-o copie publica (../conformance), sau in depozitul de dezvoltare (tools/)
|
|
function dosarulSuitei() {
|
|
const c = [path.join(AICI, '..', 'conformance'), path.join(RAD, 'tools', 'aere-conformance')];
|
|
return c.find((d) => fs.existsSync(path.join(d, 'implementare-aere.mjs'))) || c[c.length - 1];
|
|
}
|
|
export const caleaAdaptorului = () => (process.env.AERE_ORG_ADAPTOR ? path.resolve(process.env.AERE_ORG_ADAPTOR) : path.join(dosarulSuitei(), 'implementare-aere.mjs'));
|
|
/** Forma verdictului (verdict.mjs al suitei) si tabla verificatoarelor (reguli.mjs), incarcate o data; o lipsa intoarce motivul, nu arunca. */
|
|
let SUITA = null;
|
|
async function suita() {
|
|
if (SUITA) return SUITA;
|
|
const d = dosarulSuitei();
|
|
try {
|
|
const V = await import(pathToFileURL(path.join(d, 'verdict.mjs')).href);
|
|
let R = null; try { R = await import(pathToFileURL(path.join(d, 'reguli.mjs')).href); } catch { R = null; }
|
|
SUITA = { ok: true, V, R };
|
|
} catch (e) { SUITA = { ok: false, motiv: `the verdict form of the conformance suite is not at ${d} (${String(e.message).split('\n')[0]})` }; }
|
|
return SUITA;
|
|
}
|
|
function verificatorul(fel, R) {
|
|
if (fel === 'shield-console') return { id: 'control-plane-console', module: 'control-plane/consola.mjs' };
|
|
if (fel === 'pq-compliance-report') return { id: 'compliance-report', module: 'control-plane/raport-conformitate.mjs, and verify-proof.mjs (AIP-23 reference verifier) for its envelope' };
|
|
if (!R || !R.VERIFICATOARE) return { id: '?', module: '?' };
|
|
const [id, v] = Object.entries(R.VERIFICATOARE).find(([, x]) => x.artifacts.includes(fel)) || [];
|
|
if (!v) return { id: '?', module: '?' };
|
|
let module = v.sursa === 'verify-proof' ? 'verify-proof.mjs (AIP-23 reference verifier)' : '?';
|
|
if (R.MODULE && R.MODULE[v.sursa]) module = fileURLToPath(R.MODULE[v.sursa].url).split(/[\\/]/).slice(-2).join('/');
|
|
return { id, module, through: 'conformance/implementare-aere.mjs' };
|
|
}
|
|
|
|
// ---- adaptorul suitei, chemat ca program: un verdict pe rand, in ordine; orice altceva e "nu a pornit" pentru TOATE artefactele lotului
|
|
function ruleaza(cmd, args, timeoutMs, env) {
|
|
return new Promise((gata) => {
|
|
let out = '', err = '', c, expirat = false;
|
|
try { c = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env }); } catch (e) { gata({ code: null, out, err: String(e.message) }); return; }
|
|
const t = setTimeout(() => { expirat = true; c.kill(); }, timeoutMs);
|
|
c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', (d) => { err += d; });
|
|
c.on('error', (e) => { clearTimeout(t); gata({ code: null, out, err: String(e.message) }); });
|
|
c.on('close', (code) => { clearTimeout(t); gata({ code, out, err, expirat }); });
|
|
});
|
|
}
|
|
const NIVELURI = ['integrity', 'authenticity', 'anchoring', 'finality'];
|
|
/**
|
|
* Un verdict al suitei e crezut ca VERDICT numai daca are forma ei si verdictul lui e CHIAR cel pe care regula suitei (verdict.mjs) il da pentru
|
|
* nivelurile lui: INVALID <=> o regula refuzata (si `failed` = regulile nivelurilor); VALID numai cu integritatea PASSED si fara o afirmatie
|
|
* nemasurata (o cauza din `cauzePartiale`). Revizia B2 (2026-10-21): pana azi un "VALID" cu integritatea nemasurata trecea drept VALID.
|
|
*/
|
|
export function formaVerdictului(j, cauzePartiale) {
|
|
if (!(obiect(j) && ['VALID', 'INVALID', 'PARTIAL'].includes(j.verdict) && Array.isArray(j.failed) && Array.isArray(j.levels) && j.levels.length === 4)) return false;
|
|
if (!j.levels.every((l, i) => obiect(l) && l.level === NIVELURI[i] && ['PASSED', 'FAILED', 'NOT_MEASURED'].includes(l.state) && Array.isArray(l.rules)
|
|
&& l.rules.every((x) => typeof x === 'string') && (l.state === 'FAILED') === (l.rules.length > 0) && (l.state !== 'NOT_MEASURED' || typeof l.cause === 'string'))) return false;
|
|
if (!Array.isArray(cauzePartiale)) return false;
|
|
const failed = [...new Set(j.levels.flatMap((l) => l.rules))].sort();
|
|
const partial = j.levels.some((l) => l.state === 'NOT_MEASURED' && cauzePartiale.includes(l.cause));
|
|
const v = failed.length ? 'INVALID' : partial || j.levels[0].state !== 'PASSED' ? 'PARTIAL' : 'VALID';
|
|
return j.verdict === v && JSON.stringify([...j.failed].sort()) === JSON.stringify(failed);
|
|
}
|
|
/** Un lot catre adaptor: { linii, motiv } (motiv = de ce lotul nu a dat cate un rand pe artefact) */
|
|
async function unLot(items, { adaptor, online, timeoutS }) {
|
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-org-'));
|
|
try {
|
|
const lista = items.map((x, k) => { const f = path.join(T, k + '.json'); fs.writeFileSync(f, JSON.stringify({ artifact: x.artifact, input: x.input })); return { file: f, online: !!online }; });
|
|
const lf = path.join(T, 'list.json'); fs.writeFileSync(lf, JSON.stringify(lista));
|
|
const r = await ruleaza(process.execPath, [adaptor, '--batch', lf], timeoutS * 1000, process.env);
|
|
const linii = r.out.split('\n').filter((l) => l.trim());
|
|
const coada = (r.err || '').trim().split('\n').slice(-1)[0].slice(0, 200);
|
|
const motiv = linii.length === items.length ? null : `the adapter gave ${linii.length} verdict(s) for ${items.length} artifact(s) (${r.expirat ? `it did not finish in ${timeoutS} s` : 'exit ' + r.code})${coada ? ': ' + coada : ''}`;
|
|
return { linii, motiv };
|
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
|
}
|
|
/** Randurile unui lot, citite in ORDINEA artefactelor: un verdict dat pentru alt fel decat cel cerut in locul lui inseamna ca ordinea raspunsurilor
|
|
* nu se poate crede, deci niciun raspuns al lotului (revizia B2: un adaptor care raspundea invers dadea artefactului stricat verdictul altuia). */
|
|
function citesteLotul(linii, items, cauzePartiale) {
|
|
const js = linii.map((l) => { try { return JSON.parse(l); } catch { return undefined; } });
|
|
const k = js.findIndex((j, i) => obiect(j) && typeof j.verdict === 'string' && j.artifact !== items[i].artifact);
|
|
if (k >= 0) return items.map(() => ({ error: `the adapter answered for ${JSON.stringify(js[k].artifact)} in the place of ${items[k].artifact} (position ${k}): the order of its answers cannot be trusted, so none is` }));
|
|
return js.map((j) => {
|
|
if (j === undefined) return { error: 'the adapter gave a line that is not JSON' };
|
|
if (formaVerdictului(j, cauzePartiale)) return j;
|
|
return { error: obiect(j) && typeof j.error === 'string' ? 'the verifier gave no verdict: ' + j.error : 'the adapter gave something that is not a verdict of the suite\'s form (or a verdict its own levels do not give)' };
|
|
});
|
|
}
|
|
async function adaptorul(items, { adaptor, online, timeoutS, cauzePartiale }) {
|
|
if (!items.length) return [];
|
|
if (!fs.existsSync(adaptor)) return items.map(() => ({ error: `the conformance adapter is not at ${adaptor}: no verifier was run` }));
|
|
const o = { adaptor, online, timeoutS };
|
|
const lot = await unLot(items, o);
|
|
if (!lot.motiv) return citesteLotul(lot.linii, items, cauzePartiale);
|
|
if (items.length === 1) return [{ error: lot.motiv }];
|
|
// un lot care nu s-a terminat (un artefact care opreste sau tine adaptorul pe loc) nu are voie sa ia cu el verdictele celorlalte: fiecare artefact
|
|
// e judecat atunci SINGUR, cu acelasi plafon de timp (revizia B2: un esantion lent adus de pachet facea NOT_MEASURED tot lotul)
|
|
const rez = new Array(items.length); let urm = 0;
|
|
await Promise.all(Array.from({ length: Math.min(2, items.length) }, async () => {
|
|
while (urm < items.length) {
|
|
const k = urm++; const u = await unLot([items[k]], o);
|
|
const unu = u.motiv ? { error: u.motiv } : citesteLotul(u.linii, [items[k]], cauzePartiale)[0];
|
|
rez[k] = unu.error ? { error: `the batch of ${items.length} did not complete (${lot.motiv}); judged alone: ${unu.error}` } : unu;
|
|
}
|
|
}));
|
|
return rez;
|
|
}
|
|
|
|
// ---- intrarile unui artefact: ce e al organizatiei, ce e semnat de un tert, ce e al celui care verifica, ceasul
|
|
const CEAS_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
export function rezolvaIntrarile(fel, intrare, ctx, ceas) {
|
|
const cls = INTRARI[fel];
|
|
const note = { fromContext: [], selfSupplied: [], dropped: [], ignored: [], contextIgnored: [], stapled: {}, now: null };
|
|
if (!obiect(cls)) throw new Error(`no classification of the inputs of ${fel}`);
|
|
// fara prototip: o cheie `__proto__` din pachet ramane o cheie (si ajunge la verificator ca atare), nu schimba obiectul
|
|
const input = Object.create(null);
|
|
// cine castiga o intrare a verificatorului adusa de amandoi: contextul (o singura hotarare, citita de ambele bucle)
|
|
const castigaContextul = (k) => Object.hasOwn(ctx, k);
|
|
// '*': la felurile pe care modulul le citeste INTREGI (si le cere exact forma), o cheie necunoscuta e a artefactului si ajunge la el; cheile pe
|
|
// care le citeste sunt toate numite, deci '*' nu poate acoperi una a celui care verifica (proba o cere)
|
|
const clasa = (k) => (Object.hasOwn(cls, k) ? cls[k] : Object.hasOwn(cls, '*') ? cls['*'] : null);
|
|
for (const [k, v] of Object.entries(intrare)) {
|
|
const c = clasa(k);
|
|
if (c === null) note.ignored.push(k);
|
|
else if (c === A) input[k] = v;
|
|
else if (c === S) { input[k] = v; note.stapled[k] = 'pack'; }
|
|
else if (c === V) { if (!castigaContextul(k)) { input[k] = v; note.selfSupplied.push(k); } }
|
|
else note.dropped.push(k); // verifier-only si clock: o valoare din pachet nu se foloseste niciodata
|
|
}
|
|
for (const [k, v] of Object.entries(ctx)) {
|
|
const c = clasa(k);
|
|
if (c === V && !castigaContextul(k)) continue;
|
|
if (c === V || c === O || c === C) { input[k] = v; note.fromContext.push(k); }
|
|
else if (c === S) {
|
|
// listele CELUI CARE VERIFICA intai: verificatoarele judeca numai primele N elemente ale unei liste (64 de liste de rechemare la o masina),
|
|
// iar cu listele pachetului in fata, 64 de liste curate aduse de organizatie impingeau rechemarea adusa de cel care verifica afara din
|
|
// fereastra si INVALID devenea PARTIAL (revizia B2, 2026-10-21, masurat pe vectorul sold-once.manufacture-revoked)
|
|
if (Array.isArray(input[k]) && Array.isArray(v)) { input[k] = [...v, ...input[k]]; note.stapled[k] = 'context+pack'; }
|
|
else { input[k] = v; note.stapled[k] = 'context'; }
|
|
} else note.contextIgnored.push(k); // un context nu poate scrie artefactul organizatiei
|
|
}
|
|
if (cls.now === C) {
|
|
if (Object.hasOwn(input, 'now') && note.fromContext.includes('now')) note.now = { value: input.now, source: 'context' };
|
|
else { input.now = ceas.now; note.now = { value: ceas.now, source: ceas.source }; }
|
|
}
|
|
return { input, note };
|
|
}
|
|
|
|
// ---- felurile proprii: starea Shield (consola.mjs) si raportul de conformitate PQ
|
|
/** Executia (execution.json al executorului) e rularea ACESTUI plan? null daca da, altfel motivul. Executorul scrie un rand `start` cu numarul de
|
|
* actiuni ale planului, cate un rand pentru fiecare actiune `auto-aere`, in ordinea planului (si cand nu are consimtamant: SKIPPED), si un rand `end`
|
|
* care le numara (executa-migrare.mjs, `executa`). */
|
|
export function legaturaExecutiei(plan, execution) {
|
|
const recs = (Array.isArray(execution) ? execution : execution.records).map((e) => (obiect(e) ? e.record : null));
|
|
const start = recs[0], end = recs[recs.length - 1];
|
|
if (!(obiect(start) && start.type === 'start')) return 'the execution does not begin with the executor\'s start record';
|
|
if (recs.length < 2 || !(obiect(end) && end.type === 'end')) return 'the execution does not end with the executor\'s end record (cut short, or not written by the executor)';
|
|
const actiuni = plan.actions, auto = actiuni.filter((a) => obiect(a) && a.method === 'auto-aere').map((a) => a.ref);
|
|
if (start.actionsInPlan !== actiuni.length) return `the execution says its plan had ${start.actionsInPlan} action(s); the plan presented with it has ${actiuni.length}`;
|
|
const facute = recs.slice(1, -1);
|
|
if (!facute.every((r) => obiect(r) && typeof r.ref === 'string')) return 'a record between start and end is not an action record';
|
|
const refs = facute.map((r) => r.ref);
|
|
if (JSON.stringify(refs) !== JSON.stringify(auto)) {
|
|
const lipsa = auto.filter((x) => !refs.includes(x)), straine = refs.filter((x) => !auto.includes(x));
|
|
return 'the execution is not the run of the plan presented with it: ' + [lipsa.length ? `no record for ${lipsa.slice(0, 3).join(', ')}${lipsa.length > 3 ? ` and ${lipsa.length - 3} more` : ''}` : '',
|
|
straine.length ? `records for actions the plan does not have automatic: ${straine.slice(0, 3).join(', ')}${straine.length > 3 ? ` and ${straine.length - 3} more` : ''}` : '',
|
|
!lipsa.length && !straine.length ? 'the records are not one per automatic action in the plan\'s order' : ''].filter(Boolean).join('; ');
|
|
}
|
|
const s = end.summary, n = (v) => facute.filter((r) => r.verdict === v).length;
|
|
if (!(obiect(s) && s.total === facute.length && s.ok === n('OK') && s.failed === n('FAILED') && s.dryRun === n('DRY-RUN') && s.skipped === n('SKIPPED-no-consent') && s.notExecutable === actiuni.length - auto.length)) return 'the end record of the execution does not count its own records';
|
|
return null;
|
|
}
|
|
async function judecaShield(input, V) {
|
|
const acc = V.acumulator();
|
|
const b = input.bundle;
|
|
if (!(obiect(b) && b.kind === 'aere-verify-layer-bundle' && Array.isArray(b.entries) && b.entries.length > 0)) {
|
|
V.pica(acc, 'integrity', 'shield.bundle-form', 'not a sidecar bundle with entries (kind aere-verify-layer-bundle, entries[] not empty)');
|
|
for (const l of ['authenticity', 'anchoring', 'finality']) V.nemasurat(acc, l, 'not-reached', 'the bundle could not be read');
|
|
return { v: V.verdictUnic('shield-console', acc), details: null };
|
|
}
|
|
const { evalueaza } = await import(pathToFileURL(path.join(AICI, 'consola.mjs')).href);
|
|
const st = await evalueaza({ bundle: b, plan: input.plan === undefined ? null : input.plan, execution: input.execution === undefined ? null : input.execution });
|
|
if (!st.auditChain.ok) V.pica(acc, 'integrity', 'shield.audit-chain', `the audit chain is broken at seq ${st.auditChain.brokenAtSeq}: ${st.auditChain.reason}`);
|
|
if (st.envelopes.bad.length) V.pica(acc, 'integrity', 'shield.envelope-integrity', st.envelopes.bad.map((x) => `seq ${x.seq} (${x.kind}): ${x.reason}`).join('; '));
|
|
if (st.selfReportSuspect) V.pica(acc, 'integrity', 'shield.self-report', st.selfReportSuspect);
|
|
if (st.execution && !st.execution.chainOk) V.pica(acc, 'integrity', 'shield.execution-chain', `the execution chain is broken at seq ${st.execution.brokenAtSeq}: ${st.execution.reason}`);
|
|
if (st.migration && st.migration.total === null) V.pica(acc, 'integrity', 'shield.plan-form', st.migration.error);
|
|
// executia si planul prezentate impreuna trebuie sa fie aceeasi rulare (revizia B2: o executie din care lipsea o actiune, sau executia altui plan,
|
|
// iesea VALID langa planul acesta); numai cand amandoua se pot citi, altfel au deja regula lor
|
|
if (st.execution && st.execution.chainOk && st.migration && st.migration.total !== null) { const m = legaturaExecutiei(input.plan, input.execution); if (m) V.pica(acc, 'integrity', 'shield.execution-plan', m); }
|
|
V.trece(acc, 'integrity', `the console recomputed the audit chain (${st.auditChain.count} entries, head ${st.auditChain.head || '-'}) and ${st.envelopes.total} envelope digest(s)`
|
|
+ (st.migration && st.migration.total !== null ? `; plan: ${st.migration.total} action(s)` : '') + (st.execution ? `; execution: ${st.execution.actions} action(s)` : ''));
|
|
// ce poarta burafu si consola NU judeca: o semnatura, o notarizare. Purtate, sunt o afirmatie nemasurata aici (PARTIAL)
|
|
const semnate = b.entries.filter((e) => obiect(e) && obiect(e.proof) && e.proof.signature).map((e) => e.seq);
|
|
const notarizate = b.entries.filter((e) => obiect(e) && obiect(e.proof) && e.proof.notarization).map((e) => e.seq);
|
|
if (semnate.length) V.nemasurat(acc, 'authenticity', 'claim-not-checked', `entries ${semnate.join(',')} carry a signature, which the console does not judge: present each as an \`envelope\` artifact`);
|
|
else V.nemasurat(acc, 'authenticity', 'not-judged', 'no entry carries a signature; a signed head is judged as an `audit-log` artifact with its head');
|
|
const fin = notarizate.length ? ['claim-not-checked', `entries ${notarizate.join(',')} claim a notarization, which the console does not read: present each as an \`envelope\` artifact and judge with --online`]
|
|
: ['not-judged', 'the head is anchored by notarizing its envelope, judged as the artifact `envelope`'];
|
|
for (const l of ['anchoring', 'finality']) V.nemasurat(acc, l, fin[0], fin[1]);
|
|
return {
|
|
v: V.verdictUnic('shield-console', acc),
|
|
details: { console: { auditChain: { ok: st.auditChain.ok, count: st.auditChain.count, head: st.auditChain.head }, envelopes: { total: st.envelopes.total, intact: st.envelopes.intact },
|
|
migration: st.migration, execution: st.execution } },
|
|
};
|
|
}
|
|
/** Un verdict al suitei, pus inapoi intr-un acumulator (ca sa i se adauge o regula si sa se refaca verdictul in aceeasi forma). */
|
|
function inAcumulator(V, acc, v) {
|
|
for (const l of v.levels) {
|
|
if (l.state === 'FAILED') for (const r of l.rules) V.pica(acc, l.level, r, l.reason);
|
|
else if (l.state === 'NOT_MEASURED') V.nemasurat(acc, l.level, l.cause, l.reason);
|
|
else V.trece(acc, l.level, l.reason);
|
|
}
|
|
}
|
|
|
|
// ---- judecata
|
|
const RANG = { INVALID: 0, NOT_MEASURED: 1, NOT_PRESENTED: 1, PARTIAL: 2, VALID: 3 };
|
|
export const celMaiSlab = (vs) => vs.reduce((a, b) => (RANG[b] < RANG[a] ? b : a));
|
|
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
const nemasuratArtefact = (baza, motiv) => ({ ...baza, verdict: 'NOT_MEASURED', verifierVerdict: null, failed: [], levels: null, error: motiv });
|
|
|
|
function citesteContextul(context) {
|
|
if (context == null) return { ok: true, global: {}, byKind: {}, byArtifact: {}, now: null, require: null };
|
|
if (!obiect(context) || context.kind !== CONTEXT_KIND) return { ok: false, motiv: `the context is not an ${CONTEXT_KIND}` };
|
|
for (const k of ['byKind', 'byArtifact']) if (context[k] !== undefined && !(obiect(context[k]) && Object.values(context[k]).every(obiect))) return { ok: false, motiv: `context.${k} must map names to objects` };
|
|
if (context.now !== undefined && !(typeof context.now === 'string' && CEAS_RE.test(context.now))) return { ok: false, motiv: 'context.now must be an RFC 3339 time' };
|
|
if (context.require !== undefined && !(Array.isArray(context.require) && context.require.length && context.require.every((d) => ID_DOMENII.includes(d)))) return { ok: false, motiv: `context.require must list domains among ${ID_DOMENII.join(', ')}` };
|
|
return { ok: true, byKind: context.byKind || {}, byArtifact: context.byArtifact || {}, now: context.now || null, require: context.require || null };
|
|
}
|
|
|
|
/**
|
|
* Judeca un pachet de dovezi al unei organizatii.
|
|
* @param {object} pack pachetul (aere-organization-evidence-pack)
|
|
* @param {object} [o.context] contextul celui care se bazeaza pe pachet (aere-organization-context): ceas, incredere, provocari, cerinte
|
|
* @param {string[]} [o.require] domeniile cerute (peste context.require); implicit TOATE
|
|
* @param {string} [o.at] ceasul celui care verifica (RFC 3339), daca nu il da contextul; implicit ceasul masinii
|
|
* @param {boolean} [o.online] citeste lantul pentru finalitate (prin adaptor si verificatorul de referinta); implicit NU
|
|
*/
|
|
export async function judecaPachetul(pack, { context = null, require = null, at = null, online = false, adaptor = caleaAdaptorului(), timeoutS = null, packSha256 = null } = {}) {
|
|
if (require !== null && !(Array.isArray(require) && require.length && require.every((d) => ID_DOMENII.includes(d)))) throw new Error(`--require must list domains among ${ID_DOMENII.join(', ')}`);
|
|
if (at !== null && !(typeof at === 'string' && CEAS_RE.test(at))) throw new Error('--at must be an RFC 3339 time');
|
|
const ctx = citesteContextul(context);
|
|
const ceas = at ? { now: at, source: '--at' } : ctx.ok && ctx.now ? { now: ctx.now, source: 'context' } : { now: new Date().toISOString(), source: 'this machine' };
|
|
const cerute = require ? { domains: require, source: '--require' } : ctx.ok && ctx.require ? { domains: ctx.require, source: 'context' } : { domains: ID_DOMENII, source: 'default: every domain' };
|
|
const iesire = {
|
|
v: 1, kind: OUT_KIND, version: VERSIUNE,
|
|
organization: obiect(pack) && typeof pack.organization === 'string' ? pack.organization : null,
|
|
pack: { sha256: packSha256, createdAt: obiect(pack) && typeof pack.createdAt === 'string' ? pack.createdAt : null, artifacts: obiect(pack) && Array.isArray(pack.artifacts) ? pack.artifacts.length : null },
|
|
clock: ceas, network: { online: !!online, needed: [], readings: [], unmeasured: [], finality: [] }, required: cerute,
|
|
summary: null, domains: [], artifacts: [], unjudged: [], selfReport: { ignored: [], suspect: [] },
|
|
};
|
|
const opreste = (motiv) => {
|
|
iesire.domains = DOMENII.map((d) => ({ domain: d.id, title: d.title, verdict: 'NOT_PRESENTED', artifacts: [], failed: [], reason: 'nothing was judged' }));
|
|
iesire.summary = { verdict: 'NOT_MEASURED', weakest: [], notPresented: [], outsideRequired: {}, reason: motiv };
|
|
return iesire;
|
|
};
|
|
if (!ctx.ok) return opreste('the context cannot be read: ' + ctx.motiv);
|
|
if (!(obiect(pack) && pack.kind === PACK_KIND && Array.isArray(pack.artifacts))) return opreste(`the pack cannot be read: not an ${PACK_KIND} with an artifacts list`);
|
|
const S0 = await suita();
|
|
if (!S0.ok) return opreste(S0.motiv);
|
|
const { V, R } = S0;
|
|
|
|
// 1) forma fiecarui artefact, domeniul lui, intrarile rezolvate
|
|
const rezultate = []; const deJudecat = []; const vazute = new Set(); const pePozitie = new Map();
|
|
pack.artifacts.forEach((a, k) => {
|
|
const fel = obiect(a) && typeof a.artifact === 'string' ? a.artifact : null;
|
|
const id = obiect(a) && typeof a.id === 'string' && ID_RE.test(a.id) ? a.id : null;
|
|
const domeniu = fel && Object.hasOwn(DOMENIUL, fel) ? DOMENIUL[fel] : null;
|
|
if (!domeniu) { iesire.unjudged.push({ id: id || `#${k}`, artifact: fel, reason: fel ? `no verifier here judges the kind ${fel}` : 'the artifact names no kind' }); return; }
|
|
const baza = { id: id || `#${k}`, artifact: fel, domain: domeniu, verifier: verificatorul(fel, R), inputSha256: sha256(JSON.stringify(obiect(a) ? (a.input ?? null) : null)) };
|
|
const nm = (motiv) => { const r = nemasuratArtefact(baza, motiv); rezultate.push(r); pePozitie.set(k, r); };
|
|
if (!id) { nm('the artifact has no id (letters, digits, . _ : / -, at most 128)'); return; }
|
|
if (vazute.has(id)) { nm(`the id ${id} appears more than once in the pack: only its first artifact is judged`); return; }
|
|
vazute.add(id);
|
|
if (!obiect(a.input)) { nm('input is not an object'); return; }
|
|
const cx = { ...(obiect(ctx.byKind[fel]) ? ctx.byKind[fel] : {}), ...(obiect(ctx.byArtifact[id]) ? ctx.byArtifact[id] : {}) };
|
|
const { input, note } = rezolvaIntrarile(fel, a.input, cx, ceas);
|
|
const r = { ...baza, inputs: note };
|
|
rezultate.push(r); pePozitie.set(k, r); deJudecat.push({ r, input });
|
|
});
|
|
|
|
// 2) un singur lot catre adaptor (felurile suitei si plicul raportului PQ), apoi felurile proprii
|
|
const lot = [];
|
|
for (const x of deJudecat) {
|
|
if (x.r.artifact === 'shield-console') continue;
|
|
if (x.r.artifact === 'pq-compliance-report') { x.plic = lot.length; lot.push({ artifact: 'envelope', input: { envelope: x.input.envelope === undefined ? null : x.input.envelope } }); continue; }
|
|
x.lot = lot.length; lot.push({ artifact: x.r.artifact, input: x.input });
|
|
}
|
|
const verdicte = await adaptorul(lot, { adaptor, online, timeoutS: timeoutS || (online ? 1800 : 600), cauzePartiale: V.CAUZE_PARTIALE });
|
|
for (const x of deJudecat) {
|
|
let v = null, eroare = null;
|
|
try {
|
|
if (x.r.artifact === 'shield-console') { const s = await judecaShield(x.input, V); v = s.v; if (s.details) x.r.details = s.details; }
|
|
else if (x.r.artifact === 'pq-compliance-report') {
|
|
const { verifyReport, verificaDerivarea } = await import(pathToFileURL(path.join(AICI, 'raport-conformitate.mjs')).href);
|
|
const leg = obiect(x.input.report) ? verifyReport(x.input.report, x.input.envelope) : { ok: false, reason: 'the artifact carries no report' };
|
|
const pv = verdicte[x.plic];
|
|
if (pv.error && leg.ok) eroare = 'the envelope of the report was not judged: ' + pv.error;
|
|
else {
|
|
const acc = V.acumulator();
|
|
if (pv.error) for (const l of NIVELURI) V.nemasurat(acc, l, 'not-reached', 'the envelope of the report was not judged: ' + pv.error);
|
|
else inAcumulator(V, acc, pv);
|
|
if (!leg.ok) V.pica(acc, 'integrity', 'pqreport.binding', leg.reason);
|
|
else {
|
|
V.trece(acc, 'integrity', 'the report is the one bound to the envelope (canonical sha256 == evidenceHash)');
|
|
// legat nu inseamna facut de generator: plicul nu e semnat, deci un rezumat rescris si legat din nou trecea (revizia B2, 2026-10-21)
|
|
const dv = verificaDerivarea(x.input.report, x.input.envelope);
|
|
if (dv.ok === false) V.pica(acc, 'integrity', 'pqreport.derivation', dv.reason);
|
|
else if (dv.ok === null) V.nemasurat(acc, 'integrity', 'unmeasurable', dv.reason);
|
|
else V.trece(acc, 'integrity', 'the report is what the report generator gives for its own findings, and the envelope\'s statement is the one it writes');
|
|
}
|
|
v = V.verdictUnic('pq-compliance-report', acc);
|
|
if (obiect(x.input.report)) x.r.details = { report: { organization: x.input.report.organization ?? null, date: x.input.report.date ?? null,
|
|
finding: obiect(x.input.report.summary) ? x.input.report.summary.verdict ?? null : null } };
|
|
}
|
|
} else { const a = verdicte[x.lot]; if (a.error) eroare = a.error; else v = a; }
|
|
} catch (e) { eroare = 'the verifier could not run: ' + String(e.message).split('\n')[0].slice(0, 200); }
|
|
if (eroare) { Object.assign(x.r, { verdict: 'NOT_MEASURED', verifierVerdict: null, failed: [], levels: null, error: eroare }); continue; }
|
|
Object.assign(x.r, { verifierVerdict: v.verdict, verdict: v.verdict, failed: v.failed, levels: v.levels });
|
|
// intrarile verificatorului aduse de organizatie: verificatorul a spus VALID, dar pe increderea aleasa de ea; nu e VALID aici
|
|
const proprii = x.r.inputs.selfSupplied;
|
|
if (v.verdict === 'VALID' && proprii.length) {
|
|
x.r.verdict = 'PARTIAL';
|
|
x.r.capped = `the verifier's own inputs (${proprii.join(', ')}) came from the organization's pack, not from the relying party's context: the verifier said VALID on trust the organization chose itself`;
|
|
}
|
|
}
|
|
|
|
// 3) reteaua: ce a ramas nemasurat fiindca nu s-a citit lantul (consola nu citeste lantul nici online). Revizia B2 (2026-10-21), trei lucruri:
|
|
// - raportul PQ isi are nivelurile de la plicul lui, citit de adaptor ca orice plic: pana azi lipsea din `needed`;
|
|
// - o masina sau un jurnal de operare isi judeca notarizarea pe CITIRILE celui care verifica (context: anchors, readings), nu pe lant: --online
|
|
// nu le ajuta, deci stau in `readings`, nu in `needed` (pana azi rezumatul spunea "(no --online)" si cu --online dat);
|
|
// - finalitatea citita isi numeste lantul, si un testnet e spus ca testnet (adaptorul il scrie in motivul nivelului; o forma necunoscuta: chain null)
|
|
for (const r of rezultate) {
|
|
if (!r.levels || r.artifact === 'shield-console') continue;
|
|
const lantul = r.levels.filter((l) => l.level === 'anchoring' || l.level === 'finality' || r.artifact === 'ai-trust-bundle');
|
|
if (lantul.some((l) => l.state === 'NOT_MEASURED' && l.cause === 'claim-not-checked')) (CITESC_LANTUL.includes(r.artifact) ? iesire.network.needed : iesire.network.readings).push(r.id);
|
|
if (online && r.levels.some((l) => (l.level === 'anchoring' || l.level === 'finality') && l.state === 'NOT_MEASURED' && l.cause === 'unmeasurable')) iesire.network.unmeasured.push(r.id);
|
|
const fin = r.levels.find((l) => l.level === 'finality');
|
|
if (fin && fin.state === 'PASSED') {
|
|
const m = /\(chain (\d+)(, a testnet)?; the finality is that chain's\)/.exec(String(fin.reason || ''));
|
|
iesire.network.finality.push({ id: r.id, chain: m ? Number(m[1]) : null, testnet: m ? !!m[2] : null });
|
|
}
|
|
}
|
|
|
|
// 4) domeniile: fiecare cat cel mai slab artefact al lui; fara artefacte, NOT_PRESENTED
|
|
iesire.artifacts = rezultate;
|
|
iesire.domains = DOMENII.map((d) => {
|
|
const ale = rezultate.filter((r) => r.domain === d.id);
|
|
if (!ale.length) return { domain: d.id, title: d.title, verdict: 'NOT_PRESENTED', artifacts: [], failed: [], reason: 'no artifact of this domain in the pack' };
|
|
const verdict = celMaiSlab(ale.map((r) => r.verdict));
|
|
const slabe = ale.filter((r) => r.verdict === verdict).map((r) => r.id);
|
|
return { domain: d.id, title: d.title, verdict, artifacts: ale.map((r) => r.id), failed: [...new Set(ale.flatMap((r) => r.failed))].sort(),
|
|
reason: `${ale.length} artifact(s); the weakest (${verdict}): ${slabe.join(', ')}` };
|
|
});
|
|
|
|
// 5) rezumatul: nu e mai tare decat cel mai slab domeniu CERUT; un domeniu cerut si neprezentat e NOT_MEASURED pentru rezumat
|
|
const cer = iesire.domains.filter((d) => cerute.domains.includes(d.domain));
|
|
const vs = cer.map((d) => (d.verdict === 'NOT_PRESENTED' ? 'NOT_MEASURED' : d.verdict));
|
|
let verdict = celMaiSlab(vs);
|
|
const weakest = cer.filter((d, i) => RANG[vs[i]] === RANG[verdict]).map((d) => d.domain);
|
|
const notPresented = cer.filter((d) => d.verdict === 'NOT_PRESENTED').map((d) => d.domain);
|
|
const motive = [`${cer.length} required domain(s) (${cerute.source}); weakest: ${weakest.join(', ')}`];
|
|
if (notPresented.length) motive.push(`not presented: ${notPresented.join(', ')}`);
|
|
if (iesire.unjudged.length && verdict === 'VALID') { verdict = 'PARTIAL'; weakest.length = 0; motive.push(`${iesire.unjudged.length} artifact(s) of the pack no verifier here judges: the pack claims more than was measured`); }
|
|
else if (iesire.unjudged.length) motive.push(`${iesire.unjudged.length} artifact(s) not judged (unknown kind)`);
|
|
const outsideRequired = Object.fromEntries(iesire.domains.filter((d) => !cerute.domains.includes(d.domain) && d.verdict !== 'NOT_PRESENTED').map((d) => [d.domain, d.verdict]));
|
|
if (Object.values(outsideRequired).includes('INVALID')) motive.push(`INVALID outside the required domains: ${Object.entries(outsideRequired).filter(([, x]) => x === 'INVALID').map(([d]) => d).join(', ')}`);
|
|
const N = iesire.network;
|
|
if (N.needed.length) motive.push(`${N.needed.length} artifact(s) claim a notarization that was not read${online ? '' : ' (no --online)'}`);
|
|
if (N.readings.length) motive.push(`${N.readings.length} artifact(s) claim a notarization that only the relying party's own readings check (the context's anchors or readings; --online does not read them)`);
|
|
if (N.unmeasured.length) motive.push(`the chain could not be read for ${N.unmeasured.length} artifact(s)`);
|
|
const pePlasa = N.finality.filter((f) => f.testnet === true), neNumit = N.finality.filter((f) => f.chain === null);
|
|
if (pePlasa.length) motive.push(`${pePlasa.length} artifact(s) are final only on a testnet (chain ${[...new Set(pePlasa.map((f) => f.chain))].join(', ')})`);
|
|
if (neNumit.length) motive.push(`${neNumit.length} artifact(s) are final on a chain the verifier did not name`);
|
|
iesire.summary = { verdict, weakest, notPresented, outsideRequired, reason: motive.join('; ') };
|
|
|
|
// 6) verdictele scrise de pachet despre el insusi: ignorate; cele care difera de masuratoare sunt numite
|
|
const spune = (unde, x, masurat) => { if (typeof x !== 'string') return; iesire.selfReport.ignored.push({ where: unde, says: x }); if (x !== masurat) iesire.selfReport.suspect.push({ where: unde, says: x, measured: masurat }); };
|
|
spune('pack.verdict', pack.verdict, verdict);
|
|
if (obiect(pack.summary)) spune('pack.summary.verdict', pack.summary.verdict, verdict);
|
|
pack.artifacts.forEach((a, k) => {
|
|
const r = pePozitie.get(k); if (!obiect(a) || !r) return;
|
|
spune(`artifact ${r.id}: verdict`, a.verdict, r.verdict);
|
|
if (obiect(a.expected)) spune(`artifact ${r.id}: expected.verdict`, a.expected.verdict, r.verdict);
|
|
});
|
|
return iesire;
|
|
}
|
|
|
|
export const codulIesirii = (v) => ({ VALID: 0, INVALID: 1, NOT_MEASURED: 2, PARTIAL: 3 }[v] ?? 2);
|
|
|
|
function text(o) {
|
|
const L = [];
|
|
L.push(`ORGANIZATION JUDGEMENT${o.organization ? ' - ' + o.organization : ''}: ${o.summary.verdict}`);
|
|
L.push(` ${o.summary.reason}`);
|
|
L.push(` pack ${o.pack.sha256 || '-'}, ${o.pack.artifacts ?? '?'} artifact(s); clock ${o.clock.now} (${o.clock.source}); network: ${o.network.online ? 'read (--online)' : 'not read'}`
|
|
+ (o.network.needed.length ? `; needed by ${o.network.needed.join(', ')}` : '') + (o.network.readings.length ? `; the relying party's readings needed by ${o.network.readings.join(', ')}` : '')
|
|
+ (o.network.unmeasured.length ? `; unmeasured for ${o.network.unmeasured.join(', ')}` : '')
|
|
+ (o.network.finality.length ? `; final on ${o.network.finality.map((f) => `${f.chain === null ? 'an unnamed chain' : 'chain ' + f.chain + (f.testnet ? ' (a testnet)' : '')} (${f.id})`).join(', ')}` : ''));
|
|
for (const d of o.domains) L.push(` ${d.domain.padEnd(11)} ${(d.verdict === 'NOT_PRESENTED' ? 'NOT PRESENTED' : d.verdict).padEnd(13)} ${d.artifacts.length ? d.artifacts.length + ' artifact(s)' + (d.failed.length ? '; refused by ' + d.failed.join(', ') : '') : ''}${o.required.domains.includes(d.domain) ? '' : ' (not required)'}`);
|
|
const rele = o.artifacts.filter((r) => r.verdict !== 'VALID');
|
|
if (rele.length) L.push(' artifacts not VALID:');
|
|
for (const r of rele) {
|
|
const nm = (r.levels || []).filter((l) => l.state !== 'PASSED').map((l) => `${l.level} ${l.state}${l.cause ? ' (' + l.cause + ')' : ''}`).join(', ');
|
|
L.push(` ${r.id} (${r.artifact}): ${r.verdict}` + (r.failed.length ? ` - refused by ${r.failed.join(', ')}` : '') + (r.error ? ` - ${r.error}` : '') + (r.capped ? ` - ${r.capped}` : '') + (nm && !r.failed.length && !r.capped ? ` - ${nm}` : ''));
|
|
}
|
|
for (const r of o.artifacts) {
|
|
const n = r.inputs; if (!n) continue;
|
|
const parti = [];
|
|
if (n.dropped.length) parti.push(`dropped from the pack: ${n.dropped.join(', ')}`);
|
|
if (n.ignored.length) parti.push(`not an input of the verifier: ${n.ignored.join(', ')}`);
|
|
if (n.contextIgnored.length) parti.push(`context keys ignored: ${n.contextIgnored.join(', ')}`);
|
|
if (parti.length) L.push(` ${r.id}: ${parti.join('; ')}`);
|
|
}
|
|
for (const u of o.unjudged) L.push(` not judged: ${u.id} (${u.artifact || '?'}): ${u.reason}`);
|
|
for (const s of o.selfReport.suspect) L.push(` WARNING: the pack says ${s.where} = ${s.says}; measured ${s.measured} (ignored)`);
|
|
return L.join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
const a = process.argv.slice(2);
|
|
const get = (f) => { const i = a.indexOf(f); return i >= 0 ? a[i + 1] : null; };
|
|
const pf = get('--pack');
|
|
if (!pf) { console.error('usage: node organizatie.mjs --pack pack.json [--context context.json] [--require shield,identity,...] [--at <RFC 3339>] [--online] [--json]'); return 2; }
|
|
let brut; try { brut = fs.readFileSync(pf); } catch (e) { console.error('the pack cannot be read: ' + e.message); return 2; }
|
|
let pack; try { pack = JSON.parse(brut.toString('utf8')); } catch (e) { pack = { kind: 'not JSON: ' + e.message }; }
|
|
const cf = get('--context'); let context = null;
|
|
if (cf) { try { context = JSON.parse(fs.readFileSync(cf, 'utf8')); } catch (e) { console.error('the context cannot be read: ' + e.message); return 2; } }
|
|
const rq = get('--require');
|
|
const o = await judecaPachetul(pack, { context, require: rq ? rq.split(',').map((x) => x.trim()).filter(Boolean) : null, at: get('--at'), online: a.includes('--online'), packSha256: sha256(brut) });
|
|
console.log(a.includes('--json') ? JSON.stringify(o, null, 1) : text(o));
|
|
return codulIesirii(o.summary.verdict);
|
|
}
|
|
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
|
main().then((c) => { process.exitCode = c; }).catch((e) => { console.error(e.message); process.exitCode = 2; });
|
|
}
|