feat(ledger): versioned + signed anchor payload, published key rotation route - #9392
Conversation
Deploying with
|
| 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 |
5de7c63 to
9764ba7
Compare
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Bundle ReportChanges will increase total bundle size by 3.31kB (0.04%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-27 16:40:26 UTC
Review summary Nits — 5 non-blocking
Concerns raised — review before merging
📋 Copy for AI agents — paste into your coding agentDecision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk 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.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. Scroll preview
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.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more.
|
…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.
523da34 to
87b8f1a
Compare
…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.
…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).
…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.
* 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.


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.vschema version +ledgerid, plustotalCountriding alongsideseqso a truncate-and-rechain to the same length still has to match both.hashedrekord(ledger: Rekor v2 hashedrekord anchoring backend #9272) takes as a self-managed verifier key.GET /v1/public/decision-ledger/anchor-keyserves the full history, since an anchor signed under a retired key must stay verifiable forever.keyIdis 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 tocurrentKeyId: nullrather than guessing.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 coveragerowHash,seq, ortotalCountafter signing independently breaks verificationkeyId) fails verification against the real published keynpx 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 misconfigurednpx tsc --noEmitclean ·npm run ui:openapi:checkclean ·npm run cf-typegen:check— pre-existing local-workerd-version staleness confirmed on cleanorigin/maintoo (I touched neitherwrangler.jsoncnorworker-configuration.d.ts), left alone rather than risk committing a local artifact CI doesn't produce