⚠️ 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
Four BYOK key-write routes catch missing_encryption_secret and answer 503 encryption_unavailable: POST /v1/repos/:owner/:repo/ai-key (src/api/routes.ts:3467-3469), POST /v1/repos/:owner/:repo/linear-key (:3502-3504), POST /v1/internal/repos/:owner/:repo/ai-key (:5392-5394), POST /v1/internal/repos/:owner/:repo/linear-key (:5465-5467).
None of the four declares 503. The asymmetry is exact and provable: the fifth route with the identical catch, POST /v1/internal/provider-credentials/{provider} (:5432-5434), does publish 503 in the committed apps/loopover-ui/public/openapi.json. An operator who has not configured TOKEN_ENCRYPTION_SECRET gets an undocumented status from four routes and a documented one from the fifth.
In the other direction, getAiKey and getLinearKey (src/openapi/orb-and-control-route-specs.ts:251-259 and the linear-key arm of the same flatMap) declare 404: "Repo not registered". No code path can produce it. The handlers (src/api/routes.ts:3449-3454 and :3484-3489) call requireRepoWriteAccess and then return getRepositoryAiKeyStatus / getRepositoryLinearKeyStatus unconditionally. requireRepoWriteAccess (:6814-6835) -> requireRepoMaintainer (:6792-6802) -> requireSessionRepoAccess (:6774-6788) can return only 401, 403 forbidden_repo, or 403 insufficient_repo_permission — never 404. A repo with no row simply reads back { configured: false }.
Requirements
- Add
503 (description naming encryption_unavailable / an unconfigured TOKEN_ENCRYPTION_SECRET) to the set{Ai,Linear}Key and set{Ai,Linear}KeyInternal entries in src/openapi/orb-and-control-route-specs.ts.
- Remove the
404 from the get{Ai,Linear}Key entries — it is unreachable.
- Do not change any handler behaviour. The 503 stays a 503 and the GETs keep returning
{ configured: false } for an unregistered repo.
- Regenerate and commit
apps/loopover-ui/public/openapi.json.
⚠️ Required pattern: match the 503 description style already used for POST /v1/internal/provider-credentials/{provider} in src/openapi/spec.ts:2290-2305, and keep the per-kind flatMap in src/openapi/orb-and-control-route-specs.ts:247-306 as the single place the four key routes are declared — the pair must stay generated from one table so ai-key and linear-key cannot drift. It does NOT satisfy this issue to unroll the flatMap into six literal entries; to add the 503 to only the two maintainer-facing routes and not the two internal ones; or to "fix" the unreachable 404 by adding a 404 branch to the GET handlers.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the 503 to the spec without the failing-before/passing-after 503 handler test, or removing the 404 without the test that proves the GET answers 200 for an unregistered repo — does not resolve this issue.
Test Coverage Requirements
src/api/** and src/openapi/** are inside Codecov's src/** include; the 99% branch-counted patch gate applies. Both arms of the error.message === "missing_encryption_secret" catch must be covered: the 503 path and the re-throw path for any other error. Extend the existing test/unit/routes-ai-byok.test.ts rather than creating a parallel file. The 503 assertions are the named regression tests for this fix.
Expected Outcome
The published contract for the BYOK key surface matches the handlers in both directions: the 503 an operator actually hits when key storage is unconfigured is documented, and the 404 no caller can ever receive is gone.
Links & Resources
src/api/routes.ts:3449-3514, :5359-5478, :6774-6835; src/openapi/orb-and-control-route-specs.ts:244-306; src/openapi/spec.ts:2290-2305; test/unit/routes-ai-byok.test.ts.
Context
Four BYOK key-write routes catch
missing_encryption_secretand answer 503encryption_unavailable:POST /v1/repos/:owner/:repo/ai-key(src/api/routes.ts:3467-3469),POST /v1/repos/:owner/:repo/linear-key(:3502-3504),POST /v1/internal/repos/:owner/:repo/ai-key(:5392-5394),POST /v1/internal/repos/:owner/:repo/linear-key(:5465-5467).None of the four declares 503. The asymmetry is exact and provable: the fifth route with the identical catch,
POST /v1/internal/provider-credentials/{provider}(:5432-5434), does publish503in the committedapps/loopover-ui/public/openapi.json. An operator who has not configuredTOKEN_ENCRYPTION_SECRETgets an undocumented status from four routes and a documented one from the fifth.In the other direction,
getAiKeyandgetLinearKey(src/openapi/orb-and-control-route-specs.ts:251-259and thelinear-keyarm of the sameflatMap) declare404: "Repo not registered". No code path can produce it. The handlers (src/api/routes.ts:3449-3454and:3484-3489) callrequireRepoWriteAccessand then returngetRepositoryAiKeyStatus/getRepositoryLinearKeyStatusunconditionally.requireRepoWriteAccess(:6814-6835) ->requireRepoMaintainer(:6792-6802) ->requireSessionRepoAccess(:6774-6788) can return only401,403 forbidden_repo, or403 insufficient_repo_permission— never 404. A repo with no row simply reads back{ configured: false }.Requirements
503(description namingencryption_unavailable/ an unconfiguredTOKEN_ENCRYPTION_SECRET) to theset{Ai,Linear}Keyandset{Ai,Linear}KeyInternalentries insrc/openapi/orb-and-control-route-specs.ts.404from theget{Ai,Linear}Keyentries — it is unreachable.{ configured: false }for an unregistered repo.apps/loopover-ui/public/openapi.json.Deliverables
apps/loopover-ui/public/openapi.json,POST /v1/repos/{owner}/{repo}/ai-key,POST /v1/repos/{owner}/{repo}/linear-key,POST /v1/internal/repos/{owner}/{repo}/ai-key, andPOST /v1/internal/repos/{owner}/{repo}/linear-keyeach declare a503response.GET /v1/repos/{owner}/{repo}/ai-keyandGET /v1/repos/{owner}/{repo}/linear-keyno longer declare404.POSTto/v1/repos/:owner/:repo/ai-keywithTOKEN_ENCRYPTION_SECRETunset answers503with{ error: "encryption_unavailable" }, and that503is declared for that operation inbuildOpenApiSpec().POST /v1/internal/repos/:owner/:repo/linear-key.GET /v1/repos/:owner/:repo/ai-keyfor a repo with no stored key answers200withconfigured: false(proving the removed 404 is unreachable).apps/loopover-ui/public/openapi.jsonregenerated and committed.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the 503 to the spec without the failing-before/passing-after 503 handler test, or removing the 404 without the test that proves the GET answers 200 for an unregistered repo — does not resolve this issue.
Test Coverage Requirements
src/api/**andsrc/openapi/**are inside Codecov'ssrc/**include; the 99% branch-counted patch gate applies. Both arms of theerror.message === "missing_encryption_secret"catch must be covered: the 503 path and the re-throw path for any other error. Extend the existingtest/unit/routes-ai-byok.test.tsrather than creating a parallel file. The 503 assertions are the named regression tests for this fix.Expected Outcome
The published contract for the BYOK key surface matches the handlers in both directions: the 503 an operator actually hits when key storage is unconfigured is documented, and the 404 no caller can ever receive is gone.
Links & Resources
src/api/routes.ts:3449-3514,:5359-5478,:6774-6835;src/openapi/orb-and-control-route-specs.ts:244-306;src/openapi/spec.ts:2290-2305;test/unit/routes-ai-byok.test.ts.