Skip to content

openapi(security): five credential-gated operations publish a 401 with no security scheme #9707

Description

@JSONbored

⚠️ 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

  • The five registry.registerPath blocks named in the Context table are removed from src/openapi/spec.ts and re-declared as SpecEntry rows in src/openapi/internal-and-public-route-specs.ts.
  • In the regenerated apps/loopover-ui/public/openapi.json, the four /v1/internal/* operations have security: [{ "LoopOverBearer": [] }] and POST /v1/decision-ledger/anchor-attempts has security: [{ "OrbBearer": [] }].
  • Every response code those five operations declare today is still declared after the move.
  • A new case in test/unit/openapi-security-parity.test.ts asserts the list of operations that declare a 401 response while having security === undefined is empty. This case must fail on current main.
  • apps/loopover-ui/public/openapi.json regenerated and committed; test/unit/route-spec-ratchet.test.ts still 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-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions