feat(ledger): anchor persistence + public attempt log, success and failure alike - #9395
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 67ea1fe | Commit Preview URL Branch Preview URL |
Jul 27 2026, 04:24 PM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-27 16:44:35 UTC
Review summary Nits — 7 non-blocking
Decision 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.
|
320257a to
d61f18f
Compare
523da34 to
87b8f1a
Compare
…ilure 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).
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).
d61f18f to
67ea1fe
Compare
Bundle ReportChanges will increase total bundle size by 2.11kB (0.03%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feat/ledger-anchor-payload #9395 +/- ##
==============================================================
+ Coverage 88.59% 89.50% +0.90%
==============================================================
Files 839 840 +1
Lines 109729 109770 +41
Branches 26127 26141 +14
==============================================================
+ Hits 97216 98248 +1032
+ Misses 11541 10259 -1282
- Partials 972 1263 +291
Flags with carried forward coverage won't be shown. Click here to find out more.
|
fa8c60e
into
feat/ledger-anchor-payload
…on route (#9392) * feat(ledger): versioned + signed anchor payload, published key rotation 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. * fix(ledger): re-export canonicalJson from ledger-anchor.ts (#9270) 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. * feat(ledger): anchor persistence + public attempt log, success and failure 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).


Summary
Third sub-issue of #9267 (external ledger anchoring), stacked on #9270 (#9392).
migrations/0195addsdecision_ledger_anchors: every backend (#9272 Rekor, #9273 git) calls the samerecordLedgerAnchorAttemptwhether it succeeded or failed — there is no separate failure path a backend could simply not call.Why this matters, 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, not something only the operator's own logs show.
GET /v1/public/decision-ledger/anchorsserves the log, paginated newest-first (?before=cursor,?limit=), filterable by?backend=.backend_refstores the full resolvable reference — for Rekor, shard base URL + log index + key id + uuid, not a bare index that becomes unresolvable after the annual shard rotation.One bug caught and fixed during testing:
recordLedgerAnchorAttemptoriginally callednowIso()internally with no way to override it, so several inserts in a tight loop could land on the same millisecond and sort arbitrarily. MadecreatedAtinjectable, matchingloadPublicRulePrecision's existing pattern for exactly this class of testability.Closes #9271.
Test plan
npx vitest run test/unit/ledger-anchor-persistence.test.ts— 8/8, 100% statement/branch/function/line coveragenextBefore: nulllimitclamped to[1, 200]; malformedbackend_refdegrades tonullrather than throwingnpx vitest run test/integration/public-ledger-anchors-route.test.ts— 6/6: no-auth-required, failure served on the public route,?backend=/?before=/?limit=query handling, invalid backend value falls back to unfiltered rather than erroringnpm run db:migrations:check— 199 migrations OK, contiguousnpx tsc --noEmitclean (including anexactOptionalPropertyTypesfix in the route handler) ·npm run ui:openapi:checkclean