mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
Compare commits
28 Commits
fix/11002-
...
fix/10265-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97fa38799e | ||
|
|
e06f8b7ec0 | ||
|
|
5a60a46e22 | ||
|
|
7ddbaf69a4 | ||
|
|
7ffa3efaf0 | ||
|
|
7c39e95972 | ||
|
|
8643e0f57c | ||
|
|
7e48be8061 | ||
|
|
9b801b7e09 | ||
|
|
0ff0490ada | ||
|
|
b45aa9eb19 | ||
|
|
f968496cc6 | ||
|
|
c9775366f9 | ||
|
|
666e4aaca2 | ||
|
|
1c920eb8b8 | ||
|
|
02a6c3d90b | ||
|
|
ae2de4511b | ||
|
|
d01a4ae6cf | ||
|
|
9349af29c4 | ||
|
|
690f5739ad | ||
|
|
eb4fd74b13 | ||
|
|
99111f39fb | ||
|
|
4e3e53ee4d | ||
|
|
a928fad895 | ||
|
|
d61eec63b5 | ||
|
|
861ac69e4b | ||
|
|
484cb6e562 | ||
|
|
2ab16d3214 |
9
.github/workflows/dast-smoke.yml
vendored
9
.github/workflows/dast-smoke.yml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
env:
|
||||
PORT: "20128"
|
||||
INJECTION_GUARD_MODE: block
|
||||
REQUIRE_API_KEY: "false"
|
||||
run: |
|
||||
node dist/server.js > server.log 2>&1 &
|
||||
echo $! > server.pid
|
||||
@@ -64,16 +65,20 @@ jobs:
|
||||
# those 302s as "the API accepted a schema-violating request" and the configured-off
|
||||
# 400 as "rejected a schema-compliant request". Documenting the flow in the spec is
|
||||
# still right (operators need it); fuzzing it is not what this smoke is for.
|
||||
# /api/auth/login has brute-force rate limiting: repeated failed logins return 429,
|
||||
# which Schemathesis flags as rejection of schema-compliant requests.
|
||||
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
|
||||
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
|
||||
--exclude-path-regex '^/api/auth/oidc/' \
|
||||
--exclude-path-regex '^/api/auth/(oidc/|login)' \
|
||||
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
|
||||
--request-timeout 20 --suppress-health-check all --no-color
|
||||
- name: Install promptfoo
|
||||
run: npm install -g promptfoo@0.122.0
|
||||
- name: promptfoo injection-guard (blocking)
|
||||
env:
|
||||
OMNIROUTE_URL: http://localhost:20128
|
||||
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
|
||||
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
|
||||
run: promptfoo eval -c promptfooconfig.yaml --no-cache
|
||||
- name: Stop server
|
||||
if: always()
|
||||
run: kill "$(cat server.pid)" || true
|
||||
|
||||
89
Dockerfile.bun
Normal file
89
Dockerfile.bun
Normal file
@@ -0,0 +1,89 @@
|
||||
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
|
||||
FROM oven/bun:1.3.14-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
python3 \
|
||||
python-is-python3 \
|
||||
make \
|
||||
g++ \
|
||||
libsecret-1-0 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── Builder stage (100% Bun Native Install & Build) ─────────────────────────
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
# Fast Bun native package install
|
||||
RUN bun install --include=optional --quiet
|
||||
|
||||
# Compile native better-sqlite3 Node-API addon under Bun
|
||||
RUN if [ -d "node_modules/better-sqlite3" ]; then \
|
||||
(cd node_modules/better-sqlite3 && bunx node-gyp rebuild); \
|
||||
fi
|
||||
|
||||
# Fetch tls-client-node native binary if script exists
|
||||
RUN if [ -f "node_modules/tls-client-node/scripts/postinstall.js" ]; then \
|
||||
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
|
||||
fi
|
||||
|
||||
# Disable Turbopack for Bun builder stage (Turbopack V8 internal worker bindings require Node)
|
||||
ENV OMNIROUTE_USE_TURBOPACK=0
|
||||
|
||||
ARG OMNIROUTE_BASE_PATH=""
|
||||
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
|
||||
|
||||
ARG DASHBOARD_ALLOW_EMBED=""
|
||||
ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Bun native Next.js build execution
|
||||
RUN bun run --quiet build
|
||||
|
||||
# ── Runner stage (100% Bun Native Production Runtime) ──────────────────────
|
||||
FROM oven/bun:1.3.14-slim AS runner
|
||||
|
||||
LABEL org.opencontainers.image.title="omniroute" \
|
||||
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \
|
||||
org.opencontainers.image.url="https://omniroute.online" \
|
||||
org.opencontainers.image.source="https://github.com/diegosouzapw/OmniRoute" \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libsecret-1-0 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=20128
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV OMNIROUTE_MEMORY_MB=1024
|
||||
|
||||
ENV DATA_DIR=/app/data
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
COPY --from=builder /app/.build/next/standalone ./
|
||||
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
|
||||
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
|
||||
|
||||
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
|
||||
|
||||
EXPOSE 20128
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD bun healthcheck.mjs || exit 1
|
||||
|
||||
ENTRYPOINT ["bun", "bin/omniroute.mjs", "serve", "--no-open"]
|
||||
13
README.md
13
README.md
@@ -1009,6 +1009,19 @@ Full table: [Docker Guide — runtime RAM](docs/guides/DOCKER_GUIDE.md#runtime-r
|
||||
> are **not supported for production**. See
|
||||
> [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
|
||||
|
||||
**🥟 Bun**
|
||||
|
||||
Standard `bun install` and global installation (`bun install -g omniroute`) are supported via Bun runtime detection:
|
||||
- **Built-in `bun:sqlite`**: OmniRoute uses Bun's built-in `bun:sqlite` driver when running under Bun, falling back to `better-sqlite3` on Node.js or `sql.js`.
|
||||
- **Automatic Webpack bundler selection**: Development (`bun run dev`) and production builds (`bun run build`) automatically detect Bun and disable Turbopack in favor of Webpack to prevent native V8 binding incompatibilities.
|
||||
- **Dedicated Bun Dockerfile**: Multi-stage `Dockerfile.bun` for native Bun production deployments (`docker build -f Dockerfile.bun -t omniroute:bun .`).
|
||||
|
||||
```bash
|
||||
# Install and run with Bun
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
**🛠️ From source**
|
||||
|
||||
```bash
|
||||
|
||||
@@ -9,10 +9,13 @@ import { discoverPlugins } from "../plugins.mjs";
|
||||
// (instead of string-interpolating into `execSync`) prevents a malicious plugin
|
||||
// name like `foo; rm -rf ~` or `` foo`id` `` from being interpreted by the shell.
|
||||
function runNpm(args) {
|
||||
const res = spawnSync("npm", args, { stdio: "inherit", shell: false });
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
const pm = isBun ? "bun" : "npm";
|
||||
const cmdArgs = isBun && args[0] === "install" ? ["add", ...args.slice(1)] : args;
|
||||
const res = spawnSync(pm, cmdArgs, { stdio: "inherit", shell: false });
|
||||
if (res.error) throw res.error;
|
||||
if (typeof res.status === "number" && res.status !== 0) {
|
||||
throw new Error(`npm exited with code ${res.status}`);
|
||||
throw new Error(`${pm} exited with code ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,30 +114,30 @@ export function isBetterSqliteBinaryValid() {
|
||||
|
||||
export function npmInstallRuntime(pkgs, opts = {}) {
|
||||
const cwd = ensureRuntimeDir();
|
||||
// Persist to the runtime package.json (exact version) instead of --no-save so a later
|
||||
// install of a sibling runtime dep (e.g. systray2 from trayRuntime.ts, which writes to the
|
||||
// same runtime dir) does not prune this package as "extraneous" — that pruning otherwise
|
||||
// reproduces "No SQLite driver available" after a tray install removes better-sqlite3.
|
||||
// npm 12+ defaults `allowScripts` to off, silently skipping lifecycle/install
|
||||
// scripts (e.g. better-sqlite3's node-gyp/prebuild-install rebuild) unless the
|
||||
// package has a matching `allowScripts` entry — and still exits 0, masking the
|
||||
// failure (#10713). The runtime dir is a CLI-owned, non-user package.json, so
|
||||
// explicitly allowing scripts for the packages we are installing here is safe.
|
||||
const npmArgs = [
|
||||
"install",
|
||||
...pkgs,
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefer-online",
|
||||
"--save-exact",
|
||||
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
|
||||
];
|
||||
// On Windows .cmd files cannot be executed without a shell; use cmd.exe /c explicitly
|
||||
// so we never set shell:true (which would propagate env and enable injection).
|
||||
const isWin = platform() === "win32";
|
||||
const [exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
|
||||
let exe, args, displayCmd;
|
||||
if (isBun) {
|
||||
const bunArgs = ["add", ...pkgs, "--trust"];
|
||||
[exe, args] = isWin ? ["cmd.exe", ["/c", "bun", ...bunArgs]] : ["bun", bunArgs];
|
||||
displayCmd = `bun ${bunArgs.join(" ")}`;
|
||||
} else {
|
||||
const npmArgs = [
|
||||
"install",
|
||||
...pkgs,
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefer-online",
|
||||
"--save-exact",
|
||||
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
|
||||
];
|
||||
[exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
|
||||
displayCmd = `npm ${npmArgs.join(" ")}`;
|
||||
}
|
||||
|
||||
if (!opts.silent) {
|
||||
process.stdout.write(`[omniroute][runtime] npm ${npmArgs.join(" ")}\n`);
|
||||
process.stdout.write(`[omniroute][runtime] ${displayCmd}\n`);
|
||||
}
|
||||
const res = spawnSync(exe, args, {
|
||||
cwd,
|
||||
|
||||
@@ -5,10 +5,14 @@ import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./
|
||||
|
||||
async function loadSqlite() {
|
||||
if (process.versions.bun) {
|
||||
return { Database: (await import("bun:sqlite")).Database };
|
||||
try {
|
||||
return { Database: (await import("bun:sqlite")).Database, driver: "bun:sqlite" };
|
||||
} catch (bunError) {
|
||||
// fall through to better-sqlite3 if bun:sqlite fails
|
||||
}
|
||||
}
|
||||
try {
|
||||
return { Database: (await import("better-sqlite3")).default };
|
||||
return { Database: (await import("better-sqlite3")).default, driver: "better-sqlite3" };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
@@ -86,12 +90,14 @@ export function normalizeBunSqliteParams(params) {
|
||||
|
||||
export function createSqliteNativeError(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
const rebuildCmd = isBun ? "bun add better-sqlite3 --trust" : "npm rebuild better-sqlite3";
|
||||
if (message.includes("NODE_MODULE_VERSION") || message.includes("ERR_DLOPEN_FAILED")) {
|
||||
return new Error(
|
||||
"better-sqlite3 native binding is incompatible with this Node.js runtime. " +
|
||||
"Run `npm rebuild better-sqlite3` in the OmniRoute project and try again. " +
|
||||
"Or run: omniroute runtime repair " +
|
||||
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
|
||||
`better-sqlite3 native binding is incompatible with this runtime. ` +
|
||||
`Run \`${rebuildCmd}\` in the OmniRoute project and try again. ` +
|
||||
`Or run: omniroute runtime repair ` +
|
||||
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -100,10 +106,9 @@ export function createSqliteNativeError(error) {
|
||||
message.includes("Cannot find module 'better-sqlite3'")
|
||||
) {
|
||||
return new Error(
|
||||
"better-sqlite3 native binding could not be found (no prebuilt addon for this platform). " +
|
||||
"This is common under `npx`, which runs a fresh, ephemeral install that never built the addon. " +
|
||||
"Run: omniroute runtime repair " +
|
||||
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
|
||||
`better-sqlite3 native binding could not be found (no prebuilt addon for this platform). ` +
|
||||
`Run: omniroute runtime repair ` +
|
||||
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
|
||||
);
|
||||
}
|
||||
return error;
|
||||
@@ -111,7 +116,7 @@ export function createSqliteNativeError(error) {
|
||||
|
||||
async function openSqliteDatabase(dbPath, options = {}) {
|
||||
const loaded = await loadSqlite();
|
||||
if (process.versions.bun) {
|
||||
if (loaded.driver === "bun:sqlite" || (process.versions.bun && !loaded.Database)) {
|
||||
if (options.fileMustExist && !fs.existsSync(dbPath)) {
|
||||
throw new Error(`SQLite file does not exist: ${dbPath}`);
|
||||
}
|
||||
|
||||
@@ -94,10 +94,15 @@ export function ensureAndroidCacheDir(options = {}) {
|
||||
*/
|
||||
export function isFatalInstrumentationHookFailure(text) {
|
||||
if (!text) return false;
|
||||
return (
|
||||
/Unsupported platform:\s*android/i.test(text) ||
|
||||
/error occurred while loading instrumentation hook/i.test(text)
|
||||
);
|
||||
// Next.js wraps ANY throw inside instrumentation.register() with the generic
|
||||
// "An error occurred while loading instrumentation hook:" prefix, on every
|
||||
// platform (node_modules/next/dist/server/web/globals.js). That prefix alone
|
||||
// therefore cannot identify the Android/Termux cache-probe failure — a bare
|
||||
// generic instrumentation error on win32/desktop would be misreported as the
|
||||
// Android bug and hide the real cause. Only match when the text actually
|
||||
// carries the Android platform marker that Next's getCacheDirectory() emits.
|
||||
// #10028
|
||||
return /Unsupported platform:\s*android/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,18 @@ export function getSecureFloorForMajor(major) {
|
||||
}
|
||||
|
||||
export function getNodeRuntimeSupport(version = process.versions.node) {
|
||||
if (process.versions.bun) {
|
||||
return {
|
||||
nodeVersion: `bun-${process.versions.bun} (Node.js API ${version})`,
|
||||
nodeCompatible: true,
|
||||
reason: "supported-bun",
|
||||
supportedRange: SUPPORTED_NODE_RANGE + " || Bun >=1.1.0",
|
||||
supportedDisplay: SUPPORTED_NODE_DISPLAY + ", or Bun 1.1+",
|
||||
recommendedVersion: `v${RECOMMENDED_NODE_VERSION}`,
|
||||
minimumSecureVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseNodeVersion(version);
|
||||
const secureFloor = getSecureFloorForMajor(parsed.major);
|
||||
const nodeCompatible = secureFloor ? compareNodeVersions(parsed, secureFloor) >= 0 : false;
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import updateNotifier from "update-notifier";
|
||||
let updateNotifier = null;
|
||||
try {
|
||||
updateNotifier = (await import("update-notifier")).default;
|
||||
} catch {
|
||||
// update-notifier is optional in pruned standalone environments
|
||||
}
|
||||
import { isNativeBinaryCompatible } from "../scripts/build/native-binary-compat.mjs";
|
||||
import { getNodeRuntimeSupport, getNodeRuntimeWarning } from "./nodeRuntimeSupport.mjs";
|
||||
import { getDefaultDataDir } from "./cli/data-dir.mjs";
|
||||
@@ -251,8 +256,9 @@ if (shouldProvisionStorageKey(process.argv)) {
|
||||
|
||||
// Register update notifier — checks npm once per 24h, notifies on exit via stderr.
|
||||
const _pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
||||
const _notifier = updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
|
||||
const _notifier = updateNotifier ? updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 }) : null;
|
||||
process.on("exit", () => {
|
||||
if (!_notifier || !_notifier.update) return;
|
||||
if (process.env.OMNIROUTE_NO_UPDATE_NOTIFIER) return;
|
||||
if (process.env.CI) return;
|
||||
if (process.argv.includes("--quiet") || process.argv.includes("-q")) return;
|
||||
|
||||
1
changelog.d/fixes/10028-windows-instrumentation-hook.md
Normal file
1
changelog.d/fixes/10028-windows-instrumentation-hook.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(cli): stop diagnosing every Next.js instrumentation-hook failure as the Android/Termux cache bug — only the Android "Unsupported platform: android" signal now triggers the Android hint, so a win32/desktop instrumentation error surfaces its real cause instead of a useless `mkdir -p ~/.cache` (#10028)
|
||||
1
changelog.d/fixes/10265-command-code-provider-api.md
Normal file
1
changelog.d/fixes/10265-command-code-provider-api.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(command-code): route chat to the documented /provider/v1/chat/completions endpoint instead of the CLI-only /alpha/generate, which Command Code gates/blocks for external callers (#10265)
|
||||
1
changelog.d/fixes/10523-servicesupervisor-port-flake.md
Normal file
1
changelog.d/fixes/10523-servicesupervisor-port-flake.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(services): isolate probeBeforeSpawn adoption tests on distinct ports to stop the order-dependent flake (#10523)
|
||||
1
changelog.d/fixes/10986-reasoning-only-content.md
Normal file
1
changelog.d/fixes/10986-reasoning-only-content.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(command-code): surface reasoning-only output as content when a model emits no text-delta (#10986)
|
||||
1
changelog.d/fixes/10990-v0-vercel-web-static-catalog.md
Normal file
1
changelog.d/fixes/10990-v0-vercel-web-static-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Static model catalog for v0-vercel-web:** seed a static catalog for the v0-vercel-web web-cookie provider (v0-1.0-md, v0-1.5-lg, v0-1.5-md) so its dashboard "Available Models" / "Import from /models" UI serves a usable list instead of falling through to the route's 400 "does not support models listing" ([#10990](https://github.com/diegosouzapw/OmniRoute/issues/10990)).
|
||||
1
changelog.d/fixes/10997-blackbox-deprecation.md
Normal file
1
changelog.d/fixes/10997-blackbox-deprecation.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): mark the blackbox provider deprecated — api.blackbox.ai returns HTTP 404 on every path variant (sweep 2026-08-21), so the public inference surface is dead and the catalog entry now carries a deprecation notice. ([#10997](https://github.com/diegosouzapw/OmniRoute/issues/10997))
|
||||
1
changelog.d/fixes/11002-dify-key-validation.md
Normal file
1
changelog.d/fixes/11002-dify-key-validation.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): validate Dify keys against its native /v1/chat-messages endpoint (#11002)
|
||||
1
changelog.d/fixes/11050-remove-ghost-webhook-events.md
Normal file
1
changelog.d/fixes/11050-remove-ghost-webhook-events.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(webhooks):** remove 3 declared-but-never-emitted events (`provider.error`, `provider.recovered`, `combo.switched`) from `WebhookEvent` — catalog now `request.completed | request.failed | quota.exceeded | test.ping`; `POST /api/webhooks` and `PUT /api/webhooks/[id]` reject ghost values with 400; OpenAPI webhook description updated across 43 locales ([11050](https://github.com/diegosouzapw/OmniRoute/pull/11050))
|
||||
1
changelog.d/fixes/8864-uncloseai-noauth.md
Normal file
1
changelog.d/fixes/8864-uncloseai-noauth.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): treat UncloseAI as a no-auth provider so the connect form no longer forces a fake API key (#8864)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(ssrf): make `getProviderOutboundGuard()` (used for search-provider connection validation, image generation and remote image fetch) honor the local-first default `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` the same way the chat validation guard already does, so a LAN-hosted SearXNG/Brave search provider works with only the LOCAL flag set instead of silently requiring `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` ([#9123](https://github.com/diegosouzapw/OmniRoute/issues/9123)).
|
||||
3
changelog.d/fixes/release-v3850-basereds.md
Normal file
3
changelog.d/fixes/release-v3850-basereds.md
Normal file
@@ -0,0 +1,3 @@
|
||||
- fix(api): repair broken `@/lib/db/connections` import in the usage utilization route that failed the production build (#10939 follow-up)
|
||||
- chore(docs): regenerate PROVIDER_REFERENCE and refresh README diagram SVGs to the real provider count (347)
|
||||
- chore(lint): prune ESLint suppressions orphaned on the release branch
|
||||
@@ -0,0 +1 @@
|
||||
- **test(db):** replace three empty `test.skip` placeholders in the critical DB-state suite with real assertions — `resetDbInstance` must swap the singleton while the on-disk row survives, the on-disk DB must open in WAL journal mode, and `db_meta` must hold the seeded `schema_version` — so a regression in any of those invariants can no longer pass as silently green ([#10906](https://github.com/diegosouzapw/OmniRoute/pull/10906))
|
||||
1
changelog.d/maintenance/11038-filesize-baseline-fix.md
Normal file
1
changelog.d/maintenance/11038-filesize-baseline-fix.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(quality): rebaseline file-size for modelCapabilities.ts (1016->1072) drift from merged tip fixes (#11034 et al)
|
||||
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"open-sse/services/payloadRules.ts": {
|
||||
"TS2677": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
|
||||
"TS2339": 16
|
||||
"TS2339": 10
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx": {
|
||||
"TS2503": 3
|
||||
@@ -120,10 +117,6 @@
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx": {
|
||||
"TS2741": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": {
|
||||
"TS2345": 3,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
|
||||
"TS2322": 2
|
||||
},
|
||||
@@ -141,12 +134,6 @@
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
|
||||
"TS2503": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": {
|
||||
"TS2339": 15
|
||||
},
|
||||
@@ -190,9 +177,6 @@
|
||||
"src/lib/combos/builderDraft.ts": {
|
||||
"TS2741": 1
|
||||
},
|
||||
"src/lib/providers/codexFastTier.ts": {
|
||||
"TS2367": 1
|
||||
},
|
||||
"src/lib/services/htmlRewriter.ts": {
|
||||
"TS2322": 2,
|
||||
"TS2345": 2
|
||||
@@ -219,14 +203,7 @@
|
||||
"src/shared/hooks/useElectron.ts": {
|
||||
"TS2339": 19
|
||||
},
|
||||
"src/shared/providers/webSessionCredentials.ts": {
|
||||
"TS2353": 1,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/shared/schemas/cliCatalog.ts": {
|
||||
"TS2554": 2
|
||||
},
|
||||
"src/shared/services/opencodeConfig.ts": {
|
||||
"TS2345": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,21 +443,23 @@
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1250
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1067,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1073,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1298,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
|
||||
"src/lib/modelCapabilities.ts": 1016,
|
||||
"src/lib/modelCapabilities.ts": 1072,
|
||||
"_rebaseline_2026_08_21_11034_effort_variants": "DRIFT do tip (base-red #9985): modelCapabilities.ts 1016->1072 (+56) acumulado por PRs ja mergeadas no release/v3.8.50 — principalmente #11034 (resolve effort-variant capabilities a partir do modelo base), alem de #10963/#11040/#10987 growth dos catalogos. Tip puro ficou vermelho neste gate; rebaseline no tip por push direto (owner pre-autorizou crescimento legitimo). Nao tocou no arquivo da #11038.",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
|
||||
"open-sse/config/imageRegistry.ts": 1034,
|
||||
"src/sse/handlers/chatHelpers.ts": 1019,
|
||||
"src/shared/middleware/chatBodyAdmission.ts": 1005,
|
||||
"_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file).",
|
||||
"open-sse/executors/commandCode.ts": 1038,
|
||||
"open-sse/executors/commandCode.ts": 1059,
|
||||
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
|
||||
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts."
|
||||
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
|
||||
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming)."
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
{
|
||||
"open-sse/handlers/chatCore/clientUsageBuffer.ts": {
|
||||
"TS2345": 2
|
||||
},
|
||||
"open-sse/utils/stream.ts": {
|
||||
"TS2345": 2,
|
||||
"TS2322": 2
|
||||
},
|
||||
"src/lib/guardrails/videoBridgeHelpers.ts": {
|
||||
"TS2488": 1,
|
||||
"TS2365": 2,
|
||||
|
||||
16
open-sse/config/opencodeZenGoSharedModels.ts
Normal file
16
open-sse/config/opencodeZenGoSharedModels.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Models declared identically in both the `opencode-zen` and `opencode-go` provider
|
||||
* registries (same upstream family, opencode.ai/zen/*). Mirrors the GLM_SHARED_MODELS
|
||||
* pattern in glmProvider.ts: one array, spread into each sibling RegistryEntry, so a
|
||||
* metadata fix (targetFormat, supportsReasoning, ...) only has to land in one file
|
||||
* instead of drifting out of sync across registries.
|
||||
*
|
||||
* Only entries that are byte-identical across both registries belong here — a model
|
||||
* with tier-specific flags (e.g. go's effort variants, or a flag only one tier needs)
|
||||
* stays local to that registry's own `models` array.
|
||||
*/
|
||||
export const OPENCODE_ZEN_GO_SHARED_MODELS = Object.freeze([
|
||||
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
|
||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
|
||||
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
|
||||
]);
|
||||
@@ -5,6 +5,12 @@ export const blackboxProvider: RegistryEntry = {
|
||||
alias: "bb",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
// NOTE: api.blackbox.ai returns HTTP 404 on /v1/chat/completions and /v1/models
|
||||
// (empty body, all path variants) since sweep 2026-08-21; the public inference
|
||||
// surface has moved to the gated enterprise.blackbox.ai/v1 endpoint. The provider
|
||||
// is marked deprecated in src/shared/constants/providers/apikey/frontier-labs.ts —
|
||||
// this registry entry is kept intact (registration/execution unaffected), so
|
||||
// existing configured keys keep working if a restored/enterprise host is reachable.
|
||||
baseUrl: "https://api.blackbox.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.blackbox.ai/v1/models",
|
||||
authType: "apikey",
|
||||
|
||||
@@ -8,7 +8,11 @@ export const command_codeProvider: RegistryEntry = {
|
||||
format: "openai",
|
||||
executor: "command-code",
|
||||
baseUrl: "https://api.commandcode.ai",
|
||||
chatPath: "/alpha/generate",
|
||||
// Chat uses the documented /provider/v1/chat/completions (OpenAI-format)
|
||||
// endpoint — NOT the CLI-only /alpha/generate endpoint, which Command Code
|
||||
// version-gates and proxy-blocks for external callers (#10265). Discovery
|
||||
// already targets the sibling /provider/v1/models endpoint.
|
||||
chatPath: "/provider/v1/chat/completions",
|
||||
modelsUrl: "https://api.commandcode.ai/provider/v1/models",
|
||||
// The discovery response is a partial routing catalog; static registry
|
||||
// entries omitted from it can still be accepted by the gateway.
|
||||
|
||||
@@ -5,7 +5,11 @@ export const difyProvider: RegistryEntry = {
|
||||
alias: "dify",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.dify.ai/v1/chat/completions",
|
||||
// Dify does not serve /chat/completions — its native completion route is
|
||||
// POST /v1/chat-messages (validated via the dedicated dify validator, #11002).
|
||||
// Keep this as the bare API root so route suffixes build correctly and
|
||||
// self-hosted instances can override the base URL per connection.
|
||||
baseUrl: "https://api.dify.ai",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [{ id: "auto", name: "Auto" }],
|
||||
|
||||
@@ -16,7 +16,7 @@ export const hailuo_webProvider: RegistryEntry = {
|
||||
alias: "hailuo-web",
|
||||
format: "openai",
|
||||
executor: "hailuo-web",
|
||||
baseUrl: "https://www.hailuo.ai",
|
||||
baseUrl: "https://chat.minimax.io",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: HAILUO_WEB_STATIC_MODELS,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RegistryEntry } from "../../../shared.ts";
|
||||
import { OPENCODE_ZEN_GO_SHARED_MODELS } from "../../../shared.ts";
|
||||
|
||||
export const opencode_goProvider: RegistryEntry = {
|
||||
id: "opencode-go",
|
||||
@@ -13,6 +14,8 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
authPrefix: "Bearer",
|
||||
defaultContextLength: 200000,
|
||||
models: [
|
||||
...OPENCODE_ZEN_GO_SHARED_MODELS,
|
||||
|
||||
// Port from decolua/9router 8efacc11: align with official Go endpoints —
|
||||
// glm-5.2 is now advertised and Kimi chat traffic must route through
|
||||
// `kimi-k2.7-code` (the live API rejects the plain `kimi-k2.7` alias for
|
||||
@@ -25,7 +28,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
{ id: "glm-5.2-max", name: "GLM-5.2 (max effort)", supportsReasoning: true },
|
||||
{ id: "glm-5.1", name: "GLM-5.1" },
|
||||
{ id: "glm-5", name: "GLM-5" },
|
||||
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
|
||||
// kimi-k2.7-code declared identically on opencode-zen — see OPENCODE_ZEN_GO_SHARED_MODELS.
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6" },
|
||||
{ id: "kimi-k2.5", name: "Kimi K2.5" },
|
||||
// #8353: Kimi K3 base + max-effort alias from the OpenCode Go registry.
|
||||
@@ -89,7 +92,8 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: false,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
|
||||
// qwen3.6-plus / qwen3.5-plus base ids declared identically on opencode-zen — see
|
||||
// OPENCODE_ZEN_GO_SHARED_MODELS.
|
||||
{
|
||||
id: "qwen3.6-plus-high",
|
||||
name: "Qwen3.6 Plus (high effort)",
|
||||
@@ -104,7 +108,6 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: false,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
|
||||
// #8353: hy3 is the Go-tier base id (distinct from hy3-preview / hy3-free).
|
||||
{ id: "hy3", name: "Hunyuan3", contextLength: 256000, supportsReasoning: true },
|
||||
{
|
||||
@@ -138,6 +141,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: true,
|
||||
supportsAudio: true,
|
||||
supportsVideo: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "muse-spark-1.2-contributor-minimal",
|
||||
@@ -148,6 +152,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: true,
|
||||
supportsAudio: true,
|
||||
supportsVideo: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "muse-spark-1.2-contributor-low",
|
||||
@@ -158,6 +163,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: true,
|
||||
supportsAudio: true,
|
||||
supportsVideo: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "muse-spark-1.2-contributor-medium",
|
||||
@@ -168,6 +174,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: true,
|
||||
supportsAudio: true,
|
||||
supportsVideo: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "muse-spark-1.2-contributor-high",
|
||||
@@ -178,6 +185,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: true,
|
||||
supportsAudio: true,
|
||||
supportsVideo: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "muse-spark-1.2-contributor-xhigh",
|
||||
@@ -188,6 +196,7 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
supportsVision: true,
|
||||
supportsAudio: true,
|
||||
supportsVideo: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
// #8353: Grok 4.5 + effort tiers from the OpenCode Go registry.
|
||||
{ id: "grok-4.5", name: "Grok 4.5", supportsReasoning: true },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RegistryEntry } from "../../../shared.ts";
|
||||
import { OPENCODE_ZEN_GO_SHARED_MODELS } from "../../../shared.ts";
|
||||
|
||||
export const opencode_zenProvider: RegistryEntry = {
|
||||
id: "opencode-zen",
|
||||
@@ -15,6 +16,8 @@ export const opencode_zenProvider: RegistryEntry = {
|
||||
// from the live API response so new models work without a code deploy.
|
||||
passthroughModels: true,
|
||||
models: [
|
||||
...OPENCODE_ZEN_GO_SHARED_MODELS,
|
||||
|
||||
// ── Chat / Coding ──────────────────────────────────────────
|
||||
// #2900: big-pickle's upstream runs DeepSeek thinking mode — declare the
|
||||
// interleaved reasoning_content contract so follow-up/tool-use turns replay
|
||||
@@ -51,7 +54,25 @@ export const opencode_zenProvider: RegistryEntry = {
|
||||
{ id: "grok-4.6", name: "Grok 4.6" },
|
||||
|
||||
// ── Muse ───────────────────────────────────────────────────
|
||||
{ id: "muse-spark-1.2", name: "Muse Spark 1.2" },
|
||||
// Muse Spark is served by OpenCode Zen only on the OpenAI Responses API
|
||||
// endpoint, not /chat/completions (see the opencode provider's own
|
||||
// muse-spark entries, #10874/#10867) — this provider is a separate
|
||||
// registry entry for the same upstream and never got the same
|
||||
// targetFormat declaration, so requests routed here still hit
|
||||
// /chat/completions with a mismatched or unanswerable body and the
|
||||
// upstream returns an empty message.
|
||||
{
|
||||
id: "muse-spark-1.2",
|
||||
name: "Muse Spark 1.2",
|
||||
supportsReasoning: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "muse-spark-1.2-contributor-free",
|
||||
name: "Muse Spark 1.2 Contributor Free",
|
||||
supportsReasoning: true,
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
|
||||
// ── DeepSeek ────────────────────────────────────────────────
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
|
||||
@@ -66,7 +87,7 @@ export const opencode_zenProvider: RegistryEntry = {
|
||||
|
||||
// ── Kimi / Moonshot ────────────────────────────────────────
|
||||
{ id: "kimi-k3", name: "Kimi K3" },
|
||||
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
|
||||
// kimi-k2.7-code declared identically on opencode-go — see OPENCODE_ZEN_GO_SHARED_MODELS.
|
||||
|
||||
// ── Qwen ───────────────────────────────────────────────────
|
||||
// Issue #2292: Qwen models return Claude-format SSE bodies even
|
||||
@@ -74,8 +95,8 @@ export const opencode_zenProvider: RegistryEntry = {
|
||||
// through /messages and the Claude translator.
|
||||
// Issue #2822: These models are text-only — supportsVision: false
|
||||
// ensures combo routing skips them on image-bearing requests.
|
||||
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false },
|
||||
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false },
|
||||
// qwen3.5-plus / qwen3.6-plus declared identically on opencode-go — see
|
||||
// OPENCODE_ZEN_GO_SHARED_MODELS.
|
||||
|
||||
// ── Free Tier ──────────────────────────────────────────────
|
||||
// #6998 (2026-07-14): upstream free tier rotated — minimax-m2.5-free,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
GLMT_TIMEOUT_MS,
|
||||
GLM_SHARED_MODELS,
|
||||
} from "../glmProvider.ts";
|
||||
import { OPENCODE_ZEN_GO_SHARED_MODELS } from "../opencodeZenGoSharedModels.ts";
|
||||
import { MARITALK_DEFAULT_BASE_URL } from "../maritalk.ts";
|
||||
import {
|
||||
CURSOR_REGISTRY_VERSION,
|
||||
@@ -719,6 +720,7 @@ export {
|
||||
GLM_TIMEOUT_MS,
|
||||
GLMT_TIMEOUT_MS,
|
||||
GLM_SHARED_MODELS,
|
||||
OPENCODE_ZEN_GO_SHARED_MODELS,
|
||||
MARITALK_DEFAULT_BASE_URL,
|
||||
CURSOR_REGISTRY_VERSION,
|
||||
getAntigravityProviderHeaders,
|
||||
|
||||
@@ -104,6 +104,12 @@ import {
|
||||
import { applyPeerTraceHeader } from "@/shared/resilience/peerRouting";
|
||||
import { applyClineProtocolHeaders } from "@/shared/utils/clineAuth";
|
||||
import { isProbeContext } from "@/shared/utils/probeOrigin";
|
||||
import {
|
||||
parseAndValidatePublicUrl,
|
||||
parseAndValidateNonMetadataUrl,
|
||||
} from "@/shared/network/outboundUrlGuard";
|
||||
import { getProviderValidationGuard } from "@/shared/network/outboundUrlGuardPolicy";
|
||||
import { isLocalProvider, isSelfHostedChatProvider } from "@/shared/constants/providers";
|
||||
// Header helpers extracted to a pure leaf; re-exported for external importers
|
||||
// (executors + tests) that import them from "./base.ts".
|
||||
export {
|
||||
@@ -397,6 +403,29 @@ export class BaseExecutor {
|
||||
return fallback || this.config.baseUrl || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* SSRF guard for the runtime dispatch path (GHSA-4f49-hj64-448x). A persisted,
|
||||
* caller-supplied `providerSpecificData.baseUrl` reaches the fetch() calls
|
||||
* below, so a `manage`-scope actor (or, on a keyless install, an anonymous
|
||||
* one) could point a provider at loopback / internal / cloud-metadata hosts
|
||||
* and exfiltrate the stored upstream key. Mirror the provider VALIDATION
|
||||
* guard so runtime dispatch makes the same decision the validation layer
|
||||
* already makes: local / self-hosted providers are exempt (they legitimately
|
||||
* use private URLs, and the OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS opt-in still
|
||||
* applies through the guard), and for everything else `public-only` mode
|
||||
* blocks private + metadata while the default `block-metadata` mode blocks the
|
||||
* cloud-metadata IMDS pivot. Throws on a blocked URL.
|
||||
*/
|
||||
protected assertOutboundUrlAllowed(url: string): void {
|
||||
if (!url) return;
|
||||
if (isLocalProvider(this.provider) || isSelfHostedChatProvider(this.provider)) return;
|
||||
if (getProviderValidationGuard() === "public-only") {
|
||||
parseAndValidatePublicUrl(url);
|
||||
return;
|
||||
}
|
||||
parseAndValidateNonMetadataUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate protocol selected on this connection, if the provider declares one
|
||||
* that matches. Centralizes the registry lookup so every call-site resolves the
|
||||
@@ -615,6 +644,7 @@ export class BaseExecutor {
|
||||
async countTokens({ model, body, credentials, signal, log }: CountTokensInput) {
|
||||
const url = this.buildCountTokensUrl(model, credentials);
|
||||
if (!url) return null;
|
||||
this.assertOutboundUrlAllowed(url); // GHSA-4f49
|
||||
|
||||
const headers = this.buildHeaders(credentials, false);
|
||||
const requestBody =
|
||||
@@ -869,6 +899,9 @@ export class BaseExecutor {
|
||||
// Timeout only covers response start; stream stalls are handled downstream.
|
||||
const fetchStartTimeoutMs = this.getTimeoutMs();
|
||||
const fetchWithStartTimeout = async (requestUrl: string, requestOptions: RequestInit) => {
|
||||
// GHSA-4f49: guard here (not only next to the first buildUrl) so retries
|
||||
// and fallback URLs are validated too, before any bytes leave the host.
|
||||
this.assertOutboundUrlAllowed(requestUrl);
|
||||
const timeoutController = fetchStartTimeoutMs > 0 ? new AbortController() : null;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
if (timeoutController) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -430,6 +430,7 @@ export class GlmExecutor extends DefaultExecutor {
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
this.assertOutboundUrlAllowed(url); // GHSA-4f49: glm has its own fetch path
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
|
||||
@@ -33,7 +33,7 @@ import { createHash } from "node:crypto";
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { makeExecutorErrorResult as makeErrorResult, sanitizeErrorMessage } from "../utils/error.ts";
|
||||
|
||||
const BASE_URL = "https://www.hailuo.ai";
|
||||
const BASE_URL = "https://chat.minimax.io";
|
||||
const API_PATH = "/v4/api/chat/msg";
|
||||
const USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
||||
|
||||
@@ -471,6 +471,7 @@ export class NlpCloudExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
try {
|
||||
this.assertOutboundUrlAllowed(url); // GHSA-4f49: nlpcloud has its own fetch path
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { getModelTargetFormat } from "../config/providerModels.ts";
|
||||
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
|
||||
import {
|
||||
injectReasoningContentForThinkingModel,
|
||||
isThinkingMessageModel,
|
||||
@@ -125,6 +125,24 @@ export function isPremiumOpencodeModel(model: string, provider: string): boolean
|
||||
return !OPENCODE_FREE_MODELS.has(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the registry `targetFormat` for a model, aliasing `provider` first.
|
||||
*
|
||||
* `PROVIDER_MODELS` is keyed by the provider's public ALIAS (e.g. `"oc"`), not its
|
||||
* raw registry id (e.g. `"opencode"`) — mirrors `resolveChatCoreTargetFormat()`
|
||||
* (`handlers/chatCore/targetFormat.ts`), which already aliases before calling
|
||||
* `getModelTargetFormat()`. Calling it with the raw id here made every entry miss
|
||||
* silently (fell through to `"openai"`), while chatCore's own request-body
|
||||
* translation (correctly aliased) still switched to the Responses API shape for
|
||||
* `targetFormat:"openai-responses"` models — sending a Responses-shaped body to
|
||||
* the `/chat/completions` URL this executor's own `buildUrl()` kept selecting.
|
||||
* Exported for testability.
|
||||
*/
|
||||
export function resolveOpencodeTargetFormat(provider: string, model: string): string {
|
||||
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||
return getModelTargetFormat(alias, model) || "openai";
|
||||
}
|
||||
|
||||
export class OpencodeExecutor extends BaseExecutor {
|
||||
/** Delegates to `isPremiumOpencodeModel`. Exported for testability. */
|
||||
static isPremiumModel(model: string, provider: string): boolean {
|
||||
@@ -193,7 +211,10 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
return pickRotatableAccount(this.accounts, this);
|
||||
}
|
||||
|
||||
private markCooldown(account: OpencodeAccountState, kind: "transient" | "terminal" = "transient"): void {
|
||||
private markCooldown(
|
||||
account: OpencodeAccountState,
|
||||
kind: "transient" | "terminal" = "transient"
|
||||
): void {
|
||||
markAccountCooldown(account, kind);
|
||||
}
|
||||
|
||||
@@ -202,7 +223,7 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
async execute(input: ExecuteInput) {
|
||||
this._requestFormat = getModelTargetFormat(this.provider, input.model) || "openai";
|
||||
this._requestFormat = resolveOpencodeTargetFormat(this.provider, input.model);
|
||||
|
||||
// #8681: Gate premium opencode models behind a usable API key.
|
||||
// When the connection is keyless (no apiKey, no accessToken) and the model
|
||||
|
||||
@@ -10,16 +10,24 @@ import {
|
||||
import { resolveMcpCallerApiKeyId } from "../mcpCallerIdentity.ts";
|
||||
|
||||
/**
|
||||
* Resolve the memory owner id for an MCP tool call:
|
||||
* explicit arg wins, otherwise fall back to the authenticated caller's
|
||||
* principal id (HTTP auth headers on SSE/Streamable HTTP transports,
|
||||
* OMNIROUTE_API_KEY env var on stdio). Keeps MCP-stored memories under
|
||||
* the same owner id that chat-context memory uses, so retrieval in the
|
||||
* chat pipeline finds entries written via MCP.
|
||||
* Resolve the memory owner id for an MCP tool call.
|
||||
*
|
||||
* The authenticated caller's principal ALWAYS wins over a caller-supplied
|
||||
* `apiKeyId` — otherwise any MCP caller could read, write, or delete another
|
||||
* principal's memories by putting a different id in the tool arguments
|
||||
* (GHSA-cpv3-xr7r-xf8q, IDOR). The caller is resolved from the per-request HTTP
|
||||
* auth headers on SSE / Streamable HTTP transports, or from OMNIROUTE_API_KEY on
|
||||
* stdio. The explicit argument is only honored as a fallback when no caller can
|
||||
* be resolved (a bare local stdio process with no configured key — already
|
||||
* trusted), preserving the local-tooling flow. Keeps MCP-stored memories under
|
||||
* the same owner id that chat-context memory uses, so retrieval in the chat
|
||||
* pipeline finds entries written via MCP.
|
||||
*/
|
||||
async function resolveMemoryOwnerId(explicit?: string): Promise<string> {
|
||||
const caller = await resolveMcpCallerApiKeyId().catch(() => undefined);
|
||||
if (caller) return caller;
|
||||
if (explicit && explicit.trim() !== "") return explicit.trim();
|
||||
return (await resolveMcpCallerApiKeyId().catch(() => undefined)) || "mcp";
|
||||
return "mcp";
|
||||
}
|
||||
|
||||
export const MemorySearchSchema = z.object({
|
||||
|
||||
@@ -607,7 +607,7 @@ export async function prepareVirtualAutoComboInputs(
|
||||
// remaining allowance as a percentage, and a raw ">0" comparison would
|
||||
// let a reading of e.g. 0.3% (rounding noise, not real headroom) pass.
|
||||
minRemainingAllowance: 1,
|
||||
maxStateAgeMs: (settings.autoRefreshProviderQuotaInterval ?? 180) * 1000,
|
||||
maxStateAgeMs: (Number(settings.autoRefreshProviderQuotaInterval) || 180) * 1000,
|
||||
});
|
||||
if (strictFilteredPool !== pool) pool = strictFilteredPool;
|
||||
|
||||
|
||||
@@ -128,6 +128,75 @@ function tryParseJson(raw: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a tool_call `arguments` string that is actually multiple back-to-back JSON
|
||||
* objects glued together with no separator, into its individual object substrings.
|
||||
*
|
||||
* Root cause (observed on opencode/muse-spark-1.2-contributor-free via the zen
|
||||
* provider): some upstreams never vary `index`/`id` across a 2nd/3rd/… tool_call of
|
||||
* the SAME name emitted in one turn, so every delta in `buildOpenAISummary` above
|
||||
* resolves to the same accumulator key and `arguments` ends up as N JSON objects
|
||||
* concatenated with no delimiter — invalid as a single JSON value, but each object is
|
||||
* individually well-formed. Structural, not provider-specific: applies to whichever
|
||||
* upstream exhibits the same index-collision streaming bug.
|
||||
*
|
||||
* Returns `null` when `raw` is empty, already valid single JSON, or does not scan as
|
||||
* ≥2 back-to-back valid JSON values — callers must leave `arguments` untouched in
|
||||
* that case (never regress a value that used to reach the client as-is).
|
||||
*/
|
||||
export function splitConcatenatedToolCallArguments(raw: string): string[] | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
JSON.parse(raw);
|
||||
return null; // Already a single valid JSON value — nothing to split.
|
||||
} catch {
|
||||
// Fall through to the multi-value scan below.
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
let start = -1;
|
||||
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const ch = raw[i];
|
||||
if (start === -1) {
|
||||
if (ch === " " || ch === "\n" || ch === "\r" || ch === "\t") continue;
|
||||
if (ch !== "{" && ch !== "[") return null; // Not a value boundary — bail, leave untouched.
|
||||
start = i;
|
||||
}
|
||||
if (inString) {
|
||||
if (escaped) escaped = false;
|
||||
else if (ch === "\\") escaped = true;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === "{" || ch === "[") depth++;
|
||||
else if (ch === "}" || ch === "]") {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
parts.push(raw.slice(start, i + 1));
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (start !== -1 || depth !== 0 || parts.length < 2) return null;
|
||||
|
||||
for (const part of parts) {
|
||||
try {
|
||||
JSON.parse(part);
|
||||
} catch {
|
||||
return null; // One of the scanned segments isn't valid JSON — bail entirely.
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
// ─── Per-format live reducers ────────────────────────────────────────────────
|
||||
// Each reducer mirrors the corresponding build*Summary()'s original for-loop
|
||||
// body exactly (ingest = one loop iteration, finalize = the post-loop return),
|
||||
@@ -262,7 +331,27 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
message.reasoning_content = joinedReasoning;
|
||||
}
|
||||
|
||||
const finalToolCalls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
|
||||
const mergedToolCalls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
|
||||
// Expand any entry whose accumulated `arguments` turned out to be multiple
|
||||
// concatenated JSON objects (upstream never varied index/id across repeated
|
||||
// same-name tool_calls) into its own separate tool_calls entries.
|
||||
const finalToolCalls: ToolCall[] = [];
|
||||
let nextIndex = 0;
|
||||
for (const tc of mergedToolCalls) {
|
||||
const splitArgs = splitConcatenatedToolCallArguments(tc.function.arguments);
|
||||
if (!splitArgs) {
|
||||
finalToolCalls.push({ ...tc, index: nextIndex++ });
|
||||
continue;
|
||||
}
|
||||
for (const [i, args] of splitArgs.entries()) {
|
||||
finalToolCalls.push({
|
||||
id: tc.id ? `${tc.id}_split${i}` : null,
|
||||
index: nextIndex++,
|
||||
type: tc.type,
|
||||
function: { name: tc.function.name, arguments: args },
|
||||
});
|
||||
}
|
||||
}
|
||||
if (finalToolCalls.length > 0) {
|
||||
finishReason = "tool_calls";
|
||||
message.tool_calls = finalToolCalls;
|
||||
|
||||
@@ -131,12 +131,12 @@ function runNextBuild() {
|
||||
}
|
||||
|
||||
export function resolveNextBuildBundlerFlag(baseEnv = process.env) {
|
||||
// Turbopack is the default production bundler (Next 16 stable). Benchmarked on
|
||||
// this codebase: 2-3x faster than the single-threaded webpack pass (17min -> 9min
|
||||
// on a 32-core box; ~20min -> 7min on ubuntu-latest), artifact validated
|
||||
// end-to-end (standalone smoke + e2e/package/electron CI jobs). Webpack stays as
|
||||
// the explicit escape hatch (=0) for bundler-compat regressions.
|
||||
return baseEnv.OMNIROUTE_USE_TURBOPACK === "0" ? "--webpack" : "--turbopack";
|
||||
// Turbopack is the default on Node.js; on Bun or when explicitly disabled (=0),
|
||||
// use Webpack (--webpack) to avoid Turbopack V8 internal worker API mismatches.
|
||||
if (process.versions.bun || baseEnv.OMNIROUTE_USE_TURBOPACK === "0") {
|
||||
return "--webpack";
|
||||
}
|
||||
return "--turbopack";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,12 @@ if (!support.nodeCompatible) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Node.js ${support.nodeVersion} satisfies OmniRoute secure runtime policy (${support.supportedRange}).`
|
||||
);
|
||||
if (process.versions.bun) {
|
||||
console.log(
|
||||
`Bun ${process.versions.bun} (${support.nodeVersion}) satisfies OmniRoute secure runtime policy.`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Node.js ${support.nodeVersion} satisfies OmniRoute secure runtime policy (${support.supportedRange}).`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,8 +83,10 @@ const { dashboardPort } = runtimePorts;
|
||||
const hostname = process.env.HOST || "0.0.0.0";
|
||||
// Turbopack by default in dev (matches the Next 16 CLI default and the production
|
||||
// build default in build-next-isolated.mjs); OMNIROUTE_USE_TURBOPACK=0 is the
|
||||
// webpack escape hatch.
|
||||
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0";
|
||||
// webpack escape hatch. Under Bun, Turbopack native V8 bindings are unavailable,
|
||||
// so Bun automatically disables Turbopack and uses Webpack.
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0" && !isBun;
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret used to prove the trusted peer-IP stamp came from this
|
||||
// server (read by the authz middleware in the same process). See peer-stamp.mjs.
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function AddApiKeyModal({
|
||||
if (!isOpen || wasOpen) return;
|
||||
// On open, reset baseUrl and assign a unique default name so a second API key
|
||||
// for the same provider doesn't reuse "main" and trigger the backend
|
||||
// name-based upsert that would silently overwrite the first connection (#6499).
|
||||
// name-based upsert that would silently overwrite the first connection (#6499, #11033).
|
||||
setFormData((current) => ({
|
||||
...current,
|
||||
name: computeConnectionDefaultName(existingConnectionCount),
|
||||
@@ -757,6 +757,12 @@ export default function AddApiKeyModal({
|
||||
type="password"
|
||||
value={formData.apiKey}
|
||||
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !validating && !saving) {
|
||||
e.preventDefault();
|
||||
handleValidate();
|
||||
}
|
||||
}}
|
||||
className="flex-1"
|
||||
placeholder={apiCredentialPlaceholder}
|
||||
hint={apiCredentialHint}
|
||||
|
||||
@@ -4,7 +4,23 @@
|
||||
// connection. Deriving a unique default from the existing connection count keeps
|
||||
// the first connection ("main") backward-compatible while giving each subsequent
|
||||
// one a distinct name ("main-2", "main-3", …).
|
||||
export function computeConnectionDefaultName(existingConnectionCount?: number): string {
|
||||
const count = existingConnectionCount ?? 0;
|
||||
export function computeConnectionDefaultName(
|
||||
existingConnectionCountOrConnections?: number | string[] | { name?: string }[]
|
||||
): string {
|
||||
if (Array.isArray(existingConnectionCountOrConnections)) {
|
||||
const names = new Set(
|
||||
existingConnectionCountOrConnections
|
||||
.map((item) => (typeof item === "string" ? item : item?.name ?? ""))
|
||||
.filter(Boolean)
|
||||
);
|
||||
if (!names.has("main")) return "main";
|
||||
let index = 2;
|
||||
while (names.has(`main-${index}`)) {
|
||||
index++;
|
||||
}
|
||||
return `main-${index}`;
|
||||
}
|
||||
|
||||
const count = existingConnectionCountOrConnections ?? 0;
|
||||
return count <= 0 ? "main" : `main-${count + 1}`;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import { logRoutingDecision } from "@/lib/a2a/routingLogger";
|
||||
import { createA2AStream, SSE_HEADERS } from "@/lib/a2a/streaming";
|
||||
import { A2A_SKILL_HANDLERS, executeA2ATaskWithState } from "@/lib/a2a/taskExecution";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
|
||||
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
|
||||
|
||||
// ============ A2A v1.0 ↔ v0.3 compatibility layer ============
|
||||
// A2A 1.0 renamed the JSON-RPC methods (message/send → SendMessage,
|
||||
@@ -136,14 +138,25 @@ function tokensMatch(provided: string, expected: string): boolean {
|
||||
return timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
function authenticate(req: NextRequest): boolean {
|
||||
// If no API key is configured, allow all requests
|
||||
const configuredKey = process.env.OMNIROUTE_API_KEY;
|
||||
if (!configuredKey) return true;
|
||||
async function authenticate(req: NextRequest): Promise<boolean> {
|
||||
// /a2a is outside the authz proxy matcher, so the REQUIRE_API_KEY posture the
|
||||
// pipeline enforces for /v1 never ran here — the route accepted every caller
|
||||
// whenever OMNIROUTE_API_KEY was unset, which is the shipped default
|
||||
// (GHSA-v54m-6rm3-p565). Apply the same posture directly: when a client key is
|
||||
// required, demand a valid OmniRoute key; otherwise honor the legacy explicit
|
||||
// A2A key; otherwise stay keyless (the same local-first default as /v1).
|
||||
const apiKey = extractApiKey(req);
|
||||
if (isRequireApiKeyEnabled()) {
|
||||
return apiKey ? await isValidApiKey(apiKey) : false;
|
||||
}
|
||||
|
||||
const authHeader = req.headers.get("authorization") || "";
|
||||
const token = authHeader.replace(/^Bearer\s+/i, "");
|
||||
return tokensMatch(token, configuredKey);
|
||||
const configuredKey = process.env.OMNIROUTE_API_KEY;
|
||||
if (configuredKey) {
|
||||
return apiKey ? tokensMatch(apiKey, configuredKey) : false;
|
||||
}
|
||||
|
||||
// No API key required and none configured — allow (keyless local-first).
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============ JSON-RPC Helpers ============
|
||||
@@ -179,7 +192,7 @@ async function rejectIfA2ADisabled(id: string | number | null) {
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// Auth check
|
||||
if (!authenticate(req)) {
|
||||
if (!(await authenticate(req))) {
|
||||
return jsonRpcError(null, -32600, "Unauthorized: missing or invalid API key");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
type CliAgentInfo,
|
||||
detectInstalledAgents,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getAllRateLimitStatus } from "@omniroute/open-sse/services/rateLimitManager.ts";
|
||||
import {
|
||||
getStats as getSemaphoreStats,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getCompressionAnalyticsSummary } from "@/lib/db/compressionAnalytics";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { issueDashboardCsrfToken } from "@/server/authz/csrf";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { cookies } from "next/headers";
|
||||
import { jwtVerify } from "jose";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getBatch } from "@/lib/localDb";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { listBatches } from "@/lib/localDb";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
|
||||
1
src/app/api/cache/entries/route.ts
vendored
1
src/app/api/cache/entries/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
listSemanticCacheEntries,
|
||||
|
||||
1
src/app/api/cache/reasoning/route.ts
vendored
1
src/app/api/cache/reasoning/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
clearReasoningCacheAll,
|
||||
|
||||
1
src/app/api/cache/route.ts
vendored
1
src/app/api/cache/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import {
|
||||
getCacheStats,
|
||||
clearCache,
|
||||
|
||||
1
src/app/api/cache/stats/route.ts
vendored
1
src/app/api/cache/stats/route.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { clearMemoryCache, getMemoryCacheStats } from "@/lib/semanticCache";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
@@ -38,7 +38,14 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const url = new URL(request.url);
|
||||
const scope: LeaderboardScope = (url.searchParams.get("scope") || "global") as LeaderboardScope;
|
||||
const limit = Number(url.searchParams.get("limit") || 100);
|
||||
const rawLimit = url.searchParams.get("limit");
|
||||
const limit = rawLimit === null ? 100 : Number(rawLimit);
|
||||
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
||||
return NextResponse.json(
|
||||
{ error: "'limit' must be an integer between 1 and 200" },
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
const entries = await getTopN(scope, limit);
|
||||
|
||||
|
||||
@@ -18,10 +18,18 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const url = new URL(request.url);
|
||||
const scope = (url.searchParams.get("scope") || "global") as LeaderboardScope;
|
||||
const limit = Number(url.searchParams.get("limit") || 50);
|
||||
const rawLimit = url.searchParams.get("limit");
|
||||
const limit = rawLimit === null ? 50 : Number(rawLimit);
|
||||
const apiKeyId = url.searchParams.get("apiKeyId");
|
||||
|
||||
const entries = await getTopN(scope, Math.min(limit, 200));
|
||||
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
||||
return NextResponse.json(
|
||||
{ error: "'limit' must be an integer between 1 and 200" },
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
const entries = await getTopN(scope, limit);
|
||||
let myRank: number | null = null;
|
||||
let neighbors = null;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { readRunningBuildSha } from "@/lib/monitoring/buildSha";
|
||||
import { APP_CONFIG } from "@/shared/constants/config";
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
/**
|
||||
* GET /api/monitoring/health — System health overview
|
||||
@@ -20,10 +21,25 @@ import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
let healthPayloadCache: { payload: unknown; expiresAt: number } | null = null;
|
||||
const HEALTH_PAYLOAD_TTL_MS = 1000;
|
||||
|
||||
export async function GET() {
|
||||
// GHSA-mvf8-qc78-5mxm: the full health payload fingerprints the host (version,
|
||||
// node version, pid, memory, provider config). An anonymous caller — the common
|
||||
// case on a keyless install, and what a liveness/load-balancer probe needs — gets
|
||||
// only the liveness verdict; the detail is reserved for a management principal.
|
||||
function publicHealthView(payload: unknown): Record<string, unknown> {
|
||||
const p = (payload ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
status: p.status ?? "unknown",
|
||||
...(p.setupComplete !== undefined ? { setupComplete: p.setupComplete } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const fullView = (await requireManagementAuth(request, { alwaysRequireAuth: true })) === null;
|
||||
const cachedNow = Date.now();
|
||||
if (healthPayloadCache && cachedNow <= healthPayloadCache.expiresAt) {
|
||||
return NextResponse.json(healthPayloadCache.payload);
|
||||
return NextResponse.json(
|
||||
fullView ? healthPayloadCache.payload : publicHealthView(healthPayloadCache.payload)
|
||||
);
|
||||
}
|
||||
|
||||
const readHealthValue = <T>(label: string, reader: () => T, fallback: T): T => {
|
||||
@@ -187,7 +203,7 @@ export async function GET() {
|
||||
});
|
||||
|
||||
healthPayloadCache = { payload, expiresAt: Date.now() + HEALTH_PAYLOAD_TTL_MS };
|
||||
return NextResponse.json(payload);
|
||||
return NextResponse.json(fullView ? payload : publicHealthView(payload));
|
||||
} catch (error) {
|
||||
console.error("[API] GET /api/monitoring/health error:", error);
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { isValidGheUrl } from "@/shared/validation/providerSpecificData";
|
||||
import { AWS_REGION_PATTERN } from "@/lib/oauth/constants/oauth";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { startLocalServer } from "@/lib/oauth/utils/server";
|
||||
import { runWithProxyContextOrDirect } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
@@ -221,6 +222,16 @@ export async function GET(
|
||||
(requestDeviceCode as any)(provider, null, providerOverrideConfig)
|
||||
);
|
||||
} else if ((provider === "kiro" || provider === "amazon-q") && startUrl) {
|
||||
// GHSA-7x63: `region` is interpolated into the AWS OIDC endpoint URLs
|
||||
// below, which requestDeviceCode() then fetches. Validate it against the
|
||||
// canonical AWS region shape before it can steer the outbound host to an
|
||||
// attacker-chosen target (userinfo/fragment tricks → SSRF / metadata).
|
||||
if (!AWS_REGION_PATTERN.test(region)) {
|
||||
return NextResponse.json(
|
||||
{ error: "region must be a valid AWS region (e.g. us-east-1)" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const providerOverrideConfig = {
|
||||
...providerData.config,
|
||||
startUrl,
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "path";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { createProviderConnection } from "@/models";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import {
|
||||
scanCliProxyAuthDir,
|
||||
@@ -23,9 +23,9 @@ function cliProxyConfigDir(): string {
|
||||
}
|
||||
|
||||
async function requireImportAuth(request: Request) {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { extractCodexAccountInfo } from "@/lib/oauth/services/codexImport";
|
||||
import { parseCodexSessionJson } from "@/lib/oauth/utils/codexSessionImport";
|
||||
import { createProviderConnection } from "@/models";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
/**
|
||||
@@ -93,10 +93,11 @@ async function parseRequestBody(
|
||||
return { ok: true, resolved: resolved.resolved };
|
||||
}
|
||||
|
||||
async function requireAuth(request: Request): Promise<NextResponse | null> {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json(buildErrorBody(401, "Unauthorized"), { status: 401 });
|
||||
async function requireAuth(request: Request): Promise<Response | null> {
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action.
|
||||
// Require management scope (or a dashboard session) rather than accepting any
|
||||
// valid client key, which the PUBLIC /api/oauth/ classification otherwise allows.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { normalizeCodexImportRecord, flattenCodexImportPayload } from "@/lib/oauth/services/codexImport";
|
||||
import { createProviderConnection } from "@/models";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { refreshCodexToken, isUnrecoverableRefreshError } from "@omniroute/open-sse/services/tokenRefresh.ts";
|
||||
|
||||
@@ -82,10 +82,10 @@ const bodySchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
async function requireAuth(request: Request): Promise<NextResponse | null> {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
async function requireAuth(request: Request): Promise<Response | null> {
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { tryAgentAuth, tryIdeAuth } from "@/lib/cursor/tokenExtractor";
|
||||
|
||||
/**
|
||||
@@ -11,11 +11,9 @@ import { tryAgentAuth, tryIdeAuth } from "@/lib/cursor/tokenExtractor";
|
||||
* 🔒 Auth-guarded: requires JWT cookie or Bearer API key (finding #258-4).
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
if (await isAuthRequired(request)) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
// GHSA-mg76 / GHSA-gxv4: reading/importing host credentials is a management action.
|
||||
const authError = await requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
// Try Cursor IDE first (has both accessToken and machineId)
|
||||
|
||||
@@ -6,15 +6,15 @@ import { isCloudEnabled } from "@/models";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { cursorImportSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { resolveProxyForProvider } from "@/models";
|
||||
|
||||
async function requireOAuthImportAuth(request: Request) {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import {
|
||||
createProviderConnection,
|
||||
getProviderConnections,
|
||||
@@ -31,11 +31,9 @@ import {
|
||||
* 🔒 Auth-guarded: requires JWT cookie or Bearer API key.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
if (await isAuthRequired(request)) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
// GHSA-mg76 / GHSA-gxv4: reading/importing host credentials is a management action.
|
||||
const authError = await requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
if (authError) return authError;
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const targetProvider = searchParams.get("targetProvider") === "amazon-q" ? "amazon-q" : "kiro";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { kiroImportSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { findKiroConnectionByIdentity } from "@/lib/oauth/kiroConnectionIdentity";
|
||||
@@ -38,9 +38,9 @@ export function buildKiroImportError(error: unknown): string {
|
||||
}
|
||||
|
||||
async function requireOAuthImportAuth(request: Request) {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
async function upsertImportedKiroConnection(
|
||||
|
||||
@@ -14,14 +14,14 @@ import {
|
||||
extractLocalRaycastCredentials,
|
||||
isRaycastLocalExtractAvailable,
|
||||
} from "@/lib/oauth/services/raycastLocal";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { resolveProxyForProvider } from "@/models";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
|
||||
async function requireOAuthImportAuth(request: Request) {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -11,14 +11,14 @@ import { createProviderConnection } from "@/models";
|
||||
import { RaycastService } from "@/lib/oauth/services/raycast";
|
||||
import { raycastImportSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { resolveProxyForProvider } from "@/models";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
|
||||
async function requireOAuthImportAuth(request: Request) {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { createProviderConnection } from "@/models";
|
||||
import { traeImportSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
/**
|
||||
* POST /api/oauth/trae/import
|
||||
@@ -22,9 +22,9 @@ import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
* region — optional, default "US-East"
|
||||
*/
|
||||
async function requireOAuthImportAuth(request: Request) {
|
||||
if (!(await isAuthRequired(request))) return null;
|
||||
if (await isAuthenticated(request)) return null;
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
// GHSA-mg76: importing a provider connection is a state-mutating admin action;
|
||||
// require management scope (or a dashboard session), not any valid client key.
|
||||
return requireManagementAuth(request, { invalidApiKeyStatus: 401 });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
isAnthropicCompatibleProvider,
|
||||
} from "@/shared/constants/providers";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { providerModelMutationSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index";
|
||||
import {
|
||||
getProviderAuditTarget,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
import {
|
||||
getObsidianSyncStatus,
|
||||
@@ -21,10 +22,19 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const status = await getObsidianSyncStatus();
|
||||
// GHSA-62vw: the WebDAV password is reusable authentication material. Return
|
||||
// the plaintext only to a genuine management principal (dashboard session or
|
||||
// manage-scope key), never to an anonymous caller that reached this handler
|
||||
// through the requireLogin=false open mode. The dashboard's authenticated
|
||||
// reveal-password view is unaffected; anonymous callers get a set/unset flag.
|
||||
const hasManagement =
|
||||
(await requireManagementAuth(request, { alwaysRequireAuth: true })) === null;
|
||||
return NextResponse.json({
|
||||
webdavEnabled: status.webdavEnabled,
|
||||
webdavUsername: status.webdavEnabled ? status.webdavUsername : null,
|
||||
webdavPassword: status.webdavEnabled ? status.webdavPassword : null,
|
||||
webdavPassword:
|
||||
status.webdavEnabled && hasManagement ? status.webdavPassword : null,
|
||||
webdavPasswordSet: status.webdavEnabled && Boolean(status.webdavPassword),
|
||||
vaultPath: status.vaultPath,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getProviderById } from "@/shared/constants/providers";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getApiKeys } from "@/lib/db/apiKeys";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
export const dynamic = "force-dynamic";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getCallLogs } from "@/lib/usageDb";
|
||||
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
* returning combo metadata to API-key callers. Kept in a separate module so
|
||||
* the projection can be unit-tested without spinning up the Next.js route.
|
||||
*
|
||||
* #10968: the projection also reports `accountPinned` per model step — a boolean
|
||||
* derived from the stripped `connectionId`, so callers can distinguish a combo
|
||||
* that fails over between two accounts of one provider from a duplicated step.
|
||||
*
|
||||
* #3979: client-facing combo catalogs (the `/v1/combos`, VS Code and LobeHub /
|
||||
* OpenCode import surfaces) can opt into advertising the combo's resolved
|
||||
* capabilities (multimodal / reasoning / caching) so importing clients enable
|
||||
@@ -17,6 +21,19 @@ export interface PublicComboStep {
|
||||
model?: string;
|
||||
comboName?: string;
|
||||
providerId?: string;
|
||||
/**
|
||||
* #10968: whether this step pins one specific account of its provider.
|
||||
*
|
||||
* Two steps that pin different accounts of the same provider project to
|
||||
* identical `{kind, model, providerId}` objects, so a client cannot tell a
|
||||
* two-account failover from the same step listed twice. This says which it
|
||||
* is without exposing the `connectionId` the flag is derived from — not even
|
||||
* a prefix, per the issue.
|
||||
*
|
||||
* Set on every `model` step. Absent on `combo-ref`, which routes through
|
||||
* another combo and has no account of its own.
|
||||
*/
|
||||
accountPinned?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +83,10 @@ export function projectComboStep(step: Record<string, unknown>): PublicComboStep
|
||||
if (typeof step.providerId === "string" && step.providerId.length > 0) {
|
||||
out.providerId = step.providerId;
|
||||
}
|
||||
// Same shape test as providerId above. `cleanupComboConnectionRefs` drops the
|
||||
// key when the connection is deleted, so a step whose pinned account is gone
|
||||
// reports false rather than pointing at nothing.
|
||||
out.accountPinned = typeof step.connectionId === "string" && step.connectionId.length > 0;
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -68,7 +68,7 @@ async function postHandler(request, context) {
|
||||
|
||||
const response = await handleModeration({ body: { ...body, model }, credentials });
|
||||
if (response?.ok) {
|
||||
await clearRecoveredProviderState(credentials);
|
||||
await clearRecoveredProviderState(credentials as Record<string, unknown>);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -15,22 +15,15 @@ import { encryptMetadata } from "@/lib/webhookDispatcher";
|
||||
import { isEncryptionEnabled } from "@/lib/db/encryption";
|
||||
import { parseAndValidateWebhookUrl } from "@/shared/network/outboundUrlGuardPolicy";
|
||||
|
||||
import { WEBHOOK_EVENT_VALUES } from "@/lib/webhooks/eventDescriptions";
|
||||
|
||||
const WEBHOOK_KINDS = ["slack", "telegram", "discord", "custom"] as const;
|
||||
const WEBHOOK_EVENT_VALUES = [
|
||||
"*",
|
||||
"request.completed",
|
||||
"request.failed",
|
||||
"provider.error",
|
||||
"provider.recovered",
|
||||
"quota.exceeded",
|
||||
"combo.switched",
|
||||
"test.ping",
|
||||
] as const;
|
||||
const WEBHOOK_EVENT_VALUES_WITH_WILDCARD = ["*", ...WEBHOOK_EVENT_VALUES] as const;
|
||||
|
||||
const updateWebhookSchema = z
|
||||
.object({
|
||||
url: z.string().min(1).max(2000).optional(),
|
||||
events: z.array(z.enum(WEBHOOK_EVENT_VALUES)).optional(),
|
||||
events: z.array(z.enum(WEBHOOK_EVENT_VALUES_WITH_WILDCARD)).optional(),
|
||||
secret: z.string().max(500).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
@@ -14,12 +14,15 @@ import { encryptMetadata } from "@/lib/webhookDispatcher";
|
||||
import { isEncryptionEnabled } from "@/lib/db/encryption";
|
||||
import { parseAndValidateWebhookUrl } from "@/shared/network/outboundUrlGuardPolicy";
|
||||
|
||||
import { WEBHOOK_EVENT_VALUES } from "@/lib/webhooks/eventDescriptions";
|
||||
|
||||
const WEBHOOK_KINDS = ["slack", "telegram", "discord", "custom"] as const;
|
||||
const WEBHOOK_EVENT_VALUES_WITH_WILDCARD = ["*", ...WEBHOOK_EVENT_VALUES] as const;
|
||||
|
||||
const createWebhookSchema = z
|
||||
.object({
|
||||
url: z.string().min(1).max(2000),
|
||||
events: z.array(z.string()).optional().default(["*"]),
|
||||
events: z.array(z.enum(WEBHOOK_EVENT_VALUES_WITH_WILDCARD)).optional().default(["*"]),
|
||||
secret: z.string().max(500).optional(),
|
||||
description: z.string().max(1000).optional().default(""),
|
||||
kind: z.enum(WEBHOOK_KINDS).optional().default("custom"),
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
* Kubernetes-style readiness alias of /healthz.
|
||||
* Same lifecycle phase, same 200/503 bodies. Not a liveness probe.
|
||||
*/
|
||||
export { dynamic, GET, HEAD } from "../healthz/route";
|
||||
export const dynamic = "force-dynamic";
|
||||
export { GET, HEAD } from "../healthz/route";
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "خطافات الويب",
|
||||
"description": "تسجيل، وعرض، واختبار، وإزالة نقاط نهاية webhook. تكوين اشتراكات الأحداث (request.completed، وprovider.error، وbudget.exceeded، إلخ) وإدارة محاولات التسليم."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "خادم MCP",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Vebhuklar",
|
||||
"description": "Vebhuk son nöqtələrini qeydiyyatdan keçirin, siyahıya alın, sınaqdan keçirin və silin. Hadisə abunəliklərini (request.completed, provider.error, budget.exceeded və s.) konfiqurasiya edin və çatdırılmanın təkrar cəhdlərini idarə edin."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP Serveri",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Уебхукове",
|
||||
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP сървър",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "ওয়েবহুক",
|
||||
"description": "ওয়েবহুক এন্ডপয়েন্ট রেজিস্টার, তালিকাভুক্ত, পরীক্ষা এবং অপসারণ করুন। ইভেন্ট সাবস্ক্রিপশন (request.completed, provider.error, budget.exceeded ইত্যাদি) কনফিগার করুন এবং ডেলিভারি রিট্রাই পরিচালনা করুন।"
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP সার্ভার",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooky",
|
||||
"description": "Registrujte, zobrazujte, testujte a odebírejte koncové body webhooků. Konfigurujte odběry událostí (request.completed, provider.error, budget.exceeded atd.) a spravujte opakování doručení."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP server",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooks",
|
||||
"description": "Registrer, list, test og fjern webhook-slutpunkter. Konfigurer begivenhedsabonnementer (request.completed, provider.error, budget.exceeded osv.) og administrer leveringsforsøg."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP-server",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooks",
|
||||
"description": "Webhook-Endpunkte registrieren, auflisten, testen und entfernen. Ereignis-Abonnements (request.completed, provider.error, budget.exceeded etc.) konfigurieren und Zustellungsversuche verwalten."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP-Server",
|
||||
|
||||
@@ -12207,7 +12207,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooks",
|
||||
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries."
|
||||
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP Server",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooks",
|
||||
"description": "Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP Server",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "وبهوکها",
|
||||
"description": "ثبت، فهرست کردن، آزمایش و حذف نقاط پایانی وبهوک. پیکربندی اشتراکهای رویداد (request.completed، provider.error، budget.exceeded و غیره) و مدیریت تلاشهای مجدد تحویل."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "سرور MCP",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhookit",
|
||||
"description": "Rekisteröi, listaa, testaa ja poista webhook-päätepisteitä. Määritä tapahtumatilaukset (request.completed, provider.error, budget.exceeded jne.) ja hallitse toimituksen uudelleenyrityksiä."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP-palvelin",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooks",
|
||||
"description": "Enregistrer, lister, tester et supprimer des points de terminaison de webhook. Configurer les abonnements aux événements (request.completed, provider.error, budget.exceeded, etc.) et gérer les tentatives de livraison."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "Serveur MCP",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "વેબહૂક્સ",
|
||||
"description": "વેબહૂક એન્ડપોઇન્ટ્સ રજીસ્ટર કરો, સૂચિબદ્ધ કરો, ટેસ્ટ કરો અને દૂર કરો. ઇવેન્ટ સબ્સ્ક્રિપ્શન્સ (request.completed, provider.error, budget.exceeded, વગેરે) કન્ફિગર કરો અને ડિલિવરી પુનઃપ્રયાસો મેનેજ કરો."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP સર્વર",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhooks",
|
||||
"description": "רשום, הצג, בדוק והסר נקודות קצה של webhook. הגדר מנויים לאירועים (request.completed, provider.error, budget.exceeded וכו') ונהל ניסיונות מסירה חוזרים."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "שרת MCP",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "वेबहुक",
|
||||
"description": "वेबहुक एंडपॉइंट्स को रजिस्टर, सूचीबद्ध, टेस्ट और रिमूव करें। इवेंट सब्सक्रिप्शन (request.completed, provider.error, budget.exceeded, आदि) कॉन्फ़िगर करें और डिलीवरी रीट्राय प्रबंधित करें।"
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP सर्वर",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhookok",
|
||||
"description": "Webhook-végpontok regisztrálása, listázása, tesztelése és eltávolítása. Eseményfeliratkozások (request.completed, provider.error, budget.exceeded stb.) konfigurálása és a kézbesítési újrapróbálkozások kezelése."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "MCP-szerver",
|
||||
|
||||
@@ -12202,7 +12202,7 @@
|
||||
},
|
||||
"omni-webhooks": {
|
||||
"name": "Webhook",
|
||||
"description": "Daftarkan, cantumkan, uji, dan hapus endpoint webhook. Konfigurasikan langganan peristiwa (request.completed, provider.error, budget.exceeded, dll.) dan kelola upaya pengiriman ulang."
|
||||
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
|
||||
},
|
||||
"omni-mcp": {
|
||||
"name": "Server MCP",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user