fix: redact Surplus validation errors safely

This commit is contained in:
david
2026-08-27 13:10:59 +02:00
parent 2603820106
commit 23ae561c16

View File

@@ -153,7 +153,7 @@ export class SurplusClient {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({ api_key: apiKey, base_url: baseUrl }), body: JSON.stringify({ api_key: apiKey, base_url: baseUrl }),
}); }, true, [apiKey]);
if (response.text.trim() === "") { if (response.text.trim() === "") {
throw new Error("Surplus discovery returned an empty body"); throw new Error("Surplus discovery returned an empty body");
} }
@@ -209,7 +209,7 @@ export class SurplusClient {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({ api_key: apiKey, base_url: baseUrl, model }), body: JSON.stringify({ api_key: apiKey, base_url: baseUrl, model }),
}); }, true, [apiKey]);
const root = record(body, "Surplus connection response"); const root = record(body, "Surplus connection response");
if (root.ok !== true) throw new Error("Surplus provider connection test failed"); if (root.ok !== true) throw new Error("Surplus provider connection test failed");
return nonNegativeNumber(root.latency_ms, "Surplus connection latency_ms"); return nonNegativeNumber(root.latency_ms, "Surplus connection latency_ms");
@@ -229,7 +229,7 @@ export class SurplusClient {
cap_daily_usd: input.dailyCapUsd, cap_daily_usd: input.dailyCapUsd,
payout_address: input.payoutAddress, payout_address: input.payoutAddress,
}), }),
}); }, true, [input.apiKey]);
const root = record(body, "Surplus create offer response"); const root = record(body, "Surplus create offer response");
return offerId(root, "Surplus create offer response"); return offerId(root, "Surplus create offer response");
} }
@@ -282,8 +282,14 @@ export class SurplusClient {
path: string, path: string,
init: RequestInit = {}, init: RequestInit = {},
authenticated = true, authenticated = true,
additionalSecrets: readonly string[] = [],
): Promise<unknown> { ): Promise<unknown> {
const response = await this.requestText(path, init, authenticated); const response = await this.requestText(
path,
init,
authenticated,
additionalSecrets,
);
if (response.text === "") return {}; if (response.text === "") return {};
try { try {
return JSON.parse(response.text); return JSON.parse(response.text);
@@ -296,6 +302,7 @@ export class SurplusClient {
path: string, path: string,
init: RequestInit = {}, init: RequestInit = {},
authenticated = true, authenticated = true,
additionalSecrets: readonly string[] = [],
): Promise<{ status: number; text: string }> { ): Promise<{ status: number; text: string }> {
const controller = new AbortController(); const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs); const timer = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -311,7 +318,14 @@ export class SurplusClient {
const text = await readBoundedText(response, controller); const text = await readBoundedText(response, controller);
if (!response.ok) { if (!response.ok) {
throw new SurplusHttpError( throw new SurplusHttpError(
`Surplus ${path} returned HTTP ${response.status}`, `Surplus ${path} returned HTTP ${response.status}${
text
? `: ${sanitizedErrorSnippet(
text,
[this.sellerApiKey, ...additionalSecrets],
)}`
: ""
}`,
response.status, response.status,
response.headers.get("retry-after"), response.headers.get("retry-after"),
); );
@@ -502,6 +516,20 @@ function optionalNonNegativeNumber(value: unknown, field: string): number | null
return nonNegativeNumber(value, field); return nonNegativeNumber(value, field);
} }
function sanitizedErrorSnippet(
value: string,
secrets: readonly string[],
): string {
let sanitized = value;
for (const secret of secrets) {
if (secret !== "") sanitized = sanitized.replaceAll(secret, "[redacted]");
}
return sanitized
.replace(/(?:si_seller_|sk-or-v1-)[A-Za-z0-9_-]+/g, "[redacted]")
.replace(/[\r\n]+/g, " ")
.slice(0, 300);
}
function positiveSafeInteger(value: unknown, field: string): number { function positiveSafeInteger(value: unknown, field: string): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${field} must be a positive safe integer`); throw new Error(`${field} must be a positive safe integer`);