From 590690891d8c59fd4c925957537d04f3b2ab1346 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 9 Aug 2026 21:24:14 -0400 Subject: [PATCH 1/4] fix(security): fail closed on missing production registry token --- src/app/api/v1/registry/register/route.ts | 35 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/app/api/v1/registry/register/route.ts b/src/app/api/v1/registry/register/route.ts index 24255e8..c2c7ee8 100644 --- a/src/app/api/v1/registry/register/route.ts +++ b/src/app/api/v1/registry/register/route.ts @@ -8,7 +8,8 @@ * * Auth: when CAPI_REGISTRY_TOKEN is set, a matching `Authorization: Bearer …` * is required; the registration is flagged `authenticated`. When the token is - * unset (local/dev), the call is accepted but flagged `authenticated: false`. + * unset, unauthenticated registration is permitted only outside production. + * Production fails closed when registry authentication is not configured. */ import { NextResponse } from "next/server"; @@ -30,6 +31,12 @@ type CapabilityEntry = requires_approval?: boolean; }; +type AuthCheck = { + ok: boolean; + authenticated: boolean; + configurationError: boolean; +}; + function normalizeCapabilities(entries: CapabilityEntry[] | undefined): RegisteredCapability[] { if (!entries) return []; return entries.map((entry) => @@ -37,22 +44,36 @@ function normalizeCapabilities(entries: CapabilityEntry[] | undefined): Register ); } -function checkAuth(request: Request): { ok: boolean; authenticated: boolean } { +function checkAuth(request: Request): AuthCheck { const expected = process.env.CAPI_REGISTRY_TOKEN?.trim(); const header = request.headers.get("authorization")?.trim() ?? ""; const presented = header.toLowerCase().startsWith("bearer ") ? header.slice(7).trim() : header; + if (!expected) { - // No token configured — accept but mark unauthenticated (dev posture). - return { ok: true, authenticated: false }; + if (process.env.NODE_ENV === "production") { + return { ok: false, authenticated: false, configurationError: true }; + } + // Explicit local/dev/test posture: accept but mark unauthenticated. + return { ok: true, authenticated: false, configurationError: false }; } - if (presented && presented === expected) return { ok: true, authenticated: true }; - return { ok: false, authenticated: false }; + + if (presented && presented === expected) { + return { ok: true, authenticated: true, configurationError: false }; + } + + return { ok: false, authenticated: false, configurationError: false }; } export async function POST(request: Request) { const auth = checkAuth(request); + if (auth.configurationError) { + return NextResponse.json( + { error: "Registry authentication is not configured" }, + { status: 503 }, + ); + } if (!auth.ok) { return NextResponse.json( { error: "Invalid or missing registry token" }, @@ -91,4 +112,4 @@ export async function POST(request: Request) { }, { status: 201 }, ); -} +} \ No newline at end of file From d90dd9c57629a93364519b5f8ee5c8a58dd8006e Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 9 Aug 2026 21:24:30 -0400 Subject: [PATCH 2/4] test(security): block unauthenticated production registration --- .../api/v1/registry/register/route.test.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/app/api/v1/registry/register/route.test.ts b/src/app/api/v1/registry/register/route.test.ts index 2862cf9..fffa585 100644 --- a/src/app/api/v1/registry/register/route.test.ts +++ b/src/app/api/v1/registry/register/route.test.ts @@ -14,9 +14,15 @@ function post(url: string, body: unknown, headers: Record = {}) } const REGISTER_URL = "http://localhost/api/v1/registry/register"; +const ORIGINAL_NODE_ENV = process.env.NODE_ENV; afterEach(() => { delete process.env.CAPI_REGISTRY_TOKEN; + if (ORIGINAL_NODE_ENV === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = ORIGINAL_NODE_ENV; + } }); describe("POST /api/v1/registry/register", () => { @@ -36,12 +42,11 @@ describe("POST /api/v1/registry/register", () => { expect(res.status).toBe(201); const body = await res.json(); expect(body.ok).toBe(true); - expect(body.authenticated).toBe(false); // no CAPI_REGISTRY_TOKEN configured + expect(body.authenticated).toBe(false); // no CAPI_REGISTRY_TOKEN configured outside production expect(body.capabilities_registered).toBe(1); expect(body.declared_capabilities).toEqual(["identity", "sso"]); expect(body.executable_capability_ids).toContain("svc::lockerphycer-test-a::verify_token"); - // The executable capability and the service are now visible in live state. const snapshot = await (await state()).json(); expect(snapshot.services.some((s: { service_name: string }) => s.service_name === "lockerphycer-test-a")).toBe(true); expect( @@ -60,6 +65,19 @@ describe("POST /api/v1/registry/register", () => { expect(res.status).toBe(400); }); + it("fails closed in production when registry authentication is not configured", async () => { + process.env.NODE_ENV = "production"; + delete process.env.CAPI_REGISTRY_TOKEN; + const serviceName = "svc-production-missing-token"; + + const denied = await register(post(REGISTER_URL, { service_name: serviceName })); + expect(denied.status).toBe(503); + expect(await denied.json()).toEqual({ error: "Registry authentication is not configured" }); + + const svc = await (await services()).json(); + expect(svc.services.some((s: { service_name: string }) => s.service_name === serviceName)).toBe(false); + }); + it("enforces the registry token when configured", async () => { process.env.CAPI_REGISTRY_TOKEN = "s3cret-token"; @@ -86,4 +104,4 @@ describe("POST /api/v1/registry/register", () => { ); expect(missing.status).toBe(404); }); -}); +}); \ No newline at end of file From 105e43e68e3fba9bdc8de06e9082f4dc347c9e5a Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Tue, 11 Aug 2026 02:18:24 -0400 Subject: [PATCH 3/4] fix(security): fail closed outside explicit registry dev environments --- src/app/api/v1/registry/register/route.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/api/v1/registry/register/route.ts b/src/app/api/v1/registry/register/route.ts index c2c7ee8..7a9b4a8 100644 --- a/src/app/api/v1/registry/register/route.ts +++ b/src/app/api/v1/registry/register/route.ts @@ -8,8 +8,9 @@ * * Auth: when CAPI_REGISTRY_TOKEN is set, a matching `Authorization: Bearer …` * is required; the registration is flagged `authenticated`. When the token is - * unset, unauthenticated registration is permitted only outside production. - * Production fails closed when registry authentication is not configured. + * unset, unauthenticated registration is permitted only in explicitly local, + * development, or test environments. Unset/unknown/staging/production values + * fail closed when registry authentication is not configured. */ import { NextResponse } from "next/server"; @@ -37,6 +38,8 @@ type AuthCheck = { configurationError: boolean; }; +const UNAUTHENTICATED_REGISTRY_ENVIRONMENTS = new Set(["local", "development", "test"]); + function normalizeCapabilities(entries: CapabilityEntry[] | undefined): RegisteredCapability[] { if (!entries) return []; return entries.map((entry) => @@ -49,10 +52,11 @@ function checkAuth(request: Request): AuthCheck { const header = request.headers.get("authorization")?.trim() ?? ""; const presented = header.toLowerCase().startsWith("bearer ") ? header.slice(7).trim() - : header; + : ""; if (!expected) { - if (process.env.NODE_ENV === "production") { + const environment = process.env.NODE_ENV?.trim().toLowerCase() ?? ""; + if (!UNAUTHENTICATED_REGISTRY_ENVIRONMENTS.has(environment)) { return { ok: false, authenticated: false, configurationError: true }; } // Explicit local/dev/test posture: accept but mark unauthenticated. From 623baa8d0d338060f8c4aa039d426be2fa358284 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Tue, 11 Aug 2026 02:18:45 -0400 Subject: [PATCH 4/4] test(security): cover registry environment and bearer fail-closed cases --- .../api/v1/registry/register/route.test.ts | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/app/api/v1/registry/register/route.test.ts b/src/app/api/v1/registry/register/route.test.ts index fffa585..fc8a2f2 100644 --- a/src/app/api/v1/registry/register/route.test.ts +++ b/src/app/api/v1/registry/register/route.test.ts @@ -27,6 +27,7 @@ afterEach(() => { describe("POST /api/v1/registry/register", () => { it("registers a service, mirrors executable capabilities, and records declared-only names", async () => { + process.env.NODE_ENV = "test"; const res = await register( post(REGISTER_URL, { service_name: "lockerphycer-test-a", @@ -42,7 +43,7 @@ describe("POST /api/v1/registry/register", () => { expect(res.status).toBe(201); const body = await res.json(); expect(body.ok).toBe(true); - expect(body.authenticated).toBe(false); // no CAPI_REGISTRY_TOKEN configured outside production + expect(body.authenticated).toBe(false); // explicitly test-only unauthenticated posture expect(body.capabilities_registered).toBe(1); expect(body.declared_capabilities).toEqual(["identity", "sso"]); expect(body.executable_capability_ids).toContain("svc::lockerphycer-test-a::verify_token"); @@ -61,24 +62,45 @@ describe("POST /api/v1/registry/register", () => { }); it("rejects an invalid body", async () => { + process.env.NODE_ENV = "test"; const res = await register(post(REGISTER_URL, { capabilities: ["x"] })); expect(res.status).toBe(400); }); - it("fails closed in production when registry authentication is not configured", async () => { + it.each([undefined, "production", "staging", "preview-unknown"])( + "fails closed without registry authentication when NODE_ENV=%s", + async (environment) => { + if (environment === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = environment; + } + delete process.env.CAPI_REGISTRY_TOKEN; + const serviceName = `svc-missing-token-${environment ?? "unset"}`; + + const denied = await register(post(REGISTER_URL, { service_name: serviceName })); + expect(denied.status).toBe(503); + expect(await denied.json()).toEqual({ error: "Registry authentication is not configured" }); + + const svc = await (await services()).json(); + expect(svc.services.some((s: { service_name: string }) => s.service_name === serviceName)).toBe(false); + }, + ); + + it.each(["local", "development", "test"])( + "permits explicitly unauthenticated registration in %s only", + async (environment) => { + process.env.NODE_ENV = environment; + delete process.env.CAPI_REGISTRY_TOKEN; + + const res = await register(post(REGISTER_URL, { service_name: `svc-${environment}` })); + expect(res.status).toBe(201); + expect((await res.json()).authenticated).toBe(false); + }, + ); + + it("enforces the registry token when configured and requires Bearer", async () => { process.env.NODE_ENV = "production"; - delete process.env.CAPI_REGISTRY_TOKEN; - const serviceName = "svc-production-missing-token"; - - const denied = await register(post(REGISTER_URL, { service_name: serviceName })); - expect(denied.status).toBe(503); - expect(await denied.json()).toEqual({ error: "Registry authentication is not configured" }); - - const svc = await (await services()).json(); - expect(svc.services.some((s: { service_name: string }) => s.service_name === serviceName)).toBe(false); - }); - - it("enforces the registry token when configured", async () => { process.env.CAPI_REGISTRY_TOKEN = "s3cret-token"; const denied = await register( @@ -86,6 +108,11 @@ describe("POST /api/v1/registry/register", () => { ); expect(denied.status).toBe(401); + const rawTokenDenied = await register( + post(REGISTER_URL, { service_name: "svc-raw-token-denied" }, { authorization: "s3cret-token" }), + ); + expect(rawTokenDenied.status).toBe(401); + const allowed = await register( post(REGISTER_URL, { service_name: "svc-allowed" }, { authorization: "Bearer s3cret-token" }), ); @@ -94,6 +121,7 @@ describe("POST /api/v1/registry/register", () => { }); it("heartbeat refreshes a known service and 404s an unknown one", async () => { + process.env.NODE_ENV = "test"; await register(post(REGISTER_URL, { service_name: "svc-hb" })); const ok = await heartbeat(post("http://localhost/api/v1/registry/heartbeat", { service_name: "svc-hb" }));