Compare commits

...

4 Commits

Author SHA1 Message Date
Xiangzhe
6e112d4b4f fix(security): compare the OIDC state cookie in constant time
GHSA-7434-6q4c-33fh: the OIDC callback validated the CSRF `state` cookie with
`storedState !== returnedState`. `!==` short-circuits on the first differing
byte, so rejection time correlates with matching-prefix length (CWE-208).

The reporter scopes this honestly and so do we: `oidc_state` is a single-use
per-login nonce, cleared immediately after validation, and recovering it would
not by itself yield an authorization code. The reason to fix it is that this was
the one callback in the repo still comparing a secret with `!==` — every sibling
(the OAuth callback, the A2A token check, Telegram initData, the CLI token
check) already compares in constant time — and an inconsistent pattern is one
that gets copied into a context where it does matter.

Which is exactly what had already happened: `isInternalAdmissionBypass()` gated
an admission-lane bypass on `match[1].toLowerCase() === resolveSelfLoopBearer()
.toLowerCase()`. That one guards a shared secret with real consequences, so it
is fixed here too.

Both now use `timingSafeCompare()` (src/shared/utils/timingSafeCompare.ts). The
five lines it wraps were already copy-pasted into at least eight places; this is
the shared one. The existing copies are left alone — consolidating them is a
refactor, not a security fix, and does not belong in this diff.

tests/unit/timing-safe-compare.test.ts — 6 tests, the two callsite guards red
before the fix. The helper is covered for equal, differing-same-length,
differing-length, null/undefined identity, and byte-exactness (precomposed vs
decomposed "é" must not match).

Closes GHSA-7434-6q4c-33fh
2026-08-26 11:31:55 -03:00
Diego Rodrigues de Sa e Souza
91aeca0440 fix(security): redact NVIDIA_API_KEY at the log sink in the ad-hoc diag script (#11639)
Merged via /merge-batch (2026-08-26, v3.8.51). Validado no worktree combinado.
2026-08-26 10:59:26 -03:00
Webman
49749eb0d0 fix(types): clear the two type regressions red on release/v3.8.51 (#11640)
Merged via /merge-batch (2026-08-26, v3.8.51). Confirma o fix do typecheck:core que eu vinha classificando como drift pré-existente nas últimas levas (zai-web.ts mimeType) — obrigado por resolver de vez. Validado: typecheck limpo, 7/7 testes passando.
2026-08-26 10:59:19 -03:00
Bob.Hou
8d17110082 fix(build): resolve workspace: protocol dependencies to concrete semver versions during prepublish (#11540) (#11641)
Merged via /merge-batch (2026-08-26, v3.8.51). Validado: typecheck/complexity/file-size/changelog verdes, 3/3 testes do prepublish workspace-protocol passando. Obrigado pela contribuição.
2026-08-26 10:59:14 -03:00
13 changed files with 717 additions and 14 deletions

View File

@@ -90,13 +90,19 @@ async function resolveZaiBrowserAttachments(
> {
try {
// Browser-page upload: keep the original bytes/mimeType (no Cursor wire prep).
// EncodedImage.mimeType is optional on the wire type, but every producer
// reachable here (decodeDataUrl / fetchImageBytes) validates an image/*
// string before pushing; the fallback only satisfies the attachment type.
const images = await resolveCursorImages(imageUrls, { prepareForWire: false });
return {
attachments: images.map((image, index) => ({
name: zaiImageFileName(image.mimeType, index),
mimeType: image.mimeType,
buffer: image.data,
})),
attachments: images.map((image, index) => {
const mimeType = image.mimeType ?? "image/jpeg";
return {
name: zaiImageFileName(mimeType, index),
mimeType,
buffer: image.data,
};
}),
};
} catch (error) {
const message =

View File

@@ -15,6 +15,7 @@ interface ErrorResponseBody {
message: string;
type?: string;
code?: string;
reason?: string;
};
upstream_details?: Record<string, unknown> | null; // sanitized upstream provider body
}
@@ -108,6 +109,7 @@ export function sanitizeUpstreamDetails(value: unknown, depth = 0): unknown {
export type ErrorBodyClassification = {
type?: string;
code?: string;
reason?: string;
};
/**
@@ -132,6 +134,7 @@ export function buildErrorBody(
message: safeMessage,
type: classification?.type ?? errorInfo.type,
code: classification?.code ?? errorInfo.code,
reason: classification?.reason,
},
};

View File

@@ -19,12 +19,20 @@
*/
const KEY = process.env.NVIDIA_API_KEY ?? "";
const BASE_URL = process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
const BASE_URL =
process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
const MODEL = process.env.NVIDIA_MODEL || "openai/gpt-oss-120b";
// Neutralize CR/LF before logging so env-derived values (NVIDIA_MODEL, etc.)
// cannot forge extra log lines (S5145 log injection).
const line = (s = "") => console.log(String(s).replace(/[\r\n]+/g, " "));
// cannot forge extra log lines (S5145 log injection). Also strip any raw
// occurrence of the API key so an upstream error/response that echoes it
// back (e.g. inside err.stack or a validation result) never reaches the
// terminal in clear text (js/clear-text-logging, CWE-312/532).
const line = (s = "") => {
let out = String(s).replace(/[\r\n]+/g, " ");
if (KEY) out = out.split(KEY).join("[REDACTED]");
console.log(out);
};
const hr = () => line("─".repeat(72));
function show(label: string, value: unknown) {
@@ -52,8 +60,13 @@ async function partA() {
});
line(" ✅ validateProviderApiKey retornou (sem crash):");
show("resultado", result);
if (typeof (result as any)?.error === "string" && (result as any).error.includes("startsWith")) {
line(" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação.");
if (
typeof (result as any)?.error === "string" &&
(result as any).error.includes("startsWith")
) {
line(
" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação."
);
}
} catch (err: any) {
line(" ❌ validateProviderApiKey LANÇOU (crash não tratado):");

View File

@@ -23,7 +23,7 @@ import {
statSync,
chmodSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { join, dirname, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { assembleStandalone } from "./assembleStandalone.mjs";
@@ -35,6 +35,12 @@ import {
APP_STAGING_REMOVAL_PATHS,
findUnexpectedArtifactPaths,
} from "./pack-artifact-policy.ts";
import {
collectWorkspaceVersions,
findPackageJsonFiles,
hasWorkspaceProtocol,
resolvePackageJsonWorkspaceProtocols,
} from "./resolveWorkspaceProtocols.ts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -707,6 +713,33 @@ if (remainingUnexpectedFiles.length > 0) {
process.exit(1);
}
// -- Step 11: Resolve workspace: protocol dependencies -----------------
// npm/pnpm workspace protocol specifiers (workspace:*, workspace:^, ...)
// are meaningless to the npm registry and make `npm install -g omniroute`
// fail with EUNSUPPORTEDPROTOCOL. Rewrite any that leaked into published
// package.json files to the concrete workspace package version.
// Only touch files inside the staged dist/ tree; workspace member source
// package.json files must never be mutated by the publish step.
const workspaceVersions = collectWorkspaceVersions(ROOT);
const publishablePackageJsonDirs = [DIST_DIR];
const publishablePackageJsonPaths = publishablePackageJsonDirs
.flatMap((dir) => (existsSync(dir) ? findPackageJsonFiles(dir) : []))
.filter((filePath) => existsSync(filePath));
for (const pkgJsonPath of publishablePackageJsonPaths) {
let pkg: Record<string, unknown>;
try {
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as Record<string, unknown>;
} catch {
continue;
}
if (!hasWorkspaceProtocol(pkg)) continue;
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
console.log(` [resolved] Resolved workspace: protocols in ${relative(ROOT, pkgJsonPath)}`);
}
// ── Done ───────────────────────────────────────────────────
const distPkg = join(DIST_DIR, "package.json");
if (existsSync(distPkg)) {

View File

@@ -0,0 +1,228 @@
/**
* Resolve pnpm/npm workspace protocol dependencies to concrete semver versions.
*
* The npm registry clients cannot parse `workspace:` specifiers. During prepublish
* we rewrite any `workspace:*`, `workspace:^`, `workspace:~` (or explicit
* `workspace:<range>`) dependency declarations to the matching workspace package's
* actual version before npm pack/publish sees them.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import * as yaml from "js-yaml";
const WORKSPACE_PROTOCOL_RE = /^workspace:/;
const DEPENDENCY_FIELDS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
] as const;
/**
* Parse a simple workspace glob entry into concrete directories relative to a root.
* Supports entries like "packages/*" and literal directory names like "open-sse".
*/
function expandWorkspaceEntry(root: string, entry: string): string[] {
const trimmed = entry.trim();
if (!trimmed) return [];
if (!trimmed.endsWith("/*")) {
const dir = join(root, trimmed);
try {
return statSync(dir).isDirectory() ? [dir] : [];
} catch {
return [];
}
}
const parent = join(root, trimmed.slice(0, -2));
let entries: string[] = [];
try {
entries = readdirSync(parent);
} catch {
return [];
}
return entries
.map((name) => join(parent, name))
.filter((dir) => {
try {
return statSync(dir).isDirectory();
} catch {
return false;
}
});
}
/**
* Read the root package.json and, if present, pnpm-workspace.yaml to discover
* workspace member directories. Returns a map of package name -> version.
*/
export function collectWorkspaceVersions(projectRoot: string): Map<string, string> {
const versions = new Map<string, string>();
const rootPkgPath = join(projectRoot, "package.json");
let workspaceEntries: string[] = [];
try {
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf8")) as {
workspaces?: string[];
};
if (Array.isArray(rootPkg.workspaces)) {
workspaceEntries.push(...rootPkg.workspaces);
}
} catch {
// ignore unreadable root package.json
}
const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
try {
const yamlContent = readFileSync(pnpmWorkspacePath, "utf8");
const doc = yaml.load(yamlContent) as { packages?: unknown } | null | undefined;
if (doc && Array.isArray(doc.packages)) {
for (const entry of doc.packages) {
if (typeof entry === "string" && entry) {
workspaceEntries.push(entry);
}
}
}
} catch {
// ignore missing or malformed pnpm-workspace.yaml
}
const seenDirs = new Set<string>();
for (const entry of workspaceEntries) {
for (const dir of expandWorkspaceEntry(projectRoot, entry)) {
if (seenDirs.has(dir)) continue;
seenDirs.add(dir);
try {
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
name?: string;
version?: string;
};
if (pkg.name && pkg.version) {
versions.set(pkg.name, pkg.version);
}
} catch {
// skip unreadable workspace member package.json
}
}
}
return versions;
}
/**
* Resolve workspace protocol dependencies inside a package.json object.
*
* Replaces `workspace:*`, `workspace:^`, `workspace:~`, `workspace:<range>`,
* and `workspace:<packageName>` with the concrete version of the referenced
* workspace package. Throws if a workspace specifier cannot be resolved.
*/
export function resolvePackageJsonWorkspaceProtocols(
pkg: Record<string, unknown>,
workspaceVersions: Map<string, string>
): Record<string, unknown> {
const resolved: Record<string, unknown> = { ...pkg };
for (const field of DEPENDENCY_FIELDS) {
const deps = pkg[field];
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
const resolvedDeps: Record<string, string> = {};
let changed = false;
for (const [depName, versionSpec] of Object.entries(deps as Record<string, unknown>)) {
if (typeof versionSpec !== "string") {
resolvedDeps[depName] = String(versionSpec ?? "");
continue;
}
if (!WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
resolvedDeps[depName] = versionSpec;
continue;
}
const body = versionSpec.slice("workspace:".length);
let concrete: string | undefined;
if (body === "*") {
concrete = workspaceVersions.get(depName);
} else if (body === "^") {
const version = workspaceVersions.get(depName);
concrete = version ? `^${version}` : undefined;
} else if (body === "~") {
const version = workspaceVersions.get(depName);
concrete = version ? `~${version}` : undefined;
} else if (body.startsWith("^") || body.startsWith("~") || /^[\d<>=]/.test(body)) {
// Explicit range inside workspace: protocol - strip the protocol prefix.
concrete = body;
} else {
// workspace:<packageName> - resolve to that package's version.
concrete = workspaceVersions.get(body);
}
if (concrete) {
resolvedDeps[depName] = concrete;
changed = true;
} else {
throw new Error(
`Cannot resolve workspace protocol "${versionSpec}" for dependency "${depName}". ` +
"Make sure the referenced package is a declared workspace member with a version."
);
}
}
if (changed) {
resolved[field] = resolvedDeps;
}
}
return resolved;
}
/**
* Return true if any dependency field in the package contains a workspace: specifier.
*/
export function hasWorkspaceProtocol(pkg: Record<string, unknown>): boolean {
for (const field of DEPENDENCY_FIELDS) {
const deps = pkg[field];
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
for (const versionSpec of Object.values(deps as Record<string, unknown>)) {
if (typeof versionSpec === "string" && WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
return true;
}
}
}
return false;
}
/**
* Recursively walk a directory and return every package.json path found.
* Stops descending after maxDepth to avoid runaway recursion on deep trees.
*/
export function findPackageJsonFiles(dir: string, maxDepth = 10): string[] {
const results: string[] = [];
if (maxDepth < 0) return results;
let entries: string[] = [];
try {
entries = readdirSync(dir);
} catch {
return results;
}
for (const entry of entries) {
if (entry === "node_modules") continue;
const fullPath = join(dir, entry);
let stat;
try {
stat = statSync(fullPath);
} catch {
continue;
}
if (stat.isDirectory()) {
results.push(...findPackageJsonFiles(fullPath, maxDepth - 1));
} else if (entry === "package.json") {
results.push(fullPath);
}
}
return results;
}

View File

@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { getCachedSettings, updateSettings } from "@/lib/localDb";
import { SignJWT, jwtVerify, createRemoteJWKSet } from "jose";
import { cookies } from "next/headers";
import { timingSafeCompare } from "@/shared/utils/timingSafeCompare";
// Test seam (static) — allows tests to inject a cookie store and capture the minted auth_token.
// Mirrors the pattern in src/app/api/auth/login/route.ts
export const oidcCallbackInternals = {
@@ -54,7 +55,10 @@ export async function GET(request: Request) {
// Validate state from cookie (via seam so tests can capture)
const cookieStore = await oidcCallbackInternals.getCookieStore();
const storedState = cookieStore.get("oidc_state")?.value;
if (!storedState || storedState !== returnedState) {
// Constant-time: `!==` short-circuits on the first differing byte, so
// rejection time correlates with matching-prefix length (GHSA-7434-6q4c-33fh).
// The sibling OAuth callback already compares `state` this way.
if (!storedState || !timingSafeCompare(storedState, returnedState)) {
return NextResponse.redirect(new URL("/login?oidc_error=invalid_state", originEarly));
}

View File

@@ -1,4 +1,5 @@
import { createHmac } from "crypto";
import { timingSafeCompare } from "@/shared/utils/timingSafeCompare";
const ADMISSION_BYPASS_VALUE = "internal";
const SELF_LOOP_KEY = "sk_omniroute";
@@ -31,7 +32,10 @@ export function isInternalAdmissionBypass(request: Request): boolean {
const auth = request.headers.get("authorization") || "";
const match = /^bearer\s+(\S+)$/i.exec(auth.trim());
return Boolean(match && match[1].trim().toLowerCase() === resolveSelfLoopBearer().toLowerCase());
if (!match) return false;
// This gates an admission-lane bypass on a shared secret, so the compare is
// constant-time — `===` leaks matching-prefix length (GHSA-7434 class).
return timingSafeCompare(match[1].trim().toLowerCase(), resolveSelfLoopBearer().toLowerCase());
}
function fingerprint(value: string): string {

View File

@@ -66,8 +66,8 @@ export function structuralRejectionResponse(status: 413 | 503, maxMessages: numb
{
type: historyLimit ? "payload_too_large" : "server_error",
code: historyLimit ? "chat_history_too_large" : "chat_admission_busy",
reason: historyLimit ? "message_limit" : "structure_limit",
}
);
body.error.reason = historyLimit ? "message_limit" : "structure_limit";
return new Response(JSON.stringify(body), { status, headers });
}

View File

@@ -0,0 +1,27 @@
import { timingSafeEqual } from "crypto";
/**
* Constant-time string comparison for secrets, tokens and single-use nonces.
*
* `===` short-circuits on the first differing byte, so rejection time
* correlates with how much of the value the caller already guessed (CWE-208).
* That is the comparison this repo already avoids in every OAuth callback, the
* A2A token check, the Telegram initData HMAC and the CLI token check — each of
* which grew its own private copy of these five lines. This is the shared one:
* reach for it instead of writing a ninth copy, and instead of `===`.
*
* Length is not secret here (it leaks through the early return, as it does in
* every other copy) — the value being protected is the content, not its size.
* `null`/`undefined` compare by identity so a missing secret never matches a
* present one.
*/
export function timingSafeCompare(
a: string | null | undefined,
b: string | null | undefined
): boolean {
if (a == null || b == null) return a === b;
const bufA = Buffer.from(String(a), "utf8");
const bufB = Buffer.from(String(b), "utf8");
if (bufA.length !== bufB.length) return false;
return timingSafeEqual(bufA, bufB);
}

View File

@@ -0,0 +1,269 @@
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";
import {
collectWorkspaceVersions,
resolvePackageJsonWorkspaceProtocols,
hasWorkspaceProtocol,
findPackageJsonFiles,
} from "../../../scripts/build/resolveWorkspaceProtocols.ts";
function tmpDir(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function writeJson(filePath: string, data: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
function readJson(filePath: string): Record<string, unknown> {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record<string, unknown>;
}
test("resolvePackageJsonWorkspaceProtocols replaces workspace:*, workspace:^, workspace:~", () => {
const versions = new Map([
["@omniroute/open-sse", "3.8.51"],
["@omniroute/shared", "1.2.3"],
]);
const resolved = resolvePackageJsonWorkspaceProtocols(
{
name: "omniroute",
version: "3.8.51",
dependencies: {
"@omniroute/open-sse": "workspace:^",
"@omniroute/shared": "workspace:*",
lodash: "^4.17.0",
},
devDependencies: {
"@omniroute/open-sse": "workspace:~",
},
peerDependencies: {
"@omniroute/shared": "workspace:1.2.3",
},
optionalDependencies: {
"@omniroute/open-sse": "workspace:>=3.0.0",
},
},
versions
);
assert.equal((resolved.dependencies as Record<string, string>)["@omniroute/open-sse"], "^3.8.51");
assert.equal((resolved.dependencies as Record<string, string>)["@omniroute/shared"], "1.2.3");
assert.equal((resolved.dependencies as Record<string, string>).lodash, "^4.17.0");
assert.equal(
(resolved.devDependencies as Record<string, string>)["@omniroute/open-sse"],
"~3.8.51"
);
assert.equal((resolved.peerDependencies as Record<string, string>)["@omniroute/shared"], "1.2.3");
assert.equal(
(resolved.optionalDependencies as Record<string, string>)["@omniroute/open-sse"],
">=3.0.0"
);
});
test("resolvePackageJsonWorkspaceProtocols leaves non-workspace specs untouched", () => {
const resolved = resolvePackageJsonWorkspaceProtocols(
{
name: "x",
dependencies: {
a: "^1.0.0",
b: "file:../b",
c: "npm:alias@1.0.0",
},
},
new Map()
);
assert.equal((resolved.dependencies as Record<string, string>).a, "^1.0.0");
assert.equal((resolved.dependencies as Record<string, string>).b, "file:../b");
assert.equal((resolved.dependencies as Record<string, string>).c, "npm:alias@1.0.0");
assert.equal(hasWorkspaceProtocol(resolved), false);
});
test("resolvePackageJsonWorkspaceProtocols throws for unresolvable workspace protocol", () => {
assert.throws(
() =>
resolvePackageJsonWorkspaceProtocols(
{
name: "x",
dependencies: {
"@missing/pkg": "workspace:^",
},
},
new Map()
),
/Cannot resolve workspace protocol/
);
});
test("collectWorkspaceVersions reads npm workspaces and pnpm-workspace.yaml", () => {
const root = tmpDir("workspace-versions-");
writeJson(path.join(root, "package.json"), {
name: "root",
version: "0.0.0",
workspaces: ["packages/*", "open-sse"],
});
fs.mkdirSync(path.join(root, "packages", "a"), { recursive: true });
writeJson(path.join(root, "packages", "a", "package.json"), {
name: "@scope/a",
version: "1.0.0",
});
fs.mkdirSync(path.join(root, "open-sse"), { recursive: true });
writeJson(path.join(root, "open-sse", "package.json"), {
name: "@scope/open-sse",
version: "2.0.0",
});
// pnpm-workspace.yaml adds an extra directory not in npm workspaces.
fs.mkdirSync(path.join(root, "packages", "b"), { recursive: true });
writeJson(path.join(root, "packages", "b", "package.json"), {
name: "@scope/b",
version: "3.0.0",
});
fs.writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages:\n - 'packages/*'\n");
const versions = collectWorkspaceVersions(root);
assert.equal(versions.get("@scope/a"), "1.0.0");
assert.equal(versions.get("@scope/open-sse"), "2.0.0");
assert.equal(versions.get("@scope/b"), "3.0.0");
});
test("findPackageJsonFiles skips node_modules and respects maxDepth", () => {
const root = tmpDir("pkg-json-files-");
fs.mkdirSync(path.join(root, "a"), { recursive: true });
writeJson(path.join(root, "a", "package.json"), {});
fs.mkdirSync(path.join(root, "node_modules", "x"), { recursive: true });
writeJson(path.join(root, "node_modules", "x", "package.json"), {});
const files = findPackageJsonFiles(root);
assert.equal(files.length, 1);
assert.ok(files[0].endsWith(path.join("a", "package.json")));
// Build a deep tree and confirm maxDepth bounds the walk.
const deep = tmpDir("pkg-json-deep-");
let current = deep;
for (let i = 0; i < 12; i += 1) {
current = path.join(current, `level${i}`);
fs.mkdirSync(current, { recursive: true });
}
writeJson(path.join(current, "package.json"), {});
assert.equal(findPackageJsonFiles(deep, 10).length, 0);
assert.equal(findPackageJsonFiles(deep, 12).length, 1);
});
test("collectWorkspaceVersions parses pnpm-workspace.yaml with js-yaml", () => {
const root = tmpDir("pnpm-yaml-");
writeJson(path.join(root, "package.json"), { name: "root", version: "0.0.0" });
// Flow-style array, nested quotes, comments inside the packages list, and an
// unrelated top-level key before packages are all valid YAML that the old line
// scanner could not handle.
fs.writeFileSync(
path.join(root, "pnpm-workspace.yaml"),
"preferWorkspacePackages: true\n" +
"packages:\n" +
' - "packages/*"\n' +
" - 'apps/*'\n" +
" # comment inside the list\n" +
" - open-sse\n"
);
fs.mkdirSync(path.join(root, "packages", "a"), { recursive: true });
writeJson(path.join(root, "packages", "a", "package.json"), {
name: "@scope/a",
version: "1.0.0",
});
fs.mkdirSync(path.join(root, "apps", "web"), { recursive: true });
writeJson(path.join(root, "apps", "web", "package.json"), {
name: "@scope/web",
version: "2.0.0",
});
fs.mkdirSync(path.join(root, "open-sse"), { recursive: true });
writeJson(path.join(root, "open-sse", "package.json"), {
name: "@scope/open-sse",
version: "3.0.0",
});
const versions = collectWorkspaceVersions(root);
assert.equal(versions.get("@scope/a"), "1.0.0");
assert.equal(versions.get("@scope/web"), "2.0.0");
assert.equal(versions.get("@scope/open-sse"), "3.0.0");
});
test("prepublish Step 11 fixture resolves workspace: protocols in dist package.json files", () => {
const root = tmpDir("prepublish-step11-");
const distDir = path.join(root, "dist");
// Workspace member source files contain a workspace: specifier (simulating the
// monorepo source). They must NOT be mutated by the publish step.
fs.mkdirSync(path.join(root, "packages", "shared"), { recursive: true });
const sourcePkgPath = path.join(root, "packages", "shared", "package.json");
writeJson(sourcePkgPath, {
name: "@scope/shared",
version: "1.2.3",
dependencies: {
"@scope/other": "workspace:*",
},
});
fs.mkdirSync(path.join(root, "packages", "other"), { recursive: true });
writeJson(path.join(root, "packages", "other", "package.json"), {
name: "@scope/other",
version: "4.5.6",
});
writeJson(path.join(root, "package.json"), {
name: "root",
version: "0.0.0",
workspaces: ["packages/*"],
});
// The staged dist/ package.json contains workspace: specifiers that leaked
// into the publish artifact and must be rewritten to concrete versions.
fs.mkdirSync(distDir, { recursive: true });
const distPkgPath = path.join(distDir, "package.json");
writeJson(distPkgPath, {
name: "omniroute",
version: "3.8.51",
dependencies: {
"@scope/shared": "workspace:^",
"@scope/other": "workspace:*",
lodash: "^4.17.0",
},
});
// This is the same logic prepublish.ts Step 11 runs, scoped to the fixture.
const workspaceVersions = collectWorkspaceVersions(root);
const publishablePackageJsonPaths = findPackageJsonFiles(distDir).filter((filePath) =>
fs.existsSync(filePath)
);
for (const pkgJsonPath of publishablePackageJsonPaths) {
const pkg = readJson(pkgJsonPath);
if (!hasWorkspaceProtocol(pkg)) continue;
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
fs.writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
}
// dist/package.json must have concrete versions.
const distPkg = readJson(distPkgPath);
assert.equal((distPkg.dependencies as Record<string, string>)["@scope/shared"], "^1.2.3");
assert.equal((distPkg.dependencies as Record<string, string>)["@scope/other"], "4.5.6");
assert.equal((distPkg.dependencies as Record<string, string>).lodash, "^4.17.0");
assert.equal(hasWorkspaceProtocol(distPkg), false);
// Source package.json must remain untouched.
const sourcePkg = readJson(sourcePkgPath);
assert.equal((sourcePkg.dependencies as Record<string, string>)["@scope/other"], "workspace:*");
});

View File

@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
structuralRejectionResponse,
} from "../../src/shared/middleware/chatAdmissionResponses.ts";
// Pins the machine-readable error.reason contract of the chat admission
// structural rejections (#TS2339 regression guard): buildErrorBody now owns
// the reason field via ErrorBodyClassification, so the response bodies keep
// carrying it without post-construction mutation of an untyped field.
test("structuralRejectionResponse 413 carries reason=message_limit classification", () => {
const res = structuralRejectionResponse(413, 40);
assert.equal(res.status, 413);
assert.ok(!res.headers.has("Retry-After"), "413 is not retryable-by-header");
return res.text().then((raw) => {
const body = JSON.parse(raw);
assert.equal(body.error.reason, "message_limit");
assert.equal(body.error.type, "payload_too_large");
assert.equal(body.error.code, "chat_history_too_large");
assert.ok(!body.error.message.includes("at /"), "must not leak stack traces");
});
});
test("structuralRejectionResponse 503 carries reason=structure_limit and Retry-After", () => {
const res = structuralRejectionResponse(503, 40);
assert.equal(res.status, 503);
assert.equal(res.headers.get("Retry-After"), "1");
return res.text().then((raw) => {
const body = JSON.parse(raw);
assert.equal(body.error.reason, "structure_limit");
assert.equal(body.error.type, "server_error");
assert.equal(body.error.code, "chat_admission_busy");
});
});

View File

@@ -0,0 +1,61 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { timingSafeCompare } from "../../src/shared/utils/timingSafeCompare.ts";
// GHSA-7434-6q4c-33fh — the OIDC callback compared the CSRF `state` cookie with
// `!==` while every sibling callback already used a constant-time compare. Low
// severity on its own (single-use nonce), but the pattern gets copied, so the
// guard below pins the two callsites to the shared helper.
test("timingSafeCompare accepts identical values", () => {
assert.equal(timingSafeCompare("abc123", "abc123"), true);
assert.equal(timingSafeCompare("", ""), true);
});
test("timingSafeCompare rejects different values, including same-length ones", () => {
assert.equal(timingSafeCompare("abc123", "abc124"), false);
assert.equal(timingSafeCompare("abc123", "xbc123"), false);
assert.equal(timingSafeCompare("abc", "abcdef"), false);
assert.equal(timingSafeCompare("abcdef", "abc"), false);
});
test("timingSafeCompare compares null/undefined by identity, never as a match", () => {
assert.equal(timingSafeCompare(null, null), true);
assert.equal(timingSafeCompare(undefined, undefined), true);
assert.equal(timingSafeCompare(null, undefined), false);
assert.equal(timingSafeCompare(null, "abc"), false);
assert.equal(timingSafeCompare("abc", undefined), false);
assert.equal(timingSafeCompare(undefined, ""), false);
});
test("timingSafeCompare is byte-exact, not unicode-normalizing", () => {
// "é" precomposed vs decomposed — different bytes, must not match.
assert.equal(timingSafeCompare("é", "é"), false);
});
function sourceOf(relPath: string): string {
return readFileSync(fileURLToPath(new URL(`../../${relPath}`, import.meta.url)), "utf8");
}
test("the OIDC callback validates `state` with the constant-time helper", () => {
const source = sourceOf("src/app/api/auth/oidc/callback/route.ts");
assert.ok(
source.includes("timingSafeCompare"),
"oidc/callback must compare the state cookie in constant time (GHSA-7434-6q4c-33fh)"
);
assert.ok(
!/storedState\s*!==\s*returnedState/.test(source),
"the short-circuiting `!==` state comparison is back"
);
});
test("the internal admission bypass compares its bearer in constant time", () => {
const source = sourceOf("src/shared/middleware/chatAdmissionIdentity.ts");
assert.ok(
source.includes("timingSafeCompare"),
"isInternalAdmissionBypass gates a bypass on a shared secret — compare it in constant time"
);
});

View File

@@ -0,0 +1,20 @@
import test from "node:test";
import assert from "node:assert/strict";
import { resolveCursorImages } from "../../open-sse/utils/cursorImages.ts";
// zai-web maps resolveCursorImages() output into browser-upload attachments
// whose mimeType is REQUIRED. EncodedImage.mimeType is optional on the wire
// type, so zai-web carries an `?? "image/jpeg"` fallback — this test pins the
// producer contract that makes the fallback dead code in practice: every
// image that reaches a browser upload must arrive with a concrete image/*
// mime string (decodeDataUrl / fetchImageBytes validate it before pushing).
const PIXEL_PNG =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
test("resolveCursorImages (prepareForWire:false) always yields a concrete image/* mimeType", async () => {
const images = await resolveCursorImages([PIXEL_PNG], { prepareForWire: false });
assert.equal(images.length, 1);
assert.equal(typeof images[0]!.mimeType, "string");
assert.match(images[0]!.mimeType as string, /^image\//);
});