Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion workers/godlock-uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "Apache-2.0",
"type": "module",
"scripts": {
"test": "node --test steer.test.mjs presence.test.mjs pages.test.mjs engine.test.mjs runtime.test.mjs mesh.test.mjs site-presence.test.mjs donate.test.mjs receipts.test.mjs identity.test.mjs ingest.test.mjs shelves.test.mjs redline.test.mjs submit.test.mjs survival.test.mjs help.test.mjs softwares-ssot.test.mjs sot-outlet.test.mjs debate-score.test.mjs indexnow.test.mjs"
"test": "node --test steer.test.mjs presence.test.mjs pages.test.mjs engine.test.mjs runtime.test.mjs mesh.test.mjs site-presence.test.mjs donate.test.mjs receipts.test.mjs identity.test.mjs ingest.test.mjs shelves.test.mjs redline.test.mjs submit.test.mjs survival.test.mjs help.test.mjs softwares-ssot.test.mjs sot-outlet.test.mjs debate-score.test.mjs indexnow.test.mjs thread-expand.test.mjs"
},
"dependencies": {
"wrangler": "^4.127.1"
Expand Down
3 changes: 3 additions & 0 deletions workers/godlock-uk/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { donateDoc, DONATE_RAILS, donateQrIdFromPath } from "./donate.js";
import { handleRuntimeRoot, isRuntimeRequest, runtimeCors } from "./runtimeRoot.js";
import { appendLedger, verifyLedger, ledgerEntriesForId, sha256hex } from "./ledger.js";
import { receiptProofForId } from "./receiptProof.js";
import {
robotsTxt, sitemapXml, citeDoc, llmsDoc, aiDoc, siteOpenApi, mcpDiscoveryDoc, BANNER, DOWNLOAD, DOWNLOAD_STATS, DOWNLOAD_COUNT, GITHUB, AUTHOR,
indexNowKeyBody, INDEXNOW_KEY_PATH, INDEXNOW_WELL_KNOWN_PATH,
Expand Down Expand Up @@ -1276,10 +1277,12 @@ export default {
const entries = await ledgerEntriesForId(env, id);
const stats = await gatherStats(env, { wrote, ctx });
if (wantsJson(request, url)) {
const proof = await receiptProofForId(env, id);
return json({
ok: true,
receipt: publicPayload(row),
ledger: entries,
proof,
current_score: stats.current_score,
residual: stats.residual,
triad_display: stats.triad_display,
Expand Down
91 changes: 91 additions & 0 deletions workers/godlock-uk/src/receiptProof.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Receipt proof for the expand (drop-down) view of a public thread.
* Hash-linked and sealed. NOT signed: there is no signature anywhere here.
*
* Fails closed: status is "verified" only when every ledger entry for the
* receipt recomputes to its entry_hash, links to the entry before it, and the
* whole ledger verifies from genesis to the head that is reported as the seal.
* Anything else is "unverified" or "missing". Nothing is invented.
* Author: Aziel Eliab.
*/
import { hashPayload, ZERO } from "./ledger.js";

export const PROOF_NOT_SIGNED = "Hash-linked and sealed. Not signed.";

function parse(blob) {
try { return { ok: true, value: JSON.parse(blob || "{}") }; } catch { return { ok: false, value: {} }; }
}

/**
* Pure. rows = every ledger row (sequence ASC, raw payload_json), id = receipt id.
* Returns { status, receipt_hash, prev_hash, seal, sealed_at, entries, errors, not_signed }.
*/
export function buildReceiptProof(rows, id) {
const needle = String(id || "");
const all = Array.isArray(rows) ? rows : [];
const errors = [];
let expectedPrev = ZERO;
let expectedSeq = 1;
let head = ZERO;
let headAt = null;
const mine = [];
for (const row of all) {
const seq = Number(row.sequence);
if (seq !== expectedSeq) errors.push("sequence gap at " + seq);
if (String(row.previous_hash) !== expectedPrev) errors.push("previous_hash mismatch at sequence " + seq);
const p = parse(row.payload_json);
if (!p.ok) errors.push("payload_json not JSON at sequence " + seq);
const recomputed = hashPayload({
sequence: seq,
timestamp_utc: row.timestamp_utc,
action: row.action,
payload: p.value,
previous_hash: row.previous_hash,
});
const hashOk = p.ok && recomputed === row.entry_hash;
if (!hashOk) errors.push("entry_hash mismatch at sequence " + seq);
if (needle && String(row.payload_json || "").indexOf(needle) >= 0) {
mine.push({
sequence: seq,
action: String(row.action),
timestamp_utc: row.timestamp_utc,
entry_hash: String(row.entry_hash),
previous_hash: String(row.previous_hash),
hash_ok: hashOk,
link_ok: String(row.previous_hash) === expectedPrev,
});
}
expectedPrev = String(row.entry_hash);
expectedSeq = seq + 1;
head = String(row.entry_hash);
headAt = row.timestamp_utc;
}
const base = { not_signed: PROOF_NOT_SIGNED, verify_url: "/verify", entries: mine };
if (!mine.length) {
return { ...base, status: "missing", receipt_hash: null, prev_hash: null, seal: null, sealed_at: null, errors: ["no ledger entry for this receipt"] };
}
const first = mine[0];
const chainOk = errors.length === 0 && mine.every((e) => e.hash_ok && e.link_ok);
return {
...base,
status: chainOk ? "verified" : "unverified",
receipt_hash: first.entry_hash,
prev_hash: first.previous_hash,
timestamp_utc: first.timestamp_utc,
// The seal is the ledger head the chain verified up to. Only reported when it verified.
seal: chainOk ? head : null,
sealed_at: chainOk ? headAt : null,
errors,
};
}

export async function receiptProofForId(env, id) {
try {
const rows = (await env.DB.prepare(
"SELECT sequence, timestamp_utc, action, payload_json, previous_hash, entry_hash FROM ledger ORDER BY sequence ASC"
).all()).results || [];
return buildReceiptProof(rows, id);
} catch (err) {
return { status: "missing", not_signed: PROOF_NOT_SIGNED, verify_url: "/verify", entries: [], receipt_hash: null, prev_hash: null, seal: null, sealed_at: null, errors: ["ledger read failed"] };
}
}
67 changes: 66 additions & 1 deletion workers/godlock-uk/src/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,13 +507,76 @@ export function priorReceiptItems(rows, { fullChallenge = false } = {}) {
return (rows || []).map((r) => {
const challenge = fullChallenge ? challengeBlockText(r) : challengePreview(r);
return `<li>
<div class="challenge-preview">${esc(challenge)}</div>
<details class="thread" data-receipt-id="${esc(r.id)}">
<summary><span class="challenge-preview">${esc(challenge)}</span></summary>
<div class="thread-body" aria-live="polite"><p class="muted">Loading reasoning and receipt…</p></div>
</details>
${rescoreLine(r)}
<div class="muted">${esc(when(r.created_utc))}</div>
<a href="/receipt/${esc(r.id)}">Receipt</a></li>`;
}).join("");
}

/**
* Pure view model for an expanded thread. Input is the /receipt/:id?format=json
* body (or null on fetch failure). Fails closed: only status "verified" from the
* server proof is shown as verified. Never invents reasoning or a receipt.
* Kept free of outer references so it can be inlined via toString().
*/
export function threadView(j) {
var out = { reasoning: [], receipt: [], status: "missing", note: "Hash-linked and sealed. Not signed." };
if (!j || j.ok !== true || !j.receipt) {
out.receipt.push(["Receipt", "missing (could not be fetched)"]);
return out;
}
var r = j.receipt;
if (typeof r.summary === "string" && r.summary) out.reasoning.push(["Summary", r.summary]);
if (typeof r.explanation === "string" && r.explanation) out.reasoning.push(["Reasoning", r.explanation]);
if (!out.reasoning.length) out.reasoning.push(["Reasoning", "No reasoning was recorded for this receipt."]);
var p = j.proof;
if (!p || typeof p !== "object") {
out.receipt.push(["Receipt", "missing (no proof returned)"]);
return out;
}
var st = p.status === "verified" ? "verified" : (p.status === "unverified" ? "unverified" : "missing");
if (st === "verified" && !(p.receipt_hash && p.prev_hash && p.seal)) st = "unverified";
out.status = st;
out.receipt.push(["Status", st]);
out.receipt.push(["Receipt hash", p.receipt_hash || "missing"]);
out.receipt.push(["Prev hash", p.prev_hash || "missing"]);
out.receipt.push(["Seal (verified ledger head)", st === "verified" ? p.seal : "missing"]);
out.receipt.push(["Timestamp", p.timestamp_utc || "missing"]);
out.receipt.push(["Verify", p.verify_url || "/verify"]);
return out;
}

export function threadExpandScript() {
return `<script>
(function(){
var threadView=${threadView.toString()};
function row(box,k,v,mono){var p=document.createElement("p");var b=document.createElement("b");b.textContent=k+": ";p.appendChild(b);
if(k==="Verify"){var a=document.createElement("a");a.href=v;a.textContent="Verify chain";p.appendChild(a);}
else{var s=document.createElement("span");s.textContent=String(v);if(mono)s.className="hash";p.appendChild(s);}box.appendChild(p);}
function render(body,j){var m=threadView(j);body.textContent="";
m.reasoning.forEach(function(x){row(body,x[0],x[1],false);});
var h=document.createElement("div");h.className="k";h.textContent="Receipt ("+m.status+")";body.appendChild(h);
m.receipt.forEach(function(x){row(body,x[0],x[1],/hash|Seal/.test(x[0]));});
var n=document.createElement("p");n.className="muted";n.textContent=m.note;body.appendChild(n);}
document.querySelectorAll("details.thread").forEach(function(d){
d.addEventListener("toggle",function(){
if(!d.open||d.getAttribute("data-loaded"))return;
d.setAttribute("data-loaded","1");
var body=d.querySelector(".thread-body");
fetch("/receipt/"+encodeURIComponent(d.getAttribute("data-receipt-id"))+"?format=json",{headers:{"Accept":"application/json"}})
.then(function(r){return r.ok?r.json():null;})
.then(function(j){render(body,j);})
.catch(function(){render(body,null);});
});
});
})();
</script>`;
}

export function nodesLiveFromStats(stats) {
const s = stats && typeof stats === "object" ? stats : {};
const mesh = s.mesh && typeof s.mesh === "object" ? s.mesh : {};
Expand Down Expand Up @@ -641,6 +704,7 @@ ${latestHtml}
<h2>Prior receipts</h2>
<p class="muted prior-more">Newest ${esc(HOME_PRIOR_LIMIT)}. Every receipt is kept. <a href="${esc(RECEIPTS_PATH)}">Full receipts chain</a>.</p>
<ul class="prior">${list}</ul>
${threadExpandScript()}
`;
}

Expand Down Expand Up @@ -677,6 +741,7 @@ ${steerPanel(s.steer, s)}
<p class="muted"><a href="${esc(REASON_PATH)}">How it is scored</a> · <a href="/">Ask a question</a>.</p>
${receiptsPager({ page, pages, total: total != null ? total : receipts })}
<ul class="prior">${list}</ul>
${threadExpandScript()}
${ingestTipSection()}
${actReceiptsSection(rows)}
`;
Expand Down
71 changes: 71 additions & 0 deletions workers/godlock-uk/thread-expand.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { hashPayload, canonicalJson, ZERO } from "./src/ledger.js";
import { buildReceiptProof } from "./src/receiptProof.js";
import { threadView, threadExpandScript, priorReceiptItems } from "./src/ui.js";

function chain(payloads) {
const rows = []; let prev = ZERO;
payloads.forEach((payload, i) => {
const e = { sequence: i + 1, timestamp_utc: "2026-10-0" + (i + 1) + "T00:00:00.000Z", action: "submit", payload, previous_hash: prev };
const entry_hash = hashPayload(e);
rows.push({ ...e, payload_json: canonicalJson(payload), entry_hash });
prev = entry_hash;
});
return rows;
}

describe("receipt proof", () => {
const rows = chain([{ id: "aaa" }, { id: "bbb111" }, { id: "ccc" }]);
it("verifies and seals at the ledger head", () => {
const p = buildReceiptProof(rows, "bbb111");
assert.equal(p.status, "verified");
assert.equal(p.receipt_hash, rows[1].entry_hash);
assert.equal(p.prev_hash, rows[0].entry_hash);
assert.equal(p.seal, rows[2].entry_hash);
assert.match(p.not_signed, /Not signed/);
});
it("missing receipt fails closed", () => {
const p = buildReceiptProof(rows, "zzz999");
assert.equal(p.status, "missing"); assert.equal(p.seal, null);
});
it("tampered payload is unverified with no seal", () => {
const bad = rows.map((r) => ({ ...r }));
bad[1].payload_json = canonicalJson({ id: "bbb111", x: 1 });
const p = buildReceiptProof(bad, "bbb111");
assert.equal(p.status, "unverified"); assert.equal(p.seal, null);
});
it("tamper elsewhere in the chain also blocks the seal", () => {
const bad = rows.map((r) => ({ ...r })); bad[2].previous_hash = ZERO;
assert.equal(buildReceiptProof(bad, "bbb111").status, "unverified");
});
});

describe("thread expand view", () => {
it("fetch failure shows receipt missing, no reasoning invented", () => {
const v = threadView(null);
assert.equal(v.status, "missing"); assert.equal(v.reasoning.length, 0);
});
it("verified proof renders hashes, seal and verify link", () => {
const v = threadView({ ok: true, receipt: { summary: "S", explanation: "E" }, proof: { status: "verified", receipt_hash: "h1", prev_hash: "h0", seal: "h9", timestamp_utc: "t", verify_url: "/verify" } });
assert.equal(v.status, "verified");
assert.deepEqual(v.reasoning, [["Summary", "S"], ["Reasoning", "E"]]);
const m = Object.fromEntries(v.receipt);
assert.equal(m["Seal (verified ledger head)"], "h9"); assert.equal(m.Verify, "/verify");
});
it("verified without a seal is downgraded", () => {
const v = threadView({ ok: true, receipt: { summary: "S" }, proof: { status: "verified", receipt_hash: "h1", prev_hash: "h0", seal: null } });
assert.equal(v.status, "unverified");
});
it("unverified proof never shows a seal", () => {
const v = threadView({ ok: true, receipt: {}, proof: { status: "unverified", receipt_hash: "h1", prev_hash: "h0", seal: "h9" } });
assert.equal(Object.fromEntries(v.receipt)["Seal (verified ledger head)"], "missing");
});
it("prior rows render a drop-down and the page script parses", () => {
const html = priorReceiptItems([{ id: "abc", created_utc: "2026-10-01T00:00:00Z", challenge_text: "q" }]);
assert.match(html, /<details class="thread" data-receipt-id="abc">/);
const js = threadExpandScript().replace(/^<script>|<\/script>$/g, "");
assert.doesNotThrow(() => new Function(js));
assert.doesNotMatch(threadExpandScript(), /signed by|signature/i);
});
});
Loading