Skip to content

feat(ledger): GET /v1/public/decision-ledger/row/:seq — bind an anchor to the live chain - #9389

Merged
JSONbored merged 1 commit into
mainfrom
feat/ledger-row-endpoint
Jul 27, 2026
Merged

feat(ledger): GET /v1/public/decision-ledger/row/:seq — bind an anchor to the live chain#9389
JSONbored merged 1 commit into
mainfrom
feat/ledger-row-endpoint

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

First and foundational sub-issue of #9267 (external ledger anchoring) — every other sub-issue in that epic depends on this existing.

The gap this closes. An anchor published to a transparency log or a public git repo commits to a (seq, rowHash) pair. On its own, that proves only that some hash existed somewhere at some time — not that it is still this chain's hash at that seq. Nothing today can fetch a single row and recompute its chained hash: /v1/public/decision-ledger/verify walks the chain's internal self-consistency and returns only the tip plus the first break.

With this route a third party fetches the live row and recomputes sha256(prevHash || canonicalJson({seq, recordId, recordDigest, createdAt})) against what was anchored. An operator who deleted the ledger and re-chained from genesis produces a different rowHash at every anchored seq, so every published anchor then fails that comparison independently and publicly — exactly the wholesale-rewrite gap migrations/0180_decision_ledger.sql names as its own honest limit.

Public-safe by the same argument the verify route already makes: hashes, a seq, a timestamp, and the already-public record id. Never record contents (pinned by a test asserting the exact key set). 404, not 200-with-nulls, for an unappended seq, so "never appended" stays distinguishable from "appended with empty fields". The requiresApiToken exemption is added in the same commit as the route, so this can't repeat #9120's drift where a doc comment claimed unauthenticated but the exemption list disagreed.

Closes #9269.

Test plan

  • npx vitest run test/integration/public-ledger-row-route.test.ts test/integration/public-decision-ledger-routes.test.ts22 passed
  • The load-bearing test: a fetched row alone lets a third party recompute the chained hash, using only fields the route returned and no privileged access
  • Re-chaining detection proven, not asserted: builds a second ledger over different history and confirms the same seq yields a different rowHash — the anchor comparison fails, which is the whole point of the epic
  • Chain linkage: row N's prevHash equals row N−1's rowHash, so anchors at different seqs chain together
  • Negative paths: 404 for unappended seq; 400 for non-integer / zero / negative / fractional seq
  • Privacy: response key set is exactly {createdAt, prevHash, recordDigest, recordId, rowHash, seq} — no record contents
  • Added to the shared exempt-public-routes table (the guard that caught orb(trust): the 'public' decision-ledger verify endpoint requires an API token — 401 in prod, so the chain is verifiable by nobody #9120)
  • npm run ui:openapi:check clean · npx tsc --noEmit clean · openapi.test.ts passing

Note on CI: until #9285 merges, this will inherit main's 5 pre-existing test failures (salvageability, selfhost-metrics, selfhost-pg-retention ×2, worker-entry-boundary) in files this PR doesn't touch.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui d9839be Commit Preview URL

Branch Preview URL
Jul 27 2026, 03:52 PM

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 1.8kB (0.02%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.49MB 1.8kB (0.02%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-Br-8Fmv7.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-vI6mgKO9.js (New) 817.74kB 817.74kB 100.0% 🚀
openapi.json 1.09kB 534.52kB 0.2%
assets/docs.fumadocs-spike-api-reference-Bayq4IMe.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-C7Q6r7vu.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-piNx389b.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-Dg2VW8vD.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-9BSMte0j.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-C3k3f4HE.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-CyDSHHsg.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-vOfVz0qK.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-C5F9lyAT.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-iDPHv3ob.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-Bmo8s7EJ.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-B9-7yq_1.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-CtI5YXWD.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-C1rDa1F_.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-mBJLdDPU.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-BlKyeDos.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-vp6VECDW.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-BcDS4oNB.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-CzI-Qgq1.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-C0ahMYj5.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-Gv2k_wDL.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-DY5OJOZU.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-9po1aMEP.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-C3alW33x.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BlLgXx-F.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-BPHFtSFF.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-B7JVpzWp.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-BBi9yZN0.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-BQBD2Tgl.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-DmVTmVO9.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-CTHU8feD.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-CN-6B4bo.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-DrmfNEZx.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-BRYWWy1c.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-YbbdH5Bl.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-DEjvaluf.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-CH0o-yjz.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-SEBoOuTN.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-DM6QgISW.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-D2NHctCB.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-yB4GKaXS.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-DKmBt19B.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-DDYLONqF.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-DJq4gccd.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-Dp9RX9fx.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-i4q6j3QC.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-pingVr0T.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-RSnvaOC8.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-CzKDBELi.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-z-UPK7JW.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-C8WvF_SQ.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BDVLofMA.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-CPCYH9aN.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-6eHtFoFI.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-CoNlDKJv.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-Deuhwlbo.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-BH0aO4_g.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-Sjq5MR7f.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-B3E0zIEa.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-B1tvC3UZ.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-Bk3uZKSL.js (Deleted) -817.03kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-CWvXJbyA.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-z-WcqiDW.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-Ch5JrPxb.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-BiCXHFS2.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-CKmAEnQb.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-B7hJtsZX.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-CyBIEgNZ.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-cHjmmv6F.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-CCN3nBYk.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-2kHiXl0X.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-Lidjhckl.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-UkTmWy-o.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-CVXwjapH.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-Cf9OXNDJ.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-wZQPFo_s.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-D-M4aSpm.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-DA_kgYeP.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-Xj0L_kr-.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-C3cWF8EQ.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-vScLTG6K.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-vxu6aLT0.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-DVOhzGv5.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-C-0KiWx3.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-DeJxGeTT.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-NJwca65k.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-DUfeHsJW.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-CNru9zqM.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-Q67Jtt2x.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-CUnuLuB6.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-Cki_PiqC.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-DRFkdKiB.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-DzbfA-9U.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-DVC31iuu.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-DHgbHlHj.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-BQCMeuhe.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-C2EnHvbT.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-6fAu4Nfy.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-jwtSA5JF.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-CDxqrThY.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-DmrP5G3v.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-Qitnogmp.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-sc-NeVqc.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-16eD6Mlq.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-B2iXOvPB.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-B4G83W6I.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-CdRDwxa3.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-DcqucMC0.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-CFofH15y.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-CRlFz3o5.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-BxVER8BF.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-BLNMNjtJ.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-DusJxp0I.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history--b1Xp7_v.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-DeNYCIMl.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-Bu3RchpJ.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-B0cb9bFv.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-DIOP0rIp.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-Cw3Jd1VP.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-B3e62Vht.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

…r to the live chain (#9269)

First and foundational sub-issue of #9267 (external ledger anchoring). Everything
else in that epic depends on this existing.

The gap: an anchor published to a transparency log or public git repo commits to a
(seq, rowHash) pair. On its own that proves only that SOME hash existed somewhere
at some time — not that it is still THIS chain's hash at that seq. Nothing today
can fetch a single row and recompute its chained hash: verify walks internal
self-consistency and returns only the tip plus the first break.

With this route a third party fetches the live row and recomputes
sha256(prevHash || canonicalJson({seq, recordId, recordDigest, createdAt}))
against what was anchored. An operator who deleted the ledger and re-chained from
genesis produces a different rowHash at every anchored seq, so every published
anchor then fails that comparison independently and publicly — which is exactly
the wholesale-rewrite gap migrations/0180_decision_ledger.sql names as its own
honest limit.

Public-safe by the same argument the verify route already makes: hashes, a seq, a
timestamp, and the already-public record id. Never record contents (pinned by
test). 404 rather than 200-with-nulls for an unappended seq, so "never appended"
stays distinguishable from "appended with empty fields". Auth exemption added in
this same commit as the route, so it cannot repeat #9120's drift where a doc
comment claimed unauthenticated but the exemption list disagreed.
@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Important

🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪

🔍 LoopOver is reviewing…

AI analysis is in progress. This comment will update when the review is complete.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟪 Reviewing

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.57%. Comparing base (995cd78) to head (d9839be).
⚠️ Report is 5 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #9389       +/-   ##
===========================================
+ Coverage   75.38%   88.57%   +13.18%     
===========================================
  Files         275      837      +562     
  Lines       58023   109650    +51627     
  Branches     6181    26113    +19932     
===========================================
+ Hits        43739    97117    +53378     
+ Misses      14014    11561     -2453     
- Partials      270      972      +702     
Flag Coverage Δ
backend 93.57% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/api/routes.ts 95.52% <100.00%> (ø)
src/openapi/spec.ts 99.48% <100.00%> (ø)
src/review/decision-record.ts 100.00% <100.00%> (ø)

... and 697 files with indirect coverage changes

@JSONbored
JSONbored merged commit b4064b6 into main Jul 27, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/ledger-row-endpoint branch July 27, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ledger: GET /v1/public/decision-ledger/row/:seq — bind an anchor back to the live chain

1 participant