From f686e97d6c2a65fb9b0af9595d9c02450deb9ccc Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:14:40 -0300 Subject: [PATCH] fix(api): remove duplicate origin check causing LAN 403 on health-autopilot actions (#6277) (#6507) fix(api): remove duplicate origin check causing LAN 403 on health-autopilot actions (#6277). Integrated into release/v3.8.47. --- CHANGELOG.md | 1 + .../health-autopilot/actions/route.ts | 11 +++-- .../serial/provider-health-autopilot.test.ts | 42 +++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5fddee1b..a2042a8637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral - **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. - **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. - **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. +- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. ### 📝 Maintenance diff --git a/src/app/api/providers/health-autopilot/actions/route.ts b/src/app/api/providers/health-autopilot/actions/route.ts index 1cc58e5bae..ecd14550c9 100644 --- a/src/app/api/providers/health-autopilot/actions/route.ts +++ b/src/app/api/providers/health-autopilot/actions/route.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { executeProviderHealthAutopilotAction } from "@/lib/monitoring/providerHealthAutopilot"; -import { validateBrowserMutationOrigin } from "@/server/origin/publicOrigin"; import { validateBody } from "@/shared/validation/helpers"; const actionSchema = z.object({ @@ -29,11 +28,11 @@ export async function POST(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; - const originVerdict = validateBrowserMutationOrigin(request); - if (!originVerdict.ok) { - return NextResponse.json({ error: { message: "Invalid request origin" } }, { status: 403 }); - } - + // Origin validation for browser mutations is centralized in the authz pipeline + // (src/server/authz/pipeline.ts) for MANAGEMENT routes — see PR #5278. Do NOT + // re-check origin here: the pipeline strips PEER_IP_HEADER before forwarding, + // so a duplicate per-route check cannot resolve the LAN "direct-local-host" + // candidate and spuriously rejects same-origin LAN/Docker requests (#6277). try { let rawBody: unknown; try { diff --git a/tests/unit/serial/provider-health-autopilot.test.ts b/tests/unit/serial/provider-health-autopilot.test.ts index b0dafa2914..c7ea6c4fc5 100644 --- a/tests/unit/serial/provider-health-autopilot.test.ts +++ b/tests/unit/serial/provider-health-autopilot.test.ts @@ -217,6 +217,48 @@ test("provider health autopilot action rejects cross-site mutations", async () = assert.ok(unchanged.rateLimitedUntil); }); +test("provider health autopilot action accepts LAN dashboard requests (#6277)", async () => { + // #6277: Docker/LAN deployments (accessed via LAN IP, not localhost) got a + // spurious 403 "Invalid request origin" clicking "remove cooldown". Root + // cause: this route carried a DUPLICATE per-route validateBrowserMutationOrigin + // check re-added by the v3.8.42 release squash after PR #5278 centralized + // origin enforcement in the authz pipeline. The pipeline's centralized check + // (src/server/authz/pipeline.ts) strips PEER_IP_HEADER before forwarding the + // request to the route handler, so by the time this handler runs the peer + // stamp is gone — exactly what a real post-middleware request looks like. + // The route must trust the pipeline's verdict and NOT re-validate origin + // itself; without PEER_IP_HEADER, the per-route check cannot resolve the LAN + // "direct-local-host" candidate and rejects a same-origin-but-LAN mutation. + await enableManagementAuth(); + const connection = await createCooldownConnection(); + const report = await autopilot.buildProviderHealthAutopilotReport({ + provider: PROVIDER, + includeHealthy: true, + }); + const action = findAction(report, "clear_connection_cooldown"); + assert.ok(action); + + const request = await makeManagementSessionRequest( + "http://localhost/api/providers/health-autopilot/actions", + { + method: "POST", + headers: { origin: "http://192.168.1.50:20128", "sec-fetch-site": "same-origin" }, + body: { + type: action.type, + target: action.target, + preconditionsHash: action.preconditionsHash, + confirm: true, + }, + } + ); + assert.equal(request.headers.get("x-omniroute-peer-ip"), null); + + const response = await actionsRoute.POST(request); + assert.equal(response.status, 200); + const body = await response.json(); + assert.equal(body.success, true); +}); + test("provider health autopilot action rejects malformed JSON", async () => { await enableManagementAuth();