⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue.
Problem
GET /v1/public/decision-ledger/anchor-key returns {"keys":[],"currentKeyId":null} on loopover.ai today.
That response is indistinguishable across five different causes — verified by running
parseAnchorPublicKeys (src/review/ledger-anchor.ts) over each:
| Cause |
Response |
| secret never set |
{"keys":[],"currentKeyId":null} |
| set to malformed JSON |
{"keys":[],"currentKeyId":null} |
| set to an object instead of an array |
{"keys":[],"currentKeyId":null} |
set with a typo'd field (keyid vs keyId) |
{"keys":[],"currentKeyId":null} |
| set correctly but every key expired |
{"keys":[…],"currentKeyId":null} |
parseAnchorPublicKeys returns [] from four separate paths (!raw, a thrown JSON.parse, a non-array,
and a .filter that silently drops every entry) with no diagnostic anywhere. currentAnchorKey then folds
two more states together: it returns null both when NO key is current and when MORE THAN ONE is — its own
doc comment documents the ambiguous-rotation case as deliberate fail-closed behaviour, but nothing reports
which of the two happened.
The consequence is concrete. #9719 provisioned the anchor keypair and was closed. Whether that
provisioning actually took effect on the hosted Worker cannot be determined — not from outside, and not by
the operator either, because a typo in the secret looks exactly like an unset secret.
The status field does not help. publicAnchorStatus checks tipSeq === 0 before hasSigningKey, so with an
empty ledger it reports empty_ledger whether or not a key is configured:
status with empty ledger + NO key : empty_ledger
status with empty ledger + key : empty_ledger
That guard order is deliberate (its comment says it mirrors runScheduledLedgerAnchor's own so a reader
never gets "a second opinion") and should not change. The gap is that /anchor-key never got the treatment
its sibling did.
The precedent
PublicAnchorStatus exists in this same file for exactly this reason. Its doc comment:
Without this, "never configured", "no ledger to anchor", and "anchoring is healthy but has not run yet"
are all indistinguishable from outside — every one of them renders as {"anchors":[]}, which reads as a
healthy empty state.
That reasoning was applied to /anchors and not to /anchor-key, which still has the identical ambiguity.
Requirements
- A PURE diagnosis function classifying the raw env value into a discriminated status, alongside the parsed
keys. parseAnchorPublicKeys's existing signature and behaviour must not change — it has other callers
(ledger-anchor-scheduler.ts, the route) and the scheduler's guard order must stay as-is.
- Statuses must separate, at minimum: unconfigured, malformed (unparseable or non-array), every entry
invalid, valid-but-none-current (expired), more-than-one-current (ambiguous rotation), and ok.
- Entries dropped by validation must be COUNTED and surfaced even when the overall status is
ok, so one
typo'd key among three is visible rather than silently ignored.
- The raw value must never be echoed back, in any status. An operator who mis-pastes
LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY into the public LOOPOVER_LEDGER_ANCHOR_KEYS var would otherwise have
the private half published by an unauthenticated endpoint. Only a classification may be returned.
keys and currentKeyId must keep their current shape and meaning — this is additive, not a rewrite.
Deliverables
Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on src/**. Every status arm needs a test.
Expected Outcome
curl -s https://api.loopover.ai/v1/public/decision-ledger/anchor-key | jq .status answers "is anchoring
configured on this deployment, and if not, why" — for an anonymous reader and for the operator, without
publishing key material.
Links & Resources
src/review/ledger-anchor.ts (parseAnchorPublicKeys, currentAnchorKey, publicAnchorStatus),
src/api/routes.ts (the /v1/public/decision-ledger/anchor-key handler),
src/review/ledger-anchor-scheduler.ts. Related: #9719, #9271, #9267.
Problem
GET /v1/public/decision-ledger/anchor-keyreturns{"keys":[],"currentKeyId":null}on loopover.ai today.That response is indistinguishable across five different causes — verified by running
parseAnchorPublicKeys(src/review/ledger-anchor.ts) over each:{"keys":[],"currentKeyId":null}{"keys":[],"currentKeyId":null}{"keys":[],"currentKeyId":null}keyidvskeyId){"keys":[],"currentKeyId":null}{"keys":[…],"currentKeyId":null}parseAnchorPublicKeysreturns[]from four separate paths (!raw, a thrownJSON.parse, a non-array,and a
.filterthat silently drops every entry) with no diagnostic anywhere.currentAnchorKeythen foldstwo more states together: it returns
nullboth when NO key is current and when MORE THAN ONE is — its owndoc comment documents the ambiguous-rotation case as deliberate fail-closed behaviour, but nothing reports
which of the two happened.
The consequence is concrete. #9719 provisioned the anchor keypair and was closed. Whether that
provisioning actually took effect on the hosted Worker cannot be determined — not from outside, and not by
the operator either, because a typo in the secret looks exactly like an unset secret.
The status field does not help.
publicAnchorStatuscheckstipSeq === 0beforehasSigningKey, so with anempty ledger it reports
empty_ledgerwhether or not a key is configured:That guard order is deliberate (its comment says it mirrors
runScheduledLedgerAnchor's own so a readernever gets "a second opinion") and should not change. The gap is that
/anchor-keynever got the treatmentits sibling did.
The precedent
PublicAnchorStatusexists in this same file for exactly this reason. Its doc comment:That reasoning was applied to
/anchorsand not to/anchor-key, which still has the identical ambiguity.Requirements
keys.
parseAnchorPublicKeys's existing signature and behaviour must not change — it has other callers(
ledger-anchor-scheduler.ts, the route) and the scheduler's guard order must stay as-is.invalid, valid-but-none-current (expired), more-than-one-current (ambiguous rotation), and ok.
ok, so onetypo'd key among three is visible rather than silently ignored.
LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEYinto the publicLOOPOVER_LEDGER_ANCHOR_KEYSvar would otherwise havethe private half published by an unauthenticated endpoint. Only a classification may be returned.
keysandcurrentKeyIdmust keep their current shape and meaning — this is additive, not a rewrite.Deliverables
currentAnchorKeycollapse(zero current keys vs. more than one).
private-key-shaped probe assembled from fragments (the convention
forbidden-content.test.tsuses).droppedEntriesis non-zero, and status stillok, for one valid + one typo'd entry.keys/currentKeyIdunchanged for existing consumers.parseAnchorPublicKeysbehaviour unchanged, asserted.Test Coverage Requirements
99%+ Codecov patch coverage, branch-counted, on
src/**. Every status arm needs a test.Expected Outcome
curl -s https://api.loopover.ai/v1/public/decision-ledger/anchor-key | jq .statusanswers "is anchoringconfigured on this deployment, and if not, why" — for an anonymous reader and for the operator, without
publishing key material.
Links & Resources
src/review/ledger-anchor.ts(parseAnchorPublicKeys,currentAnchorKey,publicAnchorStatus),src/api/routes.ts(the/v1/public/decision-ledger/anchor-keyhandler),src/review/ledger-anchor-scheduler.ts. Related: #9719, #9271, #9267.