mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
Validado sobre o tip de `release/v3.8.51` depois de reconciliar com o #12620, que entrou primeiro nesta mesma sessão e ataca a mesma classe de problema por outra arquitetura. **A colisão e como foi resolvida.** O #12620 consertou o GHSA-qv45-56jc-4wmj adicionando `RAW_CREDENTIAL_PATTERNS` a `error.ts` e importando-os em `upstreamErrorPassthrough.ts`. Este PR resolve o mesmo problema quebrando `error.ts` em `errorSanitization.ts` + `errorPathRedaction.ts`. Mantive a divisão em módulos deste PR, porque ao comparar os dois vocabulários o dele já era mais amplo: o `STRONG_CREDENTIAL_TOKEN` daqui cobre `sk-`/`sk_` **com lookbehind e uma variante para a forma embutida** (que pega `sk-proj-…`), mais Slack `xox-`, AWS `AKIA`/`ASIA`, `github_pat_`/`ghp_`/`glpat-` e JWT de três segmentos. A única forma que o #12620 carregava e este conjunto não tinha era a chave do Google (`AIza…`) — adicionada aqui, com o mesmo quantificador limitado que os irmãos usam (AGENTS.md → PII §1, já que isso roda sobre corpos upstream não confiáveis). **A verificação não foi por inspeção.** Rodei as suítes do próprio #12620 contra esta estrutura: **48/48** em `error-sanitizer-sk-key-qv45`, `bifrost-relay-response-leak-9m72`, `search-baseurl-client-override-3f8g` e `search-baseurl-ssrf-guard` — incluindo a asserção anti-drift daquela suíte, que é o oráculo certo aqui: *para todo corpo que a camada de passthrough recusa como vazante, o sanitizador de fallback não pode devolvê-lo intacto*. Ela passa, então a propriedade de segurança dos três GHSAs sobrevive à troca de arquitetura. Os 21 arquivos de teste deste PR: **259/259**. `typecheck:core` limpo.
447 lines
15 KiB
TypeScript
447 lines
15 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { createSSEStream } from "../../open-sse/utils/stream.ts";
|
|
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
|
|
|
type Failure = { status: number; message: string; code?: string; type?: string };
|
|
|
|
async function collectUntilFailure(
|
|
chunks: string[],
|
|
sourceFormat: string,
|
|
convertedLog: string[],
|
|
mode: "passthrough" | "translate" = "passthrough",
|
|
targetFormat: string = FORMATS.OPENAI
|
|
): Promise<{ output: string; error: unknown; failure: Failure | null }> {
|
|
let failure: Failure | null = null;
|
|
const source = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
for (const chunk of chunks) controller.enqueue(new TextEncoder().encode(chunk));
|
|
controller.close();
|
|
},
|
|
});
|
|
const reader = source
|
|
.pipeThrough(
|
|
createSSEStream({
|
|
mode,
|
|
...(mode === "translate" ? { targetFormat } : {}),
|
|
sourceFormat,
|
|
...(mode === "passthrough" ? { clientResponseFormat: sourceFormat } : {}),
|
|
provider: "hostile-upstream",
|
|
model: "hostile-model",
|
|
body: { input: "hello" },
|
|
reqLogger: {
|
|
appendConvertedChunk(value: string) {
|
|
convertedLog.push(value);
|
|
},
|
|
},
|
|
onFailure(payload) {
|
|
failure = payload;
|
|
return true;
|
|
},
|
|
})
|
|
)
|
|
.getReader();
|
|
|
|
let output = "";
|
|
let error: unknown = null;
|
|
try {
|
|
while (true) {
|
|
const result = await reader.read();
|
|
if (result.done) break;
|
|
output += new TextDecoder().decode(result.value);
|
|
}
|
|
} catch (caught) {
|
|
error = caught;
|
|
}
|
|
return { output, error, failure };
|
|
}
|
|
|
|
function assertNoHostileDetail(value: string): void {
|
|
assert.doesNotMatch(value, /private-runtime\.ts/);
|
|
assert.doesNotMatch(value, /sk-stream-secret/);
|
|
assert.doesNotMatch(value, /api_key/);
|
|
}
|
|
|
|
test("translated root error frames notify onFailure and terminate with a public-safe error", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
error: {
|
|
type: "server_error",
|
|
code: "opaque-provider-code",
|
|
message:
|
|
"translated failure at /srv/omniroute/private-runtime.ts:47:6 token=sk-stream-secret-xlate",
|
|
api_key: "sk-stream-secret-abcdef",
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`data: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.CLAUDE,
|
|
convertedLog,
|
|
"translate"
|
|
);
|
|
|
|
assert.ok(result.error, "a translated upstream error must terminate the stream");
|
|
assert.match(result.output, /event: error/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure, "translated failures must reach the internal classifier");
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assert.equal(result.failure.code, "opaque-provider-code");
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("translated failed response.completed events cannot become successful Chat completions", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
type: "response.completed",
|
|
response: {
|
|
id: "resp_translate_failed",
|
|
status: "failed",
|
|
output: [],
|
|
error: {
|
|
type: "server_error",
|
|
code: "translated_completed_failure",
|
|
message:
|
|
"completed translate failure at /srv/omniroute/private-runtime.ts:58:4 token=sk-stream-secret-completed-translate",
|
|
},
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`data: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.OPENAI,
|
|
convertedLog,
|
|
"translate",
|
|
FORMATS.OPENAI_RESPONSES
|
|
);
|
|
|
|
assert.ok(result.error, "a failed Responses completion must terminate translated Chat output");
|
|
assert.match(result.output, /"error"/);
|
|
assert.doesNotMatch(result.output, /"finish_reason":"stop"/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure, "the translated failure must reach fallback classification");
|
|
assert.equal(result.failure.code, "translated_completed_failure");
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("a translated failed response.completed tail without a newline still terminates", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
type: "response.completed",
|
|
response: {
|
|
id: "resp_translate_failed_tail",
|
|
status: "failed",
|
|
output: [],
|
|
error: {
|
|
code: "translated_completed_tail_failure",
|
|
message:
|
|
"completed tail failure at /srv/omniroute/private-runtime.ts:59:4 token=sk-stream-secret-completed-tail",
|
|
},
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`data: ${JSON.stringify(raw)}`],
|
|
FORMATS.OPENAI,
|
|
convertedLog,
|
|
"translate",
|
|
FORMATS.OPENAI_RESPONSES
|
|
);
|
|
|
|
assert.ok(result.error, "a buffered failed Responses completion must terminate in flush");
|
|
assert.match(result.output, /"error"/);
|
|
assert.doesNotMatch(result.output, /"finish_reason":"stop"/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure);
|
|
assert.equal(result.failure.code, "translated_completed_tail_failure");
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("Responses response.failed is projected before forwarding, logging, and onFailure", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
type: "response.failed",
|
|
response: {
|
|
id: "resp_hostile-/srv/omniroute/private-runtime.ts-token=sk-stream-secret-id",
|
|
model: "provider-model token=sk-stream-secret-model",
|
|
status: "failed",
|
|
output: [
|
|
{
|
|
id: "msg_partial",
|
|
type: "message",
|
|
role: "assistant",
|
|
status: "in_progress",
|
|
diagnostics: {
|
|
stack: "at /srv/omniroute/private-runtime.ts:47:2",
|
|
api_key: "sk-stream-secret-output-diagnostics",
|
|
},
|
|
content: [
|
|
{
|
|
type: "output_text",
|
|
text: "safe partial output",
|
|
annotations: [
|
|
{
|
|
type: "url_citation",
|
|
url: "https://example.invalid/?token=sk-stream-secret-annotation",
|
|
title: "at /srv/omniroute/private-runtime.ts:48:2",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: "output_text",
|
|
phase: "commentary",
|
|
text: "hidden nested commentary must not be public",
|
|
},
|
|
{ type: "refusal", refusal: "safe refusal" },
|
|
],
|
|
},
|
|
{
|
|
id: "msg_commentary",
|
|
type: "message",
|
|
role: "assistant",
|
|
phase: "commentary",
|
|
content: [
|
|
{
|
|
type: "output_text",
|
|
text: "hidden commentary at /srv/omniroute/private-runtime.ts:49:2",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "msg_roleless",
|
|
type: "message",
|
|
content: [
|
|
{
|
|
type: "output_text",
|
|
text: "roleless output must not be public",
|
|
annotations: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "reasoning_private",
|
|
type: "reasoning",
|
|
encrypted_content: "sk-stream-secret-encrypted-reasoning",
|
|
summary: [
|
|
{
|
|
type: "summary_text",
|
|
text: "at /srv/omniroute/private-runtime.ts:50:2",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "call_private",
|
|
type: "function_call",
|
|
call_id: "call_private",
|
|
name: "read_private_file",
|
|
arguments:
|
|
'{"path":"/srv/omniroute/private-runtime.ts","api_key":"sk-stream-secret-tool"}',
|
|
},
|
|
{
|
|
id: "provider_private",
|
|
type: "provider_diagnostics",
|
|
diagnostics: {
|
|
stack: "at /srv/omniroute/private-runtime.ts:51:2",
|
|
api_key: "sk-stream-secret-unknown-item",
|
|
},
|
|
},
|
|
],
|
|
error: {
|
|
type: "server_error",
|
|
code: "server_error",
|
|
message: "failed at /srv/omniroute/private-runtime.ts:44:2 token=sk-stream-secret-123456",
|
|
api_key: "sk-stream-secret-abcdef",
|
|
},
|
|
last_error: {
|
|
code: "server_error",
|
|
message:
|
|
"last failure at /srv/omniroute/private-runtime.ts:45:2 token=sk-stream-secret-last",
|
|
},
|
|
message:
|
|
"sibling failure at /srv/omniroute/private-runtime.ts:46:2 token=sk-stream-secret-sibling",
|
|
diagnosis: { stack: "at /srv/omniroute/private-runtime.ts:46:2" },
|
|
settings: { api_key: "sk-stream-secret-response-setting" },
|
|
usage: {
|
|
input_tokens: 4,
|
|
output_tokens: 2,
|
|
total_tokens: 6,
|
|
input_tokens_details: {
|
|
cached_tokens: 1,
|
|
"sk-stream-secret-detail-key": 99,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`event: response.failed\ndata: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.OPENAI_RESPONSES,
|
|
convertedLog
|
|
);
|
|
|
|
assert.ok(result.error, "a failed Responses event must terminate the stream");
|
|
assert.match(result.output, /response\.failed/);
|
|
assert.match(result.output, /"last_error":\{/);
|
|
assert.match(result.output, /safe partial output/);
|
|
assert.match(result.output, /safe refusal/);
|
|
assert.match(result.output, /"annotations":\[\]/);
|
|
assert.doesNotMatch(result.output, /hidden nested commentary must not be public/);
|
|
assert.doesNotMatch(result.output, /roleless output must not be public/);
|
|
assert.match(result.output, /"cached_tokens":1/);
|
|
assert.doesNotMatch(result.output, /\[truncated\]/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.doesNotMatch(
|
|
result.output,
|
|
/"diagnosis"|"diagnostics"|"settings"|"encrypted_content"|"function_call"|"provider_diagnostics"|"phase"|"url_citation"/
|
|
);
|
|
assert.doesNotMatch(
|
|
convertedLog.join("\n"),
|
|
/"diagnosis"|"diagnostics"|"settings"|"encrypted_content"|"function_call"|"provider_diagnostics"|"phase"|"url_citation"/
|
|
);
|
|
assert.ok(result.failure);
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("failed response.completed events omit provider-only diagnostic siblings", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
type: "response.completed",
|
|
response: {
|
|
id: "resp_failed_completed",
|
|
object: "response",
|
|
created_at: 1_777_777_777,
|
|
completed_at: 1_777_777_778,
|
|
status: "failed",
|
|
output: [],
|
|
error: {
|
|
code: "server_error",
|
|
message:
|
|
"completed failure at /srv/omniroute/private-runtime.ts:55:2 token=sk-stream-secret-completed",
|
|
},
|
|
diagnosis: { stack: "at /srv/omniroute/private-runtime.ts:55:2" },
|
|
settings: { api_key: "sk-stream-secret-completed-setting" },
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`event: response.completed\ndata: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.OPENAI_RESPONSES,
|
|
convertedLog
|
|
);
|
|
|
|
assert.ok(result.error, "a failed response.completed event must terminate the stream");
|
|
assert.match(result.output, /"type":"response\.completed"/);
|
|
assert.match(result.output, /"id":"resp_failed_completed"/);
|
|
assert.match(result.output, /"created_at":1777777777/);
|
|
assert.match(result.output, /"completed_at":1777777778/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.doesNotMatch(result.output, /"diagnosis"|"settings"/);
|
|
assert.doesNotMatch(convertedLog.join("\n"), /"diagnosis"|"settings"/);
|
|
assert.ok(result.failure);
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("OpenAI root error frames without a top-level type remain failures after projection", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
error: {
|
|
type: "server_error",
|
|
code: "server_error",
|
|
message: "root failed at /srv/omniroute/private-runtime.ts:48:7 token=sk-stream-secret-root",
|
|
api_key: "sk-stream-secret-abcdef",
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`data: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.OPENAI,
|
|
convertedLog
|
|
);
|
|
|
|
assert.ok(result.error, "an OpenAI error envelope must terminate the stream");
|
|
assert.match(result.output, /"error"/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure);
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("OpenAI string error frames preserve raw classification but publish only safe text", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
error: "string failure at /srv/omniroute/private-runtime.ts:49:8 token=sk-stream-secret-string",
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`data: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.OPENAI,
|
|
convertedLog
|
|
);
|
|
|
|
assert.ok(result.error, "a string OpenAI error must terminate the stream");
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure);
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("Claude type:error is projected before forwarding and terminates the stream", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
type: "error",
|
|
error: {
|
|
type: "server_error",
|
|
code: "server_error",
|
|
message:
|
|
"claude failed at /srv/omniroute/private-runtime.ts:51:3 token=sk-stream-secret-123456",
|
|
api_key: "sk-stream-secret-abcdef",
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`event: error\ndata: ${JSON.stringify(raw)}\n\n`],
|
|
FORMATS.CLAUDE,
|
|
convertedLog
|
|
);
|
|
|
|
assert.ok(result.error, "a Claude error event must terminate the stream");
|
|
assert.match(result.output, /event: error/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure);
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|
|
|
|
test("a final response.failed frame without a trailing newline is projected before flush", async () => {
|
|
const convertedLog: string[] = [];
|
|
const raw = {
|
|
type: "response.failed",
|
|
response: {
|
|
status: "failed",
|
|
error: {
|
|
code: "server_error",
|
|
message:
|
|
"tail failed at /srv/omniroute/private-runtime.ts:61:8 token=sk-stream-secret-123456",
|
|
api_key: "sk-stream-secret-abcdef",
|
|
},
|
|
},
|
|
};
|
|
const result = await collectUntilFailure(
|
|
[`event: response.failed\ndata: ${JSON.stringify(raw)}`],
|
|
FORMATS.OPENAI_RESPONSES,
|
|
convertedLog
|
|
);
|
|
|
|
assert.ok(result.error, "a buffered failed event must terminate during flush");
|
|
assert.match(result.output, /response\.failed/);
|
|
assertNoHostileDetail(result.output);
|
|
assertNoHostileDetail(convertedLog.join("\n"));
|
|
assert.ok(result.failure);
|
|
assert.match(result.failure.message, /private-runtime\.ts/);
|
|
assertNoHostileDetail(String(result.error));
|
|
});
|