docs(wiki): auto-sync pages + cover counts with docs

github-actions[bot]
2026-07-03 19:38:56 +00:00
parent d7da8cdb3a
commit f6759bbbd5
2 changed files with 239 additions and 0 deletions

161
CORS.md Normal file

@@ -0,0 +1,161 @@
> 🌍 [View in other languages](Languages)
# CORS Configuration & Security
OmniRoute controls which **browser origins** may read cross-origin responses
from a single, centralized allowlist. The model is **fail-closed by default**:
no origin is allowed until you opt one in. This page documents how the allowlist
resolves, what `CORS_ALLOW_ALL=true` actually exposes (and, importantly, what it
does **not**), how to configure dev vs production safely, and the runtime warning
the dashboard shows when a wildcard is live.
**Source of truth:** `src/server/cors/origins.ts` (`resolveAllowedOrigin`,
`applyCorsHeaders`, `getCorsStatus`). The allowlist is applied once, in the
middleware (`src/server/authz/pipeline.ts`) — per-route handlers do not set
`Access-Control-Allow-Origin` themselves.
## How an origin is resolved
For each request the middleware computes the `Access-Control-Allow-Origin` value
in this order:
1. **`CORS_ALLOW_ALL=true`** (or the legacy `CORS_ORIGIN=*`) → echo the caller's
`Origin` back (or `*` when there is no `Origin` header), with `Vary: Origin`
so caches stay correct.
2. Otherwise, the request `Origin` is normalized (lower-cased, trailing slash
stripped) and matched against the **merged allowlist**:
- env **`CORS_ALLOWED_ORIGINS`** — comma-separated list, and
- the runtime **`corsOrigins`** setting (Dashboard → Security → _CORS Allowed
Origins_), injected via `setRuntimeAllowedOrigins()` from
`src/lib/config/runtimeSettings.ts`.
3. No match → **no `Access-Control-Allow-Origin` header is emitted**. The browser
blocks the cross-origin read. This is the intended fail-closed default.
| Env var | Meaning |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `CORS_ALLOWED_ORIGINS` | CSV of exact origins to allow (recommended). |
| `CORS_ALLOW_ALL` | `true`/`1` → echo any origin (wildcard). Dev only. |
| `CORS_ORIGIN` | Legacy. `*` behaves like `CORS_ALLOW_ALL`; a single value is added to the allowlist. |
## Threat model — what `CORS_ALLOW_ALL=true` really exposes
The generic OWASP warning ("wildcard CORS = any site can call your API") is worth
taking seriously, but OmniRoute's exposure is **narrower than the generic case**,
because of one concrete implementation fact:
> **The central `applyCorsHeaders()` never emits
> `Access-Control-Allow-Credentials`.** A browser will not expose a _credentialed_
> (cookie-bearing) cross-origin response unless the server sends
> `Access-Control-Allow-Credentials: true`. OmniRoute's shared CORS path never
> does.
What that means per surface, even with `CORS_ALLOW_ALL=true`:
| Surface | Auth mechanism | Effect of wildcard CORS |
| ----------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard / MANAGEMENT `/api/*` | Cookie session | Origin is echoed, but **without `Allow-Credentials`** the browser **blocks** the credentialed read. A malicious cross-origin site **cannot read** your authenticated dashboard responses, and the session cookie is not exposed. |
| Client API `/v1/*`, `/v1beta/*` | Bearer / `x-api-key` header | Already permissive **by design** (`relaxForTokenAuth`): browsers never auto-attach `Authorization`/`x-api-key`, so an attacker's page cannot supply your key. `CORS_ALLOW_ALL` does not widen this. |
| Public read-only (`/api/health`, …) | None | Non-sensitive; wildcard is harmless. |
So the **residual** exposure of `CORS_ALLOW_ALL=true` is limited to: (a)
non-credentialed cross-origin **reads** of already-unauthenticated data, and (b)
letting CORS **preflight pass** on management routes — which still require auth
that a cross-origin page cannot provide. It is **not** a session-hijack or
credential-theft vector on the shared CORS path.
### One genuine exception — `/api/v1/agents/`
The Cloud-Agent routes (`/api/v1/agents/{health,credentials,tasks,tasks/[id]}`) set
their **own** CORS headers
(`src/lib/cloudAgent/api.ts`, `getCloudAgentCorsHeaders`) and **do** emit
`Access-Control-Allow-Origin: <origin>|*` together with
`Access-Control-Allow-Credentials: true`. This is the single surface where
origin-echo and credentials coexist, and it is **independent of
`CORS_ALLOW_ALL`**. These routes are management-authenticated
(`requireManagementAuth`); operators who expose the dashboard off-host should be
aware that this is the one place a cross-origin credentialed read is permitted by
the response headers. Tightening it to an explicit allowlist is tracked
separately from this CORS guidance.
## Production checklist
- **Never set `CORS_ALLOW_ALL=true` in production.** Leave it unset.
- Set an **explicit** origin list — either the env var or the Security-tab field:
```bash
CORS_ALLOWED_ORIGINS="https://app.example.com, https://admin.example.com"
```
- If OmniRoute runs behind a reverse proxy / tunnel (nginx, Caddy, Cloudflare
Tunnel, Tailscale), CORS is **not** your only control — the loopback route
guard still protects spawn-capable routes (see
[ROUTE_GUARD_TIERS](./ROUTE_GUARD_TIERS.md)). Do not forge
`X-Forwarded-For: 127.0.0.1` to "fix" a 403; that re-opens the RCE class the
route guard closes.
- Confirm the runtime state: the dashboard shows a **persistent amber banner**
under Dashboard → Security → Authorization Inventory whenever
`CORS_ALLOW_ALL=true` is live, and `/api/settings/authz-inventory` returns a
`cors: { allowAll, allowedOrigins }` envelope monitoring tools can poll.
## Development convenience — allow specific local origins
You rarely need the wildcard even in dev. Allow just the dev servers you use:
```bash
# Vite (5173) + Next.js (3000) dev servers calling a local OmniRoute
CORS_ALLOWED_ORIGINS="http://localhost:5173, http://localhost:3000"
```
Origins are matched case-insensitively with the trailing slash ignored, so
`http://localhost:3000` and `http://localhost:3000/` are equivalent. The same CSV
can be set at runtime in **Dashboard → Security → CORS Allowed Origins** without a
restart.
## API keys vs cookie sessions
- **Bearer / `x-api-key` (the `/v1/*` inference surface):** browsers never attach
these automatically. CORS is not a meaningful barrier here — the API key is the
barrier — which is why that surface is intentionally permissive so browser and
Electron clients can read responses they are already entitled to.
- **Cookie session (the dashboard):** protected by the fail-closed default **and**
by the absence of `Access-Control-Allow-Credentials` on the shared path. Keep
management/dashboard origins out of any permissive config; they must stay exactly
fail-closed.
## Example: reverse proxy in front of OmniRoute
CORS is enforced by OmniRoute itself, so the proxy generally should **not** add or
rewrite `Access-Control-*` headers (double headers break browsers). Terminate TLS
and forward — let OmniRoute answer preflight:
```nginx
# nginx — forward to OmniRoute; do NOT inject Access-Control-* here
location / {
proxy_pass http://127.0.0.1:20128;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Do NOT set X-Forwarded-For to 127.0.0.1 — it defeats the loopback route guard.
}
```
Set the allowed browser origins in OmniRoute (`CORS_ALLOWED_ORIGINS` or the
Security tab), not in the proxy.
## Source files
| Concern | File |
| ----------------------------------------------- | -------------------------------------------------------------------- |
| Allowlist resolution + `getCorsStatus()` | `src/server/cors/origins.ts` |
| Middleware application (single source of truth) | `src/server/authz/pipeline.ts` |
| Settings → runtime origin injection | `src/lib/config/runtimeSettings.ts` |
| Runtime status for the dashboard | `src/app/api/settings/authz-inventory/route.ts` |
| Dashboard warning banner | `src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx` |
| CORS Allowed Origins field | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
| Cloud-Agent per-route CORS (the exception) | `src/lib/cloudAgent/api.ts` |
## See also
- [Route Guard Tiers](./ROUTE_GUARD_TIERS.md) — loopback enforcement for
spawn-capable routes (a separate, complementary control).
- [Authorization Guide](../architecture/AUTHZ_GUIDE.md) — the full auth pipeline.

@@ -0,0 +1,78 @@
> 🌍 [View in other languages](Languages)
# Provider Plugin Manifest
`open-sse/config/providerPluginManifest.ts` defines the JSON-safe provider
plugin contract. `open-sse/config/providerPluginManifestRegistry.ts` binds that
contract to the current provider registry for sidecars such as Bifrost,
CLIProxyAPI, or a future Go/Rust router. The TypeScript registry remains the
source of truth, but sidecars can consume the manifest without importing
executor code, OAuth defaults, headers, or process environment state.
The same manifest is available over HTTP at
`GET /api/v1/provider-plugin-manifest` for sidecars that run out-of-process.
OmniRoute advertises that URL to Bifrost and CLIProxyAPI via the
`X-OmniRoute-Provider-Manifest-Url` request header. Set
`OMNIROUTE_PROVIDER_MANIFEST_URL` when the sidecar needs a public or container
network URL instead of the local request origin.
## Goal
Move provider metadata toward a plugin contract so the hot request path can
eventually be owned by a lower-latency sidecar while OmniRoute keeps the
TypeScript route as the policy gate and fallback. The manifest is additive: it
does not change request routing by itself.
## Contract
The manifest contains:
- provider id and alias
- upstream format and executor name
- auth type, auth header, and optional auth prefix
- static endpoint metadata
- sidecar eligibility and explicit reasons when a provider should stay on TS
- JSON-safe model metadata such as context length, vision/reasoning flags, and
unsupported params
- capability tags including `apikey`, `oauth`, `custom-executor`,
`passthrough-models`, `responses`, and `sidecar-candidate`
The manifest intentionally excludes:
- OAuth client secrets and default secret values
- runtime environment resolution
- request headers and public credential helpers
- dynamic URL builders
- executor functions
- session pool internals
## Sidecar Use
Sidecars should treat `sidecar.eligible` as a conservative candidate signal, not
as an unconditional routing decision. The first import target should be
API-key, static-endpoint providers using the default executor. Providers with
custom web executors, OAuth/session flows, dynamic URL builders, or pool config
stay on the TypeScript fallback path until a sidecar implements equivalent
behavior and telemetry proves parity.
Suggested migration phases:
1. Generate and validate the provider plugin manifest from the TS registry.
2. Teach Bifrost or CLIProxyAPI to import the manifest for API-key/static
providers.
3. Route eligible providers through the sidecar behind `OMNIROUTE_RELAY_BACKEND`
while keeping TS fallback enabled.
4. Promote providers only when success rate, p99 latency, streaming behavior,
and unsupported-param handling match the TS path.
5. Add sidecar-native plugins for custom executors one provider family at a
time.
## Why Not Embed Providers Directly In Next
The Next frontend should not own provider execution. It should call the API
boundary. The backend can then decide whether to use the TypeScript executor,
Bifrost, CLIProxyAPI, or a future native sidecar. This keeps request signing,
allowlist checks, DB policy, and fallback behavior centralized before any
sidecar handoff.