From 5b33505ae5284fd9be39dd0a24e05cba76f94d12 Mon Sep 17 00:00:00 2001 From: Automation Date: Thu, 28 May 2026 15:03:08 +0200 Subject: [PATCH 1/3] fix(cli): allow nullable and optional apiKey in mitm start schema --- src/shared/validation/schemas.ts | 2 +- tests/unit/cli-mitm-schema.test.ts | 38 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/unit/cli-mitm-schema.test.ts diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 16e76e516ec5..6d9af29d93c5 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -1996,7 +1996,7 @@ export const v1betaGeminiGenerateSchema = z }); export const cliMitmStartSchema = z.object({ - apiKey: z.string().trim().min(1, "Missing apiKey"), + apiKey: z.string().trim().min(1).nullable().optional(), sudoPassword: z.string().optional(), }); diff --git a/tests/unit/cli-mitm-schema.test.ts b/tests/unit/cli-mitm-schema.test.ts new file mode 100644 index 000000000000..953fd5557ac6 --- /dev/null +++ b/tests/unit/cli-mitm-schema.test.ts @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { cliMitmStartSchema } from "../../src/shared/validation/schemas.ts"; +import { validateBody } from "../../src/shared/validation/helpers.ts"; + +test("cliMitmStartSchema accepts a non-empty string apiKey", () => { + const result = validateBody(cliMitmStartSchema, { + apiKey: "sk-test-key-value", + sudoPassword: "password123", + }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.apiKey, "sk-test-key-value"); + assert.equal(result.data.sudoPassword, "password123"); + } +}); + +test("cliMitmStartSchema accepts a null apiKey", () => { + const result = validateBody(cliMitmStartSchema, { + apiKey: null, + sudoPassword: "", + }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.apiKey, null); + assert.equal(result.data.sudoPassword, ""); + } +}); + +test("cliMitmStartSchema accepts an omitted apiKey", () => { + const result = validateBody(cliMitmStartSchema, { + sudoPassword: "", + }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.apiKey, undefined); + } +}); From afe3deb888ecad23ed97489ed1eeeffd4e213af9 Mon Sep 17 00:00:00 2001 From: Automation Date: Thu, 28 May 2026 16:00:07 +0200 Subject: [PATCH 2/3] fix(cli): add keyId explicitly to cliMitmStartSchema and include unit tests --- src/shared/validation/schemas.ts | 1 + tests/unit/cli-mitm-schema.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 6d9af29d93c5..8316329a947e 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -1997,6 +1997,7 @@ export const v1betaGeminiGenerateSchema = z export const cliMitmStartSchema = z.object({ apiKey: z.string().trim().min(1).nullable().optional(), + keyId: z.string().trim().min(1).nullable().optional(), sudoPassword: z.string().optional(), }); diff --git a/tests/unit/cli-mitm-schema.test.ts b/tests/unit/cli-mitm-schema.test.ts index 953fd5557ac6..c8c0306733d3 100644 --- a/tests/unit/cli-mitm-schema.test.ts +++ b/tests/unit/cli-mitm-schema.test.ts @@ -36,3 +36,26 @@ test("cliMitmStartSchema accepts an omitted apiKey", () => { assert.equal(result.data.apiKey, undefined); } }); + +test("cliMitmStartSchema accepts and parses keyId correctly", () => { + const result = validateBody(cliMitmStartSchema, { + keyId: "api-key-id-123", + sudoPassword: "password", + }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.keyId, "api-key-id-123"); + assert.equal(result.data.apiKey, undefined); + } +}); + +test("cliMitmStartSchema accepts null keyId", () => { + const result = validateBody(cliMitmStartSchema, { + keyId: null, + sudoPassword: "", + }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.keyId, null); + } +}); From da6e02f0989bfaad66af954f2f8162aef0a4282b Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 29 May 2026 01:32:37 -0300 Subject: [PATCH 3/3] fix(review): apply PR review suggestions for #2857 --- .../api/cli-tools/antigravity-mitm/route.ts | 18 +++++++------- tests/unit/cli-mitm-schema.test.ts | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/app/api/cli-tools/antigravity-mitm/route.ts b/src/app/api/cli-tools/antigravity-mitm/route.ts index f296d3141781..05b5940aba5b 100644 --- a/src/app/api/cli-tools/antigravity-mitm/route.ts +++ b/src/app/api/cli-tools/antigravity-mitm/route.ts @@ -56,21 +56,23 @@ export async function POST(request) { if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } - const { apiKey: rawApiKey, sudoPassword } = validation.data; - // (#523) Extract keyId BEFORE validation — Zod strips unknown fields! - const apiKeyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const { apiKey: rawApiKey, keyId: rawKeyId, sudoPassword } = validation.data; + const apiKeyId = rawKeyId ?? null; const apiKey = await resolveApiKey(apiKeyId, rawApiKey); + if (!apiKey || apiKey === "sk_omniroute") { + return NextResponse.json( + { error: "Missing apiKey: provide a valid apiKey or a resolvable keyId" }, + { status: 400 } + ); + } const { startMitm, getCachedPassword, setCachedPassword } = await import("@/mitm/manager.runtime"); const isWin = process.platform === "win32"; const isRootUser = !isWin && isRoot(); const pwd = sudoPassword || getCachedPassword() || ""; - if (!apiKey || (!isWin && !pwd && !isRootUser)) { - return NextResponse.json( - { error: isWin ? "Missing apiKey" : "Missing apiKey or sudoPassword" }, - { status: 400 } - ); + if (!isWin && !pwd && !isRootUser) { + return NextResponse.json({ error: "Missing sudoPassword" }, { status: 400 }); } const result = await startMitm(apiKey, pwd); diff --git a/tests/unit/cli-mitm-schema.test.ts b/tests/unit/cli-mitm-schema.test.ts index c8c0306733d3..7c723acfcde7 100644 --- a/tests/unit/cli-mitm-schema.test.ts +++ b/tests/unit/cli-mitm-schema.test.ts @@ -2,6 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { cliMitmStartSchema } from "../../src/shared/validation/schemas.ts"; import { validateBody } from "../../src/shared/validation/helpers.ts"; +import { resolveApiKey } from "../../src/shared/services/apiKeyResolver.ts"; test("cliMitmStartSchema accepts a non-empty string apiKey", () => { const result = validateBody(cliMitmStartSchema, { @@ -59,3 +60,26 @@ test("cliMitmStartSchema accepts null keyId", () => { assert.equal(result.data.keyId, null); } }); + +// Regression test: null apiKey + unresolvable keyId must yield the sentinel 'sk_omniroute', +// which the route guard must reject with a 400 rather than letting it pass to startMitm. +test("resolveApiKey returns sentinel when apiKey is null and keyId is null", async () => { + const result = await resolveApiKey(null, null); + assert.equal( + result, + "sk_omniroute", + "resolveApiKey should return the sentinel when no real key is available" + ); +}); + +test("sentinel guard condition catches sk_omniroute and null", () => { + const SENTINEL = "sk_omniroute"; + // Simulate what the route guard checks: (!apiKey || apiKey === 'sk_omniroute') + const shouldReject = (apiKey: string | null | undefined): boolean => + !apiKey || apiKey === SENTINEL; + + assert.equal(shouldReject(null), true, "null apiKey must be rejected"); + assert.equal(shouldReject(undefined), true, "undefined apiKey must be rejected"); + assert.equal(shouldReject("sk_omniroute"), true, "sentinel must be rejected"); + assert.equal(shouldReject("sk-real-key-abc"), false, "real key must be allowed"); +});