Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ ALLOW_API_KEY_REVEAL=false

# CORS configuration — controls which cross-origin browser clients can call the API.
# Used by: src/server/cors/origins.ts — sets Access-Control-Allow-Origin.
# Same-origin dashboard requests behind a reverse proxy do not need CORS; set
# NEXT_PUBLIC_BASE_URL instead. No wildcard is sent unless CORS_ALLOW_ALL=true.
# Same-origin dashboard requests behind a reverse proxy do not need CORS; they
# use session-bound CSRF protection. No wildcard is sent unless CORS_ALLOW_ALL=true.
# CORS_ALLOWED_ORIGINS=https://your-frontend.example.com
# CORS_ORIGIN=https://your-frontend.example.com # legacy single-origin alias
# CORS_ALLOW_ALL=false
Expand Down Expand Up @@ -369,23 +369,24 @@ CLOUD_URL=
# Default: 12000 (12 seconds)
# CLOUD_SYNC_TIMEOUT_MS=12000

# Public-facing base URL — CRITICAL for reverse proxy / OAuth callback setups.
# Used by: OAuth redirect_uri computation, Dashboard UI links, generated public
# URLs, and same-origin browser mutation checks.
# Set to your public URL when behind nginx/Caddy (e.g., https://omniroute.example.com).
# Public-facing base URL — required for stable reverse proxy / OAuth callback setups.
# Used by: OAuth redirect_uri computation, Dashboard UI links, and generated public URLs.
# Set to your stable public URL when OAuth callbacks or generated browser links need a
# canonical host behind nginx/Caddy (e.g., https://omniroute.example.com).
#
# Dashboard display behavior: when this variable is unset, the dashboard
# auto-detects the base URL shown in curl examples and CLI tool snippets
# from window.location.origin (the host the user is browsing). Setting it
# explicitly is only required when running behind a reverse proxy with a
# different public hostname, or when OAuth callbacks must point to a
# canonical URL.
# different public hostname, or when OAuth callbacks / generated browser links must point
# to a canonical URL. Authenticated dashboard writes use same-origin requests plus
# session-bound CSRF protection and do not require a static public base URL.
#
# Default: http://localhost:20128
NEXT_PUBLIC_BASE_URL=http://localhost:20128

# Browser-facing OmniRoute origin for generated assets in API responses.
# Highest-priority public origin override; also used by public-origin validation.
# Highest-priority public origin override; also used by non-dashboard public-origin validation.
# Used by: chatgpt-web image generation cache URLs (/v1/chatgpt-web/image/<id>).
# Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL
# but the user's browser must fetch images from a LAN, tunnel, or public origin.
Expand Down
10 changes: 6 additions & 4 deletions docs/guides/DOCKER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ services:
- omniroute-data:/app/data
environment:
- PORT=20128
# Browser-facing origin for OAuth callbacks, dashboard links, and same-origin checks.
# Browser-facing origin for OAuth callbacks, dashboard links, and generated public URLs.
- NEXT_PUBLIC_BASE_URL=https://your-domain.com
# Internal server-to-server URL for scheduled jobs / self-fetches.
- BASE_URL=http://omniroute:20128
Expand All @@ -210,9 +210,11 @@ volumes:
```

Caddy sets the standard forwarding headers for the upstream container. OmniRoute uses
`NEXT_PUBLIC_BASE_URL` as the canonical public origin; only enable `OMNIROUTE_TRUST_PROXY` for
advanced deployments where you intentionally want OmniRoute to derive the public origin from
trusted forwarded headers instead of explicit configuration.
`NEXT_PUBLIC_BASE_URL` as the canonical public origin for OAuth callbacks and generated public
links; authenticated dashboard writes use same-origin requests plus session-bound CSRF
protection. Only enable `OMNIROUTE_TRUST_PROXY` for advanced deployments where you intentionally
want OmniRoute to derive the public origin from trusted forwarded headers instead of explicit
configuration.

## Cloudflare Quick Tunnel

Expand Down
5 changes: 5 additions & 0 deletions docs/ops/TUNNELS_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ build callback URLs against the **public** hostname, not `localhost`. Otherwise
the OAuth provider redirects the user back to a URL its servers cannot reach,
and the handshake fails.

Dashboard edits and settings saves do not require pinning the tunnel hostname in
`NEXT_PUBLIC_BASE_URL`. The authenticated dashboard sends same-origin unsafe
requests with a session-bound CSRF token, so ephemeral Cloudflare Quick Tunnel
hosts can still be used for normal UI management after logging in.

Set:

```bash
Expand Down
14 changes: 8 additions & 6 deletions docs/ops/VM_DEPLOYMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ REQUIRE_API_KEY=false
# === URLs (change to your domain) ===
# Internal server-to-server base URL for scheduled jobs / self-fetches.
BASE_URL=http://127.0.0.1:20128
# Browser-facing URL used for OAuth callbacks, dashboard links, and same-origin checks.
# Browser-facing URL used for OAuth callbacks, dashboard links, and generated public URLs.
NEXT_PUBLIC_BASE_URL=https://llms.seudominio.com
# Optional explicit public origin override for generated public asset URLs.
# OMNIROUTE_PUBLIC_BASE_URL=https://llms.seudominio.com
Expand Down Expand Up @@ -243,11 +243,13 @@ Keep reverse-proxy stream timeouts aligned with your OmniRoute timeout env vars.
`FETCH_TIMEOUT_MS` / `STREAM_IDLE_TIMEOUT_MS`, raise `proxy_read_timeout` / `proxy_send_timeout`
above the same threshold.

OmniRoute uses `NEXT_PUBLIC_BASE_URL` as the canonical browser-facing origin for OAuth,
public links, and dashboard mutation origin checks. The `X-Forwarded-*` headers above are
still useful routing metadata, but they are not a replacement for setting the explicit public
URL. Only enable `OMNIROUTE_TRUST_PROXY` if OmniRoute is not directly reachable by clients and
your proxy strips/rebuilds incoming forwarded headers.
OmniRoute uses `NEXT_PUBLIC_BASE_URL` as the canonical browser-facing origin for OAuth
callbacks and generated public links. Authenticated dashboard writes use same-origin requests
plus session-bound CSRF protection, so they do not require a static public base URL. The
`X-Forwarded-*` headers above are still useful routing metadata, but they are not a replacement
for setting the explicit public URL when OAuth or generated browser links need one. Only enable
`OMNIROUTE_TRUST_PROXY` if OmniRoute is not directly reachable by clients and your proxy
strips/rebuilds incoming forwarded headers.

### 3.3 Enable and Test

Expand Down
10 changes: 5 additions & 5 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_CHAT_HARD_MAX_BODY_BYTES` | `52428800` (50 MB) | `src/shared/middleware/chatBodyAdmission.ts` | Chat-route hard cap. Bodies larger than this are rejected with `413` before being cloned/parsed, regardless of heap state. |
| `OMNIROUTE_CHAT_HEAP_SHED_RATIO` | `0.75` | `src/shared/middleware/chatBodyAdmission.ts` | Shed a large chat body with `503` + `Retry-After` once `heapUsed / heap_size_limit` reaches this ratio (`0 < r < 1`). Turns a process-wide V8 OOM under concurrent large compacts into a single graceful client retry; a healthy heap admits every body untouched. |
| `OMNIROUTE_MAX_NONSTREAMING_RESPONSE_BYTES` | `67108864` (64 MB) | `open-sse/handlers/chatCore/nonStreamingResponseBody.ts` | Hard cap for a non-streaming upstream response buffered fully into memory. Past this the upstream reader is cancelled and the request fails fast instead of growing an unbounded string until the heap is exhausted. |
| `CORS_ORIGIN` | _(unset)_ | `src/server/cors/origins.ts` | Legacy single-origin CORS allowlist. Prefer `CORS_ALLOWED_ORIGINS` for new deployments. CORS is only for cross-origin browser API clients; same-origin dashboard requests behind a reverse proxy use `NEXT_PUBLIC_BASE_URL` / public-origin validation instead. |
| `CORS_ORIGIN` | _(unset)_ | `src/server/cors/origins.ts` | Legacy single-origin CORS allowlist. Prefer `CORS_ALLOWED_ORIGINS` for new deployments. CORS is only for cross-origin browser API clients; authenticated dashboard writes use same-origin requests plus session-bound CSRF protection instead. |
| `CORS_ALLOWED_ORIGINS` | _(unset)_ | `src/server/cors/origins.ts` | Comma-separated CORS allowlist. No wildcard is sent unless `CORS_ALLOW_ALL=true` is explicitly configured. |
| `CORS_ALLOW_ALL` | `false` | `src/server/cors/origins.ts` | Development-only escape hatch to echo any browser `Origin`. Do not enable on shared or production deployments. |
| `OUTBOUND_SSRF_GUARD_ENABLED` | `true` | `src/shared/network/outboundUrlGuard.ts` | Block provider calls targeting private/loopback/link-local IP ranges. Disable only in isolated test envs. |
Expand Down Expand Up @@ -258,10 +258,10 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `OMNIROUTE_CLOUD_SYNC_SECRET` | _(empty)_ | `src/lib/cloudSync.ts` | Shared secret used to verify the HMAC-SHA256 signature of Cloud Sync responses. |
| `OMNIROUTE_CLOUD_SYNC_SECRETS` | `false` | `src/lib/cloudSync.ts` | Set to `true` to allow the Cloud Sync endpoint to overwrite local credentials. Default is `false`. |
| `OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP` | `false` | `src/app/api/providers/zed/import/route.ts` | Set to `true` to fall back to the v3.8.5 one-step "import everything" behavior without user confirmation. |
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links, generated public URLs, and same-origin browser mutation checks. **Must match your public URL behind reverse proxy.** |
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links, and generated public URLs. Set this to the stable public URL when OAuth callbacks or generated browser links must use a canonical reverse-proxy host. |
| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. |
| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. |
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | Public-origin resolver, image URLs | Highest-priority browser-facing OmniRoute origin used for public URL generation and origin validation (for example `/v1/chatgpt-web/image/<id>`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | Public-origin resolver, image URLs | Highest-priority browser-facing OmniRoute origin used for public URL generation and non-dashboard browser-origin validation (for example `/v1/chatgpt-web/image/<id>`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
| `OMNIROUTE_TRUST_PROXY` | _(unset)_ | `src/server/origin/publicOrigin.ts` | Optional trust mode for forwarded public-origin headers. Unset = do not trust `Forwarded` / `X-Forwarded-*` for security decisions. `true` / `loopback` trusts forwarded host/proto only from a token-stamped loopback proxy. `private` / `lan` also trusts private-LAN proxy peers. Prefer explicit `NEXT_PUBLIC_BASE_URL` in production. |
| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. |
| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/<id>`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. |
Expand All @@ -286,11 +286,11 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `OMNIROUTE_CODEWHISPERER_BASE_URL` | `https://codewhisperer.us-east-1.amazonaws.com` | `open-sse/services/usage.ts` | CodeWhisperer (AWS Kiro) usage limits endpoint. Override for relays / test fixtures. |

> [!IMPORTANT]
> When deploying behind a reverse proxy (nginx, Caddy), `NEXT_PUBLIC_BASE_URL` **must** be set to your public URL (e.g., `https://omniroute.example.com`). Without this, OAuth callbacks can fail because the redirect_uri won't match, generated public links can point at the internal container origin, and same-origin dashboard mutations can be rejected by browser-origin checks.
> When deploying behind a reverse proxy (nginx, Caddy), set `NEXT_PUBLIC_BASE_URL` to your stable public URL (e.g., `https://omniroute.example.com`) when OAuth callbacks or generated public links must use that hostname. Without this, OAuth callbacks can fail because the redirect_uri won't match and generated public links can point at the internal container origin.
>
> Keep `BASE_URL` as an internal loopback/container URL for server-to-server jobs. Do not use a browser `Origin` or public hostname for credential-bearing internal self-fetches.
>
> OmniRoute centralizes public-origin validation: explicit public URL env vars are trusted first; raw `Forwarded` / `X-Forwarded-*` headers are ignored unless `OMNIROUTE_TRUST_PROXY` is enabled and the immediate proxy peer is token-stamped as trusted. Do not use CORS settings to fix same-origin dashboard requests; CORS is only for cross-origin browser clients.
> Authenticated dashboard writes do not require a static public base URL: the dashboard sends same-origin unsafe requests with a session-bound CSRF token. OmniRoute still centralizes public-origin validation for non-dashboard browser integrations: explicit public URL env vars are trusted first; raw `Forwarded` / `X-Forwarded-*` headers are ignored unless `OMNIROUTE_TRUST_PROXY` is enabled and the immediate proxy peer is token-stamped as trusted. Do not use CORS settings to fix same-origin dashboard requests; CORS is only for cross-origin browser clients.

---

Expand Down
3 changes: 1 addition & 2 deletions src/app/(dashboard)/dashboard/combos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import {
normalizeIntelligentRoutingConfig,
} from "@/lib/combos/intelligentRouting";
import { resolveServerErrorMessage } from "@/lib/api/serverErrorMessage";
import { withDashboardCsrfHeader } from "@/shared/utils/dashboardCsrf";
import { useTranslations } from "next-intl";

const ModelSelectModal = dynamic(() => import("@/shared/components/ModelSelectModal"), {
Expand Down Expand Up @@ -868,7 +867,7 @@ export default function CombosPage() {
try {
const res = await fetch("/api/combos/test", {
method: "POST",
headers: await withDashboardCsrfHeader({ "Content-Type": "application/json" }),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ comboName: combo.name }),
});
const data = await res.json();
Expand Down
Loading
Loading