Skip to content

feat(ledger): versioned + signed anchor payload, published key rotation route - #9392

Merged
JSONbored merged 3 commits into
mainfrom
feat/ledger-anchor-payload
Jul 27, 2026
Merged

feat(ledger): versioned + signed anchor payload, published key rotation route#9392
JSONbored merged 3 commits into
mainfrom
feat/ledger-anchor-payload

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Second sub-issue of #9267 (external ledger anchoring) — extends buildLedgerAnchorPayload's unused {seq, rowHash, at} stub into the shape the epic actually needs: self-describing and signed.

  • Self-describing: an anchor read cold in two years, out of whatever log it landed in, must say what it is and which chain it commits to — an explicit v schema version + ledger id, plus totalCount riding alongside seq so a truncate-and-rechain to the same length still has to match both.
  • Signed: without a signature, anyone could publish a plausible-looking anchor for this ledger and "prove" a rewrite that never happened — a denial-of-integrity attack against our own record. ECDSA P-256/SHA-256 via native WebCrypto — no new dependency, and the same keypair is exactly what Rekor's hashedrekord (ledger: Rekor v2 hashedrekord anchoring backend #9272) takes as a self-managed verifier key.
  • Key rotation is first-class: GET /v1/public/decision-ledger/anchor-key serves the full history, since an anchor signed under a retired key must stay verifiable forever. keyId is derived from the key itself (sha256 of its SPKI) so it can never drift from what it names. An ambiguous rotation state (zero or several open-ended keys) fails closed to currentKeyId: null rather than guessing.
  • The private half (LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY) is a Worker secret, never served by any route, never in the repo. Unset simply means anchoring doesn't run yet — the ledger degrades to the honest pre-orb(trust): external decision-ledger anchoring — Rekor + git-commit, from tamper-evident to tamper-proof #9267 tamper-evident posture rather than failing.

Closes #9270.

Test plan

  • npx vitest run test/unit/ledger-anchor.test.ts — 16/16, 100% statement/branch/function/line coverage
  • Real generated ECDSA keypairs throughout, never a stubbed signer — the property under test is that an outsider holding only the published public key can verify or refuse an anchor
  • Tamper detection proven: mutating rowHash, seq, or totalCount after signing independently breaks verification
  • A forged anchor (different key, claiming the operator's keyId) fails verification against the real published key
  • Rotation: a retired key stays resolvable by id; an ambiguous open-ended set fails closed
  • npx vitest run test/integration/public-anchor-key-route.test.ts — 6/6: full history served, ambiguous state → null, malformed config → empty list (never 500), private key material never leaks into the public response even if misconfigured
  • Added to the shared exempt-public-routes table
  • npx tsc --noEmit clean · npm run ui:openapi:check clean · npm run cf-typegen:check — pre-existing local-workerd-version staleness confirmed on clean origin/main too (I touched neither wrangler.jsonc nor worker-configuration.d.ts), left alone rather than risk committing a local artifact CI doesn't produce

@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 fa8c60e Commit Preview URL

Branch Preview URL
Jul 27 2026, 04:50 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 3.31kB (0.04%) ⬆️. This is within the configured threshold ✅

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

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-BCf_aDPe.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-BIOn6cTE.js (New) 819.0kB 819.0kB 100.0% 🚀
openapi.json 2.05kB 536.57kB 0.38%
assets/docs.fumadocs-spike-api-reference-kW9u7muo.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-B0akRfmX.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-BoRhkA3-.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-BLpLYhIP.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-BWp7tVCW.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-BG0bXX0-.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-CAcT_Owx.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-DNaYzwzR.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-Bj6G_PLj.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-CL_JIL1c.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-CaMsv4QD.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-CXhUWBKT.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-IapkYHDM.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-BLWaE6yc.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-DluYqd5e.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-Dkl0Nmh9.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-DgTETJsG.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-Dnx9Yi01.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-lz9FPSWJ.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-Dm4oeAYO.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-BNc5FLXK.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-BOHU6EnE.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-DVEMZFg6.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-BNGeS1OD.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BM1jh1QK.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-CA4KOcp3.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-BF-mzXRa.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-DjHVzwZI.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-B0jCA4SP.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-DPwUObq6.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-Dl7dcFvS.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-BrynWu2F.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-Ce7ifkjE.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-DQ9N4krb.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-EHvRBkVN.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BebdtE4E.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-DfkGWZUJ.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-BrLMbU6e.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-DmmzeVuy.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-8kn2Qef9.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-Bde1dHe5.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-DjizPwog.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-DPqeJZ6B.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-Ba9N072B.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-C991ZAer.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-C9R7ijE6.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-BpF6pznL.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-DYB8q5ky.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-DbKrp0--.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-CZFVouPq.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-ERFeW2tk.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BDGbbA_w.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-xbm3z6OY.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-B0l045b0.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-DMcIvEEe.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-BZJFzApn.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-CmxIsM9k.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-DO8aLIId.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-paxvfzxt.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-Br-8Fmv7.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-vI6mgKO9.js (Deleted) -817.74kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-Bayq4IMe.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-C7Q6r7vu.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-piNx389b.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-Dg2VW8vD.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-9BSMte0j.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-C3k3f4HE.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-CyDSHHsg.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-vOfVz0qK.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-C5F9lyAT.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-iDPHv3ob.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-Bmo8s7EJ.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-B9-7yq_1.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-CtI5YXWD.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-C1rDa1F_.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-mBJLdDPU.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-BlKyeDos.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-vp6VECDW.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-BcDS4oNB.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-CzI-Qgq1.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-C0ahMYj5.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-Gv2k_wDL.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-DY5OJOZU.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-9po1aMEP.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-C3alW33x.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BlLgXx-F.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-BPHFtSFF.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-B7JVpzWp.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-BBi9yZN0.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BQBD2Tgl.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-DmVTmVO9.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-CTHU8feD.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-CN-6B4bo.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-DrmfNEZx.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-BRYWWy1c.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-YbbdH5Bl.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-DEjvaluf.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-CH0o-yjz.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-SEBoOuTN.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-DM6QgISW.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-D2NHctCB.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-yB4GKaXS.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-DKmBt19B.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-DDYLONqF.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-DJq4gccd.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-Dp9RX9fx.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-i4q6j3QC.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-pingVr0T.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-RSnvaOC8.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-CzKDBELi.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-z-UPK7JW.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-C8WvF_SQ.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-BDVLofMA.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-CPCYH9aN.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-6eHtFoFI.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-CoNlDKJv.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-Deuhwlbo.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-BH0aO4_g.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-Sjq5MR7f.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-B3E0zIEa.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-27 16:40:26 UTC

8 files · 1 AI reviewer · 1 blocker · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
Adds a versioned, self-describing, ECDSA-signed decision-ledger anchor payload plus a public key-rotation route, with a well-scoped test suite covering sign/verify round-trips against real WebCrypto keys, tamper detection, and rotation ambiguity handling. The route/auth wiring follows the exact established pattern from the neighboring #9120/#9269 public routes (openapi.json + spec.ts + requiresApiToken exemption all updated together), and the code correctly never exposes the private key. The external brief's 'leaked secret' flag on the test file is a false positive — that's an inline placeholder PEM literal (`MIGHAgEA`) used only to assert it never appears in a response, not a real credential.

Nits — 5 non-blocking
  • The external brief's magic-number findings (2026/2027 in comments, 16 for keyId truncation length, 500 for the digest hash) are documentation/derived constants, not real magic-number smells worth flagging.
  • src/review/ledger-anchor.ts computeAnchorKeyId truncates the sha256 digest to 16 hex chars (64 bits) — worth a one-line note on why that's an acceptable collision margin for a key identifier rather than a security-relevant value.
  • The PR description says 'Closes ledger: versioned + signed anchor payload, public key + rotation route #9270' but per this repo's contribution policy every contributor PR must close or clearly link an eligible open issue — worth confirming ledger: versioned + signed anchor payload, public key + rotation route #9270 is actually open and maintainer-authorized before merge, since I can't verify issue state from the diff alone.
  • Consider whether GET /v1/public/decision-ledger/anchor-key should have its Cache-Control TTL (300s) documented alongside the rotation cadence expectation, since a rotation published mid-window means external verifiers could serve a stale key list for up to 5 minutes plus the stale-while-revalidate window.
  • src/review/ledger-anchor.ts:pemToBytes could reject a PEM with no BEGIN/END markers up front rather than silently base64-decoding whatever garbage results — currently a malformed PEM only fails downstream inside importKey.

Concerns raised — review before merging

  • Possible leaked secret in the diff (private_key_block): The PR diff matches secret pattern(s): private_key_block. Found at: test/integration/public-anchor-key-route.test.ts:55. A committed credential must be rotated and removed from the change before merge. — Remove the secret from the diff, rotate the exposed credential, then re-run the gate.
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. Possible leaked secret in the diff (private_key_block): The PR diff matches secret pattern(s): private_key_block. Found at: test/integration/public-anchor-key-route.test.ts:55. A committed credential must be rotated and removed from the change before merge. — Remove the secret from the diff, rotate the exposed credential, then re-run the gate.

Decision drivers

  • ❌ Code review — 1 blocker (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9270
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 13 registered-repo PR(s), 13 merged, 345 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 345 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff implements exactly the requested shape ({v, ledger, seq, rowHash, totalCount, at}), signs the canonicalJson serialization with ECDSA P-256 via native crypto.subtle, and adds a GET /v1/public/decision-ledger/anchor-key route serving the full published key history with tests covering rotation (old anchors still resolve via anchorKeyById) and ambiguous-state fail-closed behavior. Key generat

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 13 PR(s), 345 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 1 step in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

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


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@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 89.50%. Comparing base (b4064b6) to head (fa8c60e).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #9392       +/-   ##
===========================================
+ Coverage   75.46%   89.50%   +14.04%     
===========================================
  Files         275      840      +565     
  Lines       58030   109770    +51740     
  Branches     6199    26141    +19942     
===========================================
+ Hits        43790    98248    +54458     
+ Misses      13970    10259     -3711     
- Partials      270     1263      +993     
Flag Coverage Δ
backend 95.23% <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.55% <100.00%> (ø)
src/openapi/spec.ts 99.49% <100.00%> (ø)
src/review/ledger-anchor-persistence.ts 100.00% <100.00%> (ø)
src/review/ledger-anchor.ts 100.00% <100.00%> (ø)

... and 699 files with indirect coverage changes

…on route (#9270)

Extends the unused {seq, rowHash, at} stub into a self-describing, signed payload,
and publishes the verifying keys.

Self-describing: an anchor read cold in two years, out of whatever log it landed
in, must say what it is and what chain it commits to — hence an explicit schema
version and ledger id. totalCount rides alongside seq so a truncate-and-rechain to
the same length still has to match both.

Signed: without a signature anyone could publish a plausible anchor for this ledger
and then "prove" a rewrite that never happened — a denial-of-integrity attack
against our own record. ECDSA P-256/SHA-256 via WebCrypto: no new dependency, and
the same keypair is what Rekor's hashedrekord takes as a self-managed verifier key
(#9272).

Key rotation is first-class: the published list is the full history, since an
anchor signed under a retired key must stay verifiable forever. keyId is derived
from the key (sha256 of SPKI) so it cannot drift from what it names. An ambiguous
rotation state (zero or several open-ended keys) fails closed rather than guessing.

The private half is a Worker secret, never served by any route and never in the
repo; unset simply means anchoring does not run, degrading to the honest
pre-#9267 tamper-evident posture rather than failing.
Lost during editing -- the re-export line only carried sha256Hex, so any
consumer importing canonicalJson from this module got undefined. Caught while
building #9273's git-commit backend, which needs it to commit the same
canonicalized payload Rekor anchors.
@JSONbored
JSONbored force-pushed the feat/ledger-anchor-payload branch from 523da34 to 87b8f1a Compare July 27, 2026 16:20
JSONbored added a commit that referenced this pull request Jul 27, 2026
…hive (#9273)

Fifth sub-issue of #9267. Secondary, complementary to Rekor: appends one
JSONL line per anchor to a public repo via the GitHub Contents API and the
existing makeInstallationOctokit chokepoint -- never a direct fetch to the
GitHub API, matching every other GitHub write in this engine.

Alone this is weaker than Rekor (GitHub is a trusted third party, force-push
rewrites it). It becomes genuinely strong combined with mirrors nobody at
LoopOver controls: GH Archive's hourly PushEvent export and Software
Heritage's on-demand archival. A rewrite becomes independently detectable by
checking an archive this repo doesn't control -- documented as a runnable
cross-mirror verification procedure in the module's own header.

Commits the identical canonicalized payload + signature Rekor anchors, so
the two backends commit to the same fact, never a reshaped copy. Read-modify-
write via the file's own sha as a compare-and-swap guard; never throws past
the caller (missing repo, auth failure, rate limit, a raced sha all record
status:'failed' via #9271's persistence, same posture as the Rekor backend).

Also fixes a real bug found while building this: ledger-anchor.ts's re-export
statement only carried sha256Hex, dropping canonicalJson entirely -- any
consumer importing it (this backend needs it to commit the same payload Rekor
anchors) got undefined. Applied at the actual origin (#9270/PR #9392) and
rebased through the whole stack, not patched over downstream.
@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 27, 2026
loopover-orb Bot pushed a commit that referenced this pull request Jul 27, 2026
…hive (#9273) (#9403)

Fifth sub-issue of #9267. Secondary, complementary to Rekor: appends one
JSONL line per anchor to a public repo via the GitHub Contents API and the
existing makeInstallationOctokit chokepoint -- never a direct fetch to the
GitHub API, matching every other GitHub write in this engine.

Alone this is weaker than Rekor (GitHub is a trusted third party, force-push
rewrites it). It becomes genuinely strong combined with mirrors nobody at
LoopOver controls: GH Archive's hourly PushEvent export and Software
Heritage's on-demand archival. A rewrite becomes independently detectable by
checking an archive this repo doesn't control -- documented as a runnable
cross-mirror verification procedure in the module's own header.

Commits the identical canonicalized payload + signature Rekor anchors, so
the two backends commit to the same fact, never a reshaped copy. Read-modify-
write via the file's own sha as a compare-and-swap guard; never throws past
the caller (missing repo, auth failure, rate limit, a raced sha all record
status:'failed' via #9271's persistence, same posture as the Rekor backend).

Also fixes a real bug found while building this: ledger-anchor.ts's re-export
statement only carried sha256Hex, dropping canonicalJson entirely -- any
consumer importing it (this backend needs it to commit the same payload Rekor
anchors) got undefined. Applied at the actual origin (#9270/PR #9392) and
rebased through the whole stack, not patched over downstream.
…ilure alike (#9395)

* feat(ledger): anchor persistence + public attempt log, success and failure alike (#9271)

Third sub-issue of #9267. migrations/0195 adds decision_ledger_anchors: every
backend calls the same recordLedgerAnchorAttempt whether it succeeded or
failed — there is no separate failure path a backend could simply not call.

Per the mechanism research on #9267: anchoring is best-effort (it must never
block a review), which means a failure could otherwise be silent. An operator
whose anchoring quietly fails could regress the ledger back to
tamper-evident-only with no visible signal. Recording every attempt on the same
public listing, in the same shape regardless of outcome, is what makes
anchoring's own health a fact anyone can observe rather than something only
the operator's own logs show.

GET /v1/public/decision-ledger/anchors serves the log, paginated newest-first,
filterable by backend. backend_ref stores the full resolvable reference (for
Rekor: shard URL + log index + key id + uuid, not a bare index that becomes
unresolvable after the annual shard rotation).

* fix(db): add decision_ledger_anchors to RAW_SQL_ONLY_TABLES (#9271)

check-schema-drift caught this on push: the migration 0195 table was never
declared in src/db/schema.ts and wasn't in the raw-SQL-only allowlist either,
matching decision_ledger's own existing entry (the table this one is the
anchoring companion to).
@JSONbored
JSONbored merged commit 0a93e29 into main Jul 27, 2026
8 checks passed
@JSONbored
JSONbored deleted the feat/ledger-anchor-payload branch July 27, 2026 16:53
JSONbored added a commit that referenced this pull request Jul 27, 2026
…hive (#9273) (#9403)

Fifth sub-issue of #9267. Secondary, complementary to Rekor: appends one
JSONL line per anchor to a public repo via the GitHub Contents API and the
existing makeInstallationOctokit chokepoint -- never a direct fetch to the
GitHub API, matching every other GitHub write in this engine.

Alone this is weaker than Rekor (GitHub is a trusted third party, force-push
rewrites it). It becomes genuinely strong combined with mirrors nobody at
LoopOver controls: GH Archive's hourly PushEvent export and Software
Heritage's on-demand archival. A rewrite becomes independently detectable by
checking an archive this repo doesn't control -- documented as a runnable
cross-mirror verification procedure in the module's own header.

Commits the identical canonicalized payload + signature Rekor anchors, so
the two backends commit to the same fact, never a reshaped copy. Read-modify-
write via the file's own sha as a compare-and-swap guard; never throws past
the caller (missing repo, auth failure, rate limit, a raced sha all record
status:'failed' via #9271's persistence, same posture as the Rekor backend).

Also fixes a real bug found while building this: ledger-anchor.ts's re-export
statement only carried sha256Hex, dropping canonicalJson entirely -- any
consumer importing it (this backend needs it to commit the same payload Rekor
anchors) got undefined. Applied at the actual origin (#9270/PR #9392) and
rebased through the whole stack, not patched over downstream.
JSONbored added a commit that referenced this pull request Jul 27, 2026
* feat(ledger): Rekor v2 hashedrekord anchoring backend (#9272)

Fourth sub-issue of #9267. Primary anchoring mechanism per the mechanism
research: hashedrekord is exactly built for a plain hash + signature — Rekor
never sees the anchor payload itself. No Fulcio, no OIDC, no new dependency
(ECDSA P-256/SHA-256 is native WebCrypto, the same keypair #9270 already
produces). Shard URL is env-configurable with a documented fallback, never
hardcoded, per the research's explicit "shards roll annually" warning.

Every path — non-2xx response, an unparseable response body, a network
exception — records status:'failed' via #9271's persistence rather than
throwing past the caller, so #9273's git backend still gets attempted even if
this one fails.

Caught and fixed a real bug during testing: this module was pre-stringifying
its own error before handing it to recordLedgerAnchorAttempt, which then ran
errorMessage() a SECOND time on what was now a plain string — and errorMessage
only extracts .message from actual Error instances, so every failure path
collapsed to the generic "unknown error" fallback regardless of what actually
went wrong. Fixed by passing the raw unknown error through and teaching
persistence to handle both an already-built string and a genuine Error, so the
real diagnostic (a 429, a shape mismatch, "network down") reaches the public
listing instead of being silently discarded.

Storing the full Rekor TransparencyLogEntry (inclusion proof + signed
checkpoint) for fully offline verification is deliberately deferred — online
verification via rekor-cli against the stored shard URL + uuid works
completely without it today.

* feat(ledger): git-commit anchoring backend, cross-verified via GH Archive (#9273) (#9403)

Fifth sub-issue of #9267. Secondary, complementary to Rekor: appends one
JSONL line per anchor to a public repo via the GitHub Contents API and the
existing makeInstallationOctokit chokepoint -- never a direct fetch to the
GitHub API, matching every other GitHub write in this engine.

Alone this is weaker than Rekor (GitHub is a trusted third party, force-push
rewrites it). It becomes genuinely strong combined with mirrors nobody at
LoopOver controls: GH Archive's hourly PushEvent export and Software
Heritage's on-demand archival. A rewrite becomes independently detectable by
checking an archive this repo doesn't control -- documented as a runnable
cross-mirror verification procedure in the module's own header.

Commits the identical canonicalized payload + signature Rekor anchors, so
the two backends commit to the same fact, never a reshaped copy. Read-modify-
write via the file's own sha as a compare-and-swap guard; never throws past
the caller (missing repo, auth failure, rate limit, a raced sha all record
status:'failed' via #9271's persistence, same posture as the Rekor backend).

Also fixes a real bug found while building this: ledger-anchor.ts's re-export
statement only carried sha256Hex, dropping canonicalJson entirely -- any
consumer importing it (this backend needs it to commit the same payload Rekor
anchors) got undefined. Applied at the actual origin (#9270/PR #9392) and
rebased through the whole stack, not patched over downstream.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ledger: versioned + signed anchor payload, public key + rotation route

1 participant