shopstack 0.2.0 → 0.2.2

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.
package/src/client.d.ts CHANGED
@@ -2,6 +2,7 @@ export interface ShopstackClientOptions {
2
2
  apiKey?: string;
3
3
  baseUrl?: string;
4
4
  fetch?: typeof fetch;
5
+ webSocket?: typeof WebSocket;
5
6
  }
6
7
 
7
8
  export interface PaymentCard {
@@ -33,6 +34,7 @@ export interface Checkout {
33
34
  | "failed"
34
35
  | "cancelled";
35
36
  revision: number;
37
+ presentation_revision: number;
36
38
  activity: string;
37
39
  intent?: {
38
40
  name: string;
@@ -40,6 +42,7 @@ export interface Checkout {
40
42
  updated_at: string;
41
43
  };
42
44
  item_url: string;
45
+ live_view_url?: string;
43
46
  required_input?: { type: "payment_card" };
44
47
  approval?: PaymentApproval;
45
48
  result?: Record<string, unknown>;
@@ -51,6 +54,7 @@ export interface Checkout {
51
54
 
52
55
  export interface RunCheckoutOptions {
53
56
  idempotencyKey?: string;
57
+ onCreated?(checkout: Checkout): void | Promise<void>;
54
58
  pollIntervalMs?: number;
55
59
  timeoutMs?: number;
56
60
  onProgress?(checkout: Checkout): void | Promise<void>;
@@ -66,6 +70,14 @@ export interface RunCheckoutOptions {
66
70
  ): string | Promise<string | undefined> | undefined;
67
71
  }
68
72
 
73
+ export interface CheckoutUpdateSubscription {
74
+ socket_url: string;
75
+ protocol: "shopstack.v1";
76
+ token: string;
77
+ expires_at: string;
78
+ presentation_revision: number;
79
+ }
80
+
69
81
  export interface AccountSignupStarted {
70
82
  id: string;
71
83
  account_type: "personal" | "developer";
@@ -89,14 +101,58 @@ export interface VerifiedAccountSignup {
89
101
  export interface SignupOptions {
90
102
  accountType: "personal" | "developer";
91
103
  email: string;
92
- idempotencyKey?: string;
93
104
  pollIntervalMs?: number;
94
105
  timeoutMs?: number;
95
- onVerificationRequired?(signup: Omit<AccountSignupStarted, "poll_token">):
96
- | void
97
- | Promise<void>;
106
+ persistence?: SignupPersistence;
107
+ onProgress?(progress: SignupProgress): void | Promise<void>;
108
+ onVerificationRequired?(
109
+ signup: Omit<AccountSignupStarted, "poll_token">,
110
+ ): void | Promise<void>;
111
+ }
112
+
113
+ export interface PendingSignupState {
114
+ accountType: "personal" | "developer";
115
+ attemptId: string;
116
+ email: string;
117
+ expiresAt?: string;
118
+ idempotencyKey: string;
119
+ pollToken?: string;
120
+ recoveryKey: string;
121
+ signupId?: string;
122
+ }
123
+
124
+ export interface SignupPersistence {
125
+ loadPending(input: {
126
+ accountType: "personal" | "developer";
127
+ email: string;
128
+ }): Promise<PendingSignupState | undefined>;
129
+ savePending(state: PendingSignupState): Promise<void>;
130
+ completePending(
131
+ state: PendingSignupState,
132
+ result: VerifiedAccountSignup,
133
+ ): Promise<void>;
134
+ deletePending(state: PendingSignupState): Promise<void>;
135
+ }
136
+
137
+ export interface SignupProgress {
138
+ state:
139
+ | "pending"
140
+ | "email_sent"
141
+ | "waiting"
142
+ | "verified"
143
+ | "expired"
144
+ | "rate_limited"
145
+ | "conflict"
146
+ | "retryable_failure"
147
+ | "complete";
148
+ account_type?: "personal" | "developer";
149
+ email?: string;
150
+ expires_at?: string;
151
+ key_type?: "user" | "developer";
98
152
  }
99
153
 
154
+ export type CompletedAccountSignup = Omit<VerifiedAccountSignup, "api_key">;
155
+
100
156
  export class ShopstackApiError extends Error {
101
157
  code: string;
102
158
  requestId?: string;
@@ -108,20 +164,17 @@ export class ShopstackClient {
108
164
  startAccountSignup(input: {
109
165
  accountType: "personal" | "developer";
110
166
  email: string;
111
- idempotencyKey?: string;
112
167
  }): Promise<AccountSignupStarted>;
113
168
  pollAccountSignup(
114
169
  signupId: string,
115
170
  pollToken: string,
116
- ): Promise<
117
- Omit<AccountSignupStarted, "poll_token"> | VerifiedAccountSignup
118
- >;
171
+ ): Promise<Omit<AccountSignupStarted, "poll_token"> | VerifiedAccountSignup>;
119
172
  waitForAccountSignup(
120
173
  signupId: string,
121
174
  pollToken: string,
122
175
  options?: { pollIntervalMs?: number; timeoutMs?: number },
123
176
  ): Promise<VerifiedAccountSignup>;
124
- signup(input: SignupOptions): Promise<VerifiedAccountSignup>;
177
+ signup(input: SignupOptions): Promise<CompletedAccountSignup>;
125
178
  createUser(input: {
126
179
  externalId: string;
127
180
  idempotencyKey?: string;
@@ -147,6 +200,14 @@ export class ShopstackClient {
147
200
  options?: { idempotencyKey?: string },
148
201
  ): Promise<Checkout>;
149
202
  getCheckout(checkoutId: string): Promise<Checkout>;
203
+ createCheckoutUpdateSubscription(
204
+ checkoutId: string,
205
+ ): Promise<CheckoutUpdateSubscription>;
206
+ waitForCheckoutUpdate(
207
+ checkoutId: string,
208
+ afterRevision: number,
209
+ waitSeconds?: number,
210
+ ): Promise<Checkout | undefined>;
150
211
  cancelCheckout(
151
212
  checkoutId: string,
152
213
  options?: { idempotencyKey?: string },
package/src/client.js CHANGED
@@ -1,4 +1,5 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import WebSocket from "ws";
2
3
 
3
4
  const TERMINAL_STATUSES = new Set(["complete", "failed", "cancelled"]);
4
5
 
@@ -16,17 +17,133 @@ function idempotencyKey(prefix) {
16
17
  return `${prefix}-${randomUUID()}`;
17
18
  }
18
19
 
20
+ function signupRecoveryKey() {
21
+ return `signup_recovery_${randomBytes(32).toString("base64url")}`;
22
+ }
23
+
24
+ function signupAttemptKey(accountType, email) {
25
+ return `${accountType}\u0000${email.trim().toLowerCase()}`;
26
+ }
27
+
19
28
  function delay(milliseconds) {
20
29
  return milliseconds <= 0
21
30
  ? Promise.resolve()
22
31
  : new Promise((resolve) => setTimeout(resolve, milliseconds));
23
32
  }
24
33
 
34
+ function presentationRevision(checkout) {
35
+ return Number.isInteger(checkout?.presentation_revision)
36
+ ? checkout.presentation_revision
37
+ : checkout.revision;
38
+ }
39
+
40
+ class CheckoutUpdateWatcher {
41
+ constructor(client, checkoutId) {
42
+ this.client = client;
43
+ this.checkoutId = checkoutId;
44
+ this.latestRevision = 0;
45
+ this.waiter = undefined;
46
+ this.socket = undefined;
47
+ this.unavailable = typeof client.WebSocket !== "function";
48
+ }
49
+
50
+ async connect() {
51
+ if (this.socket || this.unavailable) return;
52
+ try {
53
+ const subscription = await this.client.createCheckoutUpdateSubscription(
54
+ this.checkoutId,
55
+ );
56
+ const socket = new this.client.WebSocket(subscription.socket_url, [
57
+ subscription.protocol,
58
+ subscription.token,
59
+ ]);
60
+ this.socket = socket;
61
+ socket.addEventListener("message", (event) => {
62
+ let payload;
63
+ try {
64
+ payload = JSON.parse(String(event.data));
65
+ } catch {
66
+ return;
67
+ }
68
+ if (
69
+ payload?.type !== "checkout_changed" ||
70
+ !Number.isInteger(payload.presentation_revision) ||
71
+ payload.presentation_revision < 1 ||
72
+ typeof payload.terminal !== "boolean"
73
+ ) {
74
+ return;
75
+ }
76
+ this.latestRevision = Math.max(
77
+ this.latestRevision,
78
+ payload.presentation_revision,
79
+ );
80
+ this.waiter?.();
81
+ });
82
+ const unavailable = () => {
83
+ this.unavailable = true;
84
+ this.waiter?.();
85
+ };
86
+ socket.addEventListener("error", unavailable);
87
+ socket.addEventListener("close", unavailable);
88
+ } catch {
89
+ this.unavailable = true;
90
+ }
91
+ }
92
+
93
+ async wait(afterRevision, timeoutMs) {
94
+ await this.connect();
95
+ if (this.latestRevision > afterRevision) return { kind: "changed" };
96
+ if (!this.unavailable) {
97
+ const changed = await new Promise((resolve) => {
98
+ let settled = false;
99
+ const settle = (value) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ this.waiter = undefined;
103
+ clearTimeout(timer);
104
+ resolve(value);
105
+ };
106
+ this.waiter = () =>
107
+ settle(
108
+ this.latestRevision > afterRevision ? "changed" : "unavailable",
109
+ );
110
+ const timer = setTimeout(
111
+ () => settle("timeout"),
112
+ Math.max(1, Math.min(timeoutMs, 25_000)),
113
+ );
114
+ });
115
+ if (changed === "changed") return { kind: "changed" };
116
+ if (changed === "timeout") return { kind: "refresh" };
117
+ }
118
+ try {
119
+ const checkout = await this.client.waitForCheckoutUpdate(
120
+ this.checkoutId,
121
+ afterRevision,
122
+ Math.max(1, Math.min(25, Math.ceil(timeoutMs / 1_000))),
123
+ );
124
+ return checkout === undefined
125
+ ? { kind: "refresh" }
126
+ : { checkout, kind: "checkout" };
127
+ } catch {
128
+ return { kind: "unavailable" };
129
+ }
130
+ }
131
+
132
+ close() {
133
+ try {
134
+ this.socket?.close(1000, "Checkout monitoring complete");
135
+ } catch {
136
+ // Monitoring teardown must not alter the checkout result.
137
+ }
138
+ }
139
+ }
140
+
25
141
  export class ShopstackClient {
26
142
  constructor({
27
143
  apiKey,
28
- baseUrl = "https://shopstack-staging.shopstack.workers.dev/v1",
144
+ baseUrl = "https://api.shopstack.ai/v1",
29
145
  fetch: fetchImplementation = globalThis.fetch,
146
+ webSocket: webSocketImplementation = WebSocket,
30
147
  } = {}) {
31
148
  if (typeof baseUrl !== "string" || !/^https?:\/\//u.test(baseUrl)) {
32
149
  throw new TypeError("baseUrl must be an HTTP(S) URL");
@@ -37,6 +154,7 @@ export class ShopstackClient {
37
154
  this.apiKey = apiKey;
38
155
  this.baseUrl = baseUrl.replace(/\/+$/u, "");
39
156
  this.fetch = fetchImplementation;
157
+ this.WebSocket = webSocketImplementation;
40
158
  }
41
159
 
42
160
  async request(
@@ -46,6 +164,7 @@ export class ShopstackClient {
46
164
  body,
47
165
  idempotencyKey: mutationKey,
48
166
  method = "GET",
167
+ signupRecoveryKey: recoveryKey,
49
168
  } = {},
50
169
  ) {
51
170
  const headers = new Headers({ Accept: "application/json" });
@@ -55,6 +174,7 @@ export class ShopstackClient {
55
174
  }
56
175
  if (body !== undefined) headers.set("Content-Type", "application/json");
57
176
  if (mutationKey) headers.set("Idempotency-Key", mutationKey);
177
+ if (recoveryKey) headers.set("Signup-Recovery-Key", recoveryKey);
58
178
  const response = await this.fetch(`${this.baseUrl}${path}`, {
59
179
  body: body === undefined ? undefined : JSON.stringify(body),
60
180
  headers,
@@ -88,11 +208,26 @@ export class ShopstackClient {
88
208
  return payload;
89
209
  }
90
210
 
91
- startAccountSignup({ accountType, email, idempotencyKey: key } = {}) {
211
+ _startAccountSignup({
212
+ accountType,
213
+ email,
214
+ idempotencyKey: key,
215
+ recoveryKey,
216
+ }) {
92
217
  return this.request("/accounts", {
93
218
  body: { account_type: accountType, email },
94
- idempotencyKey: key ?? idempotencyKey("account"),
219
+ idempotencyKey: key,
95
220
  method: "POST",
221
+ signupRecoveryKey: recoveryKey,
222
+ });
223
+ }
224
+
225
+ startAccountSignup({ accountType, email } = {}) {
226
+ return this._startAccountSignup({
227
+ accountType,
228
+ email,
229
+ idempotencyKey: idempotencyKey("account"),
230
+ recoveryKey: signupRecoveryKey(),
96
231
  });
97
232
  }
98
233
 
@@ -123,29 +258,135 @@ export class ShopstackClient {
123
258
  async signup({
124
259
  accountType,
125
260
  email,
126
- idempotencyKey: key,
261
+ onProgress,
127
262
  onVerificationRequired,
263
+ persistence,
128
264
  pollIntervalMs = 2_000,
129
265
  timeoutMs = 15 * 60 * 1_000,
130
266
  } = {}) {
131
- const signup = await this.startAccountSignup({
267
+ const normalizedEmail = email.trim().toLowerCase();
268
+ const memoryKey = signupAttemptKey(accountType, normalizedEmail);
269
+ this._pendingSignups ??= new Map();
270
+ const storage = persistence ?? {
271
+ completePending: async (state, result) => {
272
+ this.apiKey = result.api_key;
273
+ this._pendingSignups.delete(memoryKey);
274
+ },
275
+ deletePending: async () => this._pendingSignups.delete(memoryKey),
276
+ loadPending: async () => this._pendingSignups.get(memoryKey),
277
+ savePending: async (state) => {
278
+ this._pendingSignups.set(memoryKey, structuredClone(state));
279
+ },
280
+ };
281
+ const publish = async (state, details = {}) => {
282
+ if (typeof onProgress === "function")
283
+ await onProgress({ state, ...details });
284
+ };
285
+ let pending = await storage.loadPending({
132
286
  accountType,
133
- email,
134
- idempotencyKey: key,
287
+ email: normalizedEmail,
135
288
  });
136
- if (typeof onVerificationRequired === "function") {
137
- await onVerificationRequired({
138
- account_type: signup.account_type,
139
- email: signup.email,
140
- expires_at: signup.expires_at,
141
- id: signup.id,
142
- status: signup.status,
289
+ if (pending?.expiresAt && Date.parse(pending.expiresAt) <= Date.now()) {
290
+ await storage.deletePending(pending);
291
+ await publish("expired", {
292
+ account_type: accountType,
293
+ email: normalizedEmail,
143
294
  });
295
+ pending = undefined;
144
296
  }
145
- return this.waitForAccountSignup(signup.id, signup.poll_token, {
146
- pollIntervalMs,
147
- timeoutMs,
297
+ pending ??= {
298
+ accountType,
299
+ attemptId: `attempt-${randomUUID()}`,
300
+ email: normalizedEmail,
301
+ idempotencyKey: idempotencyKey("account"),
302
+ recoveryKey: signupRecoveryKey(),
303
+ };
304
+ await storage.savePending(pending);
305
+ await publish("pending", {
306
+ account_type: accountType,
307
+ email: normalizedEmail,
148
308
  });
309
+ try {
310
+ if (!pending.signupId || !pending.pollToken) {
311
+ const started = await this._startAccountSignup({
312
+ accountType,
313
+ email: normalizedEmail,
314
+ idempotencyKey: pending.idempotencyKey,
315
+ recoveryKey: pending.recoveryKey,
316
+ });
317
+ pending = {
318
+ ...pending,
319
+ expiresAt: started.expires_at,
320
+ pollToken: started.poll_token,
321
+ signupId: started.id,
322
+ };
323
+ await storage.savePending(pending);
324
+ if (typeof onVerificationRequired === "function") {
325
+ await onVerificationRequired({
326
+ account_type: started.account_type,
327
+ email: started.email,
328
+ expires_at: started.expires_at,
329
+ id: started.id,
330
+ status: started.status,
331
+ });
332
+ }
333
+ await publish("email_sent", {
334
+ account_type: accountType,
335
+ email: normalizedEmail,
336
+ expires_at: started.expires_at,
337
+ });
338
+ }
339
+ await publish("waiting", {
340
+ account_type: accountType,
341
+ email: normalizedEmail,
342
+ expires_at: pending.expiresAt,
343
+ });
344
+ const result = await this.waitForAccountSignup(
345
+ pending.signupId,
346
+ pending.pollToken,
347
+ { pollIntervalMs, timeoutMs },
348
+ );
349
+ await storage.completePending(pending, result);
350
+ await publish("verified", {
351
+ account_type: accountType,
352
+ email: normalizedEmail,
353
+ });
354
+ await publish("complete", {
355
+ account_type: accountType,
356
+ email: normalizedEmail,
357
+ key_type: result.key_type,
358
+ });
359
+ const { api_key: _credential, ...completed } = result;
360
+ return completed;
361
+ } catch (error) {
362
+ if (error?.code === "signup_expired") {
363
+ await storage.deletePending(pending);
364
+ await publish("expired", {
365
+ account_type: accountType,
366
+ email: normalizedEmail,
367
+ });
368
+ } else if (error?.code === "rate_limited") {
369
+ await publish("rate_limited", {
370
+ account_type: accountType,
371
+ email: normalizedEmail,
372
+ });
373
+ } else if (
374
+ error?.code === "idempotency_conflict" ||
375
+ error?.code === "invalid_signup_recovery"
376
+ ) {
377
+ await storage.deletePending(pending);
378
+ await publish("conflict", {
379
+ account_type: accountType,
380
+ email: normalizedEmail,
381
+ });
382
+ } else {
383
+ await publish("retryable_failure", {
384
+ account_type: accountType,
385
+ email: normalizedEmail,
386
+ });
387
+ }
388
+ throw error;
389
+ }
149
390
  }
150
391
 
151
392
  createUser({ externalId, idempotencyKey: key } = {}) {
@@ -203,6 +444,22 @@ export class ShopstackClient {
203
444
  return this.request(`/checkout/${encodeURIComponent(checkoutId)}`);
204
445
  }
205
446
 
447
+ createCheckoutUpdateSubscription(checkoutId) {
448
+ return this.request(`/checkout/${encodeURIComponent(checkoutId)}/updates`, {
449
+ method: "POST",
450
+ });
451
+ }
452
+
453
+ waitForCheckoutUpdate(checkoutId, afterRevision, waitSeconds = 25) {
454
+ const query = new URLSearchParams({
455
+ after: String(afterRevision),
456
+ wait: String(waitSeconds),
457
+ });
458
+ return this.request(
459
+ `/checkout/${encodeURIComponent(checkoutId)}/updates?${query.toString()}`,
460
+ );
461
+ }
462
+
206
463
  cancelCheckout(checkoutId, { idempotencyKey: key } = {}) {
207
464
  return this.request(`/checkout/${encodeURIComponent(checkoutId)}/cancel`, {
208
465
  idempotencyKey: key ?? idempotencyKey("cancel"),
@@ -268,12 +525,16 @@ export class ShopstackClient {
268
525
  let checkout = await this.createCheckout(checkoutRequest, {
269
526
  idempotencyKey: options.idempotencyKey,
270
527
  });
528
+ if (typeof options.onCreated === "function") {
529
+ await options.onCreated(checkout);
530
+ }
271
531
  let progressFingerprint;
272
532
  const publishProgress = async (current) => {
273
533
  if (typeof options.onProgress !== "function") return;
274
534
  const fingerprint = JSON.stringify([
275
535
  current.status,
276
536
  current.revision,
537
+ current.presentation_revision,
277
538
  current.activity,
278
539
  current.intent?.name,
279
540
  current.intent?.phase,
@@ -285,74 +546,88 @@ export class ShopstackClient {
285
546
  progressFingerprint = fingerprint;
286
547
  await options.onProgress(current);
287
548
  };
288
- await publishProgress(checkout);
549
+ const watcher = new CheckoutUpdateWatcher(this, checkout.id);
289
550
  let handledPaymentRevision;
290
551
  let handledApprovalId;
291
552
  let handledMessageRevision;
292
- while (!TERMINAL_STATUSES.has(checkout.status)) {
293
- if (Date.now() - startedAt >= timeoutMs) {
294
- throw new ShopstackApiError("Checkout monitoring timed out.", {
295
- code: "checkout_timeout",
296
- });
297
- }
298
- checkout = await this.getCheckout(checkout.id);
299
- await publishProgress(checkout);
300
- if (
301
- checkout.required_input?.type === "payment_card" &&
302
- handledPaymentRevision !== checkout.revision
303
- ) {
304
- if (typeof options.paymentDetails !== "function") return checkout;
305
- handledPaymentRevision = checkout.revision;
306
- const card = await options.paymentDetails(checkout);
307
- if (card === undefined) return checkout;
308
- await this.providePaymentDetails(checkout.id, card, {
309
- idempotencyKey: `payment-details-${checkout.id}-${checkout.revision}`,
310
- });
311
- continue;
312
- }
313
- if (
314
- checkout.status === "approval_required" &&
315
- checkout.approval &&
316
- handledApprovalId !== checkout.approval.id
317
- ) {
318
- if (typeof options.approve !== "function") return checkout;
319
- handledApprovalId = checkout.approval.id;
320
- const approved = await options.approve(checkout.approval, checkout);
321
- if (typeof approved !== "boolean") return checkout;
322
- await this.decidePaymentApproval(
323
- checkout.id,
324
- checkout.approval.id,
325
- approved,
326
- { idempotencyKey: `approval-${checkout.approval.id}` },
553
+ try {
554
+ while (true) {
555
+ await publishProgress(checkout);
556
+ if (TERMINAL_STATUSES.has(checkout.status)) return checkout;
557
+ const elapsed = Date.now() - startedAt;
558
+ if (elapsed >= timeoutMs) {
559
+ throw new ShopstackApiError("Checkout monitoring timed out.", {
560
+ code: "checkout_timeout",
561
+ });
562
+ }
563
+ if (
564
+ checkout.required_input?.type === "payment_card" &&
565
+ handledPaymentRevision !== checkout.revision
566
+ ) {
567
+ if (typeof options.paymentDetails !== "function") return checkout;
568
+ handledPaymentRevision = checkout.revision;
569
+ const card = await options.paymentDetails(checkout);
570
+ if (card === undefined) return checkout;
571
+ checkout = await this.providePaymentDetails(checkout.id, card, {
572
+ idempotencyKey: `payment-details-${checkout.id}-${checkout.revision}`,
573
+ });
574
+ handledMessageRevision = checkout.revision;
575
+ continue;
576
+ }
577
+ if (
578
+ checkout.status === "approval_required" &&
579
+ checkout.approval &&
580
+ handledApprovalId !== checkout.approval.id
581
+ ) {
582
+ if (typeof options.approve !== "function") return checkout;
583
+ handledApprovalId = checkout.approval.id;
584
+ const approved = await options.approve(checkout.approval, checkout);
585
+ if (typeof approved !== "boolean") return checkout;
586
+ checkout = await this.decidePaymentApproval(
587
+ checkout.id,
588
+ checkout.approval.id,
589
+ approved,
590
+ { idempotencyKey: `approval-${checkout.approval.id}` },
591
+ );
592
+ if (!approved) return checkout;
593
+ continue;
594
+ }
595
+ if (
596
+ checkout.status === "help_required" &&
597
+ checkout.required_input === undefined &&
598
+ handledMessageRevision !== checkout.revision
599
+ ) {
600
+ if (typeof options.message !== "function") return checkout;
601
+ handledMessageRevision = checkout.revision;
602
+ const content = await options.message(checkout);
603
+ if (typeof content !== "string" || content.length === 0)
604
+ return checkout;
605
+ await this.sendMessage(checkout.id, content, {
606
+ idempotencyKey: `message-${checkout.id}-${checkout.revision}`,
607
+ });
608
+ continue;
609
+ }
610
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
611
+ const update = await watcher.wait(
612
+ presentationRevision(checkout),
613
+ remainingMs,
327
614
  );
328
- if (!approved) return this.getCheckout(checkout.id);
329
- continue;
330
- }
331
- if (
332
- checkout.status === "help_required" &&
333
- checkout.required_input === undefined &&
334
- handledMessageRevision !== checkout.revision
335
- ) {
336
- if (typeof options.message !== "function") return checkout;
337
- handledMessageRevision = checkout.revision;
338
- const content = await options.message(checkout);
339
- if (typeof content !== "string" || content.length === 0)
340
- return checkout;
341
- await this.sendMessage(checkout.id, content, {
342
- idempotencyKey: `message-${checkout.id}-${checkout.revision}`,
343
- });
344
- continue;
345
- }
346
- if (!TERMINAL_STATUSES.has(checkout.status)) {
347
- const pollIntervalMs =
348
- options.pollIntervalMs ??
349
- (checkout.status === "help_required" ||
350
- checkout.status === "approval_required"
351
- ? 5_000
352
- : 2_000);
353
- await delay(pollIntervalMs);
615
+ if (update.kind === "unavailable") {
616
+ const pollIntervalMs =
617
+ options.pollIntervalMs ??
618
+ (checkout.status === "help_required" ||
619
+ checkout.status === "approval_required"
620
+ ? 5_000
621
+ : 2_000);
622
+ await delay(Math.min(pollIntervalMs, remainingMs));
623
+ }
624
+ checkout =
625
+ update.kind === "checkout"
626
+ ? update.checkout
627
+ : await this.getCheckout(checkout.id);
354
628
  }
629
+ } finally {
630
+ watcher.close();
355
631
  }
356
- return checkout;
357
632
  }
358
633
  }