Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/browser-pay-event-redaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/browser-pay": minor
---

Interceptor event URLs now expose only the request origin, and refusal reasons are restricted to known relay values (exported as the `RefusalReason` type). CDP attach errors use fixed messages to avoid forwarding transport details.
4 changes: 3 additions & 1 deletion packages/browser-pay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ Refusal, nothing forwarded:
{ "outcome": "refused", "reason": "already_relayed" }
```

The reason string is passed through. Reasons David returns include `already_relayed`, `total_changed`, `key_rotated`, `card_removed`, `unknown_injection`, `injection_expired`, `payment_released`, `relay_in_progress`, `payment_not_authorized`, `destination_not_allowed`, `card_fields_invalid`, `merchant_mismatch`, and `provider_unavailable`. `relay_in_progress` is not terminal. The paused request stays open and the same idempotency key is posted again until the outcome is terminal or the deadline passes.
The `refused` event and logger pass through only these known reasons: `unknown_injection`, `injection_expired`, `payment_released`, `key_rotated`, `already_relayed`, `relay_in_progress`, `card_removed`, `payment_not_authorized`, `total_changed`, `destination_not_allowed`, `card_fields_invalid`, `merchant_mismatch`, and `provider_unavailable`. Any other reason becomes `unrecognized`. `relay_in_progress` is not terminal. The paused request stays open and the same idempotency key is posted again until the outcome is terminal or the deadline passes.

Event `url` is origin-only (`scheme://host[:port]`), with any held placeholder masked. An opaque origin uses its scheme (for example, `about:`); an unparseable URL is `""`. Paths, queries, fragments, and userinfo are never included. Attach errors use fixed messages and do not include transport details. CDP command failures and timeouts use the generic messages `CDP command failed` and `CDP command timed out`.

No answer after the send:

Expand Down
41 changes: 20 additions & 21 deletions packages/browser-pay/src/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function parseCdpMessage(value: unknown):
params: Record<string, unknown>;
sessionId?: string;
result?: unknown;
errorMessage?: string;
failed?: true;
}
| undefined {
if (!isRecord(value)) return undefined;
Expand All @@ -40,19 +40,14 @@ function parseCdpMessage(value: unknown):
const params = isRecord(value.params) ? value.params : {};
const sessionId =
typeof value.sessionId === "string" ? value.sessionId : undefined;
let errorMessage: string | undefined;
if (isRecord(value.error) && typeof value.error.message === "string") {
errorMessage = value.error.message;
} else if (value.error) {
errorMessage = "CDP call failed";
}
// The remote error text is dropped here so it can never reach a caller.
return {
...(id !== undefined ? { id } : {}),
...(method !== undefined ? { method } : {}),
params,
...(sessionId ? { sessionId } : {}),
result: value.result,
...(errorMessage ? { errorMessage } : {}),
...(value.error ? { failed: true as const } : {}),
};
}

Expand All @@ -78,16 +73,20 @@ class WebSocketClient {
this.closed = true;
this.notifyClose();
});
socket.on("error", (error: Error) => {
this.fail(
error instanceof Error ? error : new Error("CDP websocket error"),
);
socket.on("error", () => {
this.fail(new Error("CDP websocket error"));
});
}

static connect(url: string, timeoutMs: number): Promise<WebSocketClient> {
return new Promise((resolve, reject) => {
const target = new URL(url);
let target: URL;
try {
target = new URL(url);
} catch {
reject(new Error("CDP websocket URL is invalid"));
return;
}
const key = crypto.randomBytes(16).toString("base64");
const transport = target.protocol === "wss:" ? https : http;
const request = transport.request({
Expand Down Expand Up @@ -130,7 +129,9 @@ class WebSocketClient {
request.on("timeout", () =>
fail(new Error("CDP websocket connect timeout")),
);
request.on("error", fail);
request.on("error", () =>
fail(new Error("CDP websocket connect failed")),
);
request.end();
});
}
Expand Down Expand Up @@ -337,8 +338,8 @@ export class CdpConnection {
if (pending && message.id !== undefined) {
this.pending.delete(message.id);
clearTimeout(pending.timer);
if (message.errorMessage) {
pending.reject(new Error(`CDP ${message.errorMessage}`));
if (message.failed) {
pending.reject(new Error("CDP command failed"));
} else {
pending.resolve(message.result);
}
Expand Down Expand Up @@ -389,7 +390,7 @@ export class CdpConnection {
}
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`CDP ${method} timed out`));
reject(new Error("CDP command timed out"));
}, timeoutMs);
this.pending.set(id, {
resolve,
Expand All @@ -405,12 +406,10 @@ export class CdpConnection {
...(sessionId ? { sessionId } : {}),
}),
);
} catch (error) {
} catch {
clearTimeout(timer);
this.pending.delete(id);
reject(
error instanceof Error ? error : new Error("CDP connection closed"),
);
reject(new Error("CDP connection closed"));
}
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/browser-pay/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type {
InterceptorEvent,
InterceptorHandle,
InterceptorTarget,
RefusalReason,
} from "./interceptor.js";

export { httpPaymentRelay } from "./relay.js";
Expand Down
Loading
Loading