@proxy-checkout/cli 0.1.0-prx-128.108.1 → 0.1.0-prx-128.110.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.
@@ -26,6 +26,7 @@ export declare function login(input: {
26
26
  }): Promise<CliProfile>;
27
27
  export declare function status(input: {
28
28
  apiBaseUrl: string;
29
+ signal?: AbortSignal;
29
30
  token: string;
30
31
  }): Promise<CredentialExchangeResponse["profile"]>;
31
32
  export declare function logout(input: {
@@ -33,6 +34,7 @@ export declare function logout(input: {
33
34
  configStore: CliConfigStore;
34
35
  profileName: string;
35
36
  removeLocalProfile: boolean;
37
+ signal?: AbortSignal;
36
38
  token: string;
37
39
  }): Promise<void>;
38
40
  export {};
@@ -26,6 +26,7 @@ export declare function login(input: {
26
26
  }): Promise<CliProfile>;
27
27
  export declare function status(input: {
28
28
  apiBaseUrl: string;
29
+ signal?: AbortSignal;
29
30
  token: string;
30
31
  }): Promise<CredentialExchangeResponse["profile"]>;
31
32
  export declare function logout(input: {
@@ -33,6 +34,7 @@ export declare function logout(input: {
33
34
  configStore: CliConfigStore;
34
35
  profileName: string;
35
36
  removeLocalProfile: boolean;
37
+ signal?: AbortSignal;
36
38
  token: string;
37
39
  }): Promise<void>;
38
40
  export {};
package/dist/cjs/auth.js CHANGED
@@ -12,6 +12,7 @@ const deviceScope = "proxy.cli.local-webhook-development";
12
12
  const deviceGrantType = "urn:ietf:params:oauth:grant-type:device_code";
13
13
  async function login(input) {
14
14
  const signal = input.signal ?? new AbortController().signal;
15
+ const previousCredential = await input.configStore.readStoredCredential(input.profileName);
15
16
  const client = new http_client_js_1.ProxyApiClient(input.apiBaseUrl);
16
17
  const codeResult = await client.oauthRequest("/api/auth/device/code", {
17
18
  client_id: clientId,
@@ -55,17 +56,39 @@ async function login(input) {
55
56
  await revokeExchangedCredential(input.apiBaseUrl, exchanged.credential, profile, mismatch);
56
57
  throw mismatch;
57
58
  }
59
+ let saved;
58
60
  try {
59
- const saved = await input.configStore.saveProfile(input.profileName, profile, exchanged.credential);
60
- if (saved.credentialStore === "file") {
61
- input.output.notice("Warning: native credential storage is unavailable; the CLI credential is in an owner-only (0600) file fallback.");
62
- }
63
- return saved;
61
+ saved = await input.configStore.saveProfile(input.profileName, profile, exchanged.credential);
64
62
  }
65
63
  catch (error) {
66
64
  await revokeExchangedCredential(input.apiBaseUrl, exchanged.credential, profile, error);
67
65
  throw error;
68
66
  }
67
+ if (saved.credentialStore === "file") {
68
+ input.output.notice("Warning: native credential storage is unavailable; the CLI credential is in an owner-only (0600) file fallback.");
69
+ }
70
+ await revokeSupersededCredential(previousCredential, exchanged.credential, input.output);
71
+ return saved;
72
+ }
73
+ async function revokeSupersededCredential(previous, newCredential, output) {
74
+ if (!previous || previous.token === newCredential)
75
+ return;
76
+ if (!previous.token) {
77
+ output.notice(`Previous CLI credential ${previous.profile.credentialId} was not available locally and could not be automatically revoked; verify its state in the Proxy Portal.`);
78
+ return;
79
+ }
80
+ try {
81
+ await new http_client_js_1.ProxyApiClient(previous.profile.apiBaseUrl, previous.token).request("/cli/auth/logout", { method: "POST" });
82
+ }
83
+ catch (error) {
84
+ if (error instanceof errors_js_1.CliError && error.exitCode === errors_js_1.cliExitCodes.authentication)
85
+ return;
86
+ throw new errors_js_1.CliError(`The new profile is saved, but previous credential ${previous.profile.credentialId} could not be revoked automatically; revoke it in the Proxy Portal`, "cli_previous_credential_cleanup_failed", errors_js_1.cliExitCodes.network, {
87
+ cleanup_error_class: error instanceof Error ? error.name : typeof error,
88
+ cleanup_error_code: error instanceof errors_js_1.CliError ? error.code : undefined,
89
+ credential_id: previous.profile.credentialId,
90
+ });
91
+ }
69
92
  }
70
93
  async function revokeExchangedCredential(apiBaseUrl, credential, profile, originalError) {
71
94
  try {
@@ -83,12 +106,13 @@ async function revokeExchangedCredential(apiBaseUrl, credential, profile, origin
83
106
  }
84
107
  }
85
108
  async function status(input) {
86
- const response = await new http_client_js_1.ProxyApiClient(input.apiBaseUrl, input.token).request("/cli/auth/status");
109
+ const response = await new http_client_js_1.ProxyApiClient(input.apiBaseUrl, input.token).request("/cli/auth/status", { signal: input.signal });
87
110
  return response.profile;
88
111
  }
89
112
  async function logout(input) {
90
113
  await new http_client_js_1.ProxyApiClient(input.apiBaseUrl, input.token).request("/cli/auth/logout", {
91
114
  method: "POST",
115
+ signal: input.signal,
92
116
  });
93
117
  if (input.removeLocalProfile) {
94
118
  await input.configStore.removeProfile(input.profileName);
@@ -1,6 +1,6 @@
1
1
  import { CliConfigStore } from "./config.js";
2
2
  import { type CliIo } from "./output.js";
3
- export declare const cliVersion = "0.1.0-prx-128.108.1";
3
+ export declare const cliVersion = "0.1.0-prx-128.110.1";
4
4
  export interface RunCliOptions {
5
5
  configStore?: CliConfigStore;
6
6
  environment?: NodeJS.ProcessEnv;
package/dist/cjs/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CliConfigStore } from "./config.js";
2
2
  import { type CliIo } from "./output.js";
3
- export declare const cliVersion = "0.1.0-prx-128.108.1";
3
+ export declare const cliVersion = "0.1.0-prx-128.110.1";
4
4
  export interface RunCliOptions {
5
5
  configStore?: CliConfigStore;
6
6
  environment?: NodeJS.ProcessEnv;
package/dist/cjs/cli.js CHANGED
@@ -9,7 +9,7 @@ const config_js_1 = require("./config.js");
9
9
  const errors_js_1 = require("./errors.js");
10
10
  const output_js_1 = require("./output.js");
11
11
  const webhooks_js_1 = require("./webhooks.js");
12
- exports.cliVersion = "0.1.0-prx-128.108.1";
12
+ exports.cliVersion = "0.1.0-prx-128.110.1";
13
13
  async function runCli(argv, options = {}) {
14
14
  const io = options.io ?? {
15
15
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
@@ -109,7 +109,7 @@ async function executeCommand(input) {
109
109
  profileName: input.profileName,
110
110
  });
111
111
  if (input.command.path === "auth status") {
112
- const remoteProfile = await (0, auth_js_1.status)(resolved);
112
+ const remoteProfile = await (0, auth_js_1.status)({ ...resolved, signal: input.signal });
113
113
  assertRemoteTarget(remoteProfile, input);
114
114
  input.output.result({
115
115
  profile: {
@@ -122,54 +122,55 @@ async function executeCommand(input) {
122
122
  }
123
123
  if (input.command.path === "auth logout") {
124
124
  if (input.expectedMerchantId || input.expectedMode) {
125
- assertRemoteTarget(await (0, auth_js_1.status)(resolved), input);
125
+ assertRemoteTarget(await (0, auth_js_1.status)({ ...resolved, signal: input.signal }), input);
126
126
  }
127
127
  await (0, auth_js_1.logout)({
128
128
  apiBaseUrl: resolved.apiBaseUrl,
129
129
  configStore: input.configStore,
130
130
  profileName: resolved.profileName,
131
131
  removeLocalProfile: resolved.source !== "environment",
132
+ signal: input.signal,
132
133
  token: resolved.token,
133
134
  });
134
135
  input.output.result({ profile: resolved.profileName, revoked: true }, `Logged out profile ${resolved.profileName}.`);
135
136
  return;
136
137
  }
137
138
  if (input.expectedMerchantId || input.expectedMode) {
138
- assertRemoteTarget(await (0, auth_js_1.status)(resolved), input);
139
+ assertRemoteTarget(await (0, auth_js_1.status)({ ...resolved, signal: input.signal }), input);
139
140
  }
140
141
  const webhooks = new webhooks_js_1.ProxyWebhooksClient(resolved.apiBaseUrl, resolved.token);
141
142
  if (input.command.path === "webhooks endpoints list") {
142
- const endpoints = await webhooks.listEndpoints();
143
+ const endpoints = await webhooks.listEndpoints(input.signal);
143
144
  input.output.result({ webhook_endpoints: endpoints }, formatEndpointList(endpoints));
144
145
  return;
145
146
  }
146
147
  if (input.command.path === "webhooks listeners create") {
147
- const listener = await webhooks.createListener(readRequiredString(input.options, "--endpoint"));
148
+ const listener = await webhooks.createListener(readRequiredString(input.options, "--endpoint"), input.signal);
148
149
  input.output.result({ listener }, `Created listener ${listener.id}; lease expires ${listener.lease_expires_at}.`);
149
150
  return;
150
151
  }
151
152
  if (input.command.path === "webhooks listeners status") {
152
- const listener = await webhooks.getListener(readRequiredString(input.options, "--listener"));
153
+ const listener = await webhooks.getListener(readRequiredString(input.options, "--listener"), input.signal);
153
154
  input.output.result({ listener }, `Listener ${listener.id} is ${listener.status}; lease expires ${listener.lease_expires_at}.`);
154
155
  return;
155
156
  }
156
157
  if (input.command.path === "webhooks listeners heartbeat") {
157
- const listener = await webhooks.heartbeat(readRequiredString(input.options, "--listener"));
158
+ const listener = await webhooks.heartbeat(readRequiredString(input.options, "--listener"), input.signal);
158
159
  input.output.result({ listener }, `Extended listener ${listener.id} through ${listener.lease_expires_at}.`);
159
160
  return;
160
161
  }
161
162
  if (input.command.path === "webhooks listeners close") {
162
- const listener = await webhooks.close(readRequiredString(input.options, "--listener"));
163
+ const listener = await webhooks.close(readRequiredString(input.options, "--listener"), input.signal);
163
164
  input.output.result({ listener }, `Closed listener ${listener.id}.`);
164
165
  return;
165
166
  }
166
167
  if (input.command.path === "webhooks deliveries list") {
167
- const deliveries = await webhooks.listDeliveries(readRequiredString(input.options, "--listener"), readOptionalInteger(input.options, "--limit"));
168
+ const deliveries = await webhooks.listDeliveries(readRequiredString(input.options, "--listener"), readOptionalInteger(input.options, "--limit"), input.signal);
168
169
  input.output.result({ deliveries }, `${deliveries.length} local webhook deliveries.`);
169
170
  return;
170
171
  }
171
172
  if (input.command.path === "webhooks deliveries replay") {
172
- const replay = await webhooks.replay(readRequiredString(input.options, "--listener"), readRequiredString(input.options, "--delivery"), readRequiredString(input.options, "--reason"));
173
+ const replay = await webhooks.replay(readRequiredString(input.options, "--listener"), readRequiredString(input.options, "--delivery"), readRequiredString(input.options, "--reason"), input.signal);
173
174
  input.output.result(replay, "Replay scheduled.");
174
175
  return;
175
176
  }
@@ -191,13 +192,14 @@ async function executeCommand(input) {
191
192
  return;
192
193
  }
193
194
  if (input.command.path === "listen") {
194
- const endpoints = await webhooks.listEndpoints();
195
+ const endpoints = await webhooks.listEndpoints(input.signal);
195
196
  const sourceEndpointId = await selectEndpoint({
196
197
  endpoints,
197
198
  io: input.io,
198
199
  noInput: input.noInput,
199
200
  output: input.output,
200
201
  requested: readOptionalString(input.options, "--endpoint"),
202
+ signal: input.signal,
201
203
  });
202
204
  const maxEvents = readOptionalInteger(input.options, "--max-events") ?? 0;
203
205
  if (maxEvents < 0)
@@ -348,12 +350,20 @@ async function selectEndpoint(input) {
348
350
  output: input.io.stderr,
349
351
  });
350
352
  try {
351
- const answer = await prompt.question("Select a source endpoint number: ");
353
+ const answer = await prompt.question("Select a source endpoint number: ", {
354
+ signal: input.signal,
355
+ });
352
356
  const selected = input.endpoints[Number(answer) - 1];
353
357
  if (!selected)
354
358
  throw (0, errors_js_1.usageError)("Invalid endpoint selection");
355
359
  return selected.id;
356
360
  }
361
+ catch (error) {
362
+ if (input.signal.aborted) {
363
+ throw new errors_js_1.CliError("CLI command interrupted", "cli_command_interrupted", errors_js_1.cliExitCodes.interrupted);
364
+ }
365
+ throw error;
366
+ }
357
367
  finally {
358
368
  prompt.close();
359
369
  }
@@ -17,6 +17,10 @@ export interface ResolvedCredential {
17
17
  source: "environment" | "file" | "keychain" | "secret-service";
18
18
  token: string;
19
19
  }
20
+ export interface StoredProfileCredential {
21
+ profile: CliProfile;
22
+ token?: string;
23
+ }
20
24
  export interface ConfigEnvironment {
21
25
  HOME?: string;
22
26
  PATH?: string;
@@ -40,6 +44,7 @@ export declare class CliConfigStore {
40
44
  apiBaseUrl(requested?: string, profile?: CliProfile): string;
41
45
  readProfile(profileName: string): Promise<CliProfile | undefined>;
42
46
  saveProfile(profileName: string, profile: Omit<CliProfile, "credentialStore">, token: string): Promise<CliProfile>;
47
+ readStoredCredential(profileName: string): Promise<StoredProfileCredential | undefined>;
43
48
  resolveCredential(input: {
44
49
  apiBaseUrl?: string;
45
50
  profileName?: string;
@@ -17,6 +17,10 @@ export interface ResolvedCredential {
17
17
  source: "environment" | "file" | "keychain" | "secret-service";
18
18
  token: string;
19
19
  }
20
+ export interface StoredProfileCredential {
21
+ profile: CliProfile;
22
+ token?: string;
23
+ }
20
24
  export interface ConfigEnvironment {
21
25
  HOME?: string;
22
26
  PATH?: string;
@@ -40,6 +44,7 @@ export declare class CliConfigStore {
40
44
  apiBaseUrl(requested?: string, profile?: CliProfile): string;
41
45
  readProfile(profileName: string): Promise<CliProfile | undefined>;
42
46
  saveProfile(profileName: string, profile: Omit<CliProfile, "credentialStore">, token: string): Promise<CliProfile>;
47
+ readStoredCredential(profileName: string): Promise<StoredProfileCredential | undefined>;
43
48
  resolveCredential(input: {
44
49
  apiBaseUrl?: string;
45
50
  profileName?: string;
@@ -96,19 +96,58 @@ class CliConfigStore {
96
96
  }
97
97
  async saveProfile(profileName, profile, token) {
98
98
  validateToken(token);
99
- const credentialStore = await this.storeToken(profileName, token);
100
99
  const configuration = await this.readConfiguration();
100
+ const previousProfile = configuration.profiles[profileName];
101
+ const previousToken = previousProfile
102
+ ? await this.readToken(profileName, previousProfile.credentialStore)
103
+ : undefined;
104
+ const credentialStore = await this.storeToken(profileName, token);
101
105
  const storedProfile = { ...profile, credentialStore };
102
106
  configuration.profiles[profileName] = storedProfile;
103
107
  try {
104
108
  await writePrivateJson(this.configFile, configuration);
105
109
  }
106
110
  catch (error) {
107
- await this.deleteToken(profileName, credentialStore).catch(() => undefined);
111
+ try {
112
+ if (previousProfile?.credentialStore === credentialStore && previousToken !== undefined) {
113
+ await this.storeToken(profileName, previousToken);
114
+ }
115
+ else {
116
+ await this.deleteToken(profileName, credentialStore);
117
+ }
118
+ }
119
+ catch (rollbackError) {
120
+ throw credentialRollbackError(error, rollbackError);
121
+ }
108
122
  throw error;
109
123
  }
124
+ if (previousProfile && previousProfile.credentialStore !== credentialStore) {
125
+ try {
126
+ await this.deleteToken(profileName, previousProfile.credentialStore);
127
+ }
128
+ catch (error) {
129
+ configuration.profiles[profileName] = previousProfile;
130
+ try {
131
+ await writePrivateJson(this.configFile, configuration);
132
+ await this.deleteToken(profileName, credentialStore);
133
+ }
134
+ catch (rollbackError) {
135
+ throw credentialRollbackError(error, rollbackError);
136
+ }
137
+ throw error;
138
+ }
139
+ }
110
140
  return storedProfile;
111
141
  }
142
+ async readStoredCredential(profileName) {
143
+ const profile = await this.readProfile(profileName);
144
+ if (!profile)
145
+ return undefined;
146
+ const token = await this.readToken(profileName, profile.credentialStore);
147
+ if (token !== undefined)
148
+ validateToken(token);
149
+ return { profile, ...(token === undefined ? {} : { token }) };
150
+ }
112
151
  async resolveCredential(input) {
113
152
  const profileName = this.profileName(input.profileName);
114
153
  const profile = await this.readProfile(profileName);
@@ -198,7 +237,13 @@ class CliConfigStore {
198
237
  "profile",
199
238
  profileName,
200
239
  ]);
201
- return result.code === 0 ? result.stdout.trim() : undefined;
240
+ if (result.code !== 0) {
241
+ if (result.stderr.trim()) {
242
+ throw credentialStoreError("Secret Service", result.stderr);
243
+ }
244
+ return undefined;
245
+ }
246
+ return result.stdout.trim();
202
247
  }
203
248
  return (await this.readFileCredentials()).tokens[profileName];
204
249
  }
@@ -211,13 +256,16 @@ class CliConfigStore {
211
256
  return;
212
257
  }
213
258
  if (store === "secret-service") {
214
- await runProcess("secret-tool", [
259
+ const result = await runProcess("secret-tool", [
215
260
  "clear",
216
261
  "service",
217
262
  keychainService,
218
263
  "profile",
219
264
  profileName,
220
265
  ]);
266
+ if (result.code !== 0 && result.stderr.trim()) {
267
+ throw credentialStoreError("Secret Service", result.stderr);
268
+ }
221
269
  return;
222
270
  }
223
271
  const credentials = await this.readFileCredentials();
@@ -331,6 +379,12 @@ function validateToken(token) {
331
379
  function credentialStoreError(store, stderr) {
332
380
  return new errors_js_1.CliError(`${store} could not access the Proxy CLI credential`, "cli_credential_store_failed", errors_js_1.cliExitCodes.internal, { diagnostic: stderr.trim().slice(0, 200) });
333
381
  }
382
+ function credentialRollbackError(originalError, rollbackError) {
383
+ return new errors_js_1.CliError("Proxy CLI could not restore credential storage after profile replacement failed", "cli_credential_store_rollback_failed", errors_js_1.cliExitCodes.internal, {
384
+ original_error_class: originalError instanceof Error ? originalError.name : typeof originalError,
385
+ rollback_error_class: rollbackError instanceof Error ? rollbackError.name : typeof rollbackError,
386
+ });
387
+ }
334
388
  function isRecord(value) {
335
389
  return typeof value === "object" && value !== null && !Array.isArray(value);
336
390
  }
@@ -14,6 +14,7 @@ class ProxyApiClient {
14
14
  }
15
15
  async request(path, options = {}) {
16
16
  let response;
17
+ let body;
17
18
  try {
18
19
  response = await this.fetchImpl(new URL(path, `${this.baseUrl}/`), {
19
20
  body: options.body === undefined ? undefined : JSON.stringify(options.body),
@@ -26,13 +27,15 @@ class ProxyApiClient {
26
27
  ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
27
28
  },
28
29
  });
30
+ body = await readJson(response);
29
31
  }
30
32
  catch (error) {
33
+ if (error instanceof errors_js_1.CliError)
34
+ throw error;
31
35
  if (options.signal?.aborted)
32
36
  throw commandInterrupted();
33
37
  throw new errors_js_1.CliError("Could not reach the Proxy API", "cli_network_error", errors_js_1.cliExitCodes.network, { error_class: error instanceof Error ? error.name : typeof error });
34
38
  }
35
- const body = await readJson(response);
36
39
  if (!response.ok) {
37
40
  const errorBody = isRecord(body) ? body : {};
38
41
  const code = errorBody.error?.code ?? `http_${response.status}`;
@@ -46,6 +49,7 @@ class ProxyApiClient {
46
49
  }
47
50
  async oauthRequest(path, body, options = {}) {
48
51
  let response;
52
+ let responseBody;
49
53
  try {
50
54
  response = await this.fetchImpl(new URL(path, `${this.baseUrl}/`), {
51
55
  body: JSON.stringify(body),
@@ -54,14 +58,17 @@ class ProxyApiClient {
54
58
  redirect: "manual",
55
59
  signal: requestSignal(options.signal),
56
60
  });
61
+ responseBody = await readJson(response);
57
62
  }
58
63
  catch (error) {
64
+ if (error instanceof errors_js_1.CliError)
65
+ throw error;
59
66
  if (options.signal?.aborted)
60
67
  throw commandInterrupted();
61
68
  throw new errors_js_1.CliError("Could not reach the Proxy authentication service", "cli_network_error", errors_js_1.cliExitCodes.network, { error_class: error instanceof Error ? error.name : typeof error });
62
69
  }
63
70
  return {
64
- body: (await readJson(response)),
71
+ body: responseBody,
65
72
  ok: response.ok,
66
73
  status: response.status,
67
74
  };
@@ -28,23 +28,23 @@ interface DeliveryClaim {
28
28
  export declare class ProxyWebhooksClient {
29
29
  private readonly api;
30
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>;
31
+ listEndpoints(signal?: AbortSignal): Promise<WebhookEndpointRecord[]>;
32
+ createListener(sourceEndpointId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
33
+ getListener(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
34
+ heartbeat(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
35
+ close(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
36
+ listDeliveries(listenerId: string, limit?: number, signal?: AbortSignal): Promise<unknown[]>;
37
+ replay(listenerId: string, deliveryId: string, reason: string, signal?: AbortSignal): Promise<unknown>;
38
38
  claim(listenerId: string, selection?: {
39
39
  deliveryId?: string;
40
40
  replayOutboxEventId?: string;
41
- }): Promise<DeliveryClaim | null>;
41
+ }, signal?: AbortSignal): Promise<DeliveryClaim | null>;
42
42
  complete(attemptId: string, input: {
43
43
  durationMs: number;
44
44
  errorClass: "network_error" | "timeout" | null;
45
45
  responseStatusCode: number | null;
46
46
  }): Promise<unknown>;
47
- createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string): Promise<{
47
+ createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string, signal?: AbortSignal): Promise<{
48
48
  expires_at: string;
49
49
  id: string;
50
50
  inbound_url: string;
@@ -28,23 +28,23 @@ interface DeliveryClaim {
28
28
  export declare class ProxyWebhooksClient {
29
29
  private readonly api;
30
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>;
31
+ listEndpoints(signal?: AbortSignal): Promise<WebhookEndpointRecord[]>;
32
+ createListener(sourceEndpointId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
33
+ getListener(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
34
+ heartbeat(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
35
+ close(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
36
+ listDeliveries(listenerId: string, limit?: number, signal?: AbortSignal): Promise<unknown[]>;
37
+ replay(listenerId: string, deliveryId: string, reason: string, signal?: AbortSignal): Promise<unknown>;
38
38
  claim(listenerId: string, selection?: {
39
39
  deliveryId?: string;
40
40
  replayOutboxEventId?: string;
41
- }): Promise<DeliveryClaim | null>;
41
+ }, signal?: AbortSignal): Promise<DeliveryClaim | null>;
42
42
  complete(attemptId: string, input: {
43
43
  durationMs: number;
44
44
  errorClass: "network_error" | "timeout" | null;
45
45
  responseStatusCode: number | null;
46
46
  }): Promise<unknown>;
47
- createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string): Promise<{
47
+ createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string, signal?: AbortSignal): Promise<{
48
48
  expires_at: string;
49
49
  id: string;
50
50
  inbound_url: string;
@@ -25,32 +25,33 @@ class ProxyWebhooksClient {
25
25
  constructor(apiBaseUrl, token, fetchImpl) {
26
26
  this.api = new http_client_js_1.ProxyApiClient(apiBaseUrl, token, fetchImpl);
27
27
  }
28
- async listEndpoints() {
29
- return (await this.api.request("/cli/webhook-endpoints")).webhook_endpoints;
28
+ async listEndpoints(signal) {
29
+ return (await this.api.request("/cli/webhook-endpoints", { signal })).webhook_endpoints;
30
30
  }
31
- async createListener(sourceEndpointId) {
31
+ async createListener(sourceEndpointId, signal) {
32
32
  return (await this.api.request("/cli/webhook-listeners", {
33
33
  body: { source_webhook_endpoint_id: sourceEndpointId },
34
34
  method: "POST",
35
+ signal,
35
36
  })).listener;
36
37
  }
37
- async getListener(listenerId) {
38
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}`)).listener;
38
+ async getListener(listenerId, signal) {
39
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}`, { signal })).listener;
39
40
  }
40
- async heartbeat(listenerId) {
41
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/heartbeat`, { method: "POST" })).listener;
41
+ async heartbeat(listenerId, signal) {
42
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/heartbeat`, { method: "POST", signal })).listener;
42
43
  }
43
- async close(listenerId) {
44
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/close`, { method: "POST" })).listener;
44
+ async close(listenerId, signal) {
45
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/close`, { method: "POST", signal })).listener;
45
46
  }
46
- async listDeliveries(listenerId, limit) {
47
- const query = limit ? `?${new URLSearchParams({ limit: String(limit) })}` : "";
48
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries${query}`)).deliveries;
47
+ async listDeliveries(listenerId, limit, signal) {
48
+ const query = limit !== undefined ? `?${new URLSearchParams({ limit: String(limit) })}` : "";
49
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries${query}`, { signal })).deliveries;
49
50
  }
50
- async replay(listenerId, deliveryId, reason) {
51
- return this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`, { body: { reason }, method: "POST" });
51
+ async replay(listenerId, deliveryId, reason, signal) {
52
+ return this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`, { body: { reason }, method: "POST", signal });
52
53
  }
53
- async claim(listenerId, selection = {}) {
54
+ async claim(listenerId, selection = {}, signal) {
54
55
  return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/claims`, {
55
56
  method: "POST",
56
57
  body: {
@@ -59,6 +60,7 @@ class ProxyWebhooksClient {
59
60
  ? { replay_outbox_event_id: selection.replayOutboxEventId }
60
61
  : {}),
61
62
  },
63
+ signal,
62
64
  })).claim;
63
65
  }
64
66
  async complete(attemptId, input) {
@@ -71,26 +73,28 @@ class ProxyWebhooksClient {
71
73
  },
72
74
  });
73
75
  }
74
- async createStripeIngress(listenerId, webhookSecret, pspConfigId) {
76
+ async createStripeIngress(listenerId, webhookSecret, pspConfigId, signal) {
75
77
  return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/stripe-ingress`, {
76
78
  method: "POST",
77
79
  body: {
78
80
  ...(pspConfigId ? { psp_config_id: pspConfigId } : {}),
79
81
  webhook_secret: webhookSecret,
80
82
  },
83
+ signal,
81
84
  })).stripe_ingress;
82
85
  }
83
86
  }
84
87
  exports.ProxyWebhooksClient = ProxyWebhooksClient;
85
88
  async function runListener(input) {
86
89
  const destination = validateLoopbackDestination(input.forwardTo);
87
- const listener = await input.client.createListener(input.sourceEndpointId);
90
+ const listener = await input.client.createListener(input.sourceEndpointId, input.signal);
88
91
  let stripeProcess;
89
92
  let forwarded = 0;
90
93
  let heartbeatFailure;
91
94
  const heartbeatTimer = setInterval(() => {
92
- void input.client.heartbeat(listener.id).catch((error) => {
93
- heartbeatFailure = error;
95
+ void input.client.heartbeat(listener.id, input.signal).catch((error) => {
96
+ if (!input.signal.aborted)
97
+ heartbeatFailure = error;
94
98
  });
95
99
  }, heartbeatIntervalMs);
96
100
  input.output.event("listener.started", { forward_to: destination.toString(), listener }, `Listening for Proxy test webhooks on ${listener.id}; forwarding to ${destination}`);
@@ -99,7 +103,7 @@ async function runListener(input) {
99
103
  throw listenerInterrupted();
100
104
  if (input.stripe) {
101
105
  const webhookSecret = await captureStripeWebhookSecret(input.signal);
102
- const ingress = await input.client.createStripeIngress(listener.id, webhookSecret, input.pspConfigId);
106
+ const ingress = await input.client.createStripeIngress(listener.id, webhookSecret, input.pspConfigId, input.signal);
103
107
  stripeProcess = startStripeListener(ingress.inbound_url, input.output);
104
108
  input.output.event("stripe.started", {
105
109
  expires_at: ingress.expires_at,
@@ -116,7 +120,7 @@ async function runListener(input) {
116
120
  if (stripeProcess && (stripeProcess.exitCode !== null || stripeProcess.signalCode)) {
117
121
  throw new errors_js_1.CliError("Stripe CLI listener exited unexpectedly", "stripe_cli_exited", errors_js_1.cliExitCodes.dependency, { exit_code: stripeProcess.exitCode, signal: stripeProcess.signalCode });
118
122
  }
119
- const claim = await input.client.claim(listener.id);
123
+ const claim = await input.client.claim(listener.id, {}, input.signal);
120
124
  if (!claim) {
121
125
  await abortableDelay(claimPollIntervalMs, input.signal);
122
126
  continue;
@@ -160,7 +164,7 @@ async function forwardSelectedDelivery(input) {
160
164
  const claim = await input.client.claim(input.listenerId, {
161
165
  ...(input.deliveryId ? { deliveryId: input.deliveryId } : {}),
162
166
  ...(input.replayOutboxEventId ? { replayOutboxEventId: input.replayOutboxEventId } : {}),
163
- });
167
+ }, input.signal);
164
168
  if (!claim) {
165
169
  throw new errors_js_1.CliError("Selected delivery or replay is not currently claimable", "cli_delivery_not_claimable", errors_js_1.cliExitCodes.conflict);
166
170
  }
@@ -26,6 +26,7 @@ export declare function login(input: {
26
26
  }): Promise<CliProfile>;
27
27
  export declare function status(input: {
28
28
  apiBaseUrl: string;
29
+ signal?: AbortSignal;
29
30
  token: string;
30
31
  }): Promise<CredentialExchangeResponse["profile"]>;
31
32
  export declare function logout(input: {
@@ -33,6 +34,7 @@ export declare function logout(input: {
33
34
  configStore: CliConfigStore;
34
35
  profileName: string;
35
36
  removeLocalProfile: boolean;
37
+ signal?: AbortSignal;
36
38
  token: string;
37
39
  }): Promise<void>;
38
40
  export {};
package/dist/esm/auth.js CHANGED
@@ -7,6 +7,7 @@ const deviceScope = "proxy.cli.local-webhook-development";
7
7
  const deviceGrantType = "urn:ietf:params:oauth:grant-type:device_code";
8
8
  export async function login(input) {
9
9
  const signal = input.signal ?? new AbortController().signal;
10
+ const previousCredential = await input.configStore.readStoredCredential(input.profileName);
10
11
  const client = new ProxyApiClient(input.apiBaseUrl);
11
12
  const codeResult = await client.oauthRequest("/api/auth/device/code", {
12
13
  client_id: clientId,
@@ -50,17 +51,39 @@ export async function login(input) {
50
51
  await revokeExchangedCredential(input.apiBaseUrl, exchanged.credential, profile, mismatch);
51
52
  throw mismatch;
52
53
  }
54
+ let saved;
53
55
  try {
54
- const saved = await input.configStore.saveProfile(input.profileName, profile, exchanged.credential);
55
- if (saved.credentialStore === "file") {
56
- input.output.notice("Warning: native credential storage is unavailable; the CLI credential is in an owner-only (0600) file fallback.");
57
- }
58
- return saved;
56
+ saved = await input.configStore.saveProfile(input.profileName, profile, exchanged.credential);
59
57
  }
60
58
  catch (error) {
61
59
  await revokeExchangedCredential(input.apiBaseUrl, exchanged.credential, profile, error);
62
60
  throw error;
63
61
  }
62
+ if (saved.credentialStore === "file") {
63
+ input.output.notice("Warning: native credential storage is unavailable; the CLI credential is in an owner-only (0600) file fallback.");
64
+ }
65
+ await revokeSupersededCredential(previousCredential, exchanged.credential, input.output);
66
+ return saved;
67
+ }
68
+ async function revokeSupersededCredential(previous, newCredential, output) {
69
+ if (!previous || previous.token === newCredential)
70
+ return;
71
+ if (!previous.token) {
72
+ output.notice(`Previous CLI credential ${previous.profile.credentialId} was not available locally and could not be automatically revoked; verify its state in the Proxy Portal.`);
73
+ return;
74
+ }
75
+ try {
76
+ await new ProxyApiClient(previous.profile.apiBaseUrl, previous.token).request("/cli/auth/logout", { method: "POST" });
77
+ }
78
+ catch (error) {
79
+ if (error instanceof CliError && error.exitCode === cliExitCodes.authentication)
80
+ return;
81
+ throw new CliError(`The new profile is saved, but previous credential ${previous.profile.credentialId} could not be revoked automatically; revoke it in the Proxy Portal`, "cli_previous_credential_cleanup_failed", cliExitCodes.network, {
82
+ cleanup_error_class: error instanceof Error ? error.name : typeof error,
83
+ cleanup_error_code: error instanceof CliError ? error.code : undefined,
84
+ credential_id: previous.profile.credentialId,
85
+ });
86
+ }
64
87
  }
65
88
  async function revokeExchangedCredential(apiBaseUrl, credential, profile, originalError) {
66
89
  try {
@@ -78,12 +101,13 @@ async function revokeExchangedCredential(apiBaseUrl, credential, profile, origin
78
101
  }
79
102
  }
80
103
  export async function status(input) {
81
- const response = await new ProxyApiClient(input.apiBaseUrl, input.token).request("/cli/auth/status");
104
+ const response = await new ProxyApiClient(input.apiBaseUrl, input.token).request("/cli/auth/status", { signal: input.signal });
82
105
  return response.profile;
83
106
  }
84
107
  export async function logout(input) {
85
108
  await new ProxyApiClient(input.apiBaseUrl, input.token).request("/cli/auth/logout", {
86
109
  method: "POST",
110
+ signal: input.signal,
87
111
  });
88
112
  if (input.removeLocalProfile) {
89
113
  await input.configStore.removeProfile(input.profileName);
package/dist/esm/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CliConfigStore } from "./config.js";
2
2
  import { type CliIo } from "./output.js";
3
- export declare const cliVersion = "0.1.0-prx-128.108.1";
3
+ export declare const cliVersion = "0.1.0-prx-128.110.1";
4
4
  export interface RunCliOptions {
5
5
  configStore?: CliConfigStore;
6
6
  environment?: NodeJS.ProcessEnv;
package/dist/esm/cli.js CHANGED
@@ -5,7 +5,7 @@ import { CliConfigStore } from "./config.js";
5
5
  import { asCliError, CliError, cliExitCodes, usageError } from "./errors.js";
6
6
  import { CliOutput } from "./output.js";
7
7
  import { forwardSelectedDelivery, ProxyWebhooksClient, runListener, } from "./webhooks.js";
8
- export const cliVersion = "0.1.0-prx-128.108.1";
8
+ export const cliVersion = "0.1.0-prx-128.110.1";
9
9
  export async function runCli(argv, options = {}) {
10
10
  const io = options.io ?? {
11
11
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
@@ -105,7 +105,7 @@ async function executeCommand(input) {
105
105
  profileName: input.profileName,
106
106
  });
107
107
  if (input.command.path === "auth status") {
108
- const remoteProfile = await status(resolved);
108
+ const remoteProfile = await status({ ...resolved, signal: input.signal });
109
109
  assertRemoteTarget(remoteProfile, input);
110
110
  input.output.result({
111
111
  profile: {
@@ -118,54 +118,55 @@ async function executeCommand(input) {
118
118
  }
119
119
  if (input.command.path === "auth logout") {
120
120
  if (input.expectedMerchantId || input.expectedMode) {
121
- assertRemoteTarget(await status(resolved), input);
121
+ assertRemoteTarget(await status({ ...resolved, signal: input.signal }), input);
122
122
  }
123
123
  await logout({
124
124
  apiBaseUrl: resolved.apiBaseUrl,
125
125
  configStore: input.configStore,
126
126
  profileName: resolved.profileName,
127
127
  removeLocalProfile: resolved.source !== "environment",
128
+ signal: input.signal,
128
129
  token: resolved.token,
129
130
  });
130
131
  input.output.result({ profile: resolved.profileName, revoked: true }, `Logged out profile ${resolved.profileName}.`);
131
132
  return;
132
133
  }
133
134
  if (input.expectedMerchantId || input.expectedMode) {
134
- assertRemoteTarget(await status(resolved), input);
135
+ assertRemoteTarget(await status({ ...resolved, signal: input.signal }), input);
135
136
  }
136
137
  const webhooks = new ProxyWebhooksClient(resolved.apiBaseUrl, resolved.token);
137
138
  if (input.command.path === "webhooks endpoints list") {
138
- const endpoints = await webhooks.listEndpoints();
139
+ const endpoints = await webhooks.listEndpoints(input.signal);
139
140
  input.output.result({ webhook_endpoints: endpoints }, formatEndpointList(endpoints));
140
141
  return;
141
142
  }
142
143
  if (input.command.path === "webhooks listeners create") {
143
- const listener = await webhooks.createListener(readRequiredString(input.options, "--endpoint"));
144
+ const listener = await webhooks.createListener(readRequiredString(input.options, "--endpoint"), input.signal);
144
145
  input.output.result({ listener }, `Created listener ${listener.id}; lease expires ${listener.lease_expires_at}.`);
145
146
  return;
146
147
  }
147
148
  if (input.command.path === "webhooks listeners status") {
148
- const listener = await webhooks.getListener(readRequiredString(input.options, "--listener"));
149
+ const listener = await webhooks.getListener(readRequiredString(input.options, "--listener"), input.signal);
149
150
  input.output.result({ listener }, `Listener ${listener.id} is ${listener.status}; lease expires ${listener.lease_expires_at}.`);
150
151
  return;
151
152
  }
152
153
  if (input.command.path === "webhooks listeners heartbeat") {
153
- const listener = await webhooks.heartbeat(readRequiredString(input.options, "--listener"));
154
+ const listener = await webhooks.heartbeat(readRequiredString(input.options, "--listener"), input.signal);
154
155
  input.output.result({ listener }, `Extended listener ${listener.id} through ${listener.lease_expires_at}.`);
155
156
  return;
156
157
  }
157
158
  if (input.command.path === "webhooks listeners close") {
158
- const listener = await webhooks.close(readRequiredString(input.options, "--listener"));
159
+ const listener = await webhooks.close(readRequiredString(input.options, "--listener"), input.signal);
159
160
  input.output.result({ listener }, `Closed listener ${listener.id}.`);
160
161
  return;
161
162
  }
162
163
  if (input.command.path === "webhooks deliveries list") {
163
- const deliveries = await webhooks.listDeliveries(readRequiredString(input.options, "--listener"), readOptionalInteger(input.options, "--limit"));
164
+ const deliveries = await webhooks.listDeliveries(readRequiredString(input.options, "--listener"), readOptionalInteger(input.options, "--limit"), input.signal);
164
165
  input.output.result({ deliveries }, `${deliveries.length} local webhook deliveries.`);
165
166
  return;
166
167
  }
167
168
  if (input.command.path === "webhooks deliveries replay") {
168
- const replay = await webhooks.replay(readRequiredString(input.options, "--listener"), readRequiredString(input.options, "--delivery"), readRequiredString(input.options, "--reason"));
169
+ const replay = await webhooks.replay(readRequiredString(input.options, "--listener"), readRequiredString(input.options, "--delivery"), readRequiredString(input.options, "--reason"), input.signal);
169
170
  input.output.result(replay, "Replay scheduled.");
170
171
  return;
171
172
  }
@@ -187,13 +188,14 @@ async function executeCommand(input) {
187
188
  return;
188
189
  }
189
190
  if (input.command.path === "listen") {
190
- const endpoints = await webhooks.listEndpoints();
191
+ const endpoints = await webhooks.listEndpoints(input.signal);
191
192
  const sourceEndpointId = await selectEndpoint({
192
193
  endpoints,
193
194
  io: input.io,
194
195
  noInput: input.noInput,
195
196
  output: input.output,
196
197
  requested: readOptionalString(input.options, "--endpoint"),
198
+ signal: input.signal,
197
199
  });
198
200
  const maxEvents = readOptionalInteger(input.options, "--max-events") ?? 0;
199
201
  if (maxEvents < 0)
@@ -344,12 +346,20 @@ async function selectEndpoint(input) {
344
346
  output: input.io.stderr,
345
347
  });
346
348
  try {
347
- const answer = await prompt.question("Select a source endpoint number: ");
349
+ const answer = await prompt.question("Select a source endpoint number: ", {
350
+ signal: input.signal,
351
+ });
348
352
  const selected = input.endpoints[Number(answer) - 1];
349
353
  if (!selected)
350
354
  throw usageError("Invalid endpoint selection");
351
355
  return selected.id;
352
356
  }
357
+ catch (error) {
358
+ if (input.signal.aborted) {
359
+ throw new CliError("CLI command interrupted", "cli_command_interrupted", cliExitCodes.interrupted);
360
+ }
361
+ throw error;
362
+ }
353
363
  finally {
354
364
  prompt.close();
355
365
  }
@@ -17,6 +17,10 @@ export interface ResolvedCredential {
17
17
  source: "environment" | "file" | "keychain" | "secret-service";
18
18
  token: string;
19
19
  }
20
+ export interface StoredProfileCredential {
21
+ profile: CliProfile;
22
+ token?: string;
23
+ }
20
24
  export interface ConfigEnvironment {
21
25
  HOME?: string;
22
26
  PATH?: string;
@@ -40,6 +44,7 @@ export declare class CliConfigStore {
40
44
  apiBaseUrl(requested?: string, profile?: CliProfile): string;
41
45
  readProfile(profileName: string): Promise<CliProfile | undefined>;
42
46
  saveProfile(profileName: string, profile: Omit<CliProfile, "credentialStore">, token: string): Promise<CliProfile>;
47
+ readStoredCredential(profileName: string): Promise<StoredProfileCredential | undefined>;
43
48
  resolveCredential(input: {
44
49
  apiBaseUrl?: string;
45
50
  profileName?: string;
@@ -91,19 +91,58 @@ export class CliConfigStore {
91
91
  }
92
92
  async saveProfile(profileName, profile, token) {
93
93
  validateToken(token);
94
- const credentialStore = await this.storeToken(profileName, token);
95
94
  const configuration = await this.readConfiguration();
95
+ const previousProfile = configuration.profiles[profileName];
96
+ const previousToken = previousProfile
97
+ ? await this.readToken(profileName, previousProfile.credentialStore)
98
+ : undefined;
99
+ const credentialStore = await this.storeToken(profileName, token);
96
100
  const storedProfile = { ...profile, credentialStore };
97
101
  configuration.profiles[profileName] = storedProfile;
98
102
  try {
99
103
  await writePrivateJson(this.configFile, configuration);
100
104
  }
101
105
  catch (error) {
102
- await this.deleteToken(profileName, credentialStore).catch(() => undefined);
106
+ try {
107
+ if (previousProfile?.credentialStore === credentialStore && previousToken !== undefined) {
108
+ await this.storeToken(profileName, previousToken);
109
+ }
110
+ else {
111
+ await this.deleteToken(profileName, credentialStore);
112
+ }
113
+ }
114
+ catch (rollbackError) {
115
+ throw credentialRollbackError(error, rollbackError);
116
+ }
103
117
  throw error;
104
118
  }
119
+ if (previousProfile && previousProfile.credentialStore !== credentialStore) {
120
+ try {
121
+ await this.deleteToken(profileName, previousProfile.credentialStore);
122
+ }
123
+ catch (error) {
124
+ configuration.profiles[profileName] = previousProfile;
125
+ try {
126
+ await writePrivateJson(this.configFile, configuration);
127
+ await this.deleteToken(profileName, credentialStore);
128
+ }
129
+ catch (rollbackError) {
130
+ throw credentialRollbackError(error, rollbackError);
131
+ }
132
+ throw error;
133
+ }
134
+ }
105
135
  return storedProfile;
106
136
  }
137
+ async readStoredCredential(profileName) {
138
+ const profile = await this.readProfile(profileName);
139
+ if (!profile)
140
+ return undefined;
141
+ const token = await this.readToken(profileName, profile.credentialStore);
142
+ if (token !== undefined)
143
+ validateToken(token);
144
+ return { profile, ...(token === undefined ? {} : { token }) };
145
+ }
107
146
  async resolveCredential(input) {
108
147
  const profileName = this.profileName(input.profileName);
109
148
  const profile = await this.readProfile(profileName);
@@ -193,7 +232,13 @@ export class CliConfigStore {
193
232
  "profile",
194
233
  profileName,
195
234
  ]);
196
- return result.code === 0 ? result.stdout.trim() : undefined;
235
+ if (result.code !== 0) {
236
+ if (result.stderr.trim()) {
237
+ throw credentialStoreError("Secret Service", result.stderr);
238
+ }
239
+ return undefined;
240
+ }
241
+ return result.stdout.trim();
197
242
  }
198
243
  return (await this.readFileCredentials()).tokens[profileName];
199
244
  }
@@ -206,13 +251,16 @@ export class CliConfigStore {
206
251
  return;
207
252
  }
208
253
  if (store === "secret-service") {
209
- await runProcess("secret-tool", [
254
+ const result = await runProcess("secret-tool", [
210
255
  "clear",
211
256
  "service",
212
257
  keychainService,
213
258
  "profile",
214
259
  profileName,
215
260
  ]);
261
+ if (result.code !== 0 && result.stderr.trim()) {
262
+ throw credentialStoreError("Secret Service", result.stderr);
263
+ }
216
264
  return;
217
265
  }
218
266
  const credentials = await this.readFileCredentials();
@@ -325,6 +373,12 @@ function validateToken(token) {
325
373
  function credentialStoreError(store, stderr) {
326
374
  return new CliError(`${store} could not access the Proxy CLI credential`, "cli_credential_store_failed", cliExitCodes.internal, { diagnostic: stderr.trim().slice(0, 200) });
327
375
  }
376
+ function credentialRollbackError(originalError, rollbackError) {
377
+ return new CliError("Proxy CLI could not restore credential storage after profile replacement failed", "cli_credential_store_rollback_failed", cliExitCodes.internal, {
378
+ original_error_class: originalError instanceof Error ? originalError.name : typeof originalError,
379
+ rollback_error_class: rollbackError instanceof Error ? rollbackError.name : typeof rollbackError,
380
+ });
381
+ }
328
382
  function isRecord(value) {
329
383
  return typeof value === "object" && value !== null && !Array.isArray(value);
330
384
  }
@@ -11,6 +11,7 @@ export class ProxyApiClient {
11
11
  }
12
12
  async request(path, options = {}) {
13
13
  let response;
14
+ let body;
14
15
  try {
15
16
  response = await this.fetchImpl(new URL(path, `${this.baseUrl}/`), {
16
17
  body: options.body === undefined ? undefined : JSON.stringify(options.body),
@@ -23,13 +24,15 @@ export class ProxyApiClient {
23
24
  ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
24
25
  },
25
26
  });
27
+ body = await readJson(response);
26
28
  }
27
29
  catch (error) {
30
+ if (error instanceof CliError)
31
+ throw error;
28
32
  if (options.signal?.aborted)
29
33
  throw commandInterrupted();
30
34
  throw new CliError("Could not reach the Proxy API", "cli_network_error", cliExitCodes.network, { error_class: error instanceof Error ? error.name : typeof error });
31
35
  }
32
- const body = await readJson(response);
33
36
  if (!response.ok) {
34
37
  const errorBody = isRecord(body) ? body : {};
35
38
  const code = errorBody.error?.code ?? `http_${response.status}`;
@@ -43,6 +46,7 @@ export class ProxyApiClient {
43
46
  }
44
47
  async oauthRequest(path, body, options = {}) {
45
48
  let response;
49
+ let responseBody;
46
50
  try {
47
51
  response = await this.fetchImpl(new URL(path, `${this.baseUrl}/`), {
48
52
  body: JSON.stringify(body),
@@ -51,14 +55,17 @@ export class ProxyApiClient {
51
55
  redirect: "manual",
52
56
  signal: requestSignal(options.signal),
53
57
  });
58
+ responseBody = await readJson(response);
54
59
  }
55
60
  catch (error) {
61
+ if (error instanceof CliError)
62
+ throw error;
56
63
  if (options.signal?.aborted)
57
64
  throw commandInterrupted();
58
65
  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
66
  }
60
67
  return {
61
- body: (await readJson(response)),
68
+ body: responseBody,
62
69
  ok: response.ok,
63
70
  status: response.status,
64
71
  };
@@ -28,23 +28,23 @@ interface DeliveryClaim {
28
28
  export declare class ProxyWebhooksClient {
29
29
  private readonly api;
30
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>;
31
+ listEndpoints(signal?: AbortSignal): Promise<WebhookEndpointRecord[]>;
32
+ createListener(sourceEndpointId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
33
+ getListener(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
34
+ heartbeat(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
35
+ close(listenerId: string, signal?: AbortSignal): Promise<WebhookListenerRecord>;
36
+ listDeliveries(listenerId: string, limit?: number, signal?: AbortSignal): Promise<unknown[]>;
37
+ replay(listenerId: string, deliveryId: string, reason: string, signal?: AbortSignal): Promise<unknown>;
38
38
  claim(listenerId: string, selection?: {
39
39
  deliveryId?: string;
40
40
  replayOutboxEventId?: string;
41
- }): Promise<DeliveryClaim | null>;
41
+ }, signal?: AbortSignal): Promise<DeliveryClaim | null>;
42
42
  complete(attemptId: string, input: {
43
43
  durationMs: number;
44
44
  errorClass: "network_error" | "timeout" | null;
45
45
  responseStatusCode: number | null;
46
46
  }): Promise<unknown>;
47
- createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string): Promise<{
47
+ createStripeIngress(listenerId: string, webhookSecret: string, pspConfigId?: string, signal?: AbortSignal): Promise<{
48
48
  expires_at: string;
49
49
  id: string;
50
50
  inbound_url: string;
@@ -20,32 +20,33 @@ export class ProxyWebhooksClient {
20
20
  constructor(apiBaseUrl, token, fetchImpl) {
21
21
  this.api = new ProxyApiClient(apiBaseUrl, token, fetchImpl);
22
22
  }
23
- async listEndpoints() {
24
- return (await this.api.request("/cli/webhook-endpoints")).webhook_endpoints;
23
+ async listEndpoints(signal) {
24
+ return (await this.api.request("/cli/webhook-endpoints", { signal })).webhook_endpoints;
25
25
  }
26
- async createListener(sourceEndpointId) {
26
+ async createListener(sourceEndpointId, signal) {
27
27
  return (await this.api.request("/cli/webhook-listeners", {
28
28
  body: { source_webhook_endpoint_id: sourceEndpointId },
29
29
  method: "POST",
30
+ signal,
30
31
  })).listener;
31
32
  }
32
- async getListener(listenerId) {
33
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}`)).listener;
33
+ async getListener(listenerId, signal) {
34
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}`, { signal })).listener;
34
35
  }
35
- async heartbeat(listenerId) {
36
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/heartbeat`, { method: "POST" })).listener;
36
+ async heartbeat(listenerId, signal) {
37
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/heartbeat`, { method: "POST", signal })).listener;
37
38
  }
38
- async close(listenerId) {
39
- return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/close`, { method: "POST" })).listener;
39
+ async close(listenerId, signal) {
40
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/close`, { method: "POST", signal })).listener;
40
41
  }
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;
42
+ async listDeliveries(listenerId, limit, signal) {
43
+ const query = limit !== undefined ? `?${new URLSearchParams({ limit: String(limit) })}` : "";
44
+ return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries${query}`, { signal })).deliveries;
44
45
  }
45
- async replay(listenerId, deliveryId, reason) {
46
- return this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`, { body: { reason }, method: "POST" });
46
+ async replay(listenerId, deliveryId, reason, signal) {
47
+ return this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`, { body: { reason }, method: "POST", signal });
47
48
  }
48
- async claim(listenerId, selection = {}) {
49
+ async claim(listenerId, selection = {}, signal) {
49
50
  return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/claims`, {
50
51
  method: "POST",
51
52
  body: {
@@ -54,6 +55,7 @@ export class ProxyWebhooksClient {
54
55
  ? { replay_outbox_event_id: selection.replayOutboxEventId }
55
56
  : {}),
56
57
  },
58
+ signal,
57
59
  })).claim;
58
60
  }
59
61
  async complete(attemptId, input) {
@@ -66,25 +68,27 @@ export class ProxyWebhooksClient {
66
68
  },
67
69
  });
68
70
  }
69
- async createStripeIngress(listenerId, webhookSecret, pspConfigId) {
71
+ async createStripeIngress(listenerId, webhookSecret, pspConfigId, signal) {
70
72
  return (await this.api.request(`/cli/webhook-listeners/${encodeURIComponent(listenerId)}/stripe-ingress`, {
71
73
  method: "POST",
72
74
  body: {
73
75
  ...(pspConfigId ? { psp_config_id: pspConfigId } : {}),
74
76
  webhook_secret: webhookSecret,
75
77
  },
78
+ signal,
76
79
  })).stripe_ingress;
77
80
  }
78
81
  }
79
82
  export async function runListener(input) {
80
83
  const destination = validateLoopbackDestination(input.forwardTo);
81
- const listener = await input.client.createListener(input.sourceEndpointId);
84
+ const listener = await input.client.createListener(input.sourceEndpointId, input.signal);
82
85
  let stripeProcess;
83
86
  let forwarded = 0;
84
87
  let heartbeatFailure;
85
88
  const heartbeatTimer = setInterval(() => {
86
- void input.client.heartbeat(listener.id).catch((error) => {
87
- heartbeatFailure = error;
89
+ void input.client.heartbeat(listener.id, input.signal).catch((error) => {
90
+ if (!input.signal.aborted)
91
+ heartbeatFailure = error;
88
92
  });
89
93
  }, heartbeatIntervalMs);
90
94
  input.output.event("listener.started", { forward_to: destination.toString(), listener }, `Listening for Proxy test webhooks on ${listener.id}; forwarding to ${destination}`);
@@ -93,7 +97,7 @@ export async function runListener(input) {
93
97
  throw listenerInterrupted();
94
98
  if (input.stripe) {
95
99
  const webhookSecret = await captureStripeWebhookSecret(input.signal);
96
- const ingress = await input.client.createStripeIngress(listener.id, webhookSecret, input.pspConfigId);
100
+ const ingress = await input.client.createStripeIngress(listener.id, webhookSecret, input.pspConfigId, input.signal);
97
101
  stripeProcess = startStripeListener(ingress.inbound_url, input.output);
98
102
  input.output.event("stripe.started", {
99
103
  expires_at: ingress.expires_at,
@@ -110,7 +114,7 @@ export async function runListener(input) {
110
114
  if (stripeProcess && (stripeProcess.exitCode !== null || stripeProcess.signalCode)) {
111
115
  throw new CliError("Stripe CLI listener exited unexpectedly", "stripe_cli_exited", cliExitCodes.dependency, { exit_code: stripeProcess.exitCode, signal: stripeProcess.signalCode });
112
116
  }
113
- const claim = await input.client.claim(listener.id);
117
+ const claim = await input.client.claim(listener.id, {}, input.signal);
114
118
  if (!claim) {
115
119
  await abortableDelay(claimPollIntervalMs, input.signal);
116
120
  continue;
@@ -154,7 +158,7 @@ export async function forwardSelectedDelivery(input) {
154
158
  const claim = await input.client.claim(input.listenerId, {
155
159
  ...(input.deliveryId ? { deliveryId: input.deliveryId } : {}),
156
160
  ...(input.replayOutboxEventId ? { replayOutboxEventId: input.replayOutboxEventId } : {}),
157
- });
161
+ }, input.signal);
158
162
  if (!claim) {
159
163
  throw new CliError("Selected delivery or replay is not currently claimable", "cli_delivery_not_claimable", cliExitCodes.conflict);
160
164
  }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "sideEffects": false,
8
8
  "type": "module",
9
9
  "types": "./dist/esm/index.d.ts",
10
- "version": "0.1.0-prx-128.108.1",
10
+ "version": "0.1.0-prx-128.110.1",
11
11
  "bin": {
12
12
  "proxy": "./dist/esm/bin.js"
13
13
  },