mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +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.
157 lines
6.1 KiB
TypeScript
157 lines
6.1 KiB
TypeScript
/**
|
|
* QA P0 — sanitized auto-combo diagnostic trace.
|
|
* Guards the new `errorResponseWithComboDiagnostics` / `sanitizeComboDiagnostics`
|
|
* helpers: they must surface pool size + attempt order + exclusion reasons as
|
|
* both `x-omniroute-combo-*` headers and a `diagnostics` body field, while the
|
|
* sanitizer is the secret-containment boundary (only provider/model/reason ids +
|
|
* counts may ever escape — never keys/tokens/credentials).
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { errorResponseWithComboDiagnostics, sanitizeComboDiagnostics } =
|
|
await import("../../open-sse/utils/error.ts");
|
|
const { buildRecoveryHint } = await import("../../open-sse/services/combo/pinRecovery.ts");
|
|
|
|
test("combo diagnostics: headers + body carry the sanitized trace (code override preserved)", async () => {
|
|
const res = errorResponseWithComboDiagnostics(
|
|
503,
|
|
"all upstream accounts inactive",
|
|
{
|
|
poolSize: 3,
|
|
attempted: 2,
|
|
excluded: [{ provider: "openai", model: "gpt-x", reason: "exhausted" }],
|
|
attemptOrder: [{ provider: "openai", model: "gpt-x" }],
|
|
terminalReason: "all_accounts_inactive",
|
|
},
|
|
{ code: "ALL_ACCOUNTS_INACTIVE", type: "service_unavailable" }
|
|
);
|
|
|
|
assert.equal(res.status, 503);
|
|
assert.equal(res.headers.get("x-omniroute-combo-pool-size"), "3");
|
|
assert.equal(res.headers.get("x-omniroute-combo-attempted"), "2");
|
|
assert.match(res.headers.get("x-omniroute-combo-excluded") || "", /openai\/gpt-x:exhausted/);
|
|
assert.equal(res.headers.get("x-omniroute-combo-terminal-reason"), "all_accounts_inactive");
|
|
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, "ALL_ACCOUNTS_INACTIVE");
|
|
assert.equal(body.error.type, "service_unavailable");
|
|
assert.ok(body.diagnostics, "diagnostics field present in body");
|
|
assert.equal(body.diagnostics.poolSize, 3);
|
|
assert.equal(body.diagnostics.attempted, 2);
|
|
assert.equal(body.diagnostics.terminalReason, "all_accounts_inactive");
|
|
assert.equal(body.diagnostics.attemptOrder[0].provider, "openai");
|
|
});
|
|
|
|
test("combo diagnostics: sanitizer caps sizes + keeps only the whitelist keys", () => {
|
|
const dirty = {
|
|
poolSize: 1,
|
|
attempted: 1,
|
|
excluded: Array.from({ length: 200 }, (_, i) => ({
|
|
provider: "p" + i,
|
|
reason: "r".repeat(500),
|
|
})),
|
|
attemptOrder: Array.from({ length: 200 }, () => ({ provider: "p", model: "m" })),
|
|
terminalReason: "x".repeat(1000),
|
|
};
|
|
const safe = sanitizeComboDiagnostics(dirty as never);
|
|
assert.ok(safe.excluded.length <= 64, "excluded capped at 64");
|
|
assert.ok(safe.attemptOrder.length <= 64, "attemptOrder capped at 64");
|
|
assert.ok(safe.excluded[0].reason.length <= 64, "reason length clamped");
|
|
assert.ok(safe.terminalReason.length <= 200, "terminalReason length clamped");
|
|
assert.deepEqual(Object.keys(safe.excluded[0]).sort(), ["provider", "reason"]);
|
|
});
|
|
|
|
test("combo diagnostics: secret containment — non-whitelisted fields never survive", () => {
|
|
const leaky = {
|
|
poolSize: 1,
|
|
attempted: 1,
|
|
excluded: [
|
|
{ provider: "openai", reason: "exhausted", apiKey: "sk-SECRET-KEY", token: "SECRET-TOK" },
|
|
],
|
|
attemptOrder: [{ provider: "openai", model: "m", accessToken: "SECRET-OAUTH" }],
|
|
terminalReason: "t",
|
|
};
|
|
const safe = sanitizeComboDiagnostics(leaky as never);
|
|
const serialized = JSON.stringify(safe);
|
|
assert.ok(!serialized.includes("SECRET"), "no secret VALUES survive the projection");
|
|
assert.ok(!serialized.includes("apiKey"), "no apiKey KEY survives");
|
|
assert.ok(!serialized.includes("accessToken"), "no accessToken KEY survives");
|
|
assert.ok(!serialized.includes("token"), "no token KEY survives");
|
|
});
|
|
|
|
test("combo diagnostics: terminalReason with a non-Latin1 char (em dash) must not crash Response construction (#6612)", () => {
|
|
const terminalReason = "reasoning consumed 5/5 tokens — no content output";
|
|
assert.doesNotThrow(() => {
|
|
const res = errorResponseWithComboDiagnostics(
|
|
502,
|
|
`Upstream response failed quality validation: ${terminalReason}`,
|
|
{
|
|
poolSize: 4,
|
|
attempted: 1,
|
|
excluded: [
|
|
{ provider: "deepseek", model: "deepseek-v4-flash-free", reason: "quality — bad" },
|
|
],
|
|
attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }],
|
|
terminalReason,
|
|
}
|
|
);
|
|
assert.equal(res.status, 502);
|
|
});
|
|
});
|
|
|
|
test("combo diagnostics: JSON body keeps the original non-Latin1 text even though headers are ASCII-sanitized (#6612)", async () => {
|
|
const terminalReason = "reasoning consumed 5/5 tokens — no content output";
|
|
const res = errorResponseWithComboDiagnostics(
|
|
502,
|
|
`Upstream response failed quality validation: ${terminalReason}`,
|
|
{
|
|
poolSize: 1,
|
|
attempted: 1,
|
|
excluded: [],
|
|
attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }],
|
|
terminalReason,
|
|
}
|
|
);
|
|
// Header value must be a valid Latin1 ByteString — em dash (U+2014) replaced.
|
|
assert.equal(
|
|
res.headers.get("x-omniroute-combo-terminal-reason"),
|
|
terminalReason.replace("—", "?")
|
|
);
|
|
const body = await res.json();
|
|
// JSON body keeps the original, readable (unsanitized) em dash.
|
|
assert.equal(body.diagnostics.terminalReason, terminalReason);
|
|
});
|
|
|
|
test("combo diagnostics preserve every canonical recovery hint up to the existing cap", async () => {
|
|
const reasons = [
|
|
"reasoning_budget_exhausted",
|
|
"max_attempts_exceeded",
|
|
"all_accounts_inactive",
|
|
"quota_exhausted",
|
|
"all_models_failed",
|
|
"no_executable_targets",
|
|
"context_requirements_exhausted",
|
|
"all_targets_skipped",
|
|
"unknown_reason",
|
|
];
|
|
|
|
for (const reason of reasons) {
|
|
const recovery = buildRecoveryHint(reason, 30);
|
|
const response = errorResponseWithComboDiagnostics(503, "combo failed", {
|
|
poolSize: 1,
|
|
attempted: 1,
|
|
excluded: [],
|
|
attemptOrder: [],
|
|
terminalReason: reason,
|
|
recovery,
|
|
});
|
|
const body = (await response.json()) as {
|
|
recovery_hint?: { action: string; next_step: string };
|
|
};
|
|
|
|
assert.equal(body.recovery_hint?.action, recovery.action, reason);
|
|
assert.equal(body.recovery_hint?.next_step, recovery.next_step.slice(0, 200), reason);
|
|
}
|
|
});
|