519 lines
36 KiB
HTML
519 lines
36 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>AERE Verification Console</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
|
<style>
|
|
:root{
|
|
color-scheme: dark;
|
|
--bg:#0a0e13; --panel:#111721; --panel2:#0e141d; --line:#1e2732; --line2:#2a3644;
|
|
--ink:#e7edf4; --mut:#8a99ab; --dim:#5f6e80;
|
|
--accent:#39d3c8; --accent-dim:#1b6d69;
|
|
--ok:#4bcf8e; --warn:#e3ab4d; --crit:#f2685f;
|
|
--shadow:0 1px 0 rgba(255,255,255,.02), 0 12px 30px -18px rgba(0,0,0,.8);
|
|
--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
--sans:"IBM Plex Sans",system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
|
}
|
|
:root[data-theme="light"]{
|
|
color-scheme: light;
|
|
--bg:#f3f5f8; --panel:#ffffff; --panel2:#f7f9fb; --line:#e3e8ef; --line2:#d3dbe5;
|
|
--ink:#16202c; --mut:#5a6b7d; --dim:#8a99ab;
|
|
--accent:#0e928a; --accent-dim:#a7e6e1;
|
|
--ok:#12915c; --warn:#a9720a; --crit:#c93a30;
|
|
--shadow:0 1px 2px rgba(16,32,48,.06), 0 12px 28px -20px rgba(16,32,48,.35);
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
.wrap{max-width:1000px;margin:0 auto;padding-inline:16px;padding-block:0 56px}
|
|
a{color:var(--accent)}
|
|
.mono{font-family:var(--mono);font-variant-ligatures:none}
|
|
.tnum{font-variant-numeric:tabular-nums}
|
|
|
|
/* header */
|
|
header{position:sticky;top:env(safe-area-inset-top,0px);z-index:5;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
|
.hbar{max-width:1000px;margin:0 auto;padding:14px 16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
|
.mark{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em}
|
|
.glyph{width:26px;height:26px;flex:none}
|
|
.mark .sub{font-weight:500;color:var(--mut);font-size:12px;letter-spacing:.06em;text-transform:uppercase}
|
|
.host{margin-left:auto;display:flex;align-items:center;gap:8px;color:var(--mut);font-size:12.5px;min-width:0;flex-wrap:wrap}
|
|
.host .mono{color:var(--ink);overflow-wrap:anywhere;min-width:0}
|
|
.verdict{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;font-weight:700;font-size:13px;letter-spacing:.03em;border:1px solid}
|
|
.verdict.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
|
.verdict.bad{color:var(--crit);border-color:color-mix(in srgb,var(--crit) 45%,transparent);background:color-mix(in srgb,var(--crit) 12%,transparent)}
|
|
.dot{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 0 4px color-mix(in srgb,currentColor 22%,transparent)}
|
|
|
|
/* intro line */
|
|
.lead{color:var(--mut);font-size:13px;margin:18px 0 4px}
|
|
.lead b{color:var(--ink);font-weight:600}
|
|
|
|
/* tiles */
|
|
.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:16px 0 8px}
|
|
@media(max-width:760px){.tiles{grid-template-columns:repeat(2,1fr)}}
|
|
@media(max-width:420px){.tiles{grid-template-columns:1fr}}
|
|
.tile{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 15px;box-shadow:var(--shadow);display:flex;flex-direction:column;gap:6px;min-height:98px}
|
|
.tile .k{font-size:11px;text-transform:uppercase;letter-spacing:.09em;color:var(--dim);display:flex;align-items:center;gap:7px}
|
|
.tile .v{font-size:24px;font-weight:700;font-family:var(--mono);line-height:1.1}
|
|
.tile .n{font-size:12px;color:var(--mut)}
|
|
.stripe{height:3px;border-radius:2px;margin-top:auto}
|
|
.s-ok{background:var(--ok)} .s-warn{background:var(--warn)} .s-crit{background:var(--crit)} .s-acc{background:var(--accent)}
|
|
.pin{width:7px;height:7px;border-radius:50%} .pin.ok{background:var(--ok)} .pin.warn{background:var(--warn)} .pin.crit{background:var(--crit)} .pin.acc{background:var(--accent)}
|
|
|
|
/* section */
|
|
section{margin-top:28px}
|
|
h2{font-size:13px;text-transform:uppercase;letter-spacing:.1em;color:var(--mut);font-weight:600;margin:0 0 12px;display:flex;align-items:center;gap:9px}
|
|
h2 .rule{height:1px;background:var(--line);flex:1}
|
|
|
|
/* chain */
|
|
.chain{display:flex;flex-direction:column;gap:0}
|
|
.node{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:13px 15px;box-shadow:var(--shadow)}
|
|
.node + .link{height:26px;margin-left:26px;border-left:2px dashed var(--line2);position:relative}
|
|
.node + .link::after{content:"prev";position:absolute;left:12px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;color:var(--dim);letter-spacing:.05em;background:var(--bg);padding:0 6px}
|
|
.node.head{border-color:color-mix(in srgb,var(--accent) 55%,var(--line));box-shadow:var(--shadow),0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent)}
|
|
.nrow{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
.seq{font-family:var(--mono);font-weight:600;color:var(--accent);font-size:13px;min-width:30px}
|
|
.kind{display:inline-flex;align-items:center;gap:7px;font-weight:600;font-size:13.5px}
|
|
.kind .tag{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);border:1px solid var(--line2);border-radius:5px;padding:1px 6px}
|
|
.nname{color:var(--mut);font-size:12.5px}
|
|
.nat{margin-left:auto;color:var(--dim);font-size:11.5px;font-family:var(--mono)}
|
|
.headflag{font-size:10.5px;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);border-radius:999px;padding:1px 8px;letter-spacing:.05em;text-transform:uppercase}
|
|
.hashline{margin-top:9px;display:grid;grid-template-columns:auto 1fr auto;gap:8px 10px;align-items:center}
|
|
.hlab{font-family:var(--mono);font-size:10.5px;color:var(--dim);text-transform:uppercase;letter-spacing:.05em}
|
|
.hval{font-family:var(--mono);font-size:12px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.hval.dim{color:var(--mut)}
|
|
.copy{border:1px solid var(--line2);background:transparent;color:var(--mut);border-radius:6px;font-family:var(--mono);font-size:10.5px;padding:2px 8px;cursor:pointer;transition:.15s}
|
|
.copy:hover{color:var(--ink);border-color:var(--accent)}
|
|
.copy.done{color:var(--ok);border-color:var(--ok)}
|
|
|
|
/* ladder */
|
|
.ladder{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
|
|
@media(max-width:640px){.ladder{grid-template-columns:1fr 1fr}}
|
|
.step{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:6px;position:relative;opacity:.62}
|
|
.step.cur{opacity:1;background:var(--panel);border-color:color-mix(in srgb,var(--accent) 50%,var(--line));box-shadow:var(--shadow)}
|
|
.step .lv{font-family:var(--mono);font-size:11px;color:var(--dim)}
|
|
.step .nm{font-weight:600;font-size:13px}
|
|
.step .ds{font-size:11.5px;color:var(--mut)}
|
|
.step.cur .nm{color:var(--accent)}
|
|
|
|
/* migration bars */
|
|
.mig{display:grid;grid-template-columns:1.3fr 1fr;gap:14px}
|
|
@media(max-width:640px){.mig{grid-template-columns:1fr}}
|
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:15px;box-shadow:var(--shadow)}
|
|
.bar{height:10px;border-radius:6px;background:var(--panel2);overflow:hidden;display:flex;margin:10px 0}
|
|
.bar i{display:block;height:100%}
|
|
.legend{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:12px;color:var(--mut)}
|
|
.legend span{display:inline-flex;align-items:center;gap:7px}
|
|
.mrow{display:flex;justify-content:space-between;align-items:baseline;padding:7px 0;border-top:1px solid var(--line)}
|
|
.mrow:first-of-type{border-top:0}
|
|
.mrow .b{font-weight:600;color:var(--ink);font-family:var(--mono)}
|
|
|
|
/* paste */
|
|
.paste{margin-top:14px}
|
|
textarea{width:100%;min-height:120px;background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:10px;padding:12px;font-family:var(--mono);font-size:12px;resize:vertical}
|
|
textarea:focus{outline:2px solid color-mix(in srgb,var(--accent) 50%,transparent);outline-offset:1px;border-color:var(--accent)}
|
|
.row{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
|
|
button.act{border:1px solid var(--accent-dim);background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font-family:var(--sans);font-weight:600;font-size:13px;padding:8px 16px;border-radius:8px;cursor:pointer}
|
|
button.act:hover{background:color-mix(in srgb,var(--accent) 26%,transparent)}
|
|
button.ghost{border:1px solid var(--line2);background:transparent;color:var(--mut)}
|
|
.err{color:var(--crit);font-size:12.5px;font-family:var(--mono)}
|
|
.foot{margin-top:34px;padding-top:16px;border-top:1px solid var(--line);color:var(--dim);font-size:11.5px;line-height:1.7}
|
|
.foot code{font-family:var(--mono);color:var(--mut)}
|
|
.warnbox{margin-top:12px;border:1px solid color-mix(in srgb,var(--crit) 45%,transparent);background:color-mix(in srgb,var(--crit) 10%,transparent);color:var(--crit);border-radius:10px;padding:11px 13px;font-size:12.5px;overflow-wrap:anywhere}
|
|
|
|
/* organization judgement (organizatie.mjs): one card per domain, each artifact with the verifier and the evidence it rests on */
|
|
.verdict.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
|
|
.notebox{margin-top:12px;border:1px solid var(--line2);background:var(--panel2);color:var(--mut);border-radius:10px;padding:11px 13px;font-size:12.5px;overflow-wrap:anywhere}
|
|
.hmeta{font-family:var(--mono);font-size:11px;color:var(--dim);text-transform:none;letter-spacing:0}
|
|
.domains{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
|
@media(max-width:760px){.domains{grid-template-columns:minmax(0,1fr)}}
|
|
.dcard{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 15px;box-shadow:var(--shadow);min-width:0;display:flex;flex-direction:column;gap:8px}
|
|
.dcard.np{background:var(--panel2);box-shadow:none;border-style:dashed}
|
|
.dhead{display:flex;align-items:center;gap:8px 10px;flex-wrap:wrap;min-width:0}
|
|
.dtitle{font-weight:600;font-size:14px;overflow-wrap:anywhere}
|
|
.did{font-family:var(--mono);font-size:11px;color:var(--dim)}
|
|
.pill{margin-left:auto;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.04em;border:1px solid;border-radius:999px;padding:2px 9px;white-space:nowrap}
|
|
.pill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 10%,transparent)}
|
|
.pill.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 10%,transparent)}
|
|
.pill.bad{color:var(--crit);border-color:color-mix(in srgb,var(--crit) 45%,transparent);background:color-mix(in srgb,var(--crit) 10%,transparent)}
|
|
.pill.dim{color:var(--mut);border-color:var(--line2);background:transparent}
|
|
.pill.sm{margin-left:0;font-size:10px;padding:1px 7px}
|
|
.reqtag{font-size:10.5px;color:var(--dim);border:1px solid var(--line2);border-radius:5px;padding:0 6px}
|
|
.dreason{font-size:12px;color:var(--mut);overflow-wrap:anywhere}
|
|
.arow{border-top:1px solid var(--line);padding-top:8px;display:flex;flex-direction:column;gap:5px;min-width:0}
|
|
.aline{display:flex;gap:6px 8px;align-items:center;flex-wrap:wrap;min-width:0}
|
|
.aid{font-family:var(--mono);font-size:12px;font-weight:600;overflow-wrap:anywhere;min-width:0}
|
|
.akind{font-family:var(--mono);font-size:10.5px;color:var(--mut);border:1px solid var(--line2);border-radius:5px;padding:1px 6px;overflow-wrap:anywhere}
|
|
.ameta{font-size:11.5px;color:var(--mut);overflow-wrap:anywhere}
|
|
.arules{font-family:var(--mono);font-size:11.5px;color:var(--crit);overflow-wrap:anywhere}
|
|
.anote{font-size:11.5px;color:var(--warn);overflow-wrap:anywhere}
|
|
.alevels{display:flex;flex-wrap:wrap;gap:3px 10px;font-family:var(--mono);font-size:10.5px}
|
|
.alevels .p{color:var(--ok)} .alevels .f{color:var(--crit)} .alevels .n{color:var(--dim)}
|
|
.adig{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:6px;align-items:center;min-width:0}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="hbar">
|
|
<div class="mark">
|
|
<svg class="glyph" viewBox="0 0 32 32" fill="none" aria-hidden="true">
|
|
<path d="M16 2 3 8v8c0 8 5.4 12.6 13 14 7.6-1.4 13-6 13-14V8L16 2Z" fill="none" stroke="var(--accent)" stroke-width="1.6"/>
|
|
<path d="M10.5 16.2 14.4 20l7.3-8.2" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<div>AERE Verification Console<div class="sub">quantum security control plane</div></div>
|
|
</div>
|
|
<div class="host">
|
|
<span id="hostlab">host</span><span class="mono" id="host">—</span>
|
|
<span id="verdict" class="verdict ok"><span class="dot"></span><span id="vtext">OK</span></span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
<p class="lead" id="lead-console">The state of a deployment, <b>checked again independently</b> by the control plane: the audit chain is recomputed from its entries, and the integrity of every AIP-23 envelope is recomputed. The console does not trust what the sidecar says about itself.</p>
|
|
|
|
<div class="tiles" id="tiles"></div>
|
|
|
|
<p class="lead" id="lead-org" hidden>The evidence of an organization, <b>judged again domain by domain</b> by the published verifier of each artifact. A domain with no artifact is not presented, never green; a verifier that does not start is not measured; the summary is never stronger than the weakest required domain. Nothing is judged in this browser.</p>
|
|
|
|
<div id="alert"></div>
|
|
|
|
<section id="orgsec" hidden>
|
|
<h2>Domains <span class="rule"></span> <span id="orgmeta" class="hmeta"></span></h2>
|
|
<div class="domains" id="domains"></div>
|
|
</section>
|
|
|
|
<section id="sec-chain">
|
|
<h2>Audit chain <span class="rule"></span> <span id="chainmeta" class="mono" style="font-size:11px;color:var(--dim);text-transform:none;letter-spacing:0"></span></h2>
|
|
<div class="chain" id="chain"></div>
|
|
</section>
|
|
|
|
<section id="sec-ladder">
|
|
<h2>Post-quantum certificate <span class="rule"></span></h2>
|
|
<div class="ladder" id="ladder"></div>
|
|
</section>
|
|
|
|
<section id="sec-mig">
|
|
<h2>Post-quantum migration <span class="rule"></span></h2>
|
|
<div class="mig">
|
|
<div class="card">
|
|
<div style="font-weight:600;margin-bottom:2px">Migration plan</div>
|
|
<div class="n" style="color:var(--mut);font-size:12px">actions derived from the cryptographic inventory of the deployment</div>
|
|
<div class="bar" id="migbar"></div>
|
|
<div class="legend">
|
|
<span><i class="pin crit"></i> critical</span>
|
|
<span><i class="pin acc"></i> automatic</span>
|
|
<span><i class="pin warn"></i> manual</span>
|
|
</div>
|
|
</div>
|
|
<div class="card" id="migrows"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="paste">
|
|
<h2>Render your own output <span class="rule"></span></h2>
|
|
<p class="lead" style="margin-top:0">Paste the output of <code class="mono" style="color:var(--ink)">node consola.mjs --bundle b.json --json</code>, the output of <code class="mono" style="color:var(--ink)">node organizatie.mjs --pack pack.json --json</code> (every domain of an organization), or a bundle from <code class="mono" style="color:var(--ink)">node sidecar.mjs bundle</code>.</p>
|
|
<textarea id="in" spellcheck="false" placeholder='{ "kind":"aere-control-plane-console", ... }'></textarea>
|
|
<div class="row">
|
|
<button class="act" id="render">Render</button>
|
|
<button class="act ghost" id="reset">Example</button>
|
|
<span class="err" id="perr" hidden></span>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="foot">
|
|
A rendering of the output of the AUTHORITATIVE tools (<code>consola.mjs</code> and the sidecar). The verdict and the integrity of the chain and of the envelopes are computed by the tools, not in the browser: this page shows them as they are. The audit chain is an append-only hash chain where every entry covers <code>sha256(seq | prev | envelope)</code>; its head can be notarized on AereNotary (Aere Network) and read back under a post-quantum certified anchor; the finality is the one of the chain it is notarized on. The envelopes follow the AERE Proof Protocol (AIP-23). The data shown on opening is an example deployment generated by the tools; the finality step marked is the one of that example (a head that can be notarized and is not yet). For output you paste, the console does not check finality, so no step is marked.
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const GEN = "0x"+"00".repeat(32);
|
|
const short = h => !h ? "—" : (h.length>22 ? h.slice(0,12)+"…"+h.slice(-8) : h);
|
|
const KINDNAME = {
|
|
"aere-proof-of-runtime":["Runtime","the running binary is the attested one"],
|
|
"aere-proof-of-data":["Deployment","the deployed artifact, as a digest"],
|
|
"aere-audit-head":["Log head","can be notarized"],
|
|
};
|
|
|
|
// an example generated by the verification layer and the control plane (console output format 2)
|
|
const SAMPLE = {
|
|
v:2, kind:"aere-control-plane-console", host:"aws-eu-prod-1",
|
|
auditChain:{ok:true,count:3,head:"0x5f63faec605908668fee3703303b006e2ebe66b3318b9fc8d1f01e86edfb41ee",brokenAtSeq:null,reason:null},
|
|
envelopes:{total:3,intact:3,bad:[]},
|
|
selfReportSuspect:null,
|
|
migration:{total:3,critical:2,auto:2,manual:1,blocked:0},
|
|
execution:null,
|
|
verdict:"OK",
|
|
entries:[
|
|
{seq:0,kind:"aere-proof-of-runtime",at:"2026-09-26T08:00:00Z",name:"aws-eu-prod-1",statementHash:"0x1c0183e10520f70884dea2947c763413f8514606026e9fe294bc4cfdb380be67",hash:"0x6168463c93c47acbd8b40f02ed5089e87a77c07a0a2e8a431026bd7f765f8969",prev:GEN},
|
|
{seq:1,kind:"aere-proof-of-data",at:"2026-09-26T08:05:00Z",name:"checkout-api@4.2.1",statementHash:"0x5fa286c04da5fe37f4a9c9963999b4162926d81064b95af28a91a665f86b2c68",hash:"0xf2ff1835e76511a902ec02f8fa523a455d114743e1e916abe11cfc8b822579e8",prev:"0x6168463c93c47acbd8b40f02ed5089e87a77c07a0a2e8a431026bd7f765f8969"},
|
|
{seq:2,kind:"aere-proof-of-runtime",at:"2026-09-26T08:10:00Z",name:"aws-eu-prod-1",statementHash:"0xa9358d663d8ec8b3a59c7db6a2eab2ed842d2adac61f6d84d43657a7b831c4d5",hash:"0x5f63faec605908668fee3703303b006e2ebe66b3318b9fc8d1f01e86edfb41ee",prev:"0xf2ff1835e76511a902ec02f8fa523a455d114743e1e916abe11cfc8b822579e8"}
|
|
]
|
|
};
|
|
|
|
function normalize(o){
|
|
// accepts a console state OR a sidecar bundle (it has entries + chainOk); a bundle is shown as the bundle declares it, since the
|
|
// browser does not recompute anything: run consola.mjs on it for a checked verdict
|
|
if(o.kind==="aere-verify-layer-bundle" || (o.entries && !o.auditChain)){
|
|
return {
|
|
kind:"aere-control-plane-console", host:o.host||"?",
|
|
auditChain:{ok:!!o.chainOk,count:o.count??(o.entries||[]).length,head:o.head||null,brokenAtSeq:null,reason:null},
|
|
envelopes:{total:(o.entries||[]).length,intact:(o.entries||[]).length,bad:[]},
|
|
selfReportSuspect:null, migration:null, execution:null, verdict:"UNCHECKED",
|
|
note:"A bundle pasted here is shown as it declares itself (chainOk="+(!!o.chainOk)+"): the browser checks nothing. Run node consola.mjs --bundle on it for a checked verdict.",
|
|
entries:(o.entries||[]).map(e=>({seq:e.seq,kind:e.proof?.statement?.kind||"?",at:e.proof?.statement?.createdAt||"",name:e.proof?.statement?.name||e.proof?.statement?.host||"",statementHash:e.proof?.statementHash||"",hash:e.hash,prev:e.prev}))
|
|
};
|
|
}
|
|
return o;
|
|
}
|
|
|
|
function el(t,c,x){const n=document.createElement(t);if(c)n.className=c;if(x!=null)n.textContent=x;return n;}
|
|
|
|
function copyBtn(text){
|
|
const b=el("button","copy","copy");
|
|
b.addEventListener("click",async()=>{
|
|
try{await navigator.clipboard.writeText(text);}catch(e){/* the clipboard is not available here */}
|
|
b.textContent="copied";b.classList.add("done");setTimeout(()=>{b.textContent="copy";b.classList.remove("done");},1200);
|
|
});
|
|
return b;
|
|
}
|
|
|
|
// judecata organizatiei (organizatie.mjs, 2026-10-20): o VEDERE, nu a doua judecata. O iesire lipita se arata asa cum a dat-o unealta (domeniile,
|
|
// verdictele, verificatorul si amprenta dovezii fiecarui artefact); un pachet brut lipit e UNCHECKED: browserul nu judeca nimic. Un domeniu
|
|
// neprezentat nu e niciodata verde. Datele intra numai prin textContent.
|
|
const PILL = { VALID: "ok", PARTIAL: "warn", NOT_MEASURED: "warn", NOT_PRESENTED: "dim", INVALID: "bad", UNCHECKED: "bad" };
|
|
// (revizia B2, 2026-10-21) o iesire lipita poate fi editata: vederea nu judeca nimic, dar nu arata un verdict mai bun decat cel pe care il dau
|
|
// chiar randurile iesirii (domeniul nu e mai tare decat cel mai slab artefact al lui, rezumatul nu e mai tare decat cel mai slab domeniu cerut);
|
|
// cand iesirea se contrazice, se arata citirea mai slaba si se spune de ce. Un verdict necunoscut e mai slab decat toate.
|
|
const RANGV = { INVALID: 0, NOT_MEASURED: 1, NOT_PRESENTED: 1, PARTIAL: 2, VALID: 3 };
|
|
const rangV = (v) => Object.hasOwn(RANGV, String(v)) ? RANGV[v] : -1;
|
|
const slabV = (vs) => vs.reduce((a, b) => (rangV(b) < rangV(a) ? b : a));
|
|
const pill = (v) => Object.hasOwn(PILL, String(v)) ? PILL[v] : "bad";
|
|
const STRIPE = { ok: "s-ok", warn: "s-warn", bad: "s-crit", dim: "s-acc" };
|
|
const eOrg = (o) => !!o && typeof o === "object" && (o.kind === "aere-organization-judgement" || o.kind === "aere-organization-evidence-pack");
|
|
const textVerdict = (v) => v === "NOT_PRESENTED" ? "NOT PRESENTED" : v === "NOT_MEASURED" ? "NOT MEASURED" : String(v || "?");
|
|
function consoleMode(on){
|
|
for (const id of ["sec-chain","sec-ladder","sec-mig","lead-console"]) document.getElementById(id).hidden = !on;
|
|
for (const id of ["orgsec","lead-org"]) document.getElementById(id).hidden = on;
|
|
document.getElementById("hostlab").textContent = on ? "host" : "organization";
|
|
}
|
|
function tile(t){ const c=el("div","tile"); const k=el("div","k"); k.append(el("i","pin "+t.pin),document.createTextNode(t.k)); c.append(k,el("div","v",t.v),el("div","n",t.n),el("div","stripe "+t.s)); return c; }
|
|
function randArtefact(r){
|
|
const w = el("div","arow"); w.dataset.id = String(r.id); w.dataset.verdict = String(r.verdict);
|
|
const l1 = el("div","aline"); l1.append(el("span","aid",String(r.id)), el("span","akind",String(r.artifact)), el("span","pill sm "+pill(r.verdict),textVerdict(r.verdict)));
|
|
w.append(l1);
|
|
const vf = r.verifier || {};
|
|
w.append(el("div","ameta","judged by "+(vf.module||"?")+(vf.through?" through "+vf.through:"")+(r.verifierVerdict && r.verifierVerdict!==r.verdict?" · the verifier said "+r.verifierVerdict:"")));
|
|
if (Array.isArray(r.failed) && r.failed.length) w.append(el("div","arules","refused by "+r.failed.join(", ")));
|
|
if (r.error) w.append(el("div","anote","not measured: "+r.error));
|
|
if (r.capped) w.append(el("div","anote",r.capped));
|
|
if (Array.isArray(r.levels)) {
|
|
const lv = el("div","alevels");
|
|
for (const x of r.levels) { const k = x.state==="PASSED"?"p":x.state==="FAILED"?"f":"n"; const sp = el("span",k,x.level+" "+(x.state==="NOT_MEASURED"?"not measured"+(x.cause?" ("+x.cause+")":""):String(x.state).toLowerCase())); sp.title = String(x.reason||""); lv.append(sp); }
|
|
w.append(lv);
|
|
}
|
|
const dc = r.details && r.details.console;
|
|
if (dc) w.append(el("div","ameta","console: audit chain "+(dc.auditChain&&dc.auditChain.ok?"intact":"BROKEN")+", "+(dc.auditChain?dc.auditChain.count:0)+" entries; envelopes "+(dc.envelopes?dc.envelopes.intact+"/"+dc.envelopes.total:"?")+(dc.migration&&dc.migration.total!=null?"; plan "+dc.migration.total+" action(s), "+dc.migration.critical+" critical":"")+(dc.execution?"; execution "+dc.execution.actions+" action(s)":"")));
|
|
const dr = r.details && r.details.report;
|
|
if (dr && dr.finding) w.append(el("div","ameta","the report says (re-derived from its own findings; the findings are not judged here): "+dr.finding));
|
|
const n = r.inputs || {}; const parti = [];
|
|
if ((n.selfSupplied||[]).length) parti.push("verifier inputs from the pack: "+n.selfSupplied.join(", "));
|
|
if ((n.dropped||[]).length) parti.push("dropped from the pack: "+n.dropped.join(", "));
|
|
if (parti.length) w.append(el("div","ameta",parti.join(" · ")));
|
|
const dg = el("div","adig"); dg.append(el("span","hlab","evidence"), el("span","hval",r.inputSha256||"—"), copyBtn(r.inputSha256||"")); w.append(dg);
|
|
return w;
|
|
}
|
|
function renderOrg(o){
|
|
consoleMode(false);
|
|
const pachet = o.kind === "aere-organization-evidence-pack";
|
|
const s = (!pachet && o.summary && typeof o.summary === "object") ? o.summary : {};
|
|
const doms = (Array.isArray(o.domains) ? o.domains : []).filter((d) => d && typeof d === "object");
|
|
const arts = (Array.isArray(o.artifacts) ? o.artifacts : []).filter((r) => r && typeof r === "object");
|
|
const req = o.required && Array.isArray(o.required.domains) ? o.required.domains : [];
|
|
// ce se arata: niciodata mai bine decat randurile iesirii; fiecare contradictie e numita
|
|
const contrazic = [];
|
|
const aleDom = (d) => arts.filter((x) => x.domain === d.domain);
|
|
const aratatDom = new Map(doms.map((d) => {
|
|
const ale = aleDom(d);
|
|
const v = ale.length ? slabV([String(d.verdict), ...ale.map((x) => String(x.verdict))]) : "NOT_PRESENTED";
|
|
if (v !== d.verdict) contrazic.push("domain "+d.domain+" says "+d.verdict+(ale.length ? ", its artifacts give "+v : ", and has no artifact"));
|
|
return [d, v];
|
|
}));
|
|
let verdict = pachet ? "UNCHECKED" : String(s.verdict || "?");
|
|
if (!pachet) {
|
|
const din = doms.filter((d) => req.includes(d.domain)).map((d) => aratatDom.get(d) === "NOT_PRESENTED" ? "NOT_MEASURED" : aratatDom.get(d));
|
|
let v = slabV([verdict, ...din]);
|
|
if (v === "VALID" && (o.unjudged || []).length) v = "PARTIAL";
|
|
if (v !== verdict) { contrazic.push("the summary says "+verdict+", its required domains give "+v); verdict = v; }
|
|
}
|
|
const cls = pill(verdict);
|
|
document.getElementById("host").textContent = o.organization || "?";
|
|
document.getElementById("verdict").className = "verdict " + cls;
|
|
document.getElementById("vtext").textContent = textVerdict(verdict);
|
|
const net = o.network || {}; const ceas = o.clock || {};
|
|
const neprez = doms.filter((d) => aratatDom.get(d) === "NOT_PRESENTED");
|
|
// reteaua: "read" in culoarea buna numai cand chiar s-a citit tot ce se cerea, si o finalitate de testnet e spusa ca atare
|
|
const lst = (k) => Array.isArray(net[k]) ? net[k] : [];
|
|
const fin = lst("finality").filter((f) => f && typeof f === "object"), pePlasa = fin.filter((f) => f.testnet === true), neNumit = fin.filter((f) => f.chain == null);
|
|
const retea = [];
|
|
if (lst("needed").length) retea.push(lst("needed").length+" artifact(s) claim a notarization not read: run with --online");
|
|
if (lst("readings").length) retea.push(lst("readings").length+" artifact(s) need the relying party's own readings of a notarization (context)");
|
|
if (lst("unmeasured").length) retea.push("the chain could not be read for "+lst("unmeasured").length+" artifact(s)");
|
|
if (pePlasa.length) retea.push("final only on a testnet (chain "+[...new Set(pePlasa.map((f) => f.chain))].join(", ")+") for "+pePlasa.length+" artifact(s)");
|
|
if (neNumit.length) retea.push("final on a chain not named for "+neNumit.length+" artifact(s)");
|
|
const reteaBuna = !!net.online && !retea.length;
|
|
const reteaN = retea.length ? retea.join(" · ") : net.online ? (fin.length ? "final on chain "+[...new Set(fin.map((f) => f.chain))].join(", ") : "the chain was read for the claims") : "no artifact waits on the chain";
|
|
const tiles = pachet ? [
|
|
{k:"Pack", pin:"crit", v:"not judged", n:arts.length+" artifact(s) declared, none judged here", s:"s-crit"},
|
|
{k:"Domains presented", pin:"warn", v:"—", n:"run organizatie.mjs on it", s:"s-warn"},
|
|
{k:"Network", pin:"warn", v:"—", n:"nothing was read", s:"s-warn"},
|
|
{k:"Clock", pin:"acc", v:"—", n:"no verifier ran", s:"s-acc"},
|
|
] : [
|
|
{k:"Summary", pin:cls==="ok"?"ok":cls==="warn"?"warn":"crit", v:textVerdict(verdict), n:"over "+req.length+" required domain(s)"+(Array.isArray(s.weakest)&&s.weakest.length?" · weakest: "+s.weakest.join(", "):""), s:STRIPE[cls]||"s-crit"},
|
|
{k:"Domains presented", pin:neprez.length?"warn":"ok", v:(doms.length-neprez.length)+"/"+doms.length, n:neprez.length?"not presented: "+neprez.map((d)=>d.domain).join(", "):"every domain presented", s:neprez.length?"s-warn":"s-ok"},
|
|
{k:"Network", pin:reteaBuna?"ok":"warn", v:!net.online?"not read":lst("unmeasured").length?"partly read":"read", n:reteaN, s:reteaBuna?"s-ok":"s-warn"},
|
|
{k:"Clock", pin:"acc", v:String(ceas.now||"?").slice(0,10), n:"the verifier's clock, "+String(ceas.now||"").slice(11,19)+" UTC, from "+(ceas.source||"?"), s:"s-acc"},
|
|
];
|
|
const T = document.getElementById("tiles"); T.innerHTML = "";
|
|
for (const t of tiles) T.append(tile(t));
|
|
const A = document.getElementById("alert"); A.innerHTML = "";
|
|
if (pachet) A.append(el("div","warnbox","An evidence pack pasted here is not judged: the browser checks nothing. Run node organizatie.mjs --pack pack.json --context context.json --json on it and paste that output."));
|
|
else {
|
|
if (contrazic.length) A.append(el("div","warnbox","This output contradicts itself, so the weaker reading is shown: "+contrazic.join("; ")+". organizatie.mjs never writes such an output: it was edited, or made by another tool."));
|
|
if (s.reason) A.append(el("div","notebox",s.reason));
|
|
for (const x of (o.selfReport && o.selfReport.suspect) || []) A.append(el("div","warnbox","Suspect self-report, ignored: the pack says "+x.where+" = "+x.says+"; measured "+x.measured+"."));
|
|
for (const u of o.unjudged || []) A.append(el("div","warnbox","Not judged: "+u.id+" ("+(u.artifact||"?")+"): "+u.reason));
|
|
}
|
|
document.getElementById("orgmeta").textContent = pachet ? "" : ((o.pack && o.pack.sha256) ? "pack "+short(o.pack.sha256)+" · " : "")+arts.length+" artifact(s)";
|
|
const D = document.getElementById("domains"); D.innerHTML = "";
|
|
if (pachet || !doms.length) { D.append(el("div","card", pachet ? "Not judged." : "No domain in this output.")); return; }
|
|
for (const d of doms) {
|
|
const dv = aratatDom.get(d), np = dv === "NOT_PRESENTED";
|
|
const c = el("div","dcard"+(np?" np":"")); c.dataset.domain = String(d.domain); c.dataset.verdict = String(dv);
|
|
const h = el("div","dhead"); h.append(el("span","dtitle",String(d.title||d.domain)), el("span","did",String(d.domain)));
|
|
if (!req.includes(d.domain)) h.append(el("span","reqtag","not required"));
|
|
h.append(el("span","pill "+pill(dv),textVerdict(dv)));
|
|
c.append(h, el("div","dreason", np ? "No artifact of this domain in the pack: not presented, not green." : (d.reason||"")));
|
|
if (Array.isArray(d.failed) && d.failed.length) c.append(el("div","arules","refused by "+d.failed.join(", ")));
|
|
for (const r of arts.filter((x)=>x.domain===d.domain)) c.append(randArtefact(r));
|
|
D.append(c);
|
|
}
|
|
}
|
|
|
|
// `exemplu`: numai exemplul are o treapta de finalitate cunoscuta (capul lui nu e notarizat); pentru o iesire lipita consola nu masoara
|
|
// finalitatea, deci nu se marcheaza nicio treapta si caseta spune ca nu e verificata aici (2026-09-30, B-24)
|
|
function render(d, exemplu=false){
|
|
if(eOrg(d)){ renderOrg(d); return; }
|
|
consoleMode(true);
|
|
d=normalize(d);
|
|
const ok = d.verdict==="OK";
|
|
document.getElementById("host").textContent=d.host||"?";
|
|
const v=document.getElementById("verdict"); v.className="verdict "+(ok?"ok":"bad");
|
|
document.getElementById("vtext").textContent=d.verdict||"?";
|
|
|
|
// tiles
|
|
const ac=d.auditChain||{}, pl=d.envelopes||{}, mg=d.migration, ex=d.execution;
|
|
const bad=(pl.bad&&pl.bad.length);
|
|
const tiles=[
|
|
{k:"Audit chain",pin:ac.ok?"ok":"crit",v:ac.ok?"INTACT":"BROKEN",n:(ac.count??0)+" entries"+(ac.ok?"":" · broken at seq "+ac.brokenAtSeq),s:ac.ok?"s-ok":"s-crit"},
|
|
{k:"AIP-23 envelopes",pin:bad?"crit":"ok",v:(pl.intact??0)+"/"+(pl.total??0),n:bad?"intact · bad: "+pl.bad.map(x=>x.seq).join(","):"intact",s:bad?"s-crit":"s-ok"},
|
|
{k:"PQ migration",pin:mg?(mg.critical?"crit":"acc"):"acc",v:mg&&mg.total!=null?String(mg.total):"—",n:mg?(mg.total==null?(mg.error||"plan not readable"):(mg.critical+" critical · "+mg.auto+" automatic")):"no plan attached",s:mg&&mg.critical?"s-crit":"s-acc"},
|
|
ex?{k:"Execution",pin:ex.chainOk?(ex.failed?"crit":"ok"):"crit",v:ex.chainOk?"INTACT":"BROKEN",n:ex.actions+" actions · "+ex.ok+" OK · "+ex.failed+" failed",s:ex.chainOk&&!ex.failed?"s-ok":"s-crit"}
|
|
:(exemplu?{k:"Finality",pin:"warn",v:"ready",n:"example: the head can be notarized (not notarized)",s:"s-warn"}
|
|
:{k:"Finality",pin:"warn",v:"not checked",n:"the console does not check finality: notarize the head, then check it with the AIP-23 verifier",s:"s-warn"}),
|
|
];
|
|
const T=document.getElementById("tiles");T.innerHTML="";
|
|
for(const t of tiles){
|
|
const c=el("div","tile");
|
|
const k=el("div","k");k.append(el("i","pin "+t.pin),document.createTextNode(t.k));
|
|
c.append(k,el("div","v",t.v),el("div","n",t.n),el("div","stripe "+t.s));
|
|
T.append(c);
|
|
}
|
|
|
|
// alert
|
|
const A=document.getElementById("alert");A.innerHTML="";
|
|
if(d.selfReportSuspect){const w=el("div","warnbox");w.textContent="Suspect self-report: "+d.selfReportSuspect;A.append(w);}
|
|
if(d.note){const w=el("div","warnbox");w.textContent=d.note;A.append(w);}
|
|
|
|
// chain
|
|
const C=document.getElementById("chain");C.innerHTML="";
|
|
const cm=document.getElementById("chainmeta");
|
|
cm.textContent = ac.head ? ("head "+short(ac.head)) : "";
|
|
const entries=d.entries||[];
|
|
if(!entries.length){
|
|
C.append(el("div","card","No per-entry detail (only the console summary was pasted). Paste a bundle from the sidecar for the whole chain."));
|
|
}
|
|
entries.forEach((e,i)=>{
|
|
if(i>0){C.append(el("div","link"));}
|
|
const isHead = e.hash===ac.head;
|
|
const n=el("div","node"+(isHead?" head":""));
|
|
const row=el("div","nrow");
|
|
row.append(el("span","seq","#"+e.seq));
|
|
const kn=KINDNAME[e.kind]||[e.kind,""];
|
|
const kind=el("span","kind");kind.append(document.createTextNode(kn[0]));const tag=el("span","tag",String(e.kind).replace("aere-proof-of-","").replace("aere-",""));kind.append(tag);
|
|
row.append(kind);
|
|
if(e.name)row.append(el("span","nname",e.name));
|
|
if(isHead)row.append(el("span","headflag","head"));
|
|
row.append(el("span","nat",(e.at||"").replace("T"," ").replace("Z","")));
|
|
n.append(row);
|
|
const hl=el("div","hashline");
|
|
hl.append(el("span","hlab","hash"));const hv=el("span","hval",e.hash||"—");hl.append(hv,copyBtn(e.hash||""));
|
|
hl.append(el("span","hlab","prev"));const pv=el("span","hval dim",e.prev===GEN?"genesis 0x00…00":short(e.prev));hl.append(pv,el("span"));
|
|
n.append(hl);
|
|
C.append(n);
|
|
});
|
|
|
|
// ladder: the finality steps of the AIP-23 verifier; the step marked is the one of the example (a head not yet notarized)
|
|
const steps=[
|
|
["1","not notarized","the statementHash of the head is not on AereNotary yet"],
|
|
["2","pending","a notarization transaction was seen, not yet under an anchor"],
|
|
["3","anchor without certificate","in a block, under an anchor without a complete post-quantum certificate"],
|
|
["4","post-quantum","under an anchor with a Falcon / SLH-DSA certificate"],
|
|
];
|
|
const cur = exemplu ? 1 : 0;
|
|
const L=document.getElementById("ladder");L.innerHTML="";
|
|
steps.forEach((s,i)=>{
|
|
const st=el("div","step"+(i+1===cur?" cur":""));
|
|
st.append(el("div","lv","step "+s[0]),el("div","nm",s[1]),el("div","ds",s[2]));
|
|
L.append(st);
|
|
});
|
|
|
|
// migration
|
|
const mb=document.getElementById("migbar");mb.innerHTML="";mb.style.display="";
|
|
const mr=document.getElementById("migrows");mr.innerHTML="";
|
|
if(mg && mg.total){
|
|
// the bar: critical / automatic / manual scaled to their sum (a visual approximation over categories of urgency and method)
|
|
const parts=[["var(--crit)",mg.critical,"critical"],["var(--accent)",mg.auto,"automatic"],["var(--warn)",mg.manual,"manual"]];
|
|
const sum=parts.reduce((a,p)=>a+p[1],0)||1;
|
|
for(const p of parts){const i=el("i");i.style.background=p[0];i.style.width=(p[1]/sum*100)+"%";mb.append(i);}
|
|
for(const [lab,val] of [["Total actions",mg.total],["Critical",mg.critical],["Automatic",mg.auto],["Manual",mg.manual],["Blocked",mg.blocked??0]]){
|
|
const r=el("div","mrow");r.append(el("span",null,lab),el("span","b tnum",String(val)));mr.append(r);
|
|
}
|
|
} else {
|
|
mb.style.display="none";
|
|
mr.append(el("div","n","No migration plan attached. Run node consola.mjs --bundle b.json --plan plan.json."));
|
|
}
|
|
}
|
|
|
|
document.getElementById("render").addEventListener("click",()=>{
|
|
const e=document.getElementById("perr");e.hidden=true;
|
|
// (2026-10-20) un JSON stricat si o iesire care nu se poate reda sunt doua lucruri: pana azi o eroare de script la redare iesea "invalid JSON"
|
|
let o;
|
|
try{ o=JSON.parse(document.getElementById("in").value.trim()); }
|
|
catch(err){ e.hidden=false; e.textContent="invalid JSON: "+err.message; return; }
|
|
try{ render(o); }
|
|
catch(err){ e.hidden=false; e.textContent="cannot render this output: "+err.message; }
|
|
});
|
|
document.getElementById("reset").addEventListener("click",()=>{
|
|
document.getElementById("in").value=JSON.stringify(SAMPLE,null,1);
|
|
document.getElementById("perr").hidden=true; render(SAMPLE, true);
|
|
});
|
|
|
|
render(SAMPLE, true);
|
|
</script>
|
|
</body>
|
|
</html>
|