mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
* fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * feat(db): add provider param filter config store (key_value namespace) Add paramFilters.ts module for CRUD against provider_param_filters namespace in the key_value table, with in-memory cache + generation counter invalidation. Supports denylist/allowlist per provider and per model, plus auto-learn flag. Migration 118 documents the namespace (no schema change). Issue: #6625 * feat(proxy): add detectUnsupportedParam regex for auto-learning Add UNSUPPORTED_PARAM_RE and detectUnsupportedParam() to extract the offending parameter name from upstream 400 error messages like 'Unsupported parameter(s): thinking'. Issue: #6625 * feat(proxy): extend stripUnsupportedParams with config-driven denylist/allowlist Add applyConfigFilters() called after hardcoded STRIP_RULES in stripUnsupportedParams(). Config-driven rules (DB-backed via paramFilters.ts) support provider-level and model-level: 1. Provider denylist (delete body[key]) 2. Model denylist (delete body[key]) 3. Provider allowlist (restore from pre-strip snapshot) 4. Model allowlist (restore from pre-strip snapshot) Allowlist only restores keys the client actually sent — never introduces new params. Issue: #6625 * feat(proxy): wire auto-learn of unsupported params into 400-downgrade loop When a provider returns 400 with 'Unsupported parameter: X' and the provider config has autoLearn enabled, auto-detect the param name via detectUnsupportedParam(), persist it to the provider's block list via addParamToBlocklist(), then strip and retry. Issue: #6625 * test: add tests for provider param filter denylist/allowlist/auto-learn Three new test files: - param-filters-apply.test.ts — hardcoded rules regression + direct applyConfigFilters tests (no DB dependency) - param-filters-db.test.ts — CRUD against key_value, cache invalidation, full filter pipeline (DB-backed config → stripUnsupportedParams), 16 tests in isolated temp DB - param-filters-auto-learn.test.ts — UNSUPPORTED_PARAM_RE regex matching and detectUnsupportedParam edge cases All existing tests unchanged and passing. Issue: #6625 * feat(proxy): add global auto-learn flag for unsupported params Add isAutoLearnGloballyEnabled() and setGlobalAutoLearnEnabled() to paramFilters.ts. The global flag (stored as key __global__ in the provider_param_filters namespace) acts as a master switch: when enabled, ALL providers auto-learn unsupported params from 400 errors. In base.ts, the auto-learn check now evaluates: shouldAutoLearn = isAutoLearnGloballyEnabled() || perProviderConfig?.autoLearn Global flag defaults to false (opt-in). Tests cover enable/disable/ default/no-interference-with-per-provider-config. Issue: #6625 * fix: apply PR#6649 review feedback — model-scoped auto-learn and precedence order Fixes from gemini-code-assist[bot] review: - HIGH: Auto-learn now scoped to the specific model that triggered the 400 (addParamToBlocklist(this.provider, autoLearned, model)) instead of adding to the provider-level blocklist globally - HIGH: Reordered applyConfigFilters so model-level operations run AFTER provider-level operations (model denylist → model allowlist override provider allowlist → provider denylist) - MEDIUM: Include model name in auto-learn log message Adds regression test verifying model-level denylist beats provider-level allowlist. Issue: #6625 PR: #6649 * feat(ui): add provider-level param filter section to detail page Add ProviderParamFilterSection component rendered on each provider detail page, backed by GET|PUT|DELETE /api/providers/[id]/param-filters. UI allows operators to configure: - Blocked params (comma-separated, stripped from outgoing requests) - Allowed params (comma-separated, re-added after denylist stripping) - Auto-learn toggle (per-provider, enables auto-learning from 400 errors) Wired into ProviderDetailPageClient.tsx between the Playground panel and the Modals section. Issue: #6625 PR: #6649 * feat(ui): add model-level param filter fields in compat popover Extend ModelCompatPopover with Blocked params and Allowed params text inputs for model-level denylist/allowlist overrides. Model-specific block/allow data is persisted via the param-filters API endpoint (PUT /api/providers/:id/param-filters) with the model scope under the models key. Both ModelRow and PassthroughModelRow now pass providerId and modelId to the popover. Issue: #6625 PR: #6649 * chore: gitignore .claude-flow/ * fix(param-filters): review follow-ups — auth gate, error sanitization, Zod body validation, typecheck, file-size, i18n keys Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(param-filters): drop unrelated main-drift from the fork branch (deps/electron/proxy files belong to #6620/#6605/#6588, not this PR) * refactor(param-filters): split oversized functions — keep complexity gate at baseline Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(param-filters): decompose config parser helpers — keep cognitive-complexity gate at baseline Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): restore sibling #6648 bullet eaten by merge auto-resolve + re-insert #6649 entry Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
297 lines
12 KiB
TypeScript
297 lines
12 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
|
|
test("Codex apply-local auth route requires management authentication before local writes", () => {
|
|
const content = fs.readFileSync(
|
|
"src/app/api/providers/[id]/codex-auth/apply-local/route.ts",
|
|
"utf8"
|
|
);
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(request);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") <
|
|
content.indexOf("ensureCliConfigWriteAllowed()")
|
|
);
|
|
});
|
|
|
|
test("admin concurrency route requires management authentication before read or reset", () => {
|
|
const content = fs.readFileSync("src/app/api/admin/concurrency/route.ts", "utf8");
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(request);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("getAllRateLimitStatus()")
|
|
);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("resetAllSemaphores()")
|
|
);
|
|
});
|
|
|
|
test("compression analytics route requires management authentication before returning metrics", () => {
|
|
const content = fs.readFileSync("src/app/api/analytics/compression/route.ts", "utf8");
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(!content.includes("enforceApiKeyPolicy"));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(req);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(req)") <
|
|
content.indexOf("getCompressionAnalyticsSummary(")
|
|
);
|
|
});
|
|
|
|
test("administrative pricing and routing routes require management authentication", () => {
|
|
const routePaths = [
|
|
"src/app/api/pricing/route.ts",
|
|
"src/app/api/pricing/sync/route.ts",
|
|
"src/app/api/model-combo-mappings/route.ts",
|
|
"src/app/api/model-combo-mappings/[id]/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("memory management routes require management authentication", () => {
|
|
const routePaths = ["src/app/api/memory/route.ts", "src/app/api/memory/[id]/route.ts"];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("provider validation routes require management authentication before reading credentials", () => {
|
|
const routePaths = [
|
|
"src/app/api/provider-nodes/validate/route.ts",
|
|
"src/app/api/providers/validate/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("request.json()"),
|
|
`${routePath} should authenticate before parsing submitted provider credentials`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("provider param-filters route requires management authentication on GET/PUT/DELETE (#6649)", () => {
|
|
const routePath = "src/app/api/providers/[id]/param-filters/route.ts";
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
|
|
// GET, PUT, and DELETE must each gate on requireManagementAuth() before
|
|
// touching the param filter config.
|
|
const handlers = ["GET", "PUT", "DELETE"];
|
|
for (const handler of handlers) {
|
|
const handlerStart = content.indexOf(`export async function ${handler}(`);
|
|
assert.ok(handlerStart >= 0, `${routePath} is missing a ${handler} handler`);
|
|
const nextHandlerStart = content.indexOf("export async function", handlerStart + 1);
|
|
const handlerBody = content.slice(
|
|
handlerStart,
|
|
nextHandlerStart === -1 ? content.length : nextHandlerStart
|
|
);
|
|
assert.ok(
|
|
handlerBody.includes("const authError = await requireManagementAuth(request);"),
|
|
`${routePath} ${handler} handler must call requireManagementAuth(request)`
|
|
);
|
|
assert.ok(
|
|
handlerBody.includes("if (authError) return authError;"),
|
|
`${routePath} ${handler} handler must short-circuit on authError`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("Antigravity CLI (agy) credential import routes require management authentication before reading the body", () => {
|
|
// Routes that parse a JSON body — auth MUST run before request.json().
|
|
const jsonBodyRoutes = [
|
|
"src/app/api/providers/agy-auth/import/route.ts",
|
|
"src/app/api/providers/agy-auth/import-bulk/route.ts",
|
|
];
|
|
for (const routePath of jsonBodyRoutes) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("request.json()"),
|
|
`${routePath} should authenticate before parsing the submitted token`
|
|
);
|
|
}
|
|
// Routes that read non-JSON bodies (local file / uploaded ZIP) — auth still comes first.
|
|
const otherBodyRoutes = [
|
|
"src/app/api/providers/agy-auth/apply-local/route.ts",
|
|
"src/app/api/providers/agy-auth/zip-extract/route.ts",
|
|
];
|
|
for (const routePath of otherBodyRoutes) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("usage analytics and request log routes require management authentication", () => {
|
|
const routePaths = [
|
|
"src/app/api/usage/analytics/route.ts",
|
|
"src/app/api/usage/history/route.ts",
|
|
"src/app/api/usage/request-logs/route.ts",
|
|
"src/app/api/usage/logs/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("middleware hook registry routes require management authentication before storing or running code", () => {
|
|
// /api/middleware/hooks accepts arbitrary JS code that is compiled with
|
|
// `new Function` in `src/lib/middleware/registry.ts` and executed on every
|
|
// chat-completion request via `runHooks()`. Without management auth, anyone
|
|
// who can reach the server can register a hook and achieve RCE. The
|
|
// centralized authz pipeline (src/proxy.ts) is dead code in production
|
|
// because Next.js requires the file to be named middleware.ts (see commit
|
|
// 3fb72b973), so each route must self-gate.
|
|
const routePaths = [
|
|
"src/app/api/middleware/hooks/route.ts",
|
|
"src/app/api/middleware/hooks/[name]/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("registerHook(saved"),
|
|
`${routePath} must authenticate before calling registerHook (RCE surface)`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("MCP transport and inspection routes require management authentication", () => {
|
|
// /api/mcp/* is classified as LOCAL_ONLY in routeGuard.ts, but that
|
|
// classification only takes effect inside runAuthzPipeline, which is not
|
|
// invoked by Next.js in production (src/proxy.ts vs middleware.ts). Each
|
|
// route must self-enforce. requireManagementAuth covers: CLI machine
|
|
// token (loopback), dashboard session cookie, and manage-scope API key —
|
|
// matching the documented bypasses in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES.
|
|
const routePaths = [
|
|
"src/app/api/mcp/status/route.ts",
|
|
"src/app/api/mcp/tools/route.ts",
|
|
"src/app/api/mcp/audit/route.ts",
|
|
"src/app/api/mcp/audit/stats/route.ts",
|
|
"src/app/api/mcp/sse/route.ts",
|
|
"src/app/api/mcp/stream/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("management routes sanitize error.message before returning it to clients", () => {
|
|
// Hard Rule #12 (docs/security/ERROR_SANITIZATION.md): HTTP responses must
|
|
// route every err.message/err.stack through sanitizeErrorMessage() or
|
|
// buildErrorBody(). Raw error.message can leak absolute paths, stack
|
|
// traces, or upstream provider internals into client-visible JSON bodies.
|
|
const routePaths = [
|
|
"src/app/api/analytics/diversity/route.ts",
|
|
"src/app/api/db-backups/export/route.ts",
|
|
"src/app/api/db-backups/import/route.ts",
|
|
"src/app/api/db-backups/route.ts",
|
|
"src/app/api/evals/route.ts",
|
|
"src/app/api/evals/[suiteId]/route.ts",
|
|
"src/app/api/providers/[id]/models/route.ts",
|
|
"src/app/api/providers/[id]/sync-models/route.ts",
|
|
"src/app/api/providers/[id]/param-filters/route.ts",
|
|
"src/app/api/sessions/route.ts",
|
|
"src/app/api/storage/health/route.ts",
|
|
"src/app/api/sync/cloud/route.ts",
|
|
"src/app/api/telemetry/summary/route.ts",
|
|
"src/app/api/translator/history/route.ts",
|
|
"src/app/api/cache/reasoning/route.ts",
|
|
"src/app/api/cache/route.ts",
|
|
"src/app/api/models/test/route.ts",
|
|
"src/app/api/settings/proxy/test/route.ts",
|
|
];
|
|
|
|
// The pattern we forbid is `error.message` (or `e.message`/`err.message`) appearing
|
|
// inside a JSON response body or being returned directly from a helper. We require
|
|
// these files to either:
|
|
// (a) import sanitizeErrorMessage / buildErrorBody from open-sse/utils/error, OR
|
|
// (b) import createErrorResponse from @/lib/api/errorResponse (which already wraps
|
|
// message through structured envelope).
|
|
const sanitizerHints = [
|
|
'from "@omniroute/open-sse/utils/error"',
|
|
'from "@/lib/api/errorResponse"',
|
|
"sanitizeErrorMessage(",
|
|
"buildErrorBody(",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
const hasSanitizer = sanitizerHints.some((hint) => content.includes(hint));
|
|
assert.ok(
|
|
hasSanitizer,
|
|
`${routePath} must route error.message through sanitizeErrorMessage() or buildErrorBody() — Hard Rule #12`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("memory health endpoint requires management authentication", () => {
|
|
// verifyExtractionPipeline() exposes the memory subsystem state (Qdrant
|
|
// reachability, DB error paths). Same precedent as /api/db/health and
|
|
// /api/monitoring/health, which require management auth.
|
|
const routePath = "src/app/api/memory/health/route.ts";
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(request);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
});
|