diff --git a/CHANGELOG.md b/CHANGELOG.md index be0dd0d877..51d944461c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ _In development β€” bullets added per PR; finalized at release._ ### πŸ”§ Bug Fixes -- **fix(api): LAN/Tailscale dashboard access β€” host-aware CSP, GET-exempt version route, surface combo field errors** β€” three failures when opening the dashboard from a non-loopback host: (1) CSP `connect-src` was a static loopback-only string, blocking `ws://:*` WebSocket connections from LAN/Tailscale clients; the CSP is now built per-request from a validated `Host` header (`src/server/csp.ts` + `src/middleware.ts`) with a strict hostname/IPv4 regex so injection-shaped values are never interpolated; (2) `GET /api/system/version` was blocked by `LOCAL_ONLY_API_PREFIXES` for all methods despite only `POST` spawning child processes (git/npm/pm2) β€” a new `LOCAL_ONLY_API_GET_EXEMPTIONS` set exempts safe read methods for this path while keeping `POST`/`PUT`/`PATCH`/`DELETE` strictly loopback-only; (3) `COMBO_002` validation errors only surfaced the generic message β€” `firstField`/`firstMessage` are now extracted from the first Zod issue and included in the response body. ([#5083](https://github.com/diegosouzapw/OmniRoute/issues/5083) β€” thanks @KooshaPari for the diagnosis and original PR #5084) +- **fix(api): LAN/Tailscale dashboard access β€” `ws:` CSP scheme, GET-exempt version route, surface combo field errors** β€” three failures when opening the dashboard from a non-loopback host: (1) CSP `connect-src` allowed the `ws:` scheme only for loopback origins, blocking the dashboard's `ws://:*` Live WebSocket from LAN/Tailscale clients; the bare `ws:` scheme is now permitted (symmetric with the bare `wss:` already allowed), kept declarative in `next.config.mjs` with no global middleware (the project has none by design); (2) `GET /api/system/version` was blocked by `LOCAL_ONLY_API_PREFIXES` for all methods despite only `POST` spawning child processes (git/npm/pm2) β€” a new `LOCAL_ONLY_API_GET_EXEMPTIONS` set exempts safe read methods for this path while keeping `POST`/`PUT`/`PATCH`/`DELETE` strictly loopback-only; (3) `COMBO_002` validation errors only surfaced the generic message β€” `firstField`/`firstMessage` are now extracted from the first Zod issue and included in the response body. ([#5083](https://github.com/diegosouzapw/OmniRoute/issues/5083) β€” thanks @KooshaPari for the diagnosis and original PR #5084) - **fix(sse): defer `` close so it never leaks before `tool_calls` in Claudeβ†’OpenAI streaming** β€” when a Claude thinking block was followed by a tool_use block, the translator unconditionally emitted a `content: ""` chunk at `content_block_stop`, injecting a spurious assistant text chunk immediately before the `tool_calls` delta and corrupting OpenAI-compatible clients (e.g. Kimi Coding). The close marker is now deferred: it is flushed at the first `text_delta` that follows the thinking block (preserving the #4633 / decolua/9router#454 behavior for Claude Code / Cursor) or at stream finish when no tool_calls were collected. Tool-use streams never get a `text_delta` after the thinking block, so `` is never emitted into content before `tool_calls`. ([#5123](https://github.com/diegosouzapw/OmniRoute/issues/5123)) - **fix(sse): normalize array user-message content in the Command Code executor to prevent upstream 400** β€” when a client sends a user turn whose `content` is an array of content parts (e.g. `[{type:"text",text:"…"}, …]`), the raw array was forwarded verbatim to the Command Code upstream, which requires `messages[N].content` for the `user` role to be a plain string β€” resulting in `expected string, received array` / HTTP 400 on DeepSeek V4-Pro and other Command Code models. The user branch of `convertMessages` now calls `normalizeContentText()` (already used by system, assistant, and tool branches) so multi-part user content is joined to a string before dispatch. Partially addresses ([#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166)); the 0-output-token symptom on reasoning-only models is tracked separately. diff --git a/next.config.mjs b/next.config.mjs index aefe08fadd..220e747be0 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -21,7 +21,11 @@ const contentSecurityPolicy = [ "font-src 'self' https://fonts.gstatic.com data:", "img-src 'self' data: blob: https:", "media-src 'self' data: blob:", - "connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https: wss:", + // `ws:` is permitted scheme-wide (mirroring the bare `wss:` already allowed) so the + // dashboard can open `ws://:*` to its own Live WS server when + // OmniRoute is reached from a non-loopback host. Same-origin HTTP fetches stay covered + // by `'self'`; the loopback origins remain listed explicitly for clarity. (#5083) + "connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https: ws: wss:", "worker-src 'self' blob:", "manifest-src 'self'", ].join("; "); diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index 21443db57e..0000000000 --- a/src/middleware.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Next.js Edge Middleware β€” per-request Content-Security-Policy (#5083). - * - * The static CSP in next.config.mjs only covers loopback origins in - * connect-src. When OmniRoute is reached from a LAN, Tailscale, or public - * hostname the dashboard cannot establish WebSocket connections because - * ws://:* is absent from the static policy. - * - * This middleware reads the trusted Host header, validates it with a strict - * regex, and β€” for valid non-loopback hosts β€” appends - * ws://:* http://:* - * to connect-src before the response reaches the browser. - * - * The CSP header set here overrides the static next.config.mjs header - * because middleware runs before the static route headers are applied. - * The static CSP is kept in next.config.mjs as a build-time fallback for - * environments where middleware is disabled. - * - * Security: - * - Host values are validated with a bounded hostname/IPv4 regex before - * interpolation. Invalid / injection-shaped values are ignored. - * - /dashboard/providers/services/*/embed/* keeps "frame-ancestors 'self'" - * (overrides the baseline "frame-ancestors 'none'") so the embedded - * service UI can be iframed by the OmniRoute dashboard. - * - All other hard-coded security directives (object-src, form-action …) - * are preserved verbatim from the baseline. - */ - -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; -import { buildCspForHost } from "@/server/csp"; - -/** Path prefix for embedded service reverse-proxy pages (Hard Rule #17). */ -const EMBED_PREFIX = "/dashboard/providers/services/"; - -export function middleware(request: NextRequest): NextResponse { - const response = NextResponse.next(); - - const { pathname } = request.nextUrl; - const host = request.headers.get("host"); - - // Embedded service UI pages need `frame-ancestors 'self'` so that the - // OmniRoute dashboard can render them inside an