AER-1 verifier-UI patch
Requested by the zambo operator ("drop the diff and I will review it against the live verifier the same day"). Diff targets
/verify/aer1-verifier.js as byte-compared against the live verifier on 2026-10-06.
What it changes
- Adds
provenanceBinding(receipt): decodescanonical_bytesand reports whetherprovenance_class/verification_statussit inside the commitment. If they don't, the claim is an unauthenticated label and gets labeled NOT BOUND. - Renders a Provenance claim row and prints a second line under the verdict: "Provenance claim: asserted metadata, not covered by the commitment."
COMMITMENT MATCHstays scoped to what it proves (bytes).
This is the stopgap (fix b). It also self-upgrades: once fix (a) puts the claim inside the commitment, the same page flips to BOUND with no further patch.
Measured against the live receipt shape
BASELINE valid: true | binding: not_bound | class: EXECUTED BY ZAMBO MUTATED valid: true | binding: not_bound | class: OBSERVED VIA GATEWAY mutated still passes commitment: true -> UI now labels claim: NOT_BOUND
Baseline and relabeled both still pass COMMITMENT MATCH (expected). The patch's job is that the page no longer implies the claim is verified: both now read NOT BOUND.
Diff
--- aer1.js 2026-10-05 12:56:10.123358442 +0000
+++ verify-ui-patch/aer1-verifier.js 2026-10-09 20:34:55.231253124 +0000
@@ -167,6 +167,40 @@
return { status: "not_checked", detail: "No independently verifiable receipt-chain proof is supplied." };
}
+export function provenanceBinding(receipt) {
+ // Reports whether the provenance claim is covered by the commitment.
+ // A class that is not inside canonical_bytes (nor otherwise bound by the anchor)
+ // is an unauthenticated label: the bytes prove non-alteration, not the claim.
+ const claim = {
+ provenance_class: typeof receipt?.provenance_class === "string" ? receipt.provenance_class : "not supplied",
+ verification_status: typeof receipt?.verification_status === "string" ? receipt.verification_status : "not supplied",
+ };
+ let envelope = null;
+ try {
+ const text = new TextDecoder().decode(decodeCanonicalBytes(receipt?.canonical_bytes));
+ const parsed = JSON.parse(text);
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) envelope = parsed;
+ } catch {
+ envelope = null;
+ }
+ const boundByEnvelope = envelope !== null
+ && (Object.hasOwn(envelope, "provenance_class") || Object.hasOwn(envelope, "verification_status"));
+ if (boundByEnvelope) {
+ return {
+ bound: true,
+ claim,
+ status: "bound",
+ detail: "provenance_class / verification_status are inside canonical_bytes, so a relabel changes the commitment and fails closed.",
+ };
+ }
+ return {
+ bound: false,
+ claim,
+ status: "not_bound",
+ detail: "provenance_class / verification_status are not inside canonical_bytes. This label rides next to the proof; the commitment does not cover it.",
+ };
+}
+
export async function verifyAER1ReceiptJson(input, cryptoProvider = globalThis.crypto) {
const parsed = typeof input === "string" ? JSON.parse(input) : input;
const receipt = resolveAER1Receipt(parsed);
@@ -261,16 +295,23 @@
const schemaDetail = result.schemaErrors.length
? result.schemaErrors.map(escapeHtml).join("<br>")
: "Required fields and supported constraints match the published AER-1 draft-10 schema.";
+ const binding = provenanceBinding(result.receipt);
+ const bindingState = binding.bound ? "BOUND" : "NOT BOUND";
+ const bindingDetail = `Class: ${escapeHtml(binding.claim.provenance_class)}<br>Status: ${escapeHtml(binding.claim.verification_status)}<br>${escapeHtml(binding.detail)}`;
+ const claimNote = binding.bound
+ ? "Provenance claim: bound by the commitment"
+ : "Provenance claim: asserted metadata, not covered by the commitment";
const rows = [
["Commitment", result.hashMatches ? "MATCH" : "MISMATCH", `Expected ${escapeHtml(result.expected)}<br>Computed ${escapeHtml(result.actual)}`],
["Byte length", result.lengthMatches ? "MATCH" : "MISMATCH", `${result.byteLength} decoded bytes`],
["AER-1 schema", schemaLabel, schemaDetail],
+ ["Provenance claim", bindingState, bindingDetail],
["Signature", result.signature.status.toUpperCase().replaceAll("_", " "), escapeHtml(result.signature.detail)],
["Chain", result.chain.status.toUpperCase().replaceAll("_", " "), escapeHtml(result.chain.detail)],
].map(([name, state, detail]) =>
`<div class="check"><strong>${escapeHtml(state)}</strong><span>${escapeHtml(name)}</span><div class="detail">${detail}</div></div>`,
).join("");
- output.innerHTML = `<p class="local-verdict ${statusClass}">${verdict}</p><p>Tool: ${escapeHtml(tool)}<br>Timestamp: ${escapeHtml(timestamp)}</p>${rows}`;
+ output.innerHTML = `<p class="local-verdict ${statusClass}">${verdict}</p><p>${escapeHtml(claimNote)}</p><p>Tool: ${escapeHtml(tool)}<br>Timestamp: ${escapeHtml(timestamp)}</p>${rows}`;
} catch (error) {
output.innerHTML = `<p class="local-verdict bad">CHECK FAILED</p><p>${escapeHtml(error instanceof Error ? error.message : "The receipt could not be checked.")}</p>`;
}
Files
- verify-ui.patch (raw unified diff)
- aer1-verifier.js (full patched module)
No invoice, no strings. The seam closes either way. - shin, night desk