mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
* security(deps): pin and verify tls-client native artifacts * docs(changelog): link tls-client provenance PR * security(runtime): harden TLS and public error boundaries * security(runtime): resolve CodeQL error-boundary findings * security(lmarena): close public stream error boundary * fix(lmarena): normalize public error statuses * chore(quality): rebaseline chatCore.ts for the surviving log-boundary hardening open-sse/handlers/chatCore.ts 6219 -> 6287. This is the one part of #11742 that survived the rebase: sanitizeErrorMessage on the plugin onError hook, on the semaphore-timeout path and on failureMessage before it reaches console.log and the call log, sanitizeUpstreamDetails on the malformed-response log, and getSafeErrorMetadata + try/catch where hostile (Proxy) metadata could throw. That is the LOG boundary, which is broader than Hard Rule #12 (responses). The rest of the PR was dropped as already landed on the tip.
179 lines
6.9 KiB
TypeScript
179 lines
6.9 KiB
TypeScript
// tests/unit/chatcore-stream-error-result.test.ts
|
|
// Characterization of isSemaphoreCapacityError / createStreamingErrorResult /
|
|
// getUpstreamErrorIdentifier — streaming error-result helpers extracted from handleChatCore
|
|
// (chatCore god-file decomposition, #3501). Locks the semaphore code matching, the SSE error
|
|
// envelope shape (status, headers, `data: {...}\n\ndata: [DONE]\n\n` body, optional code/type), and
|
|
// the string-code extraction.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
createSafeAbortError,
|
|
formatStreamRecoveryRetryWarning,
|
|
isSemaphoreCapacityError,
|
|
createStreamingErrorResult,
|
|
getSafeErrorMetadata,
|
|
getUpstreamErrorIdentifier,
|
|
} from "../../open-sse/handlers/chatCore/streamErrorResult.ts";
|
|
|
|
test("isSemaphoreCapacityError matches the two semaphore codes only", () => {
|
|
assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_TIMEOUT" }), true);
|
|
assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_QUEUE_FULL" }), true);
|
|
assert.equal(isSemaphoreCapacityError({ code: "OTHER" }), false);
|
|
assert.equal(isSemaphoreCapacityError(null), false);
|
|
assert.equal(isSemaphoreCapacityError("SEMAPHORE_TIMEOUT"), false);
|
|
});
|
|
|
|
test("formatStreamRecoveryRetryWarning sanitizes hostile error names", () => {
|
|
const secret = "STREAM_RECOVERY_NAME_SECRET";
|
|
const credentialName = new Proxy(
|
|
{},
|
|
{
|
|
get(_target, key) {
|
|
if (key === "name") return `password=${secret} /home/alice/recovery.ts`;
|
|
throw new Error("hostile recovery metadata");
|
|
},
|
|
}
|
|
);
|
|
const credentialWarning = formatStreamRecoveryRetryWarning(1, 4, credentialName);
|
|
assert.doesNotMatch(credentialWarning, /STREAM_RECOVERY_NAME_SECRET|\/home\/alice/);
|
|
assert.match(credentialWarning, /\[REDACTED\]/);
|
|
|
|
const hostileGetter = new Proxy(
|
|
{},
|
|
{
|
|
get() {
|
|
throw new Error("hostile name getter");
|
|
},
|
|
}
|
|
);
|
|
assert.equal(
|
|
formatStreamRecoveryRetryWarning(2, 4, hostileGetter),
|
|
"transparent early-retry 2/4 after truncation"
|
|
);
|
|
});
|
|
|
|
test("createStreamingErrorResult builds an SSE error envelope with [DONE] terminator", async () => {
|
|
const result = createStreamingErrorResult(503, "boom");
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 503);
|
|
assert.equal(result.error, "boom");
|
|
assert.equal(result.response.status, 503);
|
|
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
|
|
assert.equal(result.response.headers.get("X-Accel-Buffering"), "no");
|
|
const body = await result.response.text();
|
|
assert.ok(body.startsWith("data: "));
|
|
assert.ok(body.endsWith("data: [DONE]\n\n"));
|
|
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
|
|
assert.equal(json.error.message, "boom");
|
|
});
|
|
|
|
test("createStreamingErrorResult attaches optional code and type", async () => {
|
|
const result = createStreamingErrorResult(429, "slow down", "rate_limited", "rate_limit_error");
|
|
const body = await result.response.text();
|
|
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
|
|
assert.equal(json.error.code, "rate_limited");
|
|
assert.equal(json.error.type, "rate_limit_error");
|
|
});
|
|
|
|
test("createStreamingErrorResult sanitizes code and type at the SSE boundary", async () => {
|
|
const result = createStreamingErrorResult(
|
|
502,
|
|
"upstream failed",
|
|
"sk-live-secret-value",
|
|
"server_error\nX-Leak: yes"
|
|
);
|
|
const body = await result.response.text();
|
|
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n"))) as {
|
|
error: { code: string; type: string };
|
|
};
|
|
|
|
assert.equal(json.error.code, "bad_gateway");
|
|
assert.equal(json.error.type, "server_error");
|
|
assert.doesNotMatch(body, /sk-live-secret-value|X-Leak/);
|
|
});
|
|
|
|
test("createStreamingErrorResult sanitizes message, code, and type at the SSE boundary", async () => {
|
|
const secret = "STREAM_RESULT_SECRET";
|
|
const result = createStreamingErrorResult(
|
|
502,
|
|
`upstream password=${secret} at /home/alice/stream.ts:10:2`,
|
|
"password_hunter2",
|
|
"authorization_BearerSecret"
|
|
);
|
|
const body = await result.response.text();
|
|
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
|
|
|
|
assert.equal(json.error.message, "upstream password=[REDACTED]");
|
|
assert.equal(json.error.code, "bad_gateway");
|
|
assert.equal(json.error.type, "server_error");
|
|
assert.doesNotMatch(body, new RegExp(`${secret}|/home/alice|\\bat \\S`));
|
|
assert.equal(result.error, `upstream password=${secret} at /home/alice/stream.ts:10:2`);
|
|
});
|
|
|
|
test("getUpstreamErrorIdentifier returns a non-empty string code or undefined", () => {
|
|
assert.equal(getUpstreamErrorIdentifier({ code: "ECONNRESET" }), "ECONNRESET");
|
|
assert.equal(getUpstreamErrorIdentifier({ code: "" }), undefined);
|
|
assert.equal(getUpstreamErrorIdentifier({ code: 123 }), undefined);
|
|
assert.equal(getUpstreamErrorIdentifier(null), undefined);
|
|
assert.equal(getUpstreamErrorIdentifier("ECONNRESET"), undefined);
|
|
const hostile = new Proxy(
|
|
{},
|
|
{
|
|
get() {
|
|
throw new Error("hostile code getter");
|
|
},
|
|
}
|
|
);
|
|
assert.doesNotThrow(() => isSemaphoreCapacityError(hostile));
|
|
assert.equal(getUpstreamErrorIdentifier(hostile), undefined);
|
|
const hostileAbort = new Proxy(
|
|
{},
|
|
{
|
|
get(_target, key) {
|
|
if (key === "name") return "AbortError";
|
|
throw new Error("hostile abort metadata");
|
|
},
|
|
}
|
|
);
|
|
assert.equal(getSafeErrorMetadata(hostileAbort).name, "AbortError");
|
|
let codeReads = 0;
|
|
const mutableCode = new Proxy(
|
|
{},
|
|
{
|
|
get(_target, key) {
|
|
if (key !== "code") return undefined;
|
|
codeReads += 1;
|
|
return codeReads === 1 ? "SEMAPHORE_TIMEOUT" : "OTHER";
|
|
},
|
|
}
|
|
);
|
|
assert.equal(isSemaphoreCapacityError(mutableCode), true);
|
|
assert.equal(codeReads, 1);
|
|
const safeAbort = createSafeAbortError();
|
|
assert.equal(safeAbort.name, "AbortError");
|
|
assert.equal(safeAbort.message, "Request aborted");
|
|
});
|
|
|
|
test("non-streaming runNonStreamingProviderLeg is inside a try that maps semaphore errors", async () => {
|
|
const fs = await import("node:fs");
|
|
const src = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
|
|
// `let`, not `const`, since 6077b9dd (#12867) made the finalization step reassign
|
|
// legResult. The guard is about the try/catch that wraps the call, not the keyword.
|
|
const idx = src.search(/(?:const|let) legResult = await runNonStreamingProviderLeg/);
|
|
assert.ok(idx >= 0, "non-streaming branch must exist");
|
|
const start = src.lastIndexOf("if (!stream)", idx);
|
|
const end = src.indexOf("// Streaming response", idx);
|
|
assert.ok(start >= 0 && end > start, "non-stream block bounds");
|
|
const block = src.slice(start, end);
|
|
assert.match(
|
|
block,
|
|
/try\s*\{[\s\S]*runNonStreamingProviderLeg/,
|
|
"non-stream leg must sit in a try so SEMAPHORE_TIMEOUT cannot escape handleChatCore"
|
|
);
|
|
assert.match(
|
|
block,
|
|
/isSemaphoreCapacityError/,
|
|
"same catch that maps stream semaphore errors must cover the non-stream leg"
|
|
);
|
|
});
|