@proxy-checkout/cli 0.1.0-prx-128.104.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/cjs/auth.d.cts +38 -0
  4. package/dist/cjs/auth.d.ts +38 -0
  5. package/dist/cjs/auth.js +258 -0
  6. package/dist/cjs/bin.d.cts +2 -0
  7. package/dist/cjs/bin.d.ts +2 -0
  8. package/dist/cjs/bin.js +10 -0
  9. package/dist/cjs/cli.d.cts +10 -0
  10. package/dist/cjs/cli.d.ts +10 -0
  11. package/dist/cjs/cli.js +469 -0
  12. package/dist/cjs/command-schema.d.cts +23 -0
  13. package/dist/cjs/command-schema.d.ts +23 -0
  14. package/dist/cjs/command-schema.js +238 -0
  15. package/dist/cjs/config.d.cts +55 -0
  16. package/dist/cjs/config.d.ts +55 -0
  17. package/dist/cjs/config.js +339 -0
  18. package/dist/cjs/errors.d.cts +20 -0
  19. package/dist/cjs/errors.d.ts +20 -0
  20. package/dist/cjs/errors.js +52 -0
  21. package/dist/cjs/http-client.d.cts +25 -0
  22. package/dist/cjs/http-client.d.ts +25 -0
  23. package/dist/cjs/http-client.js +102 -0
  24. package/dist/cjs/index.d.cts +5 -0
  25. package/dist/cjs/index.d.ts +5 -0
  26. package/dist/cjs/index.js +17 -0
  27. package/dist/cjs/output.d.cts +19 -0
  28. package/dist/cjs/output.d.ts +19 -0
  29. package/dist/cjs/output.js +76 -0
  30. package/dist/cjs/package.json +3 -0
  31. package/dist/cjs/webhooks.d.cts +81 -0
  32. package/dist/cjs/webhooks.d.ts +81 -0
  33. package/dist/cjs/webhooks.js +343 -0
  34. package/dist/esm/auth.d.ts +38 -0
  35. package/dist/esm/auth.js +253 -0
  36. package/dist/esm/bin.d.ts +2 -0
  37. package/dist/esm/bin.js +8 -0
  38. package/dist/esm/cli.d.ts +10 -0
  39. package/dist/esm/cli.js +465 -0
  40. package/dist/esm/command-schema.d.ts +23 -0
  41. package/dist/esm/command-schema.js +232 -0
  42. package/dist/esm/config.d.ts +55 -0
  43. package/dist/esm/config.js +333 -0
  44. package/dist/esm/errors.d.ts +20 -0
  45. package/dist/esm/errors.js +46 -0
  46. package/dist/esm/http-client.d.ts +25 -0
  47. package/dist/esm/http-client.js +98 -0
  48. package/dist/esm/index.d.ts +5 -0
  49. package/dist/esm/index.js +5 -0
  50. package/dist/esm/output.d.ts +19 -0
  51. package/dist/esm/output.js +71 -0
  52. package/dist/esm/webhooks.d.ts +81 -0
  53. package/dist/esm/webhooks.js +337 -0
  54. package/package.json +68 -0
@@ -0,0 +1,25 @@
1
+ export interface ProxyApiErrorBody {
2
+ error?: {
3
+ code?: string;
4
+ message?: string;
5
+ request_id?: string;
6
+ };
7
+ }
8
+ export declare class ProxyApiClient {
9
+ readonly baseUrl: string;
10
+ private readonly token?;
11
+ private readonly fetchImpl;
12
+ constructor(baseUrl: string, token?: string | undefined, fetchImpl?: typeof fetch);
13
+ request<T>(path: string, options?: {
14
+ body?: unknown;
15
+ method?: "DELETE" | "GET" | "PATCH" | "POST";
16
+ signal?: AbortSignal;
17
+ }): Promise<T>;
18
+ oauthRequest<T>(path: string, body: unknown, options?: {
19
+ signal?: AbortSignal;
20
+ }): Promise<{
21
+ body: T | Record<string, unknown>;
22
+ ok: boolean;
23
+ status: number;
24
+ }>;
25
+ }
@@ -0,0 +1,98 @@
1
+ import { CliError, cliExitCodes } from "./errors.js";
2
+ const requestTimeoutMs = 30_000;
3
+ export class ProxyApiClient {
4
+ baseUrl;
5
+ token;
6
+ fetchImpl;
7
+ constructor(baseUrl, token, fetchImpl = fetch) {
8
+ this.baseUrl = baseUrl;
9
+ this.token = token;
10
+ this.fetchImpl = fetchImpl;
11
+ }
12
+ async request(path, options = {}) {
13
+ let response;
14
+ try {
15
+ response = await this.fetchImpl(new URL(path, `${this.baseUrl}/`), {
16
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
17
+ method: options.method ?? "GET",
18
+ redirect: "manual",
19
+ signal: requestSignal(options.signal),
20
+ headers: {
21
+ accept: "application/json",
22
+ ...(options.body === undefined ? {} : { "content-type": "application/json" }),
23
+ ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
24
+ },
25
+ });
26
+ }
27
+ catch (error) {
28
+ if (options.signal?.aborted)
29
+ throw commandInterrupted();
30
+ throw new CliError("Could not reach the Proxy API", "cli_network_error", cliExitCodes.network, { error_class: error instanceof Error ? error.name : typeof error });
31
+ }
32
+ const body = await readJson(response);
33
+ if (!response.ok) {
34
+ const errorBody = isRecord(body) ? body : {};
35
+ const code = errorBody.error?.code ?? `http_${response.status}`;
36
+ const message = errorBody.error?.message ?? `Proxy API request failed with HTTP ${response.status}`;
37
+ throw new CliError(message, code, exitCodeForStatus(response.status), {
38
+ request_id: errorBody.error?.request_id,
39
+ status: response.status,
40
+ });
41
+ }
42
+ return body;
43
+ }
44
+ async oauthRequest(path, body, options = {}) {
45
+ let response;
46
+ try {
47
+ response = await this.fetchImpl(new URL(path, `${this.baseUrl}/`), {
48
+ body: JSON.stringify(body),
49
+ headers: { accept: "application/json", "content-type": "application/json" },
50
+ method: "POST",
51
+ redirect: "manual",
52
+ signal: requestSignal(options.signal),
53
+ });
54
+ }
55
+ catch (error) {
56
+ if (options.signal?.aborted)
57
+ throw commandInterrupted();
58
+ throw new CliError("Could not reach the Proxy authentication service", "cli_network_error", cliExitCodes.network, { error_class: error instanceof Error ? error.name : typeof error });
59
+ }
60
+ return {
61
+ body: (await readJson(response)),
62
+ ok: response.ok,
63
+ status: response.status,
64
+ };
65
+ }
66
+ }
67
+ function requestSignal(signal) {
68
+ const timeout = AbortSignal.timeout(requestTimeoutMs);
69
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
70
+ }
71
+ function commandInterrupted() {
72
+ return new CliError("CLI command interrupted", "cli_command_interrupted", cliExitCodes.interrupted);
73
+ }
74
+ function exitCodeForStatus(status) {
75
+ if (status === 401)
76
+ return cliExitCodes.authentication;
77
+ if (status === 403)
78
+ return cliExitCodes.permission;
79
+ if (status === 409)
80
+ return cliExitCodes.conflict;
81
+ if (status >= 500 || status === 429)
82
+ return cliExitCodes.network;
83
+ return cliExitCodes.usage;
84
+ }
85
+ async function readJson(response) {
86
+ const text = await response.text();
87
+ if (!text)
88
+ return {};
89
+ try {
90
+ return JSON.parse(text);
91
+ }
92
+ catch {
93
+ throw new CliError("Proxy API returned an invalid response", "cli_invalid_api_response", cliExitCodes.network, { status: response.status });
94
+ }
95
+ }
96
+ function isRecord(value) {
97
+ return typeof value === "object" && value !== null && !Array.isArray(value);
98
+ }
@@ -0,0 +1,5 @@
1
+ export { cliVersion, type RunCliOptions, runCli } from "./cli.js";
2
+ export { type CommandOptionSchema, type CommandSchema, commandSchemaDocument, commandSchemas, findCommandSchema, llmsFullDocument, } from "./command-schema.js";
3
+ export { CliConfigStore, type CliProfile, type ResolvedCredential } from "./config.js";
4
+ export { CliError, type CliExitCode, cliExitCodes } from "./errors.js";
5
+ export declare const packageName = "@proxy-checkout/cli";
@@ -0,0 +1,5 @@
1
+ export { cliVersion, runCli } from "./cli.js";
2
+ export { commandSchemaDocument, commandSchemas, findCommandSchema, llmsFullDocument, } from "./command-schema.js";
3
+ export { CliConfigStore } from "./config.js";
4
+ export { CliError, cliExitCodes } from "./errors.js";
5
+ export const packageName = "@proxy-checkout/cli";
@@ -0,0 +1,19 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { CliError } from "./errors.js";
3
+ export type OutputFormat = "human" | "json" | "jsonl";
4
+ export interface CliIo {
5
+ isTTY: boolean;
6
+ stderr: Pick<Writable, "write">;
7
+ stdin: Readable;
8
+ stdout: Pick<Writable, "write">;
9
+ }
10
+ export declare class CliOutput {
11
+ readonly format: OutputFormat;
12
+ private readonly io;
13
+ constructor(format: OutputFormat, io: CliIo);
14
+ result(value: unknown, human: string): void;
15
+ event(type: string, value: Record<string, unknown>, human: string): void;
16
+ notice(message: string): void;
17
+ error(error: CliError): void;
18
+ }
19
+ export declare function redactText(value: string): string;
@@ -0,0 +1,71 @@
1
+ export class CliOutput {
2
+ format;
3
+ io;
4
+ constructor(format, io) {
5
+ this.format = format;
6
+ this.io = io;
7
+ }
8
+ result(value, human) {
9
+ this.io.stdout.write(this.format === "human" ? `${human}\n` : `${safeJson(value)}\n`);
10
+ }
11
+ event(type, value, human) {
12
+ this.io.stdout.write(this.format === "human" ? `${human}\n` : `${safeJson({ type, ...redactObject(value) })}\n`);
13
+ }
14
+ notice(message) {
15
+ if (this.format === "human") {
16
+ this.io.stderr.write(`${redactText(message)}\n`);
17
+ }
18
+ }
19
+ error(error) {
20
+ if (this.format === "human") {
21
+ this.io.stderr.write(`Error: ${redactText(error.message)} (${error.code})\n`);
22
+ return;
23
+ }
24
+ this.io.stderr.write(`${safeJson({
25
+ error: {
26
+ code: error.code,
27
+ details: redactObject(error.details ?? {}),
28
+ message: redactText(error.message),
29
+ },
30
+ })}\n`);
31
+ }
32
+ }
33
+ export function redactText(value) {
34
+ return value
35
+ .replace(/\b(?:pcli|sk|rk)_(?:test|live)_[A-Za-z0-9_-]+\b/g, "[REDACTED_PROXY_CREDENTIAL]")
36
+ .replace(/\bwhsec_[A-Za-z0-9_-]+\b/g, "[REDACTED_WEBHOOK_SECRET]")
37
+ .replace(/\bBearer\s+[A-Za-z0-9._~-]+\b/gi, "Bearer [REDACTED]");
38
+ }
39
+ function safeJson(value) {
40
+ return JSON.stringify(redactUnknown(value));
41
+ }
42
+ function redactUnknown(value) {
43
+ if (typeof value === "string") {
44
+ return redactText(value);
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return value.map(redactUnknown);
48
+ }
49
+ if (value && typeof value === "object") {
50
+ return redactObject(value);
51
+ }
52
+ return value;
53
+ }
54
+ function redactObject(value) {
55
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
56
+ key,
57
+ isSensitiveKey(key) ? "[REDACTED]" : redactUnknown(item),
58
+ ]));
59
+ }
60
+ function isSensitiveKey(key) {
61
+ const normalized = key.toLowerCase();
62
+ return (normalized === "authorization" ||
63
+ normalized === "credential" ||
64
+ normalized === "secret" ||
65
+ normalized === "token" ||
66
+ normalized.endsWith("_credential") ||
67
+ normalized.endsWith("_secret") ||
68
+ normalized.endsWith("_signature") ||
69
+ normalized.endsWith("_token") ||
70
+ normalized.endsWith("-signature"));
71
+ }
@@ -0,0 +1,81 @@
1
+ import type { CliOutput } from "./output.js";
2
+ export interface WebhookEndpointRecord {
3
+ event_schema_version: string;
4
+ event_types: string[] | null;
5
+ id: string;
6
+ url: string;
7
+ }
8
+ export interface WebhookListenerRecord {
9
+ created_at: string;
10
+ event_schema_version: string;
11
+ event_types: string[] | null;
12
+ heartbeat_interval_seconds: number;
13
+ id: string;
14
+ last_heartbeat_at: string;
15
+ lease_expires_at: string;
16
+ source_webhook_endpoint_id: string;
17
+ status: "active" | "archived";
18
+ }
19
+ interface DeliveryClaim {
20
+ attempt_id: string;
21
+ attempt_number: number;
22
+ body: string;
23
+ headers: Record<string, string>;
24
+ kind: "delivery" | "replay";
25
+ replay_outbox_event_id: string | null;
26
+ webhook_delivery_id: string;
27
+ }
28
+ export declare class ProxyWebhooksClient {
29
+ private readonly api;
30
+ constructor(apiBaseUrl: string, token: string, fetchImpl?: typeof fetch);
31
+ listEndpoints(): Promise<WebhookEndpointRecord[]>;
32
+ createListener(sourceEndpointId: string): Promise<WebhookListenerRecord>;
33
+ getListener(listenerId: string): Promise<WebhookListenerRecord>;
34
+ heartbeat(listenerId: string): Promise<WebhookListenerRecord>;
35
+ close(listenerId: string): Promise<WebhookListenerRecord>;
36
+ listDeliveries(listenerId: string, limit?: number): Promise<unknown[]>;
37
+ replay(listenerId: string, deliveryId: string, reason: string): Promise<unknown>;
38
+ claim(listenerId: string, selection?: {
39
+ deliveryId?: string;
40
+ replayOutboxEventId?: string;
41
+ }): Promise<DeliveryClaim | null>;
42
+ complete(attemptId: string, input: {
43
+ durationMs: number;
44
+ errorClass: "network_error" | "timeout" | null;
45
+ responseStatusCode: number | null;
46
+ }): Promise<unknown>;
47
+ createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string): Promise<{
48
+ expires_at: string;
49
+ id: string;
50
+ inbound_url: string;
51
+ merchant_psp_config_id: string;
52
+ }>;
53
+ }
54
+ export declare function runListener(input: {
55
+ client: ProxyWebhooksClient;
56
+ forwardTo: string;
57
+ maxEvents: number;
58
+ output: CliOutput;
59
+ pspConfigId?: string;
60
+ signal: AbortSignal;
61
+ sourceEndpointId: string;
62
+ stripe: boolean;
63
+ }): Promise<{
64
+ forwarded: number;
65
+ listener: WebhookListenerRecord;
66
+ }>;
67
+ export declare function forwardSelectedDelivery(input: {
68
+ client: ProxyWebhooksClient;
69
+ deliveryId?: string;
70
+ forwardTo: string;
71
+ listenerId: string;
72
+ replayOutboxEventId?: string;
73
+ signal?: AbortSignal;
74
+ }): Promise<{
75
+ attempt_id: string;
76
+ attempt_number: number;
77
+ completion: unknown;
78
+ kind: DeliveryClaim["kind"];
79
+ webhook_delivery_id: string;
80
+ }>;
81
+ export {};
@@ -0,0 +1,337 @@
1
+ import { spawn } from "node:child_process";
2
+ import { CliError, cliExitCodes } from "./errors.js";
3
+ import { ProxyApiClient } from "./http-client.js";
4
+ import { redactText } from "./output.js";
5
+ const claimPollIntervalMs = 1_000;
6
+ const heartbeatIntervalMs = 30_000;
7
+ const localForwardTimeoutMs = 30_000;
8
+ const stripeEvents = [
9
+ "checkout.session.completed",
10
+ "customer.subscription.deleted",
11
+ "customer.subscription.updated",
12
+ "invoice.paid",
13
+ "invoice.payment_failed",
14
+ "payment_intent.canceled",
15
+ "payment_intent.payment_failed",
16
+ "payment_intent.succeeded",
17
+ ].join(",");
18
+ export class ProxyWebhooksClient {
19
+ api;
20
+ constructor(apiBaseUrl, token, fetchImpl) {
21
+ this.api = new ProxyApiClient(apiBaseUrl, token, fetchImpl);
22
+ }
23
+ async listEndpoints() {
24
+ return (await this.api.request("/cli/webhook-endpoints")).webhook_endpoints;
25
+ }
26
+ async createListener(sourceEndpointId) {
27
+ return (await this.api.request("/cli/webhook-listeners", {
28
+ body: { source_webhook_endpoint_id: sourceEndpointId },
29
+ method: "POST",
30
+ })).listener;
31
+ }
32
+ async getListener(listenerId) {
33
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}`)).listener;
34
+ }
35
+ async heartbeat(listenerId) {
36
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/heartbeat`, { method: "POST" })).listener;
37
+ }
38
+ async close(listenerId) {
39
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/close`, { method: "POST" })).listener;
40
+ }
41
+ async listDeliveries(listenerId, limit) {
42
+ const query = limit ? `?${new URLSearchParams({ limit: String(limit) })}` : "";
43
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries${query}`)).deliveries;
44
+ }
45
+ async replay(listenerId, deliveryId, reason) {
46
+ return this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`, { body: { reason }, method: "POST" });
47
+ }
48
+ async claim(listenerId, selection = {}) {
49
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/claims`, {
50
+ method: "POST",
51
+ body: {
52
+ ...(selection.deliveryId ? { delivery_id: selection.deliveryId } : {}),
53
+ ...(selection.replayOutboxEventId
54
+ ? { replay_outbox_event_id: selection.replayOutboxEventId }
55
+ : {}),
56
+ },
57
+ })).claim;
58
+ }
59
+ async complete(attemptId, input) {
60
+ return this.api.request(`/cli/webhook-delivery-attempts/${encodeURIComponent(attemptId)}/complete`, {
61
+ method: "POST",
62
+ body: {
63
+ duration_ms: input.durationMs,
64
+ error_class: input.errorClass,
65
+ response_status_code: input.responseStatusCode,
66
+ },
67
+ });
68
+ }
69
+ async createStripeIngress(listenerId, webhookSecret, pspConfigId) {
70
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/stripe-ingress`, {
71
+ method: "POST",
72
+ body: {
73
+ ...(pspConfigId ? { psp_config_id: pspConfigId } : {}),
74
+ webhook_secret: webhookSecret,
75
+ },
76
+ })).stripe_ingress;
77
+ }
78
+ }
79
+ export async function runListener(input) {
80
+ const destination = validateLoopbackDestination(input.forwardTo);
81
+ const listener = await input.client.createListener(input.sourceEndpointId);
82
+ let stripeProcess;
83
+ let forwarded = 0;
84
+ let heartbeatFailure;
85
+ const heartbeatTimer = setInterval(() => {
86
+ void input.client.heartbeat(listener.id).catch((error) => {
87
+ heartbeatFailure = error;
88
+ });
89
+ }, heartbeatIntervalMs);
90
+ input.output.event("listener.started", { forward_to: destination.toString(), listener }, `Listening for Proxy test webhooks on ${listener.id}; forwarding to ${destination}`);
91
+ try {
92
+ if (input.signal.aborted)
93
+ throw listenerInterrupted();
94
+ if (input.stripe) {
95
+ const webhookSecret = await captureStripeWebhookSecret(input.signal);
96
+ const ingress = await input.client.createStripeIngress(listener.id, webhookSecret, input.pspConfigId);
97
+ stripeProcess = startStripeListener(ingress.inbound_url, input.output);
98
+ input.output.event("stripe.started", {
99
+ expires_at: ingress.expires_at,
100
+ ingress_id: ingress.id,
101
+ merchant_psp_config_id: ingress.merchant_psp_config_id,
102
+ }, "Stripe CLI test webhook orchestration is active.");
103
+ }
104
+ if (input.signal.aborted)
105
+ throw listenerInterrupted();
106
+ while (!input.signal.aborted && (input.maxEvents === 0 || forwarded < input.maxEvents)) {
107
+ if (heartbeatFailure) {
108
+ throw heartbeatFailure;
109
+ }
110
+ if (stripeProcess && (stripeProcess.exitCode !== null || stripeProcess.signalCode)) {
111
+ throw new CliError("Stripe CLI listener exited unexpectedly", "stripe_cli_exited", cliExitCodes.dependency, { exit_code: stripeProcess.exitCode, signal: stripeProcess.signalCode });
112
+ }
113
+ const claim = await input.client.claim(listener.id);
114
+ if (!claim) {
115
+ await abortableDelay(claimPollIntervalMs, input.signal);
116
+ continue;
117
+ }
118
+ const completion = await forwardClaim(input.client, claim, destination, input.signal);
119
+ forwarded += 1;
120
+ input.output.event("delivery.forwarded", {
121
+ attempt_id: claim.attempt_id,
122
+ attempt_number: claim.attempt_number,
123
+ completion,
124
+ kind: claim.kind,
125
+ webhook_delivery_id: claim.webhook_delivery_id,
126
+ }, `Forwarded ${claim.kind} ${claim.webhook_delivery_id} (${formatCompletion(completion)}).`);
127
+ }
128
+ if (input.signal.aborted)
129
+ throw listenerInterrupted();
130
+ return { forwarded, listener };
131
+ }
132
+ finally {
133
+ clearInterval(heartbeatTimer);
134
+ if (stripeProcess && stripeProcess.exitCode === null && !stripeProcess.signalCode) {
135
+ stripeProcess.kill("SIGTERM");
136
+ await waitForExit(stripeProcess, 2_000);
137
+ if (stripeProcess.exitCode === null && !stripeProcess.signalCode) {
138
+ stripeProcess.kill("SIGKILL");
139
+ }
140
+ }
141
+ try {
142
+ const closed = await input.client.close(listener.id);
143
+ input.output.event("listener.closed", { listener: closed }, `Closed local webhook listener ${listener.id}.`);
144
+ }
145
+ catch (error) {
146
+ input.output.event("listener.cleanup_failed", {
147
+ error_class: error instanceof Error ? error.name : typeof error,
148
+ listener_id: listener.id,
149
+ }, `Could not confirm cleanup for listener ${listener.id}; its short lease will expire automatically.`);
150
+ }
151
+ }
152
+ }
153
+ export async function forwardSelectedDelivery(input) {
154
+ const claim = await input.client.claim(input.listenerId, {
155
+ ...(input.deliveryId ? { deliveryId: input.deliveryId } : {}),
156
+ ...(input.replayOutboxEventId ? { replayOutboxEventId: input.replayOutboxEventId } : {}),
157
+ });
158
+ if (!claim) {
159
+ throw new CliError("Selected delivery or replay is not currently claimable", "cli_delivery_not_claimable", cliExitCodes.conflict);
160
+ }
161
+ const completion = await forwardClaim(input.client, claim, validateLoopbackDestination(input.forwardTo), input.signal);
162
+ return {
163
+ attempt_id: claim.attempt_id,
164
+ attempt_number: claim.attempt_number,
165
+ completion,
166
+ kind: claim.kind,
167
+ webhook_delivery_id: claim.webhook_delivery_id,
168
+ };
169
+ }
170
+ async function forwardClaim(client, claim, destination, signal) {
171
+ const startedAt = Date.now();
172
+ let errorClass = null;
173
+ let responseStatusCode = null;
174
+ try {
175
+ const timeoutSignal = AbortSignal.timeout(localForwardTimeoutMs);
176
+ const response = await fetch(destination, {
177
+ body: claim.body,
178
+ headers: claim.headers,
179
+ method: "POST",
180
+ redirect: "manual",
181
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
182
+ });
183
+ responseStatusCode = response.status;
184
+ await response.body?.cancel().catch(() => undefined);
185
+ }
186
+ catch (error) {
187
+ errorClass =
188
+ !signal?.aborted &&
189
+ error instanceof Error &&
190
+ (error.name === "AbortError" || error.name === "TimeoutError")
191
+ ? "timeout"
192
+ : "network_error";
193
+ }
194
+ return client.complete(claim.attempt_id, {
195
+ durationMs: Math.min(Date.now() - startedAt, 300_000),
196
+ errorClass,
197
+ responseStatusCode,
198
+ });
199
+ }
200
+ async function captureStripeWebhookSecret(signal) {
201
+ const result = await captureProcess("stripe", ["listen", "--events", stripeEvents, "--print-secret"], signal, 30_000);
202
+ if (signal.aborted)
203
+ throw listenerInterrupted();
204
+ const match = `${result.stdout}\n${result.stderr}`.match(/\bwhsec_[A-Za-z0-9_-]{8,512}\b/);
205
+ if (result.code !== 0 || !match) {
206
+ throw new CliError("Stripe CLI could not provide a test webhook signing secret; authenticate Stripe CLI and try again", "stripe_cli_auth_required", cliExitCodes.dependency, { exit_code: result.code, stderr: redactText(result.stderr).slice(0, 400) });
207
+ }
208
+ return match[0];
209
+ }
210
+ function listenerInterrupted() {
211
+ return new CliError("Local webhook listener interrupted", "cli_listener_interrupted", cliExitCodes.interrupted);
212
+ }
213
+ function startStripeListener(inboundUrl, output) {
214
+ const child = spawn("stripe", ["listen", "--events", stripeEvents, "--forward-to", inboundUrl], {
215
+ shell: false,
216
+ stdio: ["pipe", "pipe", "pipe"],
217
+ });
218
+ child.stdin.end();
219
+ pipeSanitized(child.stdout, (line) => output.notice(`[stripe] ${line}`));
220
+ pipeSanitized(child.stderr, (line) => output.notice(`[stripe] ${line}`));
221
+ child.once("error", () => undefined);
222
+ return child;
223
+ }
224
+ function pipeSanitized(stream, writeLine) {
225
+ let pending = "";
226
+ stream.setEncoding("utf8");
227
+ stream.on("data", (chunk) => {
228
+ pending += chunk;
229
+ const lines = pending.split(/\r?\n/);
230
+ pending = lines.pop() ?? "";
231
+ for (const line of lines) {
232
+ if (line)
233
+ writeLine(redactText(line));
234
+ }
235
+ if (pending.length > 8_192) {
236
+ writeLine(redactText(pending.slice(0, 8_192)));
237
+ pending = "";
238
+ }
239
+ });
240
+ stream.on("end", () => {
241
+ if (pending)
242
+ writeLine(redactText(pending));
243
+ });
244
+ }
245
+ async function captureProcess(command, args, signal, timeoutMs) {
246
+ return new Promise((resolve, reject) => {
247
+ const child = spawn(command, args, { shell: false, stdio: ["pipe", "pipe", "pipe"] });
248
+ let stdout = "";
249
+ let stderr = "";
250
+ const timer = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
251
+ const abort = () => child.kill("SIGTERM");
252
+ signal.addEventListener("abort", abort, { once: true });
253
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
254
+ stdout = `${stdout}${chunk}`.slice(-65_536);
255
+ });
256
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
257
+ stderr = `${stderr}${chunk}`.slice(-65_536);
258
+ });
259
+ child.on("error", (error) => {
260
+ clearTimeout(timer);
261
+ signal.removeEventListener("abort", abort);
262
+ if (error.code === "ENOENT") {
263
+ reject(new CliError("Stripe CLI is not installed or not on PATH", "stripe_cli_not_found", cliExitCodes.dependency));
264
+ return;
265
+ }
266
+ reject(error);
267
+ });
268
+ child.on("close", (code) => {
269
+ clearTimeout(timer);
270
+ signal.removeEventListener("abort", abort);
271
+ resolve({ code: code ?? 1, stderr, stdout });
272
+ });
273
+ child.stdin.end();
274
+ });
275
+ }
276
+ function validateLoopbackDestination(value) {
277
+ let url;
278
+ try {
279
+ url = new URL(value);
280
+ }
281
+ catch {
282
+ throw new CliError("Forward destination must be a valid loopback URL", "cli_forward_destination_invalid", cliExitCodes.usage);
283
+ }
284
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
285
+ if ((url.protocol !== "http:" && url.protocol !== "https:") ||
286
+ !["127.0.0.1", "::1", "localhost"].includes(hostname) ||
287
+ url.username ||
288
+ url.password ||
289
+ url.hash) {
290
+ throw new CliError("Forward destination must be an HTTP(S) loopback URL without credentials or a fragment", "cli_forward_destination_invalid", cliExitCodes.usage);
291
+ }
292
+ return url;
293
+ }
294
+ function formatCompletion(completion) {
295
+ if (completion &&
296
+ typeof completion === "object" &&
297
+ "completion" in completion &&
298
+ completion.completion &&
299
+ typeof completion.completion === "object" &&
300
+ "status" in completion.completion) {
301
+ return String(completion.completion.status);
302
+ }
303
+ return "completed";
304
+ }
305
+ function abortableDelay(milliseconds, signal) {
306
+ if (signal.aborted)
307
+ return Promise.resolve();
308
+ return new Promise((resolve) => {
309
+ const onAbort = () => {
310
+ clearTimeout(timer);
311
+ signal.removeEventListener("abort", onAbort);
312
+ resolve();
313
+ };
314
+ const timer = setTimeout(() => {
315
+ signal.removeEventListener("abort", onAbort);
316
+ resolve();
317
+ }, milliseconds);
318
+ signal.addEventListener("abort", onAbort, { once: true });
319
+ });
320
+ }
321
+ function waitForExit(child, timeoutMs) {
322
+ if (child.exitCode !== null || child.signalCode)
323
+ return Promise.resolve();
324
+ return new Promise((resolve) => {
325
+ const onExit = () => {
326
+ clearTimeout(timer);
327
+ resolve();
328
+ };
329
+ const timer = setTimeout(() => {
330
+ child.removeListener("exit", onExit);
331
+ resolve();
332
+ }, timeoutMs);
333
+ child.once("exit", onExit);
334
+ if (child.exitCode !== null || child.signalCode)
335
+ onExit();
336
+ });
337
+ }