From bc8468172c1ef7f9e0a458b96358578c3986bc70 Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem Date: Wed, 2 Sep 2026 14:51:12 +0100 Subject: [PATCH] fix: bound appraisal payment diagnostics for #282 --- services/appraisal-api/src/client.test.ts | 7 ++++++- services/appraisal-api/src/client.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/services/appraisal-api/src/client.test.ts b/services/appraisal-api/src/client.test.ts index 757f4d6..c2d603a 100644 --- a/services/appraisal-api/src/client.test.ts +++ b/services/appraisal-api/src/client.test.ts @@ -3,7 +3,7 @@ import { x402HTTPClient } from "@x402/core/client"; import { Keypair } from "@stellar/stellar-sdk"; import { describe, test } from "node:test"; -import { createPaidFetch, X402PaymentError, AppraisalResponseParseError } from "./client.js"; +import { createPaidFetch, X402PaymentError, AppraisalResponseParseError, MAX_PAYMENT_ERROR_DIAGNOSTIC_LENGTH, sanitizePaymentErrorDiagnostic } from "./client.js"; const TEST_SECRET = Keypair.random().secret(); const VALID_402_BODY = { @@ -33,6 +33,11 @@ function buildResponse(status: number, body: string | undefined, headers?: Recor } describe("createPaidFetch response parsing", () => { + test("bounds diagnostics and redacts credential fields", () => { + const diagnostic = sanitizePaymentErrorDiagnostic(JSON.stringify({ token: "secret", detail: "x".repeat(1000) })); + assert.ok(diagnostic.length <= MAX_PAYMENT_ERROR_DIAGNOSTIC_LENGTH); + assert.doesNotMatch(diagnostic, /secret/); + }); test("throws a typed parse error for an unpaid empty response body", async () => { const paidFetch = createPaidFetch({ secret: TEST_SECRET }); const originalFetch = globalThis.fetch; diff --git a/services/appraisal-api/src/client.ts b/services/appraisal-api/src/client.ts index f2f6c4d..5ca454b 100644 --- a/services/appraisal-api/src/client.ts +++ b/services/appraisal-api/src/client.ts @@ -28,6 +28,14 @@ export interface PaidResult { settlement?: SettleResponse; } +export const MAX_PAYMENT_ERROR_DIAGNOSTIC_LENGTH = 512; +const SENSITIVE_FIELD = /("?(?:secret|token|password|authorization|privateKey|private_key|apiKey|api_key)"?\s*:\s*)"?[^,}\s]+/gi; + +/** Bound provider diagnostics and redact common credential fields before display/logging. */ +export function sanitizePaymentErrorDiagnostic(body: string): string { + return body.slice(0, MAX_PAYMENT_ERROR_DIAGNOSTIC_LENGTH).replace(SENSITIVE_FIELD, '$1[REDACTED]'); +} + export class AppraisalResponseParseError extends Error { readonly name = "AppraisalResponseParseError"; readonly status: number;