security(lmarena): close public stream error boundary

This commit is contained in:
diegosouzapw
2026-09-01 21:32:02 -03:00
parent e5d1bbc9a9
commit d66748406f
3 changed files with 391 additions and 57 deletions

View File

@@ -158,11 +158,11 @@ export class LMArenaExecutor extends BaseExecutor {
} catch (error) {
if (isTlsClientUnavailableError(error)) {
log?.error?.("LMArenaExecutor", `TLS client unavailable: ${sanitizeLMArenaError(error)}`);
return mapTlsUnavailable(error, url, headers, transformedBody);
return mapTlsUnavailable(url, headers, transformedBody);
}
const publicMessage = sanitizeLMArenaError(error);
log?.error?.("LMArenaExecutor", `Request failed: ${publicMessage}`);
return mapNetworkError(error, url, headers, transformedBody);
const logMessage = sanitizeLMArenaError(error);
log?.error?.("LMArenaExecutor", `Request failed: ${logMessage}`);
return mapNetworkError(url, headers, transformedBody);
}
}

View File

@@ -3,6 +3,7 @@
* the executor methods stay under complexity / max-lines gates.
*/
import { isCloudflareChallenge } from "../../services/lmarenaTlsClient.ts";
import { buildErrorBody } from "../../utils/error.ts";
import { sanitizeLMArenaError } from "./error.ts";
import { markLMArenaCatalogModelDead } from "./models.ts";
import { parseArenaSSE } from "./stream.ts";
@@ -15,9 +16,133 @@ const SAFE_ARENA_STREAM_ERROR_NAMES = new Set([
"BodyTimeoutError",
]);
function projectArenaStreamError(error: unknown, publicMessage: string): Error {
const projected = new Error(publicMessage) as Error & { statusCode?: number };
type ArenaPublicFailure =
| { kind: "missing-cookie" }
| {
kind: "bot-block";
status: number;
reason: "cloudflare" | "token-present" | "token-needed";
}
| { kind: "http-status"; status: number }
| { kind: "tls-unavailable" }
| { kind: "network" }
| { kind: "upstream-event" }
| { kind: "stream-transport" };
interface ArenaProjectedPublicFailure {
status: number;
message: string;
type: string;
code: string;
}
type ArenaProjectedStreamError = Error & {
statusCode?: number;
type?: string;
code?: string;
};
const ARENA_PUBLIC_MESSAGES = {
missingCookie:
"Arena requires a session cookie. Paste the full Cookie header from arena.ai (include arena-auth-prod-v1.* chunks and ideally cf_clearance).",
cloudflareBot:
"Arena blocked by Cloudflare bot management. Use a residential/browser-grade network if needed, paste a fresh full Cookie header (include cf_clearance / __cf_bm when present), and optionally set providerSpecificData.recaptchaV3Token from a live browser session.",
botTokenNeeded:
"If this persists, supply a browser reCAPTCHA v3 token via credentials.providerSpecificData.recaptchaV3Token (in addition to the session cookie).",
tlsUnavailable:
"Arena TLS impersonation unavailable: Arena upstream error. Install/repair tls-client-node native binary.",
upstream: "Arena upstream error",
upstreamStream: "Arena upstream stream error",
} as const;
function normalizeArenaStatus(status: number, fallback: number): number {
return Number.isInteger(status) && status >= 200 && status <= 599 ? status : fallback;
}
function projectArenaPublicFailure(failure: ArenaPublicFailure): ArenaProjectedPublicFailure {
switch (failure.kind) {
case "missing-cookie":
return {
status: 401,
message: ARENA_PUBLIC_MESSAGES.missingCookie,
type: "authentication_error",
code: "missing_cookie",
};
case "bot-block": {
const status = normalizeArenaStatus(failure.status, 403);
let message: string;
switch (failure.reason) {
case "cloudflare":
message = ARENA_PUBLIC_MESSAGES.cloudflareBot;
break;
case "token-present":
message = `Arena API error: ${status}`;
break;
case "token-needed":
message = `Arena API error: ${status}. ${ARENA_PUBLIC_MESSAGES.botTokenNeeded}`;
break;
default: {
const exhaustiveReason: never = failure.reason;
return exhaustiveReason;
}
}
return {
status,
message,
type: "api_error",
code: "cloudflare_or_bot",
};
}
case "http-status": {
const status = normalizeArenaStatus(failure.status, 502);
return {
status,
message: `Arena API error: ${status}`,
type: "api_error",
code: String(status),
};
}
case "tls-unavailable":
return {
status: 502,
message: ARENA_PUBLIC_MESSAGES.tlsUnavailable,
type: "upstream_error",
code: "TLS_CLIENT_UNAVAILABLE",
};
case "network":
return {
status: 502,
message: ARENA_PUBLIC_MESSAGES.upstream,
type: "network_error",
code: "request_failed",
};
case "upstream-event":
return {
status: 502,
message: ARENA_PUBLIC_MESSAGES.upstream,
type: "api_error",
code: "lmarena_error",
};
case "stream-transport":
return {
status: 502,
message: ARENA_PUBLIC_MESSAGES.upstreamStream,
type: "upstream_error",
code: "lmarena_stream_error",
};
default: {
const exhaustiveFailure: never = failure;
return exhaustiveFailure;
}
}
}
function projectArenaStreamError(error: unknown): Error {
const publicError = buildArenaPublicError({ kind: "stream-transport" }).body.error;
const projected = new Error(publicError.message) as ArenaProjectedStreamError;
projected.stack = undefined;
projected.type = publicError.type;
projected.code = publicError.code;
if (!error || typeof error !== "object") return projected;
try {
@@ -25,9 +150,14 @@ function projectArenaStreamError(error: unknown, publicMessage: string): Error {
if (typeof name === "string" && SAFE_ARENA_STREAM_ERROR_NAMES.has(name)) {
projected.name = name;
}
const statusCode = Number((error as { statusCode?: unknown }).statusCode);
if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599) {
projected.statusCode = statusCode;
const rawStatusCode = (error as { statusCode?: unknown }).statusCode;
if (
typeof rawStatusCode === "number" &&
Number.isInteger(rawStatusCode) &&
rawStatusCode >= 400 &&
rawStatusCode <= 599
) {
projected.statusCode = rawStatusCode;
}
} catch {
// Hostile thrown values must not escape through coercing metadata accessors.
@@ -35,19 +165,23 @@ function projectArenaStreamError(error: unknown, publicMessage: string): Error {
return projected;
}
export function errorResponse(
status: number,
message: string,
type: string,
code: string
): Response {
const publicMessage = sanitizeLMArenaError(message);
return new Response(
JSON.stringify({
error: { message: publicMessage, type, code },
function buildArenaPublicError(failure: ArenaPublicFailure) {
const projected = projectArenaPublicFailure(failure);
return {
status: projected.status,
body: buildErrorBody(projected.status, projected.message, undefined, {
type: projected.type,
code: projected.code,
}),
{ status, headers: { "Content-Type": "application/json" } }
);
};
}
function errorResponse(failure: ArenaPublicFailure): Response {
const projected = buildArenaPublicError(failure);
return new Response(JSON.stringify(projected.body), {
status: projected.status,
headers: { "Content-Type": "application/json" },
});
}
export function missingCookieResult(
@@ -56,12 +190,7 @@ export function missingCookieResult(
transformedBody: unknown
) {
return {
response: errorResponse(
401,
"Arena requires a session cookie. Paste the full Cookie header from arena.ai (include arena-auth-prod-v1.* chunks and ideally cf_clearance).",
"authentication_error",
"missing_cookie"
),
response: errorResponse({ kind: "missing-cookie" }),
url,
headers,
transformedBody,
@@ -74,12 +203,17 @@ function isBotOrChallenge(status: number, text: string | null | undefined): bool
return Boolean(text && text.trimStart().startsWith("<!DOCTYPE"));
}
function botBlockMessage(text: string | null | undefined, hasRecaptcha: boolean, status: number) {
if (isCloudflareChallenge(text)) {
return "Arena blocked by Cloudflare bot management. Use a residential/browser-grade network if needed, paste a fresh full Cookie header (include cf_clearance / __cf_bm when present), and optionally set providerSpecificData.recaptchaV3Token from a live browser session.";
}
if (hasRecaptcha) return `Arena API error: ${status}`;
return `Arena API error: ${status}. If this persists, supply a browser reCAPTCHA v3 token via credentials.providerSpecificData.recaptchaV3Token (in addition to the session cookie).`;
function botBlockFailure(
text: string | null | undefined,
hasRecaptcha: boolean,
status: number
): ArenaPublicFailure {
const reason = isCloudflareChallenge(text)
? "cloudflare"
: hasRecaptcha
? "token-present"
: "token-needed";
return { kind: "bot-block", status, reason };
}
/** Map non-2xx / CF TLS results to an executor failure payload, or null if OK. */
@@ -96,12 +230,7 @@ export function mapFailedTlsResult(opts: {
const { status, text, hasRecaptcha, model, arenaModelId, url, headers, transformedBody } = opts;
if (isBotOrChallenge(status, text)) {
return {
response: errorResponse(
status || 403,
botBlockMessage(text, hasRecaptcha, status),
"api_error",
"cloudflare_or_bot"
),
response: errorResponse(botBlockFailure(text, hasRecaptcha, status)),
url,
headers,
transformedBody,
@@ -116,7 +245,7 @@ export function mapFailedTlsResult(opts: {
// Fail closed: TLS error bodies can contain upstream stacks, causes, or internal identifiers.
// Preserve the HTTP classification without projecting any body-derived text to the caller.
return {
response: errorResponse(status, `Arena API error: ${status}`, "api_error", String(status)),
response: errorResponse({ kind: "http-status", status }),
url,
headers,
transformedBody,
@@ -124,18 +253,12 @@ export function mapFailedTlsResult(opts: {
}
export function mapTlsUnavailable(
error: Error,
url: string,
headers: Record<string, string>,
transformedBody: unknown
) {
return {
response: errorResponse(
502,
`Arena TLS impersonation unavailable: ${sanitizeLMArenaError(error)}. Install/repair tls-client-node native binary.`,
"upstream_error",
"TLS_CLIENT_UNAVAILABLE"
),
response: errorResponse({ kind: "tls-unavailable" }),
url,
headers,
transformedBody,
@@ -143,13 +266,12 @@ export function mapTlsUnavailable(
}
export function mapNetworkError(
message: unknown,
url: string,
headers: Record<string, string>,
transformedBody: unknown
) {
return {
response: errorResponse(502, sanitizeLMArenaError(message), "network_error", "request_failed"),
response: errorResponse({ kind: "network" }),
url,
headers,
transformedBody,
@@ -214,10 +336,11 @@ function handleArenaEventLine(
return false;
}
if (event.type === "error") {
const upstreamError = buildArenaPublicError({ kind: "upstream-event" });
enqueueSse(controller, {
...baseChunk(model),
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
error: { message: sanitizeLMArenaError(event.content) },
error: upstreamError.body.error,
});
controller.close();
return true;
@@ -290,9 +413,9 @@ export function createOpenAIArenaStream(opts: {
}
emitStopAndDone(controller, model);
} catch (error) {
const publicMessage = sanitizeLMArenaError(error, "Arena upstream stream error");
log?.error?.("LMArenaExecutor", `Streaming error: ${publicMessage}`);
controller.error(projectArenaStreamError(error, publicMessage));
const logMessage = sanitizeLMArenaError(error, "Arena upstream stream error");
log?.error?.("LMArenaExecutor", `Streaming error: ${logMessage}`);
controller.error(projectArenaStreamError(error));
} finally {
await cleanupReader();
if (signal) signal.removeEventListener("abort", onAbort);
@@ -312,7 +435,7 @@ export async function handleNonStreamingArenaResponse(
): Promise<Response> {
const text = await response.text();
let fullText = "";
let error: string | null = null;
let hasUpstreamError = false;
for (const line of text.split("\n")) {
if (!line.trim()) continue;
@@ -321,12 +444,14 @@ export async function handleNonStreamingArenaResponse(
if (!event) continue;
if (event.type === "text" && event.content) fullText += event.content;
else if (event.type === "error") {
error = event.content || "Unknown error";
hasUpstreamError = true;
break;
} else if (event.type === "done") break;
}
if (error) return errorResponse(502, error, "api_error", "lmarena_error");
if (hasUpstreamError) {
return errorResponse({ kind: "upstream-event" });
}
return new Response(
JSON.stringify({

View File

@@ -647,6 +647,36 @@ describe("LMArena Executor", () => {
}
});
it("normalizes an invalid upstream status before building the public error", async () => {
__setTlsFetchOverrideForTesting(async () => ({
status: 700,
headers: new Headers({ "Content-Type": "text/plain" }),
text: "InvalidStatusInternalFailure secret-status-id",
body: null,
}));
try {
const result = await new LMArenaExecutor().execute({
model: TEST_ARENA_MODEL_ID,
body: { messages: [{ role: "user", content: "Hello" }] },
credentials: { cookie: "session=test" },
signal: new AbortController().signal,
log: null,
});
assert.equal(result.response.status, 502);
const responseText = await result.response.text();
assert.deepEqual(JSON.parse(responseText).error, {
message: "Arena API error: 502",
type: "api_error",
code: "502",
});
assert.doesNotMatch(responseText, /InvalidStatusInternalFailure|secret-status-id/i);
} finally {
__setTlsFetchOverrideForTesting(null);
}
});
it("does not expose structured upstream error details while preserving classification", async () => {
const executor = new LMArenaExecutor();
__setTlsFetchOverrideForTesting(async () => ({
@@ -749,6 +779,7 @@ describe("LMArena Executor", () => {
assert.equal(errorLogs.length, 1);
const responseText = await result.response.text();
const json = JSON.parse(responseText);
assert.equal(json.error?.message, "Arena upstream error");
assert.equal(json.error?.type, "network_error");
assert.equal(json.error?.code, "request_failed");
const publicOutput = `${errorLogs.join("\n")}\n${responseText}`;
@@ -866,6 +897,42 @@ describe("LMArena Executor", () => {
}
});
it("does not expose TLS-client failure details in the public response", async () => {
const tlsFailure = new TlsClientUnavailableError(
"NativeTlsBridgeFailure secret-adapter-id at /srv/private/lmarena-native.ts:22:4"
) as TlsClientUnavailableError & { cause?: unknown };
tlsFailure.cause = new Error("NativeTlsCause secret-cause-id");
__setTlsFetchOverrideForTesting(async () => {
throw tlsFailure;
});
try {
const result = await new LMArenaExecutor().execute({
model: TEST_ARENA_MODEL_ID,
body: { messages: [{ role: "user", content: "Hello" }] },
credentials: { cookie: "session=test" },
signal: new AbortController().signal,
log: null,
});
assert.equal(result.response.status, 502);
const responseText = await result.response.text();
const json = JSON.parse(responseText);
assert.deepEqual(json.error, {
message:
"Arena TLS impersonation unavailable: Arena upstream error. Install/repair tls-client-node native binary.",
type: "upstream_error",
code: "TLS_CLIENT_UNAVAILABLE",
});
assert.doesNotMatch(
responseText,
/NativeTlsBridgeFailure|secret-adapter-id|lmarena-native|NativeTlsCause|secret-cause-id/i
);
} finally {
__setTlsFetchOverrideForTesting(null);
}
});
it("uses a stable public fallback for blank network and upstream event errors", async (t) => {
const stackOnly = "\n at SecretOnlyFrame (/srv/private/lmarena-stack-only.ts:2:3)";
const cases = [
@@ -918,6 +985,43 @@ describe("LMArena Executor", () => {
}
});
it("does not expose non-streaming upstream event details", async () => {
const upstreamFailure =
"ArenaEventInternalFailure secret-event-id\n" +
" at ArenaEventAdapter (/srv/private/lmarena-event.ts:31:8)";
__setTlsFetchOverrideForTesting(async () => ({
status: 200,
headers: new Headers({ "Content-Type": "text/event-stream" }),
text: `3:${JSON.stringify(upstreamFailure)}\n`,
body: null,
}));
try {
const result = await new LMArenaExecutor().execute({
model: TEST_ARENA_MODEL_ID,
body: { messages: [{ role: "user", content: "Hello" }] },
credentials: { cookie: "session=test" },
signal: new AbortController().signal,
log: null,
});
assert.equal(result.response.status, 502);
const responseText = await result.response.text();
const json = JSON.parse(responseText);
assert.deepEqual(json.error, {
message: "Arena upstream error",
type: "api_error",
code: "lmarena_error",
});
assert.doesNotMatch(
responseText,
/ArenaEventInternalFailure|secret-event-id|ArenaEventAdapter|lmarena-event/i
);
} finally {
__setTlsFetchOverrideForTesting(null);
}
});
it("uses a stable public fallback for blank streaming event errors", async () => {
const stackOnly = "\n at SecretOnlyFrame (/srv/private/lmarena-stream-stack-only.ts:2:3)";
const encoded = new TextEncoder().encode(`data: 3:${JSON.stringify(stackOnly)}\n\n`);
@@ -950,13 +1054,118 @@ describe("LMArena Executor", () => {
.filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
.map((line) => JSON.parse(line.slice(6)))
.find((chunk) => chunk.error);
assert.equal(payload?.error?.message, "Arena upstream error");
assert.deepEqual(payload?.error, {
message: "Arena upstream error",
type: "api_error",
code: "lmarena_error",
});
assert.doesNotMatch(responseText, /SecretOnlyFrame|lmarena-stream-stack-only/);
} finally {
__setTlsFetchOverrideForTesting(null);
}
});
it("does not expose streaming upstream event details", async () => {
const upstreamFailure =
"StreamingArenaInternalFailure secret-stream-id\n" +
" at StreamingArenaAdapter (/srv/private/lmarena-stream-event.ts:44:9)";
const encoded = new TextEncoder().encode(`data: 3:${JSON.stringify(upstreamFailure)}\n\n`);
__setTlsFetchOverrideForTesting(async () => ({
status: 200,
headers: new Headers({ "Content-Type": "text/event-stream" }),
text: null,
body: new ReadableStream({
start(controller) {
controller.enqueue(encoded);
controller.close();
},
}),
}));
try {
const result = await new LMArenaExecutor().execute({
model: TEST_ARENA_MODEL_ID,
body: { messages: [{ role: "user", content: "Hello" }], stream: true },
stream: true,
credentials: { cookie: "session=test" },
signal: new AbortController().signal,
log: null,
});
assert.equal(result.response.status, 200);
const responseText = await result.response.text();
const payload = responseText
.split("\n")
.filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
.map((line) => JSON.parse(line.slice(6)))
.find((chunk) => chunk.error);
assert.deepEqual(payload?.error, {
message: "Arena upstream error",
type: "api_error",
code: "lmarena_error",
});
assert.doesNotMatch(
responseText,
/StreamingArenaInternalFailure|secret-stream-id|StreamingArenaAdapter|lmarena-stream-event/i
);
} finally {
__setTlsFetchOverrideForTesting(null);
}
});
it("does not expose transport errors rejected by the upstream stream", async () => {
const streamFailure = new Error(
"ArenaStreamTransportFailure secret-transport-id at /srv/private/lmarena-reader.ts:52:6"
) as Error & { cause?: unknown; statusCode?: number };
streamFailure.cause = new Error("ArenaStreamCause secret-stream-cause-id");
streamFailure.statusCode = 502;
__setTlsFetchOverrideForTesting(async () => ({
status: 200,
headers: new Headers({ "Content-Type": "text/event-stream" }),
text: null,
body: new ReadableStream({
start(controller) {
controller.error(streamFailure);
},
}),
}));
try {
const result = await new LMArenaExecutor().execute({
model: TEST_ARENA_MODEL_ID,
body: { messages: [{ role: "user", content: "Hello" }], stream: true },
stream: true,
credentials: { cookie: "session=test" },
signal: new AbortController().signal,
log: null,
});
assert.equal(result.response.status, 200);
await assert.rejects(result.response.text(), (error: unknown) => {
assert.ok(error instanceof Error);
assert.equal(error.message, "Arena upstream stream error");
assert.equal(error.stack, undefined);
const projected = error as Error & {
cause?: unknown;
statusCode?: number;
type?: string;
code?: string;
};
assert.equal(projected.cause, undefined);
assert.equal(projected.statusCode, 502);
assert.equal(projected.type, "upstream_error");
assert.equal(projected.code, "lmarena_stream_error");
assert.doesNotMatch(
error.message,
/ArenaStreamTransportFailure|secret-transport-id|lmarena-reader|ArenaStreamCause|secret-stream-cause-id/i
);
return true;
});
} finally {
__setTlsFetchOverrideForTesting(null);
}
});
it("forwards optional browser reCAPTCHA token from credentials", () => {
const executor = new LMArenaExecutor();
const body = access(executor).transformRequest(