fix(review): resolve /review-reviews battery findings (LEDGER-1..11) on v3.8.14 (#3350)

Integrated into release/v3.8.14 — /review-reviews battery hardening (LEDGER-1..11) for #3338 custom-headers + #3333 kiro, plus cycle-test drift fixes (#3329/#3330/#3332).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-07 01:57:54 -03:00
committed by GitHub
parent 559b97c0e8
commit 9c2beb7312
15 changed files with 562 additions and 129 deletions

View File

@@ -21,6 +21,14 @@ _Development cycle in progress — entries are added as work merges into `releas
- **fix(startup):** correct the #3292 auto-refresh daemon import (`@/open-sse/...``@omniroute/open-sse/services/autoRefreshDaemon`); the `@/` alias maps to `src/`, so the daemon silently never ran in the built standalone (non-fatal "Cannot find module", caught at runtime). Adds a regression test banning `@/open-sse/*` imports in `src/`. ([#3335](https://github.com/diegosouzapw/OmniRoute/pull/3335) — thanks @diegosouzapw)
- **fix(electron):** wrap `autoUpdater.checkForUpdates()` so a 404/offline/rate-limited update check can no longer surface as an unhandled rejection (the `error` event still notifies the user); fixes the macOS-intel packaged-app smoke failure. ([#3339](https://github.com/diegosouzapw/OmniRoute/pull/3339) — thanks @diegosouzapw)
### 📝 Maintenance
- **fix(review):** harden the per-provider custom-headers feature surfaced by the `/review-reviews` battery — `updateProviderNode` no longer wipes stored `custom_headers_json` on a partial update that omits the field; `customHeadersSchema` reuses the canonical `upstreamHeadersRecordSchema` guards (CRLF/control-char/length/16-max) and rejects auth header names via a single shared `isForbiddenCustomHeaderName()` denylist (executor + schema no longer keep divergent copies); custom headers now reach the wire for `anthropic-compatible-cc-*` nodes and override the executor's own `Content-Type`/`Accept` case-insensitively instead of duplicating them; and `rowToCamel` normalizes a NULL `_json` column to `baseKey: null`. (thanks @diegosouzapw)
- **fix(catalog):** flag every `minimax-m3` registry entry `supportsVision` (not just the opencode free tier) so the vision-bridge guardrail and the compression layer agree the model is multimodal on all tiers (completes #3328). (thanks @diegosouzapw)
- **fix(oauth):** Kiro Builder ID import forwards the requested `region` to the OIDC validation refresh (no longer pinned to `us-east-1`), prefers the region-matching cached SSO client registration over the first file found, and falls `expiresIn` back to 3600 on the OIDC path. (thanks @diegosouzapw)
- **fix(db):** migration `095` gains an `isSchemaAlreadyApplied` guard so a fresh DB (where `SCHEMA_SQL` already creates `custom_headers_json`) skips it cleanly instead of throwing-then-catching a duplicate-column error. (thanks @diegosouzapw)
- **test:** align stale cycle tests with shipped behavior — NVIDIA `minimaxai/minimax-m3` removal (#3329), the 29th feature flag (`PROXY_AUTO_SELECT_ENABLED`, #3332), and the OpenCode `~/.config` path on Windows (#3330). (thanks @diegosouzapw)
---
## [3.8.13] — 2026-06-06

View File

@@ -1035,7 +1035,12 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
{ id: "gpt-5-mini", name: "GPT-5 Mini", targetFormat: "openai-responses" },
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", targetFormat: "openai-responses" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini", targetFormat: "openai-responses" },
{ id: "gpt-5.4", name: "GPT-5.4", targetFormat: "openai-responses", supportsXHighEffort: true },
{
id: "gpt-5.4",
name: "GPT-5.4",
targetFormat: "openai-responses",
supportsXHighEffort: true,
},
{ id: "gpt-5.5", name: "GPT-5.5", ...GPT_5_5_CODEX_CAPABILITIES },
{
id: "claude-haiku-4.5",
@@ -1224,7 +1229,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
{ id: "gemini-3-flash-solo", name: "Gemini 3 Flash" },
// #3110: MiniMax M3 via Trae
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576 },
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "minimax-m2.7", name: "MiniMax M2.7" },
{ id: "kimi-k2.5", name: "Kimi K2.5" },
{ id: "gpt-5.4", name: "GPT 5.4" },
@@ -1460,7 +1465,13 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
{ id: "mimo-v2-pro", name: "MiMo-V2-Pro" },
{ id: "mimo-v2-omni", name: "MiMo-V2-Omni" },
// #3110: MiniMax M3 via OpenCode Go tier
{ id: "minimax-m3", name: "MiniMax M3", targetFormat: "claude", contextLength: 1048576 },
{
id: "minimax-m3",
name: "MiniMax M3",
targetFormat: "claude",
contextLength: 1048576,
supportsVision: true,
},
{ id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" },
{ id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" },
// Issue #2292: Qwen models on opencode-go reject oa-compat format
@@ -1546,7 +1557,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
// ── MiniMax ────────────────────────────────────────────────
// #3110: MiniMax M3 — frontier coding model with 1M context
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576 },
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "minimax-m2.5", name: "MiniMax M2.5" },
{ id: "minimax-m2.7", name: "MiniMax M2.7" },
@@ -2229,7 +2240,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
models: [
// T12/T28: MiniMax default upgraded from M2.5 to M2.7
// #3110: MiniMax M3 — frontier coding model with 1M context
{ id: "MiniMax-M3", name: "MiniMax M3", contextLength: 1048576 },
{ id: "MiniMax-M3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMax-M2.7-highspeed", name: "MiniMax M2.7 Highspeed" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
@@ -2252,7 +2263,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
models: [
// Keep parity with minimax to ensure model discovery works for minimax-cn connections.
// #3110: MiniMax M3 — frontier coding model with 1M context
{ id: "MiniMax-M3", name: "MiniMax M3", contextLength: 1048576 },
{ id: "MiniMax-M3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMax-M2.7-highspeed", name: "MiniMax M2.7 Highspeed" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
@@ -2711,7 +2722,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro" },
{ id: "mimo-v2.5", name: "MiMo-V2.5" },
// #3110: MiniMax M3 via OpenCode Zen
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576 },
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "minimax-m2.7", name: "MiniMax M2.7" },
{ id: "minimax-m2.5", name: "MiniMax M2.5" },
{ id: "llama-4-maverick", name: "Llama 4 Maverick" },
@@ -3184,7 +3195,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
{ id: "kimi-k2.6", name: "Kimi K2.6" },
{ id: "glm-5.1", name: "GLM 5.1" },
// #3110: MiniMax M3 via Ollama
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576 },
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
{ id: "minimax-m2.7", name: "MiniMax M2.7" },
{ id: "gemma4:31b", name: "Gemma 4 31B" },
{ id: "nemotron-3-super", name: "NVIDIA Nemotron 3 Super" },
@@ -4338,9 +4349,7 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
baseUrls: ["https://amelia.chipotle.com"],
authType: "none",
authHeader: "none",
models: [
{ id: "pepper-1", name: "Pepper (Chipotle AI 🌯)" },
],
models: [{ id: "pepper-1", name: "Pepper (Chipotle AI 🌯)" }],
passthroughModels: true,
},
};

View File

@@ -29,9 +29,52 @@ import { buildOciChatUrl } from "../config/oci.ts";
import { buildSapChatUrl, getSapResourceGroup } from "../config/sap.ts";
import { buildMaritalkChatUrl } from "../config/maritalk.ts";
import { LOCAL_PROVIDERS } from "@/shared/constants/providers";
import { isForbiddenCustomHeaderName } from "@/shared/constants/upstreamHeaders";
import type { PoolConfig } from "../services/sessionPool/types.ts";
/**
* Apply operator-configured per-provider custom headers onto an outgoing header
* map. Defense-in-depth on top of the Zod `customHeadersSchema`:
* - skip hop-by-hop/framing AND auth header names (canonical denylist, so a row
* written before the schema tightening still can't override credential auth);
* - skip control-char (CR/LF/NUL) names/values before they reach undici;
* - assign case-insensitively, replacing any existing same-named header (e.g.
* the executor's own Content-Type/Accept) instead of emitting a duplicate.
* Used for every *-compatible node, INCLUDING anthropic-compatible-cc-* (whose
* header builder returns early, so custom headers must be merged in explicitly).
*/
function applyCustomHeaders(headers: Record<string, string>, rawCustomHeaders: unknown): void {
let customHeaders: Record<string, unknown> | null = null;
if (
rawCustomHeaders &&
typeof rawCustomHeaders === "object" &&
!Array.isArray(rawCustomHeaders)
) {
customHeaders = rawCustomHeaders as Record<string, unknown>;
} else if (typeof rawCustomHeaders === "string") {
try {
const parsed = JSON.parse(rawCustomHeaders);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
customHeaders = parsed as Record<string, unknown>;
}
} catch {
/* ignore invalid JSON */
}
}
if (!customHeaders) return;
for (const [k, v] of Object.entries(customHeaders)) {
if (typeof k !== "string" || typeof v !== "string") continue;
if (isForbiddenCustomHeaderName(k)) continue;
if (/[\r\n\0]/.test(k) || /[\r\n]/.test(v)) continue;
const lower = k.toLowerCase();
for (const existing of Object.keys(headers)) {
if (existing.toLowerCase() === lower) delete headers[existing];
}
headers[k] = v;
}
}
function normalizeBaseUrl(baseUrl) {
return (baseUrl || "").trim().replace(/\/$/, "");
}
@@ -375,11 +418,15 @@ export class DefaultExecutor extends BaseExecutor {
break;
default:
if (isClaudeCodeCompatible(this.provider)) {
return buildClaudeCodeCompatibleHeaders(
const ccHeaders = buildClaudeCodeCompatibleHeaders(
effectiveKey || credentials.accessToken || "",
stream,
credentials?.providerSpecificData?.ccSessionId
);
// CC nodes are also anthropic-compatible-*, so honor operator custom
// headers here (the early return skips the shared block below).
applyCustomHeaders(ccHeaders, credentials.providerSpecificData?.customHeaders);
return ccHeaders;
}
if (this.provider?.startsWith?.("anthropic-compatible-")) {
if (effectiveKey) {
@@ -424,31 +471,7 @@ export class DefaultExecutor extends BaseExecutor {
this.provider?.startsWith?.("anthropic-compatible-");
if (isCompatibleProvider) {
const rawCustomHeaders = credentials.providerSpecificData?.customHeaders;
let customHeaders: Record<string, string> | null = null;
if (rawCustomHeaders && typeof rawCustomHeaders === "object" && !Array.isArray(rawCustomHeaders)) {
customHeaders = rawCustomHeaders as Record<string, string>;
} else if (typeof rawCustomHeaders === "string") {
try {
const parsed = JSON.parse(rawCustomHeaders);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
customHeaders = parsed;
}
} catch { /* ignore invalid JSON */ }
}
if (customHeaders) {
const forbidden = new Set([
"host", "connection", "content-length", "keep-alive",
"proxy-connection", "transfer-encoding", "te", "trailer", "upgrade",
]);
const authHeaders = new Set(["authorization", "x-api-key", "x-goog-api-key", "api-key"]);
for (const [k, v] of Object.entries(customHeaders)) {
if (typeof k !== "string" || typeof v !== "string") continue;
if (forbidden.has(k.toLowerCase())) continue;
if (authHeaders.has(k.toLowerCase())) continue;
headers[k] = v;
}
}
applyCustomHeaders(headers, credentials.providerSpecificData?.customHeaders);
}
// Forward client request metadata headers (from OpenCode or similar clients)

View File

@@ -463,15 +463,21 @@ export function rowToCamel(row: unknown): JsonRecord | null {
} catch {
result[camelKey] = v;
}
} else if (camelKey.endsWith("Json") && typeof v === "string") {
} else if (camelKey.endsWith("Json")) {
// Convention: any column with a `_json` suffix is JSON-encoded TEXT.
// Surface the parsed object under the friendlier name (key minus the
// "Json" suffix) — e.g. quotaWindowThresholdsJson → quotaWindowThresholds.
// A NULL/absent column normalizes to `baseKey: null` (not the suffixed
// key) so read and write paths expose a consistent shape.
const baseKey = camelKey.slice(0, -"Json".length);
try {
result[baseKey] = JSON.parse(v);
} catch {
result[baseKey] = null;
if (typeof v === "string") {
try {
result[baseKey] = JSON.parse(v);
} catch {
result[baseKey] = null;
}
} else {
result[baseKey] = v == null ? null : v;
}
} else {
result[camelKey] = v;
@@ -1273,7 +1279,7 @@ export function getDbInstance(): SqliteDatabase {
) {
throw e;
}
preservedCriticalState = captureCriticalDbState(sqliteFile);
preservedCriticalState = captureCriticalDbState(sqliteFile);
// SAFETY: Never delete the database — rename to backup so data can be recovered.
// The old code would silently destroy all user data on any probe failure.

View File

@@ -399,6 +399,8 @@ function isSchemaAlreadyApplied(
switch (migration.version) {
case "003":
return hasColumn(db, "provider_nodes", "chat_path");
case "095":
return hasColumn(db, "provider_nodes", "custom_headers_json");
case "005":
return hasColumn(db, "combos", "system_message");
case "007":
@@ -910,7 +912,9 @@ export function runMigrations(db: SqliteAdapter, options?: { isNewDb?: boolean }
}
return isMissing;
});
const deferredUnsupported = pending.filter((migration) => isDeferredUnsupportedMigration(db, migration));
const deferredUnsupported = pending.filter((migration) =>
isDeferredUnsupportedMigration(db, migration)
);
const actionablePending = pending.filter(
(migration) => !deferredUnsupported.some((deferred) => deferred.version === migration.version)
);
@@ -920,7 +924,9 @@ export function runMigrations(db: SqliteAdapter, options?: { isNewDb?: boolean }
}
if (deferredUnsupported.length > 0) {
const summary = deferredUnsupported.map((migration) => `${migration.version}_${migration.name}`).join(", ");
const summary = deferredUnsupported
.map((migration) => `${migration.version}_${migration.name}`)
.join(", ");
console.warn(
`[Migration] Deferring optional FTS5 migrations on driver ${db.driver}: ${summary}. ` +
`Memory search will fall back until a SQLite driver with FTS5 support is available.`

View File

@@ -372,9 +372,7 @@ export async function createProviderConnection(data: JsonRecord) {
// Same sanitization for rateLimitOverrides — keep in-memory representation
// in sync with what gets persisted.
if ("rateLimitOverrides" in connection) {
connection.rateLimitOverrides = sanitizeRateLimitOverrides(
connection.rateLimitOverrides
);
connection.rateLimitOverrides = sanitizeRateLimitOverrides(connection.rateLimitOverrides);
}
_insertConnectionRow(db, encryptConnectionFields({ ...connection }));
@@ -835,6 +833,14 @@ export async function updateProviderNode(id: string, data: JsonRecord) {
if (data.customHeaders !== undefined) {
merged["customHeadersJson"] = data.customHeaders ? JSON.stringify(data.customHeaders) : null;
} else {
// Partial update that omits customHeaders must PRESERVE the stored value.
// rowToCamel surfaces the column under `customHeaders` (suffix stripped),
// never `customHeadersJson`, so read the raw stored JSON from `existing`
// directly instead of relying on the (absent) merged key — otherwise the
// UPDATE would bind null and silently wipe the saved headers.
const existingJson = (existing as JsonRecord).custom_headers_json;
merged["customHeadersJson"] = typeof existingJson === "string" ? existingJson : null;
}
db.prepare(

View File

@@ -226,7 +226,7 @@ export class KiroService {
return {
accessToken: retryData.accessToken,
refreshToken: retryData.refreshToken || refreshToken,
expiresIn: retryData.expiresIn,
expiresIn: retryData.expiresIn || 3600,
_newClientId: newReg.clientId,
_newClientSecret: newReg.clientSecret,
_newClientSecretExpiresAt: newReg.clientSecretExpiresAt,
@@ -246,9 +246,13 @@ export class KiroService {
const data = await response.json();
return {
// Builder ID / IDC OIDC refresh: no profileArn (the social path supplies
// one; Builder ID connections legitimately have none). expiresIn falls
// back to 3600 so the import route never computes Date(NaN) if upstream
// omits it (the social path already guards the same way).
accessToken: data.accessToken,
refreshToken: data.refreshToken || refreshToken,
expiresIn: data.expiresIn,
expiresIn: data.expiresIn || 3600,
};
}
@@ -290,7 +294,7 @@ export class KiroService {
}
// Try to read cached clientId/clientSecret from AWS SSO cache (Builder ID tokens)
const cachedClient = await this.readCachedClientCredentials();
const cachedClient = await this.readCachedClientCredentials(region);
// Attempt 1: Try Builder ID refresh using cached credentials
if (cachedClient) {
@@ -299,10 +303,16 @@ export class KiroService {
clientId: cachedClient.clientId,
clientSecret: cachedClient.clientSecret,
authMethod: "builder-id",
// Forward the requested region so a non-us-east-1 Builder ID validates
// against the right OIDC endpoint instead of defaulting to us-east-1.
region,
});
return {
accessToken: result.accessToken,
refreshToken: result.refreshToken || refreshToken,
// profileArn is intentionally absent for Builder ID (OIDC) imports —
// only the social-auth path returns one, and Builder ID connections
// don't require it. The Kiro executor adds profileArn conditionally.
profileArn: result.profileArn,
expiresIn: result.expiresIn,
authMethod: "builder-id",
@@ -351,7 +361,9 @@ export class KiroService {
* The cache is located at ~/.aws/sso/cache/ and contains JSON files from
* the OIDC client registration step of the device code flow.
*/
private async readCachedClientCredentials(): Promise<{ clientId: string; clientSecret: string } | null> {
private async readCachedClientCredentials(
region?: string
): Promise<{ clientId: string; clientSecret: string } | null> {
try {
const { readdir, readFile } = await import("fs/promises");
const { homedir } = await import("os");
@@ -359,18 +371,42 @@ export class KiroService {
const cachePath = join(homedir(), ".aws", "sso", "cache");
const files = await readdir(cachePath);
const candidates: {
clientId: string;
clientSecret: string;
region?: string;
expiresAt?: string;
}[] = [];
for (const file of files) {
if (!file.endsWith(".json")) continue;
try {
const content = await readFile(join(cachePath, file), "utf-8");
const data = JSON.parse(content);
if (data.clientId && data.clientSecret) {
return { clientId: data.clientId, clientSecret: data.clientSecret };
candidates.push({
clientId: data.clientId,
clientSecret: data.clientSecret,
region: data.region,
expiresAt: data.clientSecretExpiresAt || data.expiresAt,
});
}
} catch {
continue;
}
}
if (candidates.length === 0) return null;
// A host can cache OIDC client registrations for several SSO sessions;
// adopting the wrong pair makes the Builder ID refresh fail. Prefer a
// registration whose region matches the requested import region, then —
// among the candidates — the one with the latest secret expiry, instead
// of blindly taking the first file readdir happens to return.
const byLatestExpiry = (a: { expiresAt?: string }, b: { expiresAt?: string }): number =>
String(b.expiresAt || "").localeCompare(String(a.expiresAt || ""));
const matching = region ? candidates.filter((c) => c.region === region) : [];
const ordered = (matching.length > 0 ? matching : candidates).slice().sort(byLatestExpiry);
const best = ordered[0];
return { clientId: best.clientId, clientSecret: best.clientSecret };
} catch {
// Cache not available
}

View File

@@ -20,3 +20,23 @@ const FORBIDDEN = new Set(
export function isForbiddenUpstreamHeaderName(name: string): boolean {
return FORBIDDEN.has(String(name).trim().toLowerCase());
}
/**
* Auth headers that must come from the connection's credentials, never from
* operator-set per-provider custom headers. Kept here as the single source of
* truth so the Zod `customHeadersSchema` (schemas.ts) and the executor's
* apply-loop (open-sse/executors/default.ts) cannot drift apart.
*/
const FORBIDDEN_AUTH = new Set(
["authorization", "x-api-key", "x-goog-api-key", "api-key"].map((s) => s.toLowerCase())
);
/**
* Forbidden for operator-supplied custom headers: the hop-by-hop/framing set
* PLUS auth headers (owned by the credential layer). Use this in both the
* validation schema and the executor so there is one canonical denylist.
*/
export function isForbiddenCustomHeaderName(name: string): boolean {
const n = String(name).trim().toLowerCase();
return isForbiddenUpstreamHeaderName(n) || FORBIDDEN_AUTH.has(n);
}

View File

@@ -8,7 +8,10 @@ import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/c
import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
import { providerAllowsOptionalApiKey } from "@/shared/constants/providers";
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
import { isForbiddenUpstreamHeaderName } from "@/shared/constants/upstreamHeaders";
import {
isForbiddenUpstreamHeaderName,
isForbiddenCustomHeaderName,
} from "@/shared/constants/upstreamHeaders";
import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts";
function isHttpUrl(value: string): boolean {
@@ -1917,31 +1920,20 @@ export const updateKeyPermissionsSchema = z
}
});
const customHeadersSchema = z
.record(z.string(), z.string())
// Reuse the canonical upstream-headers record schema (control-char / whitespace
// / ":" / 128-name / 4096-value / max-16 guards) so per-provider custom headers
// inherit the same hardening as `modelCompat.upstreamHeaders` — then additionally
// reject auth header names (the credential layer owns those; the executor drops
// them at send time, so reject up front for an actionable error instead of a
// silent no-op). Single denylist source: isForbiddenCustomHeaderName.
const customHeadersSchema = upstreamHeadersRecordSchema
.refine((rec) => !Object.keys(rec).some((k) => isForbiddenCustomHeaderName(k)), {
message:
"Custom headers cannot include hop-by-hop, framing, or auth headers " +
"(authorization / x-api-key / x-goog-api-key / api-key)",
})
.nullable()
.optional()
.refine(
(val) => {
if (!val) return true;
const forbidden = new Set([
"host",
"connection",
"content-length",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"te",
"trailer",
"upgrade",
]);
for (const key of Object.keys(val)) {
if (forbidden.has(key.toLowerCase())) return false;
}
return true;
},
{ message: "Custom headers contain forbidden hop-by-hop or framing headers" }
);
.optional();
export const createProviderNodeSchema = z
.object({

View File

@@ -231,23 +231,28 @@ describe("resolveOpencodeConfigPath — cross-platform", () => {
assert.equal(result, path.join("/home/dev", ".config", "opencode", "opencode.json"));
});
it("should resolve on Windows with APPDATA", () => {
it("should resolve on Windows under ~/.config (XDG, NOT %APPDATA% — #3330)", () => {
// #3330: OpenCode reads its config from ~/.config/opencode on every
// platform, including Windows (%USERPROFILE%\.config). %APPDATA% is ignored.
const result = resolveOpencodeConfigPathFn(
"win32",
{ APPDATA: "C:\\Users\\dev\\AppData\\Roaming" },
"C:\\Users\\dev"
);
assert.equal(
result,
path.join("C:\\Users\\dev\\AppData\\Roaming", "opencode", "opencode.json")
);
assert.equal(result, path.join("C:\\Users\\dev", ".config", "opencode", "opencode.json"));
});
it("should fallback to home/AppData/Roaming on Windows without APPDATA", () => {
it("should resolve on Windows under ~/.config without APPDATA (#3330)", () => {
const result = resolveOpencodeConfigPathFn("win32", {}, "C:\\Users\\dev");
assert.equal(
result,
path.join("C:\\Users\\dev", "AppData", "Roaming", "opencode", "opencode.json")
assert.equal(result, path.join("C:\\Users\\dev", ".config", "opencode", "opencode.json"));
});
it("should honor XDG_CONFIG_HOME on Windows too (#3330)", () => {
const result = resolveOpencodeConfigPathFn(
"win32",
{ XDG_CONFIG_HOME: "D:\\xdg" },
"C:\\Users\\dev"
);
assert.equal(result, path.join("D:\\xdg", "opencode", "opencode.json"));
});
});

View File

@@ -12,7 +12,8 @@ const providersDb = await import("../../src/lib/db/providers.ts");
const providerNodesRoute = await import("../../src/app/api/provider-nodes/route.ts");
const providerNodesIdRoute = await import("../../src/app/api/provider-nodes/[id]/route.ts");
const { OPENAI_COMPATIBLE_PREFIX } = await import("../../src/shared/constants/providers.ts");
const { createProviderNodeSchema, updateProviderNodeSchema } = await import("../../src/shared/validation/schemas.ts");
const { createProviderNodeSchema, updateProviderNodeSchema } =
await import("../../src/shared/validation/schemas.ts");
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
async function resetStorage() {
@@ -49,7 +50,12 @@ test.after(async () => {
test("createProviderNodeSchema accepts valid customHeaders as record of strings", () => {
const validInputs = [
{ name: "Test", prefix: "test", apiType: "chat", customHeaders: { "X-Custom-1": "value1" } },
{ name: "Test", prefix: "test", apiType: "chat", customHeaders: { "X-Header": "value", "X-Another": "value2" } },
{
name: "Test",
prefix: "test",
apiType: "chat",
customHeaders: { "X-Header": "value", "X-Another": "value2" },
},
{ name: "Test", prefix: "test", apiType: "chat", customHeaders: {} },
{ name: "Test", prefix: "test", apiType: "chat" },
];
@@ -77,8 +83,15 @@ test("createProviderNodeSchema rejects customHeaders with non-string values", ()
test("createProviderNodeSchema rejects forbidden hop-by-hop headers", () => {
const forbiddenHeaders = [
"host", "connection", "content-length", "keep-alive",
"proxy-connection", "transfer-encoding", "te", "trailer", "upgrade",
"host",
"connection",
"content-length",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"te",
"trailer",
"upgrade",
];
for (const header of forbiddenHeaders) {
@@ -89,7 +102,7 @@ test("createProviderNodeSchema rejects forbidden hop-by-hop headers", () => {
}
const result = createProviderNodeSchema.safeParse({
customHeaders: { "HOST": "evil", "Content-Length": "999" },
customHeaders: { HOST: "evil", "Content-Length": "999" },
});
assert.equal(result.success, false, "Should reject case-insensitive forbidden headers");
});
@@ -118,7 +131,7 @@ test("updateProviderNodeSchema rejects forbidden headers", () => {
name: "Test",
prefix: "test",
baseUrl: "https://test.com",
customHeaders: { "host": "evil.com" },
customHeaders: { host: "evil.com" },
});
assert.equal(result.success, false);
});
@@ -181,10 +194,9 @@ test("provider nodes route update modifies customHeaders", async () => {
baseUrl: "https://updated.example.com/v1",
customHeaders: { "X-Updated": "updated-value", "X-New": "new-header" },
};
const updateResponse = await providerNodesIdRoute.PUT(
makeUpdateRequest(nodeId, updateBody),
{ params: Promise.resolve({ id: nodeId }) }
);
const updateResponse = await providerNodesIdRoute.PUT(makeUpdateRequest(nodeId, updateBody), {
params: Promise.resolve({ id: nodeId }),
});
const updated = (await updateResponse.json()) as any;
assert.equal(updateResponse.status, 200);
@@ -207,17 +219,16 @@ test("provider nodes route update can clear customHeaders by passing null", asyn
const created = (await createResponse.json()) as any;
const nodeId = created.node.id;
const clearBody = {
const clearBody = {
name: "Node Without Headers",
prefix: "no-headers",
apiType: "chat",
baseUrl: "https://noclear.example.com/v1",
customHeaders: null,
};
const updateResponse = await providerNodesIdRoute.PUT(
makeUpdateRequest(nodeId, clearBody),
{ params: Promise.resolve({ id: nodeId }) }
);
const updateResponse = await providerNodesIdRoute.PUT(makeUpdateRequest(nodeId, clearBody), {
params: Promise.resolve({ id: nodeId }),
});
const updated = (await updateResponse.json()) as any;
assert.equal(updateResponse.status, 200);
@@ -227,7 +238,7 @@ const clearBody = {
test("DefaultExecutor.buildHeaders applies customHeaders from providerSpecificData", () => {
const executor = new DefaultExecutor("openai-compatible-test");
const headers = executor.buildHeaders(
const headers = executor.buildHeaders(
{
apiKey: "test-key",
providerSpecificData: {
@@ -258,7 +269,7 @@ test("DefaultExecutor.buildHeaders does NOT override auth headers with customHea
providerSpecificData: {
baseUrl: "https://proxy.example.com/v1",
customHeaders: {
"Authorization": "Bearer fake-token",
Authorization: "Bearer fake-token",
"x-api-key": "fake-key",
"X-Custom": "custom-value",
},
@@ -281,9 +292,9 @@ test("DefaultExecutor.buildHeaders blocks forbidden hop-by-hop headers from cust
providerSpecificData: {
baseUrl: "https://proxy.example.com/v1",
customHeaders: {
"host": "evil.com",
host: "evil.com",
"content-length": "999",
"connection": "close",
connection: "close",
"X-Legitimate": "good-header",
},
},
@@ -476,7 +487,7 @@ test("DefaultExecutor.execute does NOT send forbidden headers from customHeaders
providerSpecificData: {
baseUrl: "https://test.proxy.com/v1",
customHeaders: {
"host": "evil.com",
host: "evil.com",
"content-length": "9999",
"X-Legitimate": "good",
},
@@ -516,7 +527,7 @@ test("DefaultExecutor.execute does NOT allow customHeaders to override Authoriza
providerSpecificData: {
baseUrl: "https://test.proxy.com/v1",
customHeaders: {
"Authorization": "Bearer forged-key",
Authorization: "Bearer forged-key",
},
},
},
@@ -542,7 +553,9 @@ test("db: createProviderNode and getProviderNodeById handle customHeaders as JSO
assert.deepEqual(node.customHeaders, { "X-DB-Header": "db-value", "X-Another": "another" });
const retrieved = await providersDb.getProviderNodeById("openai-compatible-chat-custom-headers-db");
const retrieved = await providersDb.getProviderNodeById(
"openai-compatible-chat-custom-headers-db"
);
assert.deepEqual(retrieved.customHeaders, { "X-DB-Header": "db-value", "X-Another": "another" });
});
@@ -559,15 +572,17 @@ test("db: updateProviderNode modifies customHeaders", async () => {
assert.deepEqual(node.customHeaders, { "X-Initial": "initial-value" });
const updated = await providersDb.updateProviderNode(
"openai-compatible-chat-update-custom",
{ customHeaders: { "X-Updated": "updated-value", "X-New-Header": "new" } }
);
const updated = await providersDb.updateProviderNode("openai-compatible-chat-update-custom", {
customHeaders: { "X-Updated": "updated-value", "X-New-Header": "new" },
});
assert.deepEqual(updated.customHeaders, { "X-Updated": "updated-value", "X-New-Header": "new" });
const retrieved = await providersDb.getProviderNodeById("openai-compatible-chat-update-custom");
assert.deepEqual(retrieved.customHeaders, { "X-Updated": "updated-value", "X-New-Header": "new" });
assert.deepEqual(retrieved.customHeaders, {
"X-Updated": "updated-value",
"X-New-Header": "new",
});
});
test("db: updateProviderNode can clear customHeaders by passing null", async () => {
@@ -583,10 +598,9 @@ test("db: updateProviderNode can clear customHeaders by passing null", async ()
assert.deepEqual(node.customHeaders, { "X-ToClear": "clear-me" });
const updated = await providersDb.updateProviderNode(
"openai-compatible-chat-clear-custom",
{ customHeaders: null }
);
const updated = await providersDb.updateProviderNode("openai-compatible-chat-clear-custom", {
customHeaders: null,
});
assert.equal(updated.customHeaders, null);
});
});

View File

@@ -31,13 +31,13 @@ const {
// Test group 1 — Flag definitions registry
// ──────────────────────────────────────────────────────
describe("featureFlagDefinitions", () => {
it("has exactly 28 flag definitions", () => {
assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, 28);
it("has exactly 29 flag definitions", () => {
assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, 29);
});
it("has unique keys for all flags", () => {
const keys = FEATURE_FLAG_DEFINITIONS.map((d) => d.key);
assert.strictEqual(new Set(keys).size, 28);
assert.strictEqual(new Set(keys).size, 29);
});
it("has valid categories for all flags", () => {
@@ -222,9 +222,9 @@ describe("resolveFeatureFlag", () => {
});
describe("resolveAllFeatureFlags", () => {
it("returns all 28 flags", () => {
it("returns all 29 flags", () => {
const all = resolveAllFeatureFlags();
assert.strictEqual(all.length, 28);
assert.strictEqual(all.length, 29);
});
it("marks DB-overridden flags with source 'db'", () => {

View File

@@ -82,7 +82,11 @@ test("validateImportToken falls back to social auth when no cached creds exist",
// Social-auth refresh endpoint.
if (u.includes("auth.desktop.kiro.dev")) {
return new Response(
JSON.stringify({ accessToken: "access-social", refreshToken: "social-rt", expiresIn: 3600 }),
JSON.stringify({
accessToken: "access-social",
refreshToken: "social-rt",
expiresIn: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
@@ -103,6 +107,78 @@ test("validateImportToken falls back to social auth when no cached creds exist",
assert.equal(result.accessToken, "access-social");
});
// LEDGER-6 (/review-reviews v3.8.14): the Builder ID validation refresh must
// forward the requested region to the OIDC endpoint, not default to us-east-1.
test("validateImportToken forwards the region to the OIDC endpoint (LEDGER-6)", async () => {
makeFakeSsoCache(tmpHome, { clientId: "cid", clientSecret: "secret" });
const calledEndpoints: string[] = [];
globalThis.fetch = (async (url: string | URL | Request) => {
const u = String(url);
calledEndpoints.push(u);
if (u.includes("oidc.") && u.endsWith("/token")) {
return new Response(
JSON.stringify({ accessToken: "a", refreshToken: "r", expiresIn: 3600 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
throw new Error(`unexpected fetch to ${u}`);
}) as typeof fetch;
const svc = new KiroService();
await svc.validateImportToken("aorAAAAAGtoken", "eu-west-1");
assert.ok(
calledEndpoints.some((u) => u.includes("oidc.eu-west-1.amazonaws.com/token")),
`expected the eu-west-1 OIDC endpoint, got: ${calledEndpoints.join(", ")}`
);
assert.ok(
!calledEndpoints.some((u) => u.includes("oidc.us-east-1.amazonaws.com/token")),
"must not fall back to us-east-1 when a region was requested"
);
});
// LEDGER-8 (/review-reviews v3.8.14): with multiple cached SSO client
// registrations, the one whose region matches the import must be chosen rather
// than whichever readdir returns first.
test("validateImportToken prefers the region-matching cached client (LEDGER-8)", async () => {
const cacheDir = path.join(tmpHome, ".aws", "sso", "cache");
fs.mkdirSync(cacheDir, { recursive: true });
// "a-" sorts first so naive first-match would pick the wrong (us-east-1) pair.
fs.writeFileSync(
path.join(cacheDir, "a-useast.json"),
JSON.stringify({
clientId: "cid-useast",
clientSecret: "secret-useast",
region: "us-east-1",
expiresAt: "2099-01-01T00:00:00Z",
})
);
fs.writeFileSync(
path.join(cacheDir, "z-euwest.json"),
JSON.stringify({
clientId: "cid-euwest",
clientSecret: "secret-euwest",
region: "eu-west-1",
expiresAt: "2099-01-01T00:00:00Z",
})
);
globalThis.fetch = (async (url: string | URL | Request) => {
const u = String(url);
if (u.includes("oidc.") && u.endsWith("/token")) {
return new Response(
JSON.stringify({ accessToken: "a", refreshToken: "r", expiresIn: 3600 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
throw new Error(`unexpected fetch to ${u}`);
}) as typeof fetch;
const svc = new KiroService();
const result = await svc.validateImportToken("aorAAAAAGtoken", "eu-west-1");
assert.equal(result.clientId, "cid-euwest", "must pick the eu-west-1 registration");
assert.equal(result.clientSecret, "secret-euwest");
});
test("validateImportToken rejects malformed refresh tokens before touching the cache", async () => {
let fetched = false;
globalThis.fetch = (async () => {

View File

@@ -75,12 +75,10 @@ describe("MiniMax M3 model registration (#3110)", () => {
assert.equal(m3.contextLength, 1_048_576);
});
it("nvidia provider has minimaxai/minimax-m3 with 1M context", () => {
it("nvidia provider does NOT list minimaxai/minimax-m3 (removed in #3329 — 404 upstream)", () => {
const entry = REGISTRY.nvidia;
assert.ok(entry, "nvidia registry entry must exist");
const m3 = entry.models.find((m) => m.id === "minimaxai/minimax-m3");
assert.ok(m3, "minimaxai/minimax-m3 must be in nvidia models");
assert.equal(m3.name, "MiniMax M3");
assert.equal(m3.contextLength, 1_048_576);
assert.equal(m3, undefined, "NVIDIA NIM does not host minimaxai/minimax-m3 (see #3329)");
});
});

View File

@@ -0,0 +1,234 @@
/**
* Regression guards for the /review-reviews battery findings (v3.8.14).
* Each test maps to a LEDGER-* item from the consolidated review.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-review-reviews-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const { createProviderNodeSchema } = await import("../../src/shared/validation/schemas.ts");
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
test.beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ── LEDGER-1: updateProviderNode must preserve custom headers on partial update ──
test("LEDGER-1: updateProviderNode preserves customHeaders when the field is omitted", async () => {
const created = await providersDb.createProviderNode({
type: "openai-compatible",
name: "node-a",
prefix: "nodea",
apiType: "chat",
baseUrl: "https://proxy.example.com/v1",
customHeaders: { "X-Tenant": "acme", "X-Env": "prod" },
});
// Partial update that does NOT resend customHeaders (only renames the node).
const updated = await providersDb.updateProviderNode(created.id as string, { name: "node-a2" });
assert.deepEqual(
updated?.customHeaders,
{ "X-Tenant": "acme", "X-Env": "prod" },
"omitting customHeaders on update must NOT wipe stored headers"
);
// And reading it back confirms persistence.
const fetched = await providersDb.getProviderNodeById(created.id as string);
assert.deepEqual(fetched?.customHeaders, { "X-Tenant": "acme", "X-Env": "prod" });
});
test("LEDGER-1: updateProviderNode still clears headers when explicitly set to null", async () => {
const created = await providersDb.createProviderNode({
type: "openai-compatible",
name: "node-b",
prefix: "nodeb",
apiType: "chat",
baseUrl: "https://proxy.example.com/v1",
customHeaders: { "X-Tenant": "acme" },
});
const updated = await providersDb.updateProviderNode(created.id as string, {
customHeaders: null,
});
assert.equal(updated?.customHeaders, null, "explicit null must clear headers");
});
// ── LEDGER-2 / LEDGER-3: schema reuses canonical guards + rejects auth headers ──
test("LEDGER-2: customHeaders rejects CRLF / control-char values", () => {
const bad = createProviderNodeSchema.safeParse({
type: "openai-compatible",
name: "n",
prefix: "n",
apiType: "chat",
baseUrl: "https://x/v1",
customHeaders: { "X-Inject": "foo\r\nX-Evil: bar" },
});
assert.equal(bad.success, false, "CRLF in a header value must be rejected at the schema");
});
test("LEDGER-2: customHeaders rejects control-char / whitespace / ':' in names", () => {
for (const badName of ["X Bad", "X:Bad", "X\r\nBad"]) {
const res = createProviderNodeSchema.safeParse({
type: "openai-compatible",
name: "n",
prefix: "n",
apiType: "chat",
baseUrl: "https://x/v1",
customHeaders: { [badName]: "v" },
});
assert.equal(res.success, false, `header name "${badName}" must be rejected`);
}
});
test("LEDGER-2: customHeaders rejects more than 16 entries", () => {
const many: Record<string, string> = {};
for (let i = 0; i < 17; i++) many[`X-H${i}`] = "v";
const res = createProviderNodeSchema.safeParse({
type: "openai-compatible",
name: "n",
prefix: "n",
apiType: "chat",
baseUrl: "https://x/v1",
customHeaders: many,
});
assert.equal(res.success, false, "more than 16 custom headers must be rejected");
});
test("LEDGER-3: customHeaders rejects auth header names at the schema boundary", () => {
for (const authName of ["Authorization", "x-api-key", "X-Goog-Api-Key", "api-key"]) {
const res = createProviderNodeSchema.safeParse({
type: "openai-compatible",
name: "n",
prefix: "n",
apiType: "chat",
baseUrl: "https://x/v1",
customHeaders: { [authName]: "Bearer x" },
});
assert.equal(res.success, false, `auth header "${authName}" must be rejected (no silent drop)`);
}
});
test("LEDGER-2: valid custom headers still pass", () => {
const res = createProviderNodeSchema.safeParse({
type: "openai-compatible",
name: "n",
prefix: "n",
apiType: "chat",
baseUrl: "https://x/v1",
customHeaders: { "X-Tenant": "acme", "X-Trace-Id": "abc-123" },
});
assert.equal(res.success, true, res.success ? "" : JSON.stringify(res.error?.issues));
});
// ── LEDGER-4: every minimax-m3 registry entry is flagged multimodal ──
test("LEDGER-4: all minimax-m3 registry entries set supportsVision (matches lite.ts)", () => {
const entries: { id: string; supportsVision?: boolean }[] = [];
for (const provider of Object.values(
REGISTRY as Record<string, { models?: { id: string; supportsVision?: boolean }[] }>
)) {
for (const m of provider.models || []) {
if (/minimax-m3/i.test(m.id)) entries.push(m);
}
}
assert.ok(entries.length >= 6, `expected several minimax-m3 entries, got ${entries.length}`);
const unflagged = entries.filter((m) => m.supportsVision !== true).map((m) => m.id);
assert.deepEqual(
unflagged,
[],
`these minimax-m3 entries miss supportsVision: ${unflagged.join(", ")}`
);
});
// ── LEDGER-5: anthropic-compatible-cc-* nodes honor custom headers ──
test("LEDGER-5: custom headers reach the wire for anthropic-compatible-cc-* nodes", () => {
const executor = new DefaultExecutor("anthropic-compatible-cc-test");
const headers = executor.buildHeaders(
{
accessToken: "tok",
providerSpecificData: { customHeaders: { "X-CC-Custom": "yes" } },
},
false
) as Record<string, string>;
assert.equal(headers["X-CC-Custom"], "yes", "CC node must apply operator custom headers");
});
// ── LEDGER-10: case-insensitive override — no duplicate Content-Type/Accept ──
test("LEDGER-10: a custom content-type overrides (not duplicates) the executor's Content-Type", () => {
const executor = new DefaultExecutor("openai-compatible-test");
const headers = executor.buildHeaders(
{
apiKey: "k",
providerSpecificData: {
baseUrl: "https://x/v1",
customHeaders: { "content-type": "application/custom" },
},
},
false
) as Record<string, string>;
const ctKeys = Object.keys(headers).filter((k) => k.toLowerCase() === "content-type");
assert.equal(
ctKeys.length,
1,
`exactly one content-type header expected, got ${ctKeys.join(", ")}`
);
assert.equal(headers[ctKeys[0]], "application/custom");
});
test("LEDGER-3: executor still drops auth/forbidden custom headers (defense-in-depth)", () => {
const executor = new DefaultExecutor("openai-compatible-test");
const headers = executor.buildHeaders(
{
apiKey: "real-key",
providerSpecificData: {
baseUrl: "https://x/v1",
customHeaders: { Authorization: "Bearer evil", Host: "evil.example", "X-Ok": "ok" },
},
},
false
) as Record<string, string>;
assert.equal(headers["X-Ok"], "ok");
assert.notEqual(
headers["Authorization"],
"Bearer evil",
"auth must not be overridden by a custom header"
);
assert.ok(!Object.keys(headers).some((k) => k.toLowerCase() === "host"));
});
// ── LEDGER-9: rowToCamel normalizes a NULL _json column to the base key ──
test("LEDGER-9: rowToCamel surfaces a NULL _json column under the base key as null", () => {
const row = { id: "x", custom_headers_json: null, name: "n" };
const camel = core.rowToCamel(row);
assert.equal(camel?.customHeaders, null, "NULL _json column should be customHeaders: null");
assert.ok(
!("customHeadersJson" in (camel as object)),
"the suffixed key must not leak on the null path"
);
});
// ── LEDGER-11: a node created on a fresh DB round-trips the custom_headers_json column ──
test("LEDGER-11: fresh-DB migration leaves provider_nodes.custom_headers_json usable", async () => {
const created = await providersDb.createProviderNode({
type: "openai-compatible",
name: "fresh",
prefix: "fresh",
apiType: "chat",
baseUrl: "https://x/v1",
customHeaders: { "X-A": "1" },
});
const fetched = await providersDb.getProviderNodeById(created.id as string);
assert.deepEqual(fetched?.customHeaders, { "X-A": "1" });
});