mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 23:52:18 +03:00
* fix(logging): redact client IPs and account prefixes by default ProxyEgress and AUTH logs exposed client IPs, egress IPs, and account prefixes at info level — a privacy leak in multi-tenant/shared-log environments. Now redacted by default, only shown when debugMode=true. Fixes #10348 * fix(sse): default SSE comment lines to disabled Strict SSE clients (WorkBuddy, etc.) JSON.parse every SSE line and crash on comment lines. Changed OMNIROUTE_SSE_COMMENTS default from enabled to disabled. Operators can opt in with OMNIROUTE_SSE_COMMENTS=on. Fixes #10524 * fix(logging): gate AUTH account-prefix redaction on a narrow flag, not debugMode The proxy-log redaction half of #10348 is superseded by an already-merged fix (PROXY_LOG_INCLUDE_IPS, decoupled from debugMode). The remaining gap was the chat.ts AUTH log line ("Using <provider> account: <prefix>..."), which this PR gated on the broad `debugMode` setting. `debugMode` is a general dashboard-visibility toggle unrelated to log privacy — coupling redaction to it means any future, unrelated change to debugMode's default silently changes whether account prefixes leak into logs. Add a dedicated AUTH_LOG_INCLUDE_ACCOUNT_ID feature flag (default off, security category) and gate the AUTH log line on it via isFeatureFlagEnabled(), which reads the DB override synchronously on every call (no stale in-memory cache to invalidate) and fails safe to redacted on any lookup error. Also update the SSE-comments tests/docs that still asserted the old enabled-by-default behavior (tests/unit/sseHeartbeat.test.ts, tests/unit/sse-comments-optout-9305.test.ts, docs/reference/ENVIRONMENT.md) to match the new default-off behavior from this PR's earlier commit. Refs #10348, #10524 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
sseCommentsEnabled,
|
|
shapeForClientFormat,
|
|
createSseHeartbeatTransform,
|
|
HEARTBEAT_SHAPES,
|
|
} from "../../open-sse/utils/sseHeartbeat.ts";
|
|
|
|
function withEnv(value: string | undefined, fn: () => void) {
|
|
const prev = process.env.OMNIROUTE_SSE_COMMENTS;
|
|
try {
|
|
if (value === undefined) delete process.env.OMNIROUTE_SSE_COMMENTS;
|
|
else process.env.OMNIROUTE_SSE_COMMENTS = value;
|
|
fn();
|
|
} finally {
|
|
if (prev === undefined) delete process.env.OMNIROUTE_SSE_COMMENTS;
|
|
else process.env.OMNIROUTE_SSE_COMMENTS = prev;
|
|
}
|
|
}
|
|
|
|
async function withEnvAsync<T>(value: string | undefined, fn: () => Promise<T>): Promise<T> {
|
|
const prev = process.env.OMNIROUTE_SSE_COMMENTS;
|
|
try {
|
|
if (value === undefined) delete process.env.OMNIROUTE_SSE_COMMENTS;
|
|
else process.env.OMNIROUTE_SSE_COMMENTS = value;
|
|
return await fn();
|
|
} finally {
|
|
if (prev === undefined) delete process.env.OMNIROUTE_SSE_COMMENTS;
|
|
else process.env.OMNIROUTE_SSE_COMMENTS = prev;
|
|
}
|
|
}
|
|
|
|
async function collectHeartbeatOutput(
|
|
shape: (typeof HEARTBEAT_SHAPES)[keyof typeof HEARTBEAT_SHAPES]
|
|
) {
|
|
const enc = new TextEncoder();
|
|
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
const input = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(enc.encode("data: ordinary\n\n"));
|
|
closeTimer = setTimeout(() => controller.close(), 35);
|
|
},
|
|
cancel() {
|
|
if (closeTimer) clearTimeout(closeTimer);
|
|
},
|
|
});
|
|
|
|
return new Response(
|
|
input.pipeThrough(createSseHeartbeatTransform({ shape, intervalMs: 10 }))
|
|
).text();
|
|
}
|
|
|
|
test("sseCommentsEnabled defaults to false when the env var is unset (#10524)", () => {
|
|
withEnv(undefined, () => assert.equal(sseCommentsEnabled(), false));
|
|
});
|
|
|
|
test("sseCommentsEnabled is false for 'off', 'false', '0', 'no' (case-insensitive)", () => {
|
|
withEnv("off", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("OFF", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("false", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("FALSE", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("0", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("no", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("NO", () => assert.equal(sseCommentsEnabled(), false));
|
|
withEnv("on", () => assert.equal(sseCommentsEnabled(), true));
|
|
withEnv("yes", () => assert.equal(sseCommentsEnabled(), true));
|
|
withEnv("1", () => assert.equal(sseCommentsEnabled(), true));
|
|
});
|
|
|
|
test("shapeForClientFormat maps known client formats", () => {
|
|
assert.equal(shapeForClientFormat("claude"), HEARTBEAT_SHAPES.ANTHROPIC_PING);
|
|
assert.equal(shapeForClientFormat("openai"), HEARTBEAT_SHAPES.OPENAI_CHUNK);
|
|
assert.equal(
|
|
shapeForClientFormat("openai-responses"),
|
|
HEARTBEAT_SHAPES.OPENAI_RESPONSES_IN_PROGRESS
|
|
);
|
|
assert.equal(shapeForClientFormat(undefined), HEARTBEAT_SHAPES.COMMENT);
|
|
});
|
|
|
|
test("comment opt-out suppresses only COMMENT heartbeats, not data-event heartbeats", async () => {
|
|
await withEnvAsync("off", async () => {
|
|
const comment = await collectHeartbeatOutput(HEARTBEAT_SHAPES.COMMENT);
|
|
assert.doesNotMatch(comment, /: keepalive/, "comment heartbeat should be suppressed");
|
|
assert.match(comment, /data: ordinary/, "ordinary data should pass through unchanged");
|
|
|
|
const openAI = await collectHeartbeatOutput(HEARTBEAT_SHAPES.OPENAI_CHUNK);
|
|
assert.match(openAI, /"object":"chat\.completion\.chunk"/);
|
|
|
|
const anthropic = await collectHeartbeatOutput(HEARTBEAT_SHAPES.ANTHROPIC_PING);
|
|
assert.match(anthropic, /event: ping\ndata: \{"type":"ping"\}/);
|
|
|
|
const responses = await collectHeartbeatOutput(HEARTBEAT_SHAPES.OPENAI_RESPONSES_IN_PROGRESS);
|
|
assert.match(responses, /data: \{"type":"response\.in_progress"\}/);
|
|
});
|
|
});
|