Compare commits

..

1 Commits

Author SHA1 Message Date
backryun
bc7a9c369c fix(security): resolve open CodeQL alerts
Alert 806 (js/insecure-randomness, open-sse/executors/tinycms.ts): the
TinyCMS nonce is signed into x-secure-signature and reused as
x-secure-nonce / x-session-id, so the Math.random() fallback made a
signed request predictable and replayable. Use randomUUID() from
node:crypto unconditionally.

Alert 811 (js/double-escaping, chatgpt-web adapters/environment.ts):
decodeXmlText() decoded & before " / ', so the bare & it
produced was re-consumed and the text was unescaped twice
(" collapsed to "). These values become the trusted Codex
sandbox cwd / workspace_roots, so the double-unescape silently rewrote
the workspace boundary. Decode & last.

Alerts 813/814 (js/incomplete-url-substring-sanitization, test files):
replace the includes() URL checks with exact comparisons
(new URL(url).hostname === ... and an explicit === over the recorded
URL array). Both assertions get strictly tighter.

Regression guards: tests/unit/tinycms-secure-nonce-randomness.test.ts
and tests/unit/chatgpt-web-environment-double-unescape.test.ts, both
failing before the fix and passing after.
2026-08-12 10:59:01 -03:00
21 changed files with 699 additions and 334 deletions

View File

@@ -1 +0,0 @@
- **fix(build):** repair the broken Turbopack production build, the red lint gate and a runtime crash on `release/v3.8.50`. Six independent module-level defects, each from a different PR, had accumulated because the `Build` CI job is advisory rather than blocking: a lost closing brace in `modelSelectModalHelpers.ts` that swallowed `PROVIDER_TEST_CHUNK_SIZE` into a function body (#9011); `handleFalVideoGeneration` imported twice in `videoGeneration.ts` after the provider-neutral Fal module superseded the standalone handler (#9982 over #9969); `catalog.ts` still re-exporting and calling the injectable stale-while-revalidate policy that #9199 deliberately replaced with a fixed 30 s bound when it landed on top of #8728 — the consumer and the #8728 test suite were never realigned; two dangling statements left in `catalogCache.ts::scheduleBackgroundRefresh` referencing undeclared `inFlight`/`promise`, which made **every** stale-while-revalidate read throw a `ReferenceError` at runtime (a defect the build never caught, surfaced here by the realigned test); a generated wasm-bindgen sidecar URL in `tinycmsSigner.ts` that Turbopack resolves at build time even though the WASM module ships inlined as base64 (#8736/#10087); `conolDiscovery.ts` importing `getProviderOutboundGuard` from `outboundUrlGuard` instead of the sibling `outboundUrlGuardPolicy` module that actually exports it (#8974) — fixed on the consumer side, since re-exporting it would put a `@/`-aliased import into the module the packaged CLI loads without a tsconfig (#7682); and an unbalanced brace in `tests/unit/db-adapters/driverFactory.test.ts` where a new case was inserted between the preceding test's `finally` block and its `});`, so the whole file stopped parsing and the SQLite driver-cascade coverage silently stopped running since 2026-08-11 (#9173).

View File

@@ -1,3 +1,5 @@
import { randomUUID } from "node:crypto";
import { BaseExecutor, type ExecuteInput, type ExecutorExecuteResult } from "./base.ts";
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
import { initTinyCmsWasm, generateSecurePayload } from "./tinycmsSigner.ts";
@@ -28,9 +30,9 @@ async function fetchChallenge(uuid: string): Promise<any> {
const res = await fetch(CHALLENGE_URL, {
method: "GET",
headers: {
"uuid": uuid,
uuid: uuid,
"x-origin": "https://gov.freegpt.win",
"Accept": "application/json",
Accept: "application/json",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
},
});
@@ -67,10 +69,11 @@ export class TinyCmsExecutor extends BaseExecutor {
const challengeObj = await fetchChallenge(uuid);
const timestamp = Date.now().toString();
const nonceJs =
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
// Security context: this nonce is signed into `x-secure-signature` and
// reused as the session id, so it must be unpredictable. `node:crypto`
// randomUUID() is always available on the supported runtime — never fall
// back to Math.random() (CodeQL js/insecure-randomness).
const nonceJs = randomUUID();
const securePayload = generateSecurePayload(
uuid,
@@ -122,12 +125,7 @@ export class TinyCmsExecutor extends BaseExecutor {
transformedBody: bodyObj,
};
} catch (err: any) {
return makeErrorResult(
500,
`TinyCMS Error: ${err.message}`,
body,
CHAT_URL
);
return makeErrorResult(500, `TinyCMS Error: ${err.message}`, body, CHAT_URL);
}
}
}

View File

@@ -438,14 +438,7 @@ async function __wbg_init(module_or_path) {
}
if (module_or_path === undefined) {
// Upstream wasm-bindgen glue defaults to a sidecar binary resolved via
// `new URL(<sidecar>, import.meta.url)`. OmniRoute ships the module inlined as
// WASM_BASE64 instead — no sidecar exists in the repo — and the only caller,
// initTinyCmsWasm(), always passes that decoded Buffer explicitly, so this
// branch is unreachable. The literal URL still had to go: Turbopack resolves
// `new URL(<literal>, import.meta.url)` statically, so keeping it failed
// `next build` with a "Module not found" for the missing sidecar.
throw new Error('TinyCMS WASM module must be supplied explicitly (see initTinyCmsWasm)');
module_or_path = new URL('wasm_signer_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();

View File

@@ -18,6 +18,7 @@ import { handleNovitaVideoGeneration } from "./videoGeneration/novitaHandler.ts"
import { handleXaiVideoGeneration } from "./videoGeneration/xaiGrokImagineHandler.ts";
import { handleSegmindVideoGeneration } from "./videoGeneration/providers/segmind.ts";
import { handleAdobeFireflyVideoGeneration } from "./videoGeneration/adobeFireflyHandler.ts";
import { handleFalVideoGeneration } from "./videoGeneration/falHandler.ts";
import { handleOpenAIVideoGeneration } from "./videoGeneration/openai.ts";
import { getVideoJobPreset, handleVideoJobGeneration } from "./videoGeneration/job.ts";
import {

View File

@@ -0,0 +1,254 @@
import { saveCallLog } from "@/lib/usageDb";
import {
FetchTimeoutError,
fetchWithTimeout,
getConfiguredTimeout,
} from "@/shared/utils/fetchTimeout";
import { sanitizeErrorMessage } from "../../utils/error.ts";
interface FalVideoBody {
prompt?: unknown;
aspect_ratio?: unknown;
duration?: unknown;
resolution?: unknown;
quality?: unknown;
generate_audio?: unknown;
poll_interval_ms?: unknown;
[key: string]: unknown;
}
interface FalCredentials {
apiKey?: unknown;
accessToken?: unknown;
}
interface FalProviderConfig {
baseUrl: string;
}
interface FalLog {
info?: (scope: string, message: string, meta?: unknown) => void;
error?: (scope: string, message: string) => void;
}
function stringValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function numberValue(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function grokDuration(value: unknown, fallback = 6): number {
const numeric = numberValue(value);
if (numeric !== undefined) return Math.round(numeric);
if (typeof value === "string") {
const match = value.trim().match(/^(\d+)s$/);
if (match) return Number(match[1]);
}
return fallback;
}
function falDuration(value: unknown, fallback = "8s"): string {
if (typeof value === "string" && /^(4|6|8)s$/.test(value)) return value;
const numeric = numberValue(value);
return numeric && [4, 6, 8].includes(numeric) ? `${numeric}s` : fallback;
}
export function buildFalVideoPayload(model: string, body: FalVideoBody): Record<string, unknown> {
const prompt = stringValue(body.prompt) || "";
const aspectRatio = stringValue(body.aspect_ratio) || "16:9";
const resolution = stringValue(body.resolution) || (body.quality === "hd" ? "1080p" : "720p");
if (model.startsWith("xai/grok-imagine-video/")) {
return {
prompt,
aspect_ratio: aspectRatio,
duration: grokDuration(body.duration),
resolution,
};
}
return {
prompt,
aspect_ratio: aspectRatio,
duration: falDuration(body.duration),
resolution,
generate_audio: typeof body.generate_audio === "boolean" ? body.generate_audio : true,
};
}
function absoluteFalUrl(value: unknown, baseUrl: string): string | undefined {
const url = stringValue(value);
if (!url) return undefined;
if (url.startsWith("http://") || url.startsWith("https://")) return url;
return `${baseUrl.replace(/\/$/, "")}/${url.replace(/^\//, "")}`;
}
function normalizeFalVideoResponse(payload: unknown) {
const record = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
const video =
record.video && typeof record.video === "object"
? (record.video as Record<string, unknown>)
: null;
const url = video && typeof video.url === "string" ? video.url.trim() : "";
if (!url) {
return {
success: false as const,
status: 502,
error: "Fal video generation returned no video URL",
};
}
return {
success: true as const,
data: {
created: typeof record.created === "number" ? record.created : Math.floor(Date.now() / 1000),
data: [{ url, format: "mp4" }],
},
};
}
function falModelPath(model: string): string {
return model.startsWith("xai/") ? model : `fal-ai/${model}`;
}
function getToken(credentials: FalCredentials | null | undefined): string {
return String(credentials?.apiKey || credentials?.accessToken || "");
}
function responseError(payload: unknown): string {
return sanitizeErrorMessage(JSON.stringify(payload).slice(0, 500));
}
export async function handleFalVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}: {
model: string;
provider: string;
providerConfig: FalProviderConfig;
body: FalVideoBody;
credentials: FalCredentials | null | undefined;
log?: FalLog | null;
}) {
const token = getToken(credentials);
if (!token) return { success: false as const, status: 401, error: "Fal API key is required" };
const startTime = Date.now();
const timeoutMs = getConfiguredTimeout();
const pollIntervalMs = Math.max(100, numberValue(body.poll_interval_ms) || 1000);
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
const queueUrl = `${baseUrl}/${falModelPath(model)}`;
const headers = {
Authorization: `Key ${token}`,
"Content-Type": "application/json",
};
log?.info?.("VIDEO", `${provider}/${model} (fal-ai-video)`, {
prompt: stringValue(body.prompt)?.slice(0, 200) || "",
});
try {
const createResponse = await fetchWithTimeout(queueUrl, {
method: "POST",
headers,
body: JSON.stringify(buildFalVideoPayload(model, body)),
timeoutMs,
});
const createPayload = await createResponse.json().catch(() => ({}));
if (!createResponse.ok) {
const error = responseError(createPayload);
log?.error?.("VIDEO", `Fal create failed (${createResponse.status}): ${error}`);
return { success: false as const, status: createResponse.status, error };
}
const requestId = stringValue(createPayload.request_id);
if (!requestId) return normalizeFalVideoResponse(createPayload);
const statusUrl =
absoluteFalUrl(createPayload.status_url, baseUrl) ||
`${queueUrl}/requests/${requestId}/status`;
const responseUrl =
absoluteFalUrl(createPayload.response_url, baseUrl) || `${queueUrl}/requests/${requestId}`;
const deadline = startTime + timeoutMs;
while (Date.now() < deadline) {
const remainingMs = Math.max(1000, deadline - Date.now());
const statusResponse = await fetchWithTimeout(statusUrl, {
headers: { Authorization: `Key ${token}` },
timeoutMs: Math.min(timeoutMs, remainingMs),
});
const statusPayload = await statusResponse.json().catch(() => ({}));
if (!statusResponse.ok) {
return {
success: false as const,
status: statusResponse.status,
error: responseError(statusPayload),
};
}
const status = stringValue(statusPayload.status);
if (status === "COMPLETED") {
const resultResponse = await fetchWithTimeout(responseUrl, {
headers: { Authorization: `Key ${token}` },
timeoutMs: Math.min(timeoutMs, Math.max(1000, deadline - Date.now())),
});
const resultPayload = await resultResponse.json().catch(() => ({}));
if (!resultResponse.ok) {
return {
success: false as const,
status: resultResponse.status,
error: responseError(resultPayload),
};
}
const result = normalizeFalVideoResponse(resultPayload);
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: result.success ? 200 : result.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
...(result.success ? {} : { error: result.error }),
}).catch(() => {});
return result;
}
if (status && !["IN_QUEUE", "IN_PROGRESS"].includes(status)) {
return {
success: false as const,
status: 502,
error: `Fal video generation ended with status ${status}`,
};
}
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remainingMs)));
}
return {
success: false as const,
status: 504,
error: `Fal video generation timed out after ${timeoutMs}ms`,
};
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const isTimeout =
error instanceof FetchTimeoutError || (error as { name?: string }).name === "AbortError";
const status = isTimeout ? 504 : 502;
const safeMessage = sanitizeErrorMessage(message);
log?.error?.("VIDEO", `Fal request failed: ${safeMessage}`);
return { success: false as const, status, error: `Fal video provider error: ${safeMessage}` };
}
}

View File

@@ -1,10 +0,0 @@
/**
* Service boundary for Z.ai web-cookie credential parsing.
*
* `extractZaiToken` is pure credential parsing, not request execution, but it lives in
* `executors/zai-web/protocol.ts` alongside the transport it was written for. App routes
* and `src/lib` consumers must not import from `open-sse/executors/**` (G14 import
* boundary — see EXECUTOR_IMPORT_RESTRICTION in eslint.config.mjs), so they go through
* this service instead of reaching into the executor tree.
*/
export { extractZaiToken } from "../executors/zai-web/protocol.ts";

View File

@@ -237,12 +237,15 @@ function trustedEnvironmentText(parsed: CodexParsedRequest): string {
}
function decodeXmlText(value: string): string {
// `&amp;` MUST be decoded last: decoding it first produces a bare `&` that the
// later passes re-consume, so `&amp;quot;` would collapse to `"` instead of the
// literal `&quot;` (double-unescape — CodeQL js/double-escaping).
return value
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&amp;", "&")
.replaceAll("&quot;", '"')
.replaceAll("&#39;", "'");
.replaceAll("&#39;", "'")
.replaceAll("&amp;", "&");
}
function uniqueAbsolutePaths(values: string[], field: string): string[] {

View File

@@ -1,5 +1,5 @@
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuardPolicy";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { resolveConolCredentials } from "@omniroute/open-sse/services/conolAuth.ts";
import {
CONOL_FALLBACK_MODELS,

View File

@@ -23,7 +23,7 @@ import { QWEN_CLOUD_TEXT_MODELS } from "@omniroute/open-sse/config/providers/reg
import { filterAlibabaFreeEligibleModels } from "@omniroute/open-sse/services/alibabaFreeTierDiscovery.ts";
import { shouldUseLiveAlibabaFreeModelDiscovery } from "@omniroute/open-sse/services/alibabaFreeTier.ts";
import { isDashscopeTextModelId } from "@omniroute/open-sse/services/dashscopeTextModels.ts";
import { extractZaiToken } from "@omniroute/open-sse/services/zaiWebCredentials.ts";
import { extractZaiToken } from "@omniroute/open-sse/executors/zai-web.ts";
import { normalizeOpenAiLikeModelsResponse } from "./normalizers";
const QWEN_CLOUD_TEXT_MODEL_IDS = new Set(QWEN_CLOUD_TEXT_MODELS.map((model) => model.id));

View File

@@ -118,18 +118,25 @@ export { getCustomVisionCapabilityFields };
// lives in ./catalogCache. Re-exported here because the existing tests import the
// hooks from this module, and CATALOG_STALE_WHILE_REVALIDATE_MS is part of the
// documented behavior of this endpoint.
import { CATALOG_CACHE_TTL_MS_DEFAULT, resolveCachedCatalogResponse } from "./catalogCache";
import {
CATALOG_CACHE_TTL_MS_DEFAULT,
resolveCachedCatalogResponse,
type CatalogCachePolicy,
} from "./catalogCache";
export {
CATALOG_STALE_WHILE_REVALIDATE_MS,
getCatalogStaleWhileRevalidateMs,
__resetCatalogBuilderRunsForTest,
__getCatalogBuilderRunsForTest,
__expireCatalogCacheForTest,
__setCatalogCacheEntryForTest,
__flushCatalogBackgroundRefreshForTest,
__forceCatalogInFlightRejectionForTest,
__setCatalogStaleWhileRevalidateAccessorForTest,
__setCatalogStaleWhileRevalidateMsForTest,
} from "./catalogCache";
export type { CachedCatalog } from "./catalogCache";
export type { CachedCatalog, CatalogCachePolicy } from "./catalogCache";
const BUILTIN_AUTO_YIELD_INTERVAL = 8;
@@ -143,7 +150,8 @@ function yieldCatalogBuildTurn(): Promise<void> {
*/
export async function getUnifiedModelsResponse(
request: Request,
corsHeaders: Record<string, string> = {}
corsHeaders: Record<string, string> = {},
cachePolicy: CatalogCachePolicy = {}
) {
const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request });
@@ -176,6 +184,7 @@ export async function getUnifiedModelsResponse(
request,
{ corsHeaders, diagnosticHeaders },
buildCatalogPayload,
cachePolicy,
{
hideAutoCombos: settingsForAuth?.hideAutoCombos === true,
hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true,

View File

@@ -200,10 +200,12 @@ function scheduleBackgroundRefresh(
});
}, 0);
});
inFlight = { version: lastSeenCatalogCacheVersion, promise };
// Nobody on the stale path awaits this, so pre-handle the rejection; a cold-path
// caller that joins it via catalogInFlight attaches its own handler and still
// observes the failure.
refreshPromise.catch(() => {});
promise.catch(() => {});
catalogInFlight.set(cacheKey, { generation, promise: refreshPromise });
refreshPromise

View File

@@ -139,8 +139,6 @@ export function isProviderModelHidden(
return false;
}
return hiddenModelsByProvider.get(providerId)?.has(modelId) ?? false;
}
/** Matches the provider-page "Test All Models" concurrency (#chunks of 3). */
export const PROVIDER_TEST_CHUNK_SIZE = 3;

View File

@@ -0,0 +1,83 @@
/**
* CodeQL alert 811 — js/double-escaping (HIGH) on
* `open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/environment.ts`.
*
* `decodeXmlText()` unescapes the XML entities of the trusted Codex
* `<environment_context>` block. It decoded `&amp;` BEFORE `&quot;` / `&#39;`,
* so the `&` it produced was re-consumed by a later `replaceAll` and the text
* was unescaped twice: `&amp;quot;` collapsed to `"` instead of `&quot;`.
*
* These values become sandbox `cwd` / `workspace_roots` paths, so a
* double-unescape silently rewrites the trusted workspace boundary.
* `&amp;` must be decoded LAST.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { extractChatGptTurnEnvironment } from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/environment.ts";
function parsedRequestWithCwd(cwdLiteral: string) {
const environmentText = [
"<environment_context>",
` <cwd>${cwdLiteral}</cwd>`,
" <sandbox_mode>read-only</sandbox_mode>",
"</environment_context>",
].join("\n");
const turnMetadata = { internal_chat_message_metadata_passthrough: { turn_id: "turn-1" } };
return {
context: { tools: [] },
_rawBody: {
client_metadata: {
"x-codex-turn-metadata": JSON.stringify({ thread_id: "thread-1", turn_id: "turn-1" }),
},
input: [
{ type: "message", role: "system", content: [{ type: "input_text", text: "sys" }] },
{
type: "message",
role: "user",
content: [{ type: "input_text", text: environmentText }],
...turnMetadata,
},
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "hello" }],
...turnMetadata,
},
],
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
}
test("decoding the trusted Codex environment does not double-unescape &amp;quot;", () => {
const env = extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/ws&amp;quot;dir"));
assert.equal(
env.cwd,
"/tmp/ws&quot;dir",
'`&amp;quot;` must decode to the literal text `&quot;`, not to a double-unescaped `"`'
);
});
test("decoding the trusted Codex environment does not double-unescape &amp;lt; / &amp;#39;", () => {
assert.equal(
extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/ws&amp;lt;dir")).cwd,
"/tmp/ws&lt;dir"
);
assert.equal(
extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/ws&amp;#39;dir")).cwd,
"/tmp/ws&#39;dir"
);
});
test("single-level XML entities still decode normally", () => {
assert.equal(extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a&amp;b")).cwd, "/tmp/a&b");
assert.equal(
extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a&quot;b")).cwd,
'/tmp/a"b'
);
assert.equal(extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a&#39;b")).cwd, "/tmp/a'b");
assert.equal(extractChatGptTurnEnvironment(parsedRequestWithCwd("/tmp/a&gt;b")).cwd, "/tmp/a>b");
});

View File

@@ -25,19 +25,13 @@ const core = await import("@/lib/db/core.ts");
const { createCombo } = await import("@/lib/db/combos");
const { executeImageCombo } = await import("@omniroute/open-sse/services/imageCombo");
type LogEntry = { level: string; tag: unknown; msg: unknown };
function createLog() {
const entries: LogEntry[] = [];
const record =
(level: string) =>
(tag: unknown, msg: unknown): number =>
entries.push({ level, tag, msg });
const entries: any[] = [];
return {
info: record("info"),
warn: record("warn"),
error: record("error"),
debug: record("debug"),
info: (tag: any, msg: any) => entries.push({ level: "info", tag, msg }),
warn: (tag: any, msg: any) => entries.push({ level: "warn", tag, msg }),
error: (tag: any, msg: any) => entries.push({ level: "error", tag, msg }),
debug: (tag: any, msg: any) => entries.push({ level: "debug", tag, msg }),
entries,
};
}
@@ -58,13 +52,13 @@ function createMockAuth() {
}
async function cleanupTestDataDir() {
let lastError: unknown;
let lastError: any;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
return;
} catch (error: unknown) {
} catch (error: any) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 25));
}

View File

@@ -264,8 +264,6 @@ describe("driverFactory", () => {
second.close();
first.close();
}
});
// Cursor renewal plan, Task 2 Step 5: tryIdeAuth() now passes a
// busy-timeout to tryOpenSync() on every driver path, since it's invoked
// from an unattended sweep tick (not just the human-attended auto-import

View File

@@ -1,20 +1,3 @@
/**
* Stale-while-revalidate for the /v1/models catalog cache (#8728).
*
* Contract note: #8728 originally shipped an injectable `CatalogCachePolicy`
* (a per-call SWR accessor + refresh scheduler) and an unbounded
* `CATALOG_STALE_WHILE_REVALIDATE_MS = Number.POSITIVE_INFINITY`. #9199 landed
* afterwards and deliberately replaced both: the window is now a fixed 30 s
* constant and the refresh is scheduled internally via `setTimeout(…, 0)`.
* See catalogCache.ts — an unbounded window let a refresh that kept failing pin
* an ancient catalog forever.
*
* These tests were left asserting the removed API, which is what broke the
* production build (catalog.ts re-exported three symbols that no longer exist).
* They are realigned here to the shipped contract: the BEHAVIOR #8728 added —
* an expired-but-recent successful entry is served immediately while a refresh
* runs behind it — is still fully covered.
*/
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
@@ -27,6 +10,8 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const readCache = await import("../../src/lib/db/readCache.ts");
const catalogCache = await import("../../src/app/api/v1/models/catalogCache.ts");
type RefreshTask = () => Promise<void>;
function request() {
return new Request("http://localhost/v1/models");
}
@@ -40,11 +25,28 @@ function payload(body: string, status = 200): catalogCache.CatalogPayload {
};
}
async function resolve(build: (request: Request) => Promise<catalogCache.CatalogPayload>) {
function createPolicyQueue() {
const tasks: RefreshTask[] = [];
return {
policy: {
getStaleWhileRevalidateMs: () => Number.POSITIVE_INFINITY,
scheduleBackgroundRefresh: (task: RefreshTask) => {
tasks.push(task);
},
},
tasks,
};
}
async function resolve(
build: (request: Request) => Promise<catalogCache.CatalogPayload>,
policy = createPolicyQueue().policy
) {
return catalogCache.resolveCachedCatalogResponse(
request(),
{ corsHeaders: {}, diagnosticHeaders: {} },
build
build,
policy
);
}
@@ -56,88 +58,146 @@ test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("the SWR window is a bounded constant, not an unbounded accessor", () => {
// #9199 replaced the injectable POSITIVE_INFINITY accessor with this bound so a
// refresh that keeps failing cannot pin an old catalog forever.
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, 30_000);
assert.ok(Number.isFinite(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS));
test("production SWR policy is unbounded and reset restores the default accessor", () => {
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, Number.POSITIVE_INFINITY);
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
catalogCache.__setCatalogStaleWhileRevalidateAccessorForTest(() => 0);
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), 0);
catalogCache.__resetCatalogBuilderRunsForTest();
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
});
test("a fresh entry is replayed without running the builder again", async () => {
const first = await resolve(async () => payload("fresh"));
assert.equal(await first.text(), "fresh");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
test("reset detaches scheduled work before it can run", async () => {
const { policy, tasks } = createPolicyQueue();
await resolve(async () => payload("old"), policy);
catalogCache.__expireCatalogCacheForTest();
await resolve(async () => payload("detached"), policy);
assert.equal(tasks.length, 1);
const second = await resolve(async () => payload("SHOULD NOT BUILD"));
assert.equal(await second.text(), "fresh");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
catalogCache.__resetCatalogBuilderRunsForTest();
await tasks[0]();
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 0);
});
test("an expired successful entry is served immediately while a refresh runs behind it", async () => {
await resolve(async () => payload("old"));
test("ordinary TTL expiry serves the last success indefinitely and schedules one refresh per key", async () => {
const { policy, tasks } = createPolicyQueue();
const initial = await resolve(async () => payload("old"), policy);
assert.equal(await initial.text(), "old");
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
const staleResponses = await Promise.all(
Array.from({ length: 5 }, () => resolve(async () => payload("new"), policy))
);
assert.deepEqual(
await Promise.all(staleResponses.map((response) => response.text())),
Array(5).fill("old")
);
assert.equal(tasks.length, 1, "concurrent stale reads must schedule exactly one refresh");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
await tasks[0]();
const refreshed = await resolve(async () => payload("unexpected"), policy);
assert.equal(await refreshed.text(), "new");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("unsuccessful cold payloads are returned but never cached", async () => {
const first = await resolve(async () => payload("temporary failure", 503));
assert.equal(first.status, 503);
assert.equal(await first.text(), "temporary failure");
const second = await resolve(async () => payload("recovered"));
assert.equal(second.status, 200);
assert.equal(await second.text(), "recovered");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("failed background refresh retains the prior successful snapshot and permits retry", async (t) => {
t.mock.method(console, "error", () => {});
const { policy, tasks } = createPolicyQueue();
assert.equal(await (await resolve(async () => payload("old"), policy)).text(), "old");
catalogCache.__expireCatalogCacheForTest();
// The stale body comes back on THIS call — the caller never waits for the rebuild.
const stale = await resolve(async () => payload("new"));
assert.equal(await stale.text(), "old", "the expired-but-recent entry must be served as-is");
await catalogCache.__flushCatalogBackgroundRefreshForTest();
const refreshed = await resolve(async () => payload("SHOULD NOT BUILD"));
assert.equal(await refreshed.text(), "new", "the background refresh must replace the snapshot");
});
test("an entry aged past the SWR window is not served stale", async () => {
await resolve(async () => payload("ancient"));
catalogCache.__expireCatalogCacheForTest(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS + 1_000);
const rebuilt = await resolve(async () => payload("rebuilt"));
assert.equal(await rebuilt.text(), "rebuilt", "past the window the caller must wait for a build");
});
test("a cached non-200 is never replayed as stale", async () => {
// Replaying a cached error as "stale" would mask an intermittent failure behind
// a fake success forever.
catalogCache.__setCatalogCacheEntryForTest(request(), {
body: "boom",
headers: {},
status: 500,
expiresAt: Date.now() - 1,
});
const res = await resolve(async () => payload("recovered"));
assert.equal(res.status, 200);
assert.equal(await res.text(), "recovered");
});
test("concurrent cold requests share a single builder run", async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const build = async () => {
await gate;
return payload("coalesced");
};
const inFlight = [resolve(build), resolve(build), resolve(build)];
release();
const bodies = await Promise.all((await Promise.all(inFlight)).map((r) => r.text()));
assert.deepEqual(bodies, ["coalesced", "coalesced", "coalesced"]);
assert.equal(
catalogCache.__getCatalogBuilderRunsForTest(),
1,
"identical concurrent requests must coalesce onto one in-flight build (#6408)"
await (
await resolve(async () => {
throw new Error("temporary failure");
}, policy)
).text(),
"old"
);
await tasks.shift()!();
assert.equal(
await (await resolve(async () => payload("temporary failure", 503), policy)).text(),
"old"
);
assert.equal(tasks.length, 1, "a failed refresh must release single-flight state for retry");
await tasks.shift()!();
assert.equal(await (await resolve(async () => payload("new"), policy)).text(), "old");
assert.equal(tasks.length, 1, "an unsuccessful payload must also permit another refresh");
await tasks.shift()!();
assert.equal(await (await resolve(async () => payload("unused"), policy)).text(), "new");
});
test("a state change invalidates the cache so the next read rebuilds", async () => {
await resolve(async () => payload("before"));
test("hard invalidation drops snapshots, detaches old work, and guards old-generation writeback", async () => {
let resolveOld!: (value: catalogCache.CatalogPayload) => void;
const oldPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveOld = resolvePromise;
});
let currentBuildStarted = false;
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveCurrent = resolvePromise;
});
readCache.invalidateDbCache();
const oldRequest = resolve(async () => oldPayload);
await Promise.resolve();
const after = await resolve(async () => payload("after"));
assert.equal(await after.text(), "after", "a write must be reflected on the very next read");
readCache.invalidateModelCatalogCache();
const currentRequest = resolve(async () => {
currentBuildStarted = true;
return currentPayload;
});
await Promise.resolve();
assert.equal(currentBuildStarted, true, "the first post-write read must start a current build");
resolveCurrent(payload("current"));
assert.equal(await (await currentRequest).text(), "current");
resolveOld(payload("old"));
assert.equal(await (await oldRequest).text(), "old");
const cached = await resolve(async () => payload("unexpected"));
assert.equal(await cached.text(), "current", "old completion must not overwrite current cache");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("hard invalidation clears a completed snapshot and makes the next read block", async () => {
assert.equal(await (await resolve(async () => payload("old"))).text(), "old");
readCache.invalidateModelCatalogCache();
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveCurrent = resolvePromise;
});
let settled = false;
const next = resolve(async () => currentPayload).then((response) => {
settled = true;
return response;
});
await Promise.resolve();
assert.equal(settled, false, "post-write reads may block and must not serve the old snapshot");
resolveCurrent(payload("current"));
assert.equal(await (await next).text(), "current");
});

View File

@@ -1,142 +0,0 @@
/**
* Regression guard for the broken Turbopack production build on release/v3.8.50.
*
* Six independent module-level defects, each from a different PR, made `npm run build`
* fail. Most are *link-time* errors, so those cases are expressed as "this module must be
* importable / statically resolvable" rather than as behavioral assertions:
*
* 1. src/shared/components/modelSelectModalHelpers.ts — a lost `}` (#9011) left
* `export const PROVIDER_TEST_CHUNK_SIZE` inside a function body.
* 2. open-sse/handlers/videoGeneration.ts — `handleFalVideoGeneration` imported
* twice from two different modules (#9982 landed on top of #9969).
* 3. src/app/api/v1/models/catalog.ts — re-exported three SWR symbols that #9199
* deliberately removed from ./catalogCache when it replaced #8728's injectable
* policy with a fixed 30 s bound. The consumer was never updated.
* 4. src/app/api/v1/models/catalogCache.ts — the same PR left two dangling statements
* referencing undeclared `inFlight` / `promise`, so every stale-while-revalidate
* read threw a ReferenceError at RUNTIME (this one the build never caught).
* 5. open-sse/executors/tinycmsSigner.ts — generated wasm-bindgen glue kept a
* `new URL('wasm_signer_bg.wasm', import.meta.url)` default that no file backs.
* 6. src/app/api/providers/[id]/models/conolDiscovery.ts — imported
* `getProviderOutboundGuard` from ./outboundUrlGuard, which does not export it (#8974).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("modelSelectModalHelpers exports survive isProviderModelHidden (#9011 missing brace)", async () => {
const helpers = await import("@/shared/components/modelSelectModalHelpers");
// The missing `}` swallowed everything after isProviderModelHidden into its body,
// so this constant stopped being a module-level export.
assert.equal(helpers.PROVIDER_TEST_CHUNK_SIZE, 3);
const hidden = helpers.parseHiddenModelsByProvider({ openai: ["gpt-4o"] });
assert.equal(helpers.isProviderModelHidden(hidden, "openai", "gpt-4o"), true);
assert.equal(helpers.isProviderModelHidden(hidden, "openai", "gpt-4o-mini"), false);
assert.equal(helpers.isProviderModelHidden(hidden, "anthropic", "gpt-4o"), false);
});
test("videoGeneration handler links with a single handleFalVideoGeneration binding (#9982)", async () => {
// A duplicate import binding is an ESM early SyntaxError, so the import itself is
// the assertion. The Fal video path must resolve to the provider-neutral module
// that #9982 added (it also covers the #9969 Grok Imagine routing).
const mod = await import("@omniroute/open-sse/handlers/videoGeneration.ts");
assert.equal(typeof mod.handleVideoGeneration, "function");
const source = readFileSync(path.join(repoRoot, "open-sse/handlers/videoGeneration.ts"), "utf8");
const falImports = source.match(/import \{ handleFalVideoGeneration \}/g) ?? [];
assert.equal(falImports.length, 1, "handleFalVideoGeneration must be imported exactly once");
assert.match(source, /handleFalVideoGeneration \} from "\.\/mediaGeneration\/fal\.ts"/);
});
test("catalog.ts re-exports only what catalogCache actually exports (#9199)", async () => {
// #9199 deliberately replaced #8728's injectable SWR policy with a fixed bound, but
// left catalog.ts re-exporting the three removed symbols. Re-exporting a binding the
// source module does not export is a link-time error, so this import IS the assertion.
const catalog = await import("@/app/api/v1/models/catalog");
const catalogCache = await import("@/app/api/v1/models/catalogCache");
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, 30_000);
assert.equal(catalog.CATALOG_STALE_WHILE_REVALIDATE_MS, 30_000);
// The accessor/policy trio must stay gone — re-adding it would resurrect the
// unbounded window #9199 removed (a failing refresh could pin an old catalog forever).
for (const removed of [
"getCatalogStaleWhileRevalidateMs",
"__setCatalogStaleWhileRevalidateAccessorForTest",
"__setCatalogStaleWhileRevalidateMsForTest",
]) {
assert.equal(
(catalogCache as Record<string, unknown>)[removed],
undefined,
`${removed} was removed by #9199 and must not come back`
);
}
});
test("the stale-while-revalidate path does not throw a ReferenceError (#9199 merge residue)", async () => {
// The mangled merge left `inFlight = { … promise }` referencing two undeclared
// identifiers inside scheduleBackgroundRefresh, so EVERY stale read crashed at runtime.
const catalogCache = await import("@/app/api/v1/models/catalogCache");
catalogCache.__resetCatalogBuilderRunsForTest();
const payload = (body: string) => ({
body,
headers: { "content-type": "application/json" },
status: 200,
cacheTTL: 60_000,
});
const call = (body: string) =>
catalogCache.resolveCachedCatalogResponse(
new Request("http://localhost/v1/models"),
{ corsHeaders: {}, diagnosticHeaders: {} },
async () => payload(body)
);
await call("first");
catalogCache.__expireCatalogCacheForTest();
const stale = await call("second");
assert.equal(await stale.text(), "first", "the stale body must be served, not a crash");
await catalogCache.__flushCatalogBackgroundRefreshForTest();
});
test("conolDiscovery resolves getProviderOutboundGuard from the policy module (#8974)", async () => {
// outboundUrlGuard.ts does NOT export getProviderOutboundGuard — importing it from
// there is a link-time error, so the import below is the assertion.
const mod = await import("@/app/api/providers/[id]/models/conolDiscovery");
assert.equal(typeof mod.maybeHandleConolModelDiscovery, "function");
// The fix must stay on the consumer side. outboundUrlGuard.ts is loaded by the packaged
// CLI, where no tsconfig resolves `@/*`, so re-exporting the policy helpers from it
// (they pull in featureFlags → the DB layer) would break `omniroute setup-opencode`
// (#7682). Guard that nobody "fixes" this by adding the re-export instead.
const guardSource = readFileSync(
path.join(repoRoot, "src/shared/network/outboundUrlGuard.ts"),
"utf8"
);
assert.doesNotMatch(
guardSource,
/^\s*(import|export)[^\n]*from\s+["']@\//m,
"outboundUrlGuard.ts must stay free of @/-aliased imports/re-exports (#7682)"
);
});
test("tinycmsSigner has no build-time-resolvable wasm sidecar URL (#8736/#10087)", () => {
const source = readFileSync(path.join(repoRoot, "open-sse/executors/tinycmsSigner.ts"), "utf8");
// The wasm binary ships inlined as WASM_BASE64; there is no wasm_signer_bg.wasm file.
// Turbopack statically resolves `new URL(<literal>, import.meta.url)`, so leaving the
// generated default in place fails `next build` with "Module not found".
assert.ok(source.includes("const WASM_BASE64 ="), "wasm binary must stay inlined");
assert.doesNotMatch(
source,
/new URL\(\s*['"]wasm_signer_bg\.wasm['"]/,
"generated wasm-bindgen sidecar URL must not survive — Turbopack resolves it at build time"
);
});

View File

@@ -17,10 +17,7 @@ import assert from "node:assert/strict";
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
import { getExecutor, TinyCmsExecutor } from "../../open-sse/executors/index.ts";
import {
setupDomMocks,
type DomMockRestore,
} from "../../open-sse/executors/tinycmsSigner.ts";
import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts";
// tinycmsSigner.ts intentionally does NOT install its window/document/canvas
// shims as a module-load side effect (see setupDomMocks() there) — doing so
@@ -41,9 +38,10 @@ after(() => {
// ── Catalog / WEB_COOKIE_PROVIDERS ────────────────────────────────────────────
test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
"tinycms-web"
] as Record<string, unknown>;
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["tinycms-web"] as Record<
string,
unknown
>;
assert.ok(p, "WEB_COOKIE_PROVIDERS['tinycms-web'] must exist");
assert.equal(p.id, "tinycms-web");
assert.equal(p.alias, "tcw");
@@ -51,9 +49,10 @@ test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
});
test("tinycms-web WEB_COOKIE_PROVIDERS entry is marked as free-tier", () => {
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
"tinycms-web"
] as Record<string, unknown>;
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["tinycms-web"] as Record<
string,
unknown
>;
assert.equal(p.hasFree, true);
assert.ok(typeof p.freeNote === "string" && (p.freeNote as string).length > 0);
assert.ok(typeof p.authHint === "string" && (p.authHint as string).length > 0);
@@ -79,10 +78,7 @@ test("tinycms-web registry has all expected models", () => {
assert.ok(ids.includes("gpt-5-free"), "gpt-5-free must be registered");
assert.ok(ids.includes("gpt-5.3-free"), "gpt-5.3-free must be registered");
assert.ok(
ids.includes("gpt-5.3-thinking-free"),
"gpt-5.3-thinking-free must be registered"
);
assert.ok(ids.includes("gpt-5.3-thinking-free"), "gpt-5.3-thinking-free must be registered");
assert.ok(ids.includes("deepseek-v4-flash"), "deepseek-v4-flash must be registered");
assert.ok(ids.includes("claude-sonnet-5"), "claude-sonnet-5 must be registered");
assert.ok(ids.includes("gemini-3.5-flash"), "gemini-3.5-flash must be registered");
@@ -140,10 +136,7 @@ test("TinyCmsExecutor returns 401 when UUID is missing", async () => {
assert.equal(result.response.status, 401);
const body = await result.response.json();
const errMsg = body?.error?.message || "";
assert.ok(
errMsg.includes("Invalid or missing device UUID"),
"error must mention missing UUID"
);
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
// Hard Rule #12: must NOT leak stack traces
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
});
@@ -161,10 +154,7 @@ test("TinyCmsExecutor returns 401 when UUID does not start with 'R'", async () =
assert.equal(result.response.status, 401);
const body = await result.response.json();
const errMsg = body?.error?.message || "";
assert.ok(
errMsg.includes("Invalid or missing device UUID"),
"error must mention missing UUID"
);
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
});
@@ -172,7 +162,7 @@ test("TinyCmsExecutor returns the standard executor response envelope on success
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input) => {
const url = String(input);
if (url.includes("api64.ipify.org")) {
if (new URL(url).hostname === "api64.ipify.org") {
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
headers: { "Content-Type": "application/json" },
});
@@ -217,10 +207,7 @@ test("TinyCmsExecutor returns the standard executor response envelope on success
test("initTinyCmsWasm module exports expected functions", async () => {
const signer = await import("../../open-sse/executors/tinycmsSigner.ts");
assert.ok(
typeof signer.initTinyCmsWasm === "function",
"must export initTinyCmsWasm function"
);
assert.ok(typeof signer.initTinyCmsWasm === "function", "must export initTinyCmsWasm function");
assert.ok(
typeof signer.generateSecurePayload === "function",
"must export generateSecurePayload function"
@@ -254,10 +241,7 @@ test("TinyCmsExecutor sanitizes errors (no stack traces in error response)", asy
assert.ok(result.response, "response must be present");
const body = await result.response.json();
const errMsg = body?.error?.message || "";
assert.ok(
errMsg.includes("Invalid or missing device UUID"),
"error must mention missing UUID"
);
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path (Hard Rule #12)");
});
@@ -274,12 +258,6 @@ test("tinycms-web credential requirement is kind: token with app-config-uuid", a
assert.equal(req.credentialName, "app-config-uuid");
assert.equal(req.acceptsFullCookieHeader, false);
assert.ok(Array.isArray(req.storageKeys), "must have storageKeys array");
assert.ok(
(req.storageKeys as string[]).includes("apiKey"),
"apiKey must be in storageKeys"
);
assert.ok(
(req.storageKeys as string[]).includes("uuid"),
"uuid must be in storageKeys"
);
assert.ok((req.storageKeys as string[]).includes("apiKey"), "apiKey must be in storageKeys");
assert.ok((req.storageKeys as string[]).includes("uuid"), "uuid must be in storageKeys");
});

View File

@@ -134,11 +134,8 @@ await test("chave quota-exclusive não constrói o catálogo completo", async (t
process.env.EXPOSE_CC_DISCOVERY_ALIASES = "1";
// A chave do cache é `prefix|isCodex|apiKey|configuredOnly` — um query param
// qualquer NÃO a invalida, então a resposta do subteste anterior seria servida.
// #9199 removed the injectable SWR window; age the entry past the fixed
// 30 s constant instead so the next read rebuilds rather than serving stale.
v1ModelsCatalog.__expireCatalogCacheForTest(
v1ModelsCatalog.CATALOG_STALE_WHILE_REVALIDATE_MS + 1_000
);
v1ModelsCatalog.__setCatalogStaleWhileRevalidateMsForTest(0);
v1ModelsCatalog.__expireCatalogCacheForTest(1);
try {
const res = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models", {
@@ -154,6 +151,9 @@ await test("chave quota-exclusive não constrói o catálogo completo", async (t
`ids=${JSON.stringify(body.data.map((m) => m.id).slice(0, 6))}`
);
} finally {
v1ModelsCatalog.__setCatalogStaleWhileRevalidateMsForTest(
v1ModelsCatalog.CATALOG_STALE_WHILE_REVALIDATE_MS
);
if (prev === undefined) delete process.env.EXPOSE_CC_DISCOVERY_ALIASES;
else process.env.EXPOSE_CC_DISCOVERY_ALIASES = prev;
}

View File

@@ -0,0 +1,142 @@
/**
* CodeQL alert 806 — js/insecure-randomness (HIGH) on
* `open-sse/executors/tinycms.ts`.
*
* The TinyCMS executor derives `x-secure-nonce` / `x-session-id` from a nonce
* that is fed into the upstream request signature (`generateSecurePayload`).
* That is a security context, so the nonce must never fall back to
* `Math.random()` — a predictable nonce lets an observer replay or forge a
* signed request.
*
* The regression guard runs the executor with a `globalThis.crypto` that has no
* `randomUUID` (the exact condition that used to select the `Math.random()`
* fallback) and asserts the emitted nonce is still a cryptographically strong
* UUID.
*/
import test, { before, after } from "node:test";
import assert from "node:assert/strict";
import { TinyCmsExecutor } from "../../open-sse/executors/index.ts";
import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts";
let restoreDomMocks: DomMockRestore;
before(() => {
restoreDomMocks = setupDomMocks();
});
after(() => {
restoreDomMocks();
});
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
test("TinyCMS nonce stays cryptographically strong when globalThis.crypto has no randomUUID", async () => {
const originalFetch = globalThis.fetch;
const originalCryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")!;
const realCrypto = globalThis.crypto;
// Keep every other WebCrypto capability, drop only `randomUUID`. This is the
// branch that previously fell back to `Math.random()`.
Object.defineProperty(globalThis, "crypto", {
configurable: true,
value: {
getRandomValues: (array: ArrayBufferView) => realCrypto.getRandomValues(array as never),
subtle: realCrypto.subtle,
},
});
const seenHeaders: Record<string, string>[] = [];
globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
const url = String(input);
if (new URL(url).hostname === "api64.ipify.org") {
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
headers: { "Content-Type": "application/json" },
});
}
if (new URL(url).pathname === "/api/challenge") {
return new Response(
JSON.stringify({
challenge: "test",
challengeId: "challenge-id",
expiresAt: Date.now() + 60_000,
version: "1",
difficulty: 0,
}),
{ headers: { "Content-Type": "application/json" } }
);
}
seenHeaders.push((init?.headers ?? {}) as Record<string, string>);
return new Response("upstream body", { status: 200 });
}) as typeof fetch;
try {
await new TinyCmsExecutor().execute({
model: "gpt-5-free",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "Rtest-device" },
});
assert.equal(seenHeaders.length, 1, "the executor must reach the chat endpoint exactly once");
const headers = seenHeaders[0]!;
assert.match(
headers["x-secure-nonce"] ?? "",
UUID_RE,
"x-secure-nonce must be a crypto-strong UUID, never a Math.random() fallback"
);
assert.match(
headers["x-session-id"] ?? "",
UUID_RE,
"x-session-id must be a crypto-strong UUID, never a Math.random() fallback"
);
} finally {
globalThis.fetch = originalFetch;
Object.defineProperty(globalThis, "crypto", originalCryptoDescriptor);
}
});
test("consecutive TinyCMS nonces are unique", async () => {
const originalFetch = globalThis.fetch;
const nonces: string[] = [];
globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
const url = String(input);
if (new URL(url).hostname === "api64.ipify.org") {
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
headers: { "Content-Type": "application/json" },
});
}
if (new URL(url).pathname === "/api/challenge") {
return new Response(
JSON.stringify({
challenge: "test",
challengeId: "challenge-id",
expiresAt: Date.now() + 60_000,
version: "1",
difficulty: 0,
}),
{ headers: { "Content-Type": "application/json" } }
);
}
nonces.push(((init?.headers ?? {}) as Record<string, string>)["x-secure-nonce"] ?? "");
return new Response("upstream body", { status: 200 });
}) as typeof fetch;
try {
const executor = new TinyCmsExecutor();
for (let i = 0; i < 3; i += 1) {
await executor.execute({
model: "gpt-5-free",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "Rtest-device" },
});
}
assert.equal(nonces.length, 3);
assert.equal(new Set(nonces).size, 3, "each request must carry a distinct nonce");
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -55,14 +55,19 @@ test("#8014: ZaiWebExecutor must POST to the current chat.z.ai v2 chat-completio
assert.ok(requested.length > 0, "the direct path must actually reach fetch");
assert.ok(
!requested.includes(STALE_URL),
// Exact-URL match (not a substring test): `requested` holds whole URLs.
!requested.some((url) => url === STALE_URL),
`zai-web executor POSTed to the stale endpoint — matches #8014's model-independent 404 "Not Found"`
);
// The executor also probes the homepage for the frontend version and calls
// /api/v1/chats/new first, so pick the completions request by its path.
const completions = requested.filter((u) => new URL(u).pathname.endsWith("/chat/completions"));
assert.equal(completions.length, 1, `expected exactly one completions request, got ${requested}`);
assert.equal(
completions.length,
1,
`expected exactly one completions request, got ${requested}`
);
assert.equal(
new URL(completions[0]).pathname,
"/api/v2/chat/completions",