Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -24419,9 +24419,6 @@
},
"403": {
"description": "Insufficient control-panel role"
},
"404": {
"description": "Repo not registered"
}
},
"parameters": [
Expand Down Expand Up @@ -24470,6 +24467,9 @@
},
"403": {
"description": "Insufficient control-panel role"
},
"503": {
"description": "encryption_unavailable — TOKEN_ENCRYPTION_SECRET is not configured"
}
},
"parameters": [
Expand Down Expand Up @@ -24598,6 +24598,9 @@
},
"401": {
"description": "Invalid internal token"
},
"503": {
"description": "encryption_unavailable — TOKEN_ENCRYPTION_SECRET is not configured"
}
},
"parameters": [
Expand Down Expand Up @@ -24684,9 +24687,6 @@
},
"403": {
"description": "Insufficient control-panel role"
},
"404": {
"description": "Repo not registered"
}
},
"parameters": [
Expand Down Expand Up @@ -24735,6 +24735,9 @@
},
"403": {
"description": "Insufficient control-panel role"
},
"503": {
"description": "encryption_unavailable — TOKEN_ENCRYPTION_SECRET is not configured"
}
},
"parameters": [
Expand Down Expand Up @@ -24863,6 +24866,9 @@
},
"401": {
"description": "Invalid internal token"
},
"503": {
"description": "encryption_unavailable — TOKEN_ENCRYPTION_SECRET is not configured"
}
},
"parameters": [
Expand Down
9 changes: 6 additions & 3 deletions src/openapi/orb-and-control-route-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ const REPO_ROUTES: SpecEntry[] = [
tags: ["Repositories", "Bring your own key"],
summary: `Report whether a ${label} key is configured for this repo (never the key itself)`,
auth: "session",
responses: { 200: { description: "Key presence and metadata" }, 404: { description: "Repo not registered" }, ...SESSION_AUTH_RESPONSES },
// No 404 (#9709): the GET returns { configured: false } for an unregistered repo, never a 404.
responses: { 200: { description: "Key presence and metadata" }, ...SESSION_AUTH_RESPONSES },
},
{
method: "post",
Expand All @@ -264,7 +265,8 @@ const REPO_ROUTES: SpecEntry[] = [
tags: ["Repositories", "Bring your own key"],
summary: `Store a ${label} key for this repo`,
auth: "session",
responses: { 200: { description: "Key stored" }, 400: { description: "Malformed key" }, ...SESSION_AUTH_RESPONSES },
// 503 (#9709): the handler answers encryption_unavailable when TOKEN_ENCRYPTION_SECRET is unconfigured.
responses: { 200: { description: "Key stored" }, 400: { description: "Malformed key" }, 503: { description: "encryption_unavailable — TOKEN_ENCRYPTION_SECRET is not configured" }, ...SESSION_AUTH_RESPONSES },
},
{
method: "delete",
Expand All @@ -291,7 +293,8 @@ const REPO_ROUTES: SpecEntry[] = [
tags: ["Bring your own key", "Internal"],
summary: `Store this repo's ${label} key from the control plane`,
auth: "internal",
responses: { 200: { description: "Key stored" }, 400: { description: "Malformed key" }, ...INTERNAL_AUTH_RESPONSES },
// 503 (#9709): mirrors the public setter — encryption_unavailable when TOKEN_ENCRYPTION_SECRET is unset.
responses: { 200: { description: "Key stored" }, 400: { description: "Malformed key" }, 503: { description: "encryption_unavailable — TOKEN_ENCRYPTION_SECRET is not configured" }, ...INTERNAL_AUTH_RESPONSES },
},
{
method: "delete",
Expand Down
35 changes: 35 additions & 0 deletions test/unit/routes-ai-byok.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createApp } from "../../src/api/routes";
import { buildOpenApiSpec } from "../../src/openapi/spec";
import { createSessionForGitHubUser } from "../../src/auth/security";
import { getRepositorySettings, upsertInstallation, upsertPullRequestFromGitHub, upsertRepositorySettings, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { getRepositoryCollaboratorPermission } from "../../src/github/app";
Expand Down Expand Up @@ -225,6 +226,40 @@ describe("maintainer BYOK key route", () => {
expect(res.status).toBe(503);
expect(await res.json()).toMatchObject({ error: "encryption_unavailable" });
});

describe("the spec declares the statuses the key routes actually return (#9709)", () => {
it("POST /v1/repos/:owner/:repo/ai-key answers 503 without a secret AND declares 503 in the spec", async () => {
const app = createApp();
const env = createTestEnv({});
const res = await app.request(`/v1/repos/${REPO}/ai-key`, { method: "POST", headers: apiHeaders(env), body: JSON.stringify({ provider: "openai", key: "sk-openai-valid-key-123456" }) }, env);
expect(res.status).toBe(503);
expect(await res.json()).toMatchObject({ error: "encryption_unavailable" });
const spec = buildOpenApiSpec();
expect(Object.keys(spec.paths["/v1/repos/{owner}/{repo}/ai-key"]?.post?.responses ?? {})).toContain("503");
});

it("POST /v1/internal/repos/:owner/:repo/linear-key answers 503 without a secret AND declares 503 in the spec", async () => {
const app = createApp();
const env = createTestEnv({});
const internalHeaders = { authorization: `Bearer ${env.INTERNAL_JOB_TOKEN}`, "content-type": "application/json" };
const res = await app.request(`/v1/internal/repos/${REPO}/linear-key`, { method: "POST", headers: internalHeaders, body: JSON.stringify({ key: "lin_api_valid_linear_key_1234567890" }) }, env);
expect(res.status).toBe(503);
expect(await res.json()).toMatchObject({ error: "encryption_unavailable" });
const spec = buildOpenApiSpec();
expect(Object.keys(spec.paths["/v1/internal/repos/{owner}/{repo}/linear-key"]?.post?.responses ?? {})).toContain("503");
});

it("GET /v1/repos/:owner/:repo/ai-key answers 200 { configured: false } for a repo with no key — proving the removed 404 was unreachable", async () => {
const app = createApp();
const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET });
const res = await app.request(`/v1/repos/${REPO}/ai-key`, { headers: apiHeaders(env) }, env);
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({ configured: false });
// And the spec no longer claims a 404 for that GET.
const spec = buildOpenApiSpec();
expect(Object.keys(spec.paths["/v1/repos/{owner}/{repo}/ai-key"]?.get?.responses ?? {})).not.toContain("404");
});
});
});

describe("maintainer route authz (session-scoped)", () => {
Expand Down
Loading