diff --git a/server/routes/ops/skills-sh/mirror-test.post.ts b/server/routes/ops/skills-sh/mirror-test.post.ts index 80b39ad4..37d10b88 100644 --- a/server/routes/ops/skills-sh/mirror-test.post.ts +++ b/server/routes/ops/skills-sh/mirror-test.post.ts @@ -1,5 +1,6 @@ import { getVercelOidcToken } from "@vercel/oidc"; import { defineEventHandler, getHeader, readBody } from "h3"; +import { resolveConvexProxyEnv } from "../../../convexProxy"; import { buildSkillsShMirrorProofSnapshotId, fetchSkillsShMirrorBatch, @@ -191,7 +192,7 @@ export function createSkillsShMirrorRoute(target: keyof typeof ROUTE_CONFIG) { return defineEventHandler(async (event) => { const policy = target === "production" - ? getSkillsShCatalogProductionSourcePolicy(process.env) + ? getSkillsShCatalogProductionSourcePolicy(resolveConvexProxyEnv(process.env)) : getSkillsShCatalogTestSourcePolicy(process.env); if (!policy.allowed) return jsonResponse({ error: "not_found" }, 404); const authorization = getHeader(event, "authorization")?.trim() ?? ""; diff --git a/server/skillsShMirrorProductionRoute.test.ts b/server/skillsShMirrorProductionRoute.test.ts index 744c96dd..873b7065 100644 --- a/server/skillsShMirrorProductionRoute.test.ts +++ b/server/skillsShMirrorProductionRoute.test.ts @@ -9,6 +9,7 @@ const buildProofSnapshotIdMock = vi.fn(); const measureProofSourceMock = vi.fn(); const parseProofSnapshotIdMock = vi.fn(); const productionPolicyMock = vi.fn(); +const resolveConvexProxyEnvMock = vi.fn(); vi.mock("h3", () => ({ defineEventHandler: (handler: unknown) => handler, @@ -20,6 +21,10 @@ vi.mock("@vercel/oidc", () => ({ getVercelOidcToken: (...args: unknown[]) => getVercelOidcTokenMock(...args), })); +vi.mock("./convexProxy", () => ({ + resolveConvexProxyEnv: (...args: unknown[]) => resolveConvexProxyEnvMock(...args), +})); + vi.mock("./skillsShCatalogSource", () => ({ buildSkillsShMirrorProofSnapshotId: (...args: unknown[]) => buildProofSnapshotIdMock(...args), fetchSkillsShMirrorBatch: vi.fn(), @@ -47,6 +52,8 @@ describe("skills.sh production mirror route", () => { measureProofSourceMock.mockReset(); parseProofSnapshotIdMock.mockReset(); productionPolicyMock.mockReset(); + resolveConvexProxyEnvMock.mockReset(); + resolveConvexProxyEnvMock.mockImplementation((env) => env); productionPolicyMock.mockReturnValue({ allowed: true, environment: "production" }); getHeaderMock.mockReturnValue("Bearer github-actions-oidc"); getVercelOidcTokenMock.mockResolvedValue("vercel-production-oidc"); @@ -81,6 +88,27 @@ describe("skills.sh production mirror route", () => { expect(convexFetch).toHaveBeenCalledOnce(); }); + it("validates production source policy against bundled frontend identity", async () => { + const resolvedEnv = { + VERCEL_ENV: "production", + VITE_CLAWHUB_DEPLOY_ENV: "production", + VITE_CONVEX_URL: "https://wry-manatee-359.convex.cloud", + }; + resolveConvexProxyEnvMock.mockReturnValue(resolvedEnv); + readBodyMock.mockResolvedValue({ operation: "status" }); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(JSON.stringify({ control: null, runs: [] }))), + ); + + const handler = (await import("./routes/ops/skills-sh/mirror.post")).default; + const response = (await handler({} as never)) as Response; + + expect(response.status).toBe(200); + expect(resolveConvexProxyEnvMock).toHaveBeenCalledWith(process.env); + expect(productionPolicyMock).toHaveBeenCalledWith(resolvedEnv); + }); + it("fails closed before the operator call when the production source policy is not exact", async () => { productionPolicyMock.mockReturnValue({ allowed: false,