⚠️ 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. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
applySecurityMetadata (src/openapi/spec.ts:2444-2498) only fills in a stanza when requiresApiToken(path) is true. requiresApiToken deliberately returns false for /v1/internal/* (src/auth/route-auth.ts:79) and for /v1/decision-ledger/anchor-attempts (:57) — not because those are open, but because each carries its own credential check. The result is that legacy registerPath operations in those families end up with the security key entirely absent.
src/openapi/define-route.ts:78-88 states the distinction that makes this a defect: "[], not undefined: an empty security array is OpenAPI's explicit 'this operation needs no credential', where an ABSENT one means 'not stated'." These five operations are not "not stated" — they are bearer-gated, and each already publishes a 401 response describing the rejection of a caller who omits that bearer:
| Operation |
Registered at |
Real gate |
POST /v1/decision-ledger/anchor-attempts |
src/openapi/spec.ts:1922 |
isAuthorizedIngest(env.LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, ...) -> 401, src/api/routes.ts:1381 |
GET /v1/internal/provider-credentials/{provider} |
src/openapi/spec.ts:2267 |
/v1/internal/* middleware, src/api/routes.ts:1216-1220 |
POST /v1/internal/provider-credentials/{provider} |
src/openapi/spec.ts:2290 |
same |
DELETE /v1/internal/provider-credentials/{provider} |
src/openapi/spec.ts:2307 |
same |
POST /v1/internal/bounties/import |
src/openapi/spec.ts:2400 |
same |
A document that declares a 401 but no security scheme is self-contradictory: a generated client has no credential to send, and a reader cannot tell these apart from a genuinely public route.
test/unit/openapi-security-parity.test.ts cannot catch this. Its two directional assertions (:52-57, :59-64) both require operation.security to be present — an absent stanza satisfies neither filter, so the whole class is invisible to the suite.
Retiring applySecurityMetadata's guesswork in favour of declared auth levels was requirement 5 of #9531; these five are residue of that migration.
Requirements
- Register all five operations through
registerRouteSpec from src/openapi/internal-and-public-route-specs.ts, each carrying an explicit auth, and delete their registry.registerPath blocks from src/openapi/spec.ts.
- The four
/v1/internal/* operations declare auth: "internal" (emitting [{ LoopOverBearer: [] }]).
POST /v1/decision-ledger/anchor-attempts declares auth: "orb" (emitting [{ OrbBearer: [] }]) — it is gated by its own bearer (LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN), not a LoopOver API token, the same posture the orb level already exists for per src/openapi/define-route.ts:68-77.
- The migrated entries must preserve every response code the current blocks declare, including
POST /v1/internal/provider-credentials/{provider}'s 503 and POST /v1/decision-ledger/anchor-attempts's 413 and 422.
test/unit/openapi-security-parity.test.ts gains an assertion that no operation in the document has an absent security key when it declares a 401 response.
- Regenerate and commit
apps/loopover-ui/public/openapi.json.
⚠️ Required pattern: mirror the existing INTERNAL_OTHER / MISC_ROUTES entries in src/openapi/internal-and-public-route-specs.ts:136-344 — a SpecEntry with method, path, operationId, tags, summary, auth, responses. It does NOT satisfy this issue to patch applySecurityMetadata to guess a stanza for these paths (that is the second parallel model #9531 deleted); to add a document-level security default; or to leave the registerPath blocks in spec.ts and bolt a security field onto them by hand.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example moving the four internal operations but leaving anchor-attempts behind, or adding the new parity assertion without making it pass — does not resolve this issue.
Test Coverage Requirements
src/openapi/** is inside Codecov's src/** include; the 99% branch-counted patch gate applies. The moved entries are data consumed by registerRouteSpec, exercised by buildOpenApiSpec() in the security-parity and ratchet tests. The new "401 without a scheme" assertion is the named regression test for this fix and must assert an empty list of offenders, not merely that the five known paths are fixed.
Expected Outcome
No operation in the published document declares a 401 while stating no credential. A generated client for the provider-credential rotation surface and the anchor-report ingest surface knows which bearer to send.
Links & Resources
src/openapi/spec.ts:1922, :2267, :2290, :2307, :2400, :2444-2498; src/openapi/define-route.ts:68-88; src/auth/route-auth.ts:55-57, :79; src/api/routes.ts:1216-1220, :1380-1394; test/unit/openapi-security-parity.test.ts:52-64. Related closed work: #9531.
Context
applySecurityMetadata(src/openapi/spec.ts:2444-2498) only fills in a stanza whenrequiresApiToken(path)is true.requiresApiTokendeliberately returns false for/v1/internal/*(src/auth/route-auth.ts:79) and for/v1/decision-ledger/anchor-attempts(:57) — not because those are open, but because each carries its own credential check. The result is that legacyregisterPathoperations in those families end up with thesecuritykey entirely absent.src/openapi/define-route.ts:78-88states the distinction that makes this a defect: "[], not undefined: an empty security array is OpenAPI's explicit 'this operation needs no credential', where an ABSENT one means 'not stated'." These five operations are not "not stated" — they are bearer-gated, and each already publishes a401response describing the rejection of a caller who omits that bearer:POST /v1/decision-ledger/anchor-attemptssrc/openapi/spec.ts:1922isAuthorizedIngest(env.LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN, ...)-> 401,src/api/routes.ts:1381GET /v1/internal/provider-credentials/{provider}src/openapi/spec.ts:2267/v1/internal/*middleware,src/api/routes.ts:1216-1220POST /v1/internal/provider-credentials/{provider}src/openapi/spec.ts:2290DELETE /v1/internal/provider-credentials/{provider}src/openapi/spec.ts:2307POST /v1/internal/bounties/importsrc/openapi/spec.ts:2400A document that declares a 401 but no security scheme is self-contradictory: a generated client has no credential to send, and a reader cannot tell these apart from a genuinely public route.
test/unit/openapi-security-parity.test.tscannot catch this. Its two directional assertions (:52-57,:59-64) both requireoperation.securityto be present — an absent stanza satisfies neither filter, so the whole class is invisible to the suite.Retiring
applySecurityMetadata's guesswork in favour of declaredauthlevels was requirement 5 of #9531; these five are residue of that migration.Requirements
registerRouteSpecfromsrc/openapi/internal-and-public-route-specs.ts, each carrying an explicitauth, and delete theirregistry.registerPathblocks fromsrc/openapi/spec.ts./v1/internal/*operations declareauth: "internal"(emitting[{ LoopOverBearer: [] }]).POST /v1/decision-ledger/anchor-attemptsdeclaresauth: "orb"(emitting[{ OrbBearer: [] }]) — it is gated by its own bearer (LOOPOVER_LEDGER_ANCHOR_REPORT_TOKEN), not a LoopOver API token, the same posture theorblevel already exists for persrc/openapi/define-route.ts:68-77.POST /v1/internal/provider-credentials/{provider}'s503andPOST /v1/decision-ledger/anchor-attempts's413and422.test/unit/openapi-security-parity.test.tsgains an assertion that no operation in the document has an absentsecuritykey when it declares a401response.apps/loopover-ui/public/openapi.json.Deliverables
registry.registerPathblocks named in the Context table are removed fromsrc/openapi/spec.tsand re-declared asSpecEntryrows insrc/openapi/internal-and-public-route-specs.ts.apps/loopover-ui/public/openapi.json, the four/v1/internal/*operations havesecurity: [{ "LoopOverBearer": [] }]andPOST /v1/decision-ledger/anchor-attemptshassecurity: [{ "OrbBearer": [] }].test/unit/openapi-security-parity.test.tsasserts the list of operations that declare a401response while havingsecurity === undefinedis empty. This case must fail on currentmain.apps/loopover-ui/public/openapi.jsonregenerated and committed;test/unit/route-spec-ratchet.test.tsstill passes in both directions.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example moving the four internal operations but leaving
anchor-attemptsbehind, or adding the new parity assertion without making it pass — does not resolve this issue.Test Coverage Requirements
src/openapi/**is inside Codecov'ssrc/**include; the 99% branch-counted patch gate applies. The moved entries are data consumed byregisterRouteSpec, exercised bybuildOpenApiSpec()in the security-parity and ratchet tests. The new "401 without a scheme" assertion is the named regression test for this fix and must assert an empty list of offenders, not merely that the five known paths are fixed.Expected Outcome
No operation in the published document declares a 401 while stating no credential. A generated client for the provider-credential rotation surface and the anchor-report ingest surface knows which bearer to send.
Links & Resources
src/openapi/spec.ts:1922,:2267,:2290,:2307,:2400,:2444-2498;src/openapi/define-route.ts:68-88;src/auth/route-auth.ts:55-57,:79;src/api/routes.ts:1216-1220,:1380-1394;test/unit/openapi-security-parity.test.ts:52-64. Related closed work: #9531.