@usethrottle/cart 3.7.0 → 3.9.0

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/README.md CHANGED
@@ -88,15 +88,58 @@ await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 2 });
88
88
  await cart.applyDiscount('SAVE10');
89
89
  await cart.selectShipping({ methodId: 'fedex_ground', displayName: 'FedEx Ground', rateAmount: 599 });
90
90
 
91
+ // Set buyer-editable fields. `customerEmail` is what makes a guest cart
92
+ // visible to abandoned-cart recovery. `null` clears a field.
93
+ await cart.update({ customerEmail: 'buyer@example.com', notes: 'Leave at the door' });
94
+
91
95
  // Hand off to hosted/embedded checkout and redirect the buyer.
92
96
  const { checkoutUrl } = await cart.checkout({
93
97
  returnUrl: 'https://shop.example.com/thanks',
94
98
  cancelUrl: 'https://shop.example.com/cart',
99
+ // Leave the session `active` instead of `converted`, so a buyer who backs
100
+ // out of checkout returns to a live cart instead of an expired one.
101
+ keepOpen: true,
95
102
  });
96
103
  ```
97
104
 
105
+ `create()` and `get()` also return a `branding` block (`CartBranding`:
106
+ `logoUrl`, `primaryColor`, `merchantName`, each nullable) so a storefront
107
+ rendering its own cart UI doesn't need a separate call to brand it.
108
+
109
+ To bind to an existing session without a publishable token — e.g. Throttle's
110
+ own hosted cart page, or a storefront restoring a saved session id — use
111
+ `CartSessionClient.forSession()` instead of `new CartSessionClient()`:
112
+
113
+ ```ts
114
+ const cart = CartSessionClient.forSession(savedCartSessionId);
115
+ await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 1 });
116
+ ```
117
+
98
118
  The opaque `cart_…` session id is scoped to exactly one cart. See the
99
- [cart sessions guide](https://docs.usethrottle.dev/developers/cart-sessions).
119
+ [cart sessions guide](https://usethrottle.dev/docs/developers/cart-sessions).
120
+
121
+ ### Hosted cart page
122
+
123
+ Skip building cart UI entirely and send the buyer to Throttle's own hosted
124
+ cart page with `hostedCartUrl()`:
125
+
126
+ ```ts
127
+ const session = await cart.create({
128
+ metadata: {
129
+ returnUrl: 'https://shop.example.com/thanks',
130
+ cancelUrl: 'https://shop.example.com/cart',
131
+ storefrontUrl: 'https://shop.example.com',
132
+ },
133
+ });
134
+
135
+ location.href = cart.hostedCartUrl(); // https://checkout.usethrottle.dev/cart/cart_...
136
+ ```
137
+
138
+ `returnUrl`, `cancelUrl`, and `storefrontUrl` in `metadata` are all optional.
139
+ Omitting `storefrontUrl` just removes the hosted page's "Continue shopping"
140
+ button — checkout still works. Omitting `returnUrl` or `cancelUrl` falls back
141
+ to Throttle's own `/complete` page and this same hosted cart page,
142
+ respectively.
100
143
 
101
144
  ## Net-N invoice terms
102
145
 
@@ -179,7 +222,7 @@ await client.shippingTax.pushExternalSnapshot({
179
222
 
180
223
  ## Subscriptions
181
224
 
182
- Pass `type: 'subscription'` and a `recurring` block on the line item:
225
+ Mark a line item with `type: 'subscription'`:
183
226
 
184
227
  ```ts
185
228
  await client.items.add(cart.id, {
@@ -187,12 +230,15 @@ await client.items.add(cart.id, {
187
230
  name: 'Pro Plan',
188
231
  unitPrice: 1900,
189
232
  quantity: 1,
190
- recurring: { interval: 'month', count: null }, // count: null = until cancelled
191
233
  });
192
234
  ```
193
235
 
194
- Then call `client.carts.checkout` as normal. Set `count` to a number to cap
195
- the billing cycles.
236
+ Then call `client.carts.checkout` as normal.
237
+
238
+ The billing schedule is **not** settable through this endpoint. The add-item
239
+ body is strict and has no `recurring` field, so sending one is rejected with
240
+ `Unsupported field(s): recurring` — the recurring plan is attached by the
241
+ subscription engine, not by the cart API.
196
242
 
197
243
  ## Events
198
244
 
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  CartNotFoundError: () => CartNotFoundError,
25
25
  CartNotOpenError: () => CartNotOpenError,
26
26
  CartSessionClient: () => CartSessionClient,
27
+ CartTimeoutError: () => CartTimeoutError,
27
28
  StorefrontQuoteClient: () => StorefrontQuoteClient,
28
29
  ThrottleApiError: () => ThrottleApiError,
29
30
  ThrottleError: () => import_errors2.ThrottleError,
@@ -53,6 +54,17 @@ var CartNotFoundError = class extends ThrottleApiError {
53
54
  this.name = "CartNotFoundError";
54
55
  }
55
56
  };
57
+ var CartTimeoutError = class extends import_errors.ThrottleError {
58
+ constructor(args) {
59
+ super({
60
+ code: "request_timeout",
61
+ statusCode: 408,
62
+ message: `Request timed out after ${args.timeoutMs}ms`,
63
+ details: { timeoutMs: args.timeoutMs, method: args.method, path: args.path }
64
+ });
65
+ this.name = "CartTimeoutError";
66
+ }
67
+ };
56
68
  function cartErrorFromResponse(args) {
57
69
  if (args.statusCode === 409 && args.code === "cart_not_open") {
58
70
  return new CartNotOpenError({ message: args.message, statusCode: args.statusCode, details: args.details });
@@ -76,7 +88,7 @@ var CartClient = class {
76
88
  if (!opts.apiKey) throw new Error("CartClient: apiKey is required");
77
89
  this.apiKey = opts.apiKey;
78
90
  this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
79
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
91
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
80
92
  this.timeoutMs = opts.timeoutMs ?? 3e4;
81
93
  }
82
94
  async request(method, path, body) {
@@ -171,13 +183,13 @@ var StorefrontQuoteClient = class {
171
183
  origin;
172
184
  constructor(opts) {
173
185
  if (!opts.applicationId) throw new Error("StorefrontQuoteClient: applicationId is required");
174
- if (!opts.quoteToken?.startsWith("pk_")) {
175
- throw new Error("StorefrontQuoteClient: quoteToken must be a publishable pk_ token");
186
+ if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
187
+ throw new Error("StorefrontQuoteClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
176
188
  }
177
189
  this.applicationId = opts.applicationId;
178
190
  this.quoteToken = opts.quoteToken;
179
191
  this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
180
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
192
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
181
193
  this.timeoutMs = opts.timeoutMs ?? 15e3;
182
194
  this.origin = opts.origin;
183
195
  }
@@ -214,7 +226,7 @@ var StorefrontQuoteClient = class {
214
226
  };
215
227
 
216
228
  // src/cart-session.ts
217
- var CartSessionClient = class {
229
+ var CartSessionClient = class _CartSessionClient {
218
230
  applicationId;
219
231
  environmentId;
220
232
  quoteToken;
@@ -223,17 +235,18 @@ var CartSessionClient = class {
223
235
  timeoutMs;
224
236
  origin;
225
237
  sessionId = null;
238
+ isForSessionClient = false;
226
239
  constructor(opts) {
227
240
  if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
228
241
  if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
229
- if (!opts.quoteToken?.startsWith("pk_")) {
230
- throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
242
+ if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
243
+ throw new Error("CartSessionClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
231
244
  }
232
245
  this.applicationId = opts.applicationId;
233
246
  this.environmentId = opts.environmentId;
234
247
  this.quoteToken = opts.quoteToken;
235
248
  this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
236
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
249
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
237
250
  this.timeoutMs = opts.timeoutMs ?? 15e3;
238
251
  this.origin = opts.origin;
239
252
  }
@@ -248,6 +261,11 @@ var CartSessionClient = class {
248
261
  }
249
262
  /** Create a new cart session and bind this client to it. */
250
263
  async create(input = {}) {
264
+ if (this.isForSessionClient) {
265
+ throw new Error(
266
+ "CartSessionClient: this client was created with forSession() and cannot call create(). Construct a CartSessionClient with a real pk_ publishable token instead."
267
+ );
268
+ }
251
269
  const data = await this.request("POST", "/api/v1/cart-sessions", {
252
270
  applicationId: this.applicationId,
253
271
  environmentId: this.environmentId,
@@ -255,17 +273,38 @@ var CartSessionClient = class {
255
273
  ...input
256
274
  });
257
275
  this.sessionId = data.cartSessionId;
258
- return data;
276
+ return {
277
+ ...data,
278
+ branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
279
+ };
259
280
  }
260
281
  /** Read the current session + cart snapshot. */
261
- get() {
262
- return this.request("GET", this.basePath());
282
+ async get() {
283
+ const data = await this.request("GET", this.basePath());
284
+ return {
285
+ ...data,
286
+ branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
287
+ };
288
+ }
289
+ /**
290
+ * Unit count + total for a cart badge, without pulling the whole cart.
291
+ *
292
+ * Works on a client built with {@link CartSessionClient.forSession} — a
293
+ * storefront that restored a `cart_…` id from `localStorage` and holds no
294
+ * publishable token in the browser.
295
+ */
296
+ async summary() {
297
+ return this.request("GET", `${this.basePath()}/summary`);
263
298
  }
264
299
  async addItem(input) {
265
300
  return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
266
301
  }
267
302
  async updateItem(itemId, input) {
268
- return (await this.request("PATCH", `${this.basePath()}/items/${encodeURIComponent(itemId)}`, input)).cart;
303
+ return (await this.request(
304
+ "PATCH",
305
+ `${this.basePath()}/items/${encodeURIComponent(itemId)}`,
306
+ input
307
+ )).cart;
269
308
  }
270
309
  async removeItem(itemId) {
271
310
  return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
@@ -282,6 +321,38 @@ var CartSessionClient = class {
282
321
  async removeDiscount() {
283
322
  return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
284
323
  }
324
+ /** Set buyer-editable cart fields. Pass `null` to clear one. */
325
+ async update(input) {
326
+ return (await this.request("PATCH", this.basePath(), input)).cart;
327
+ }
328
+ /**
329
+ * Bind to an existing session without a publishable token.
330
+ *
331
+ * `create()` needs a `pk_` token; every other route is authorized by the
332
+ * opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
333
+ * hosted cart page, or a storefront restoring from localStorage — would
334
+ * otherwise have to invent a token to satisfy the constructor.
335
+ */
336
+ static forSession(cartSessionId, opts = {}) {
337
+ const client = new _CartSessionClient({
338
+ applicationId: "session",
339
+ environmentId: "session",
340
+ quoteToken: "pk_session",
341
+ ...opts
342
+ });
343
+ client.isForSessionClient = true;
344
+ return client.resume(cartSessionId);
345
+ }
346
+ /**
347
+ * The Throttle-hosted cart page for this session. Send a buyer here to let
348
+ * Throttle run the cart UI instead of building one.
349
+ */
350
+ hostedCartUrl(baseUrl = "https://checkout.usethrottle.dev") {
351
+ if (!this.sessionId) {
352
+ throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
353
+ }
354
+ return `${baseUrl.replace(/\/+$/, "")}/cart/${encodeURIComponent(this.sessionId)}`;
355
+ }
285
356
  /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
286
357
  checkout(input) {
287
358
  return this.request("POST", `${this.basePath()}/checkout-session`, input);
@@ -294,7 +365,11 @@ var CartSessionClient = class {
294
365
  }
295
366
  async request(method, path, body) {
296
367
  const ctrl = new AbortController();
297
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
368
+ let timedOut = false;
369
+ const timer = setTimeout(() => {
370
+ timedOut = true;
371
+ ctrl.abort();
372
+ }, this.timeoutMs);
298
373
  try {
299
374
  const headers = { "content-type": "application/json" };
300
375
  if (this.origin) headers.origin = this.origin;
@@ -314,6 +389,9 @@ var CartSessionClient = class {
314
389
  });
315
390
  }
316
391
  return json.data;
392
+ } catch (err) {
393
+ if (timedOut) throw new CartTimeoutError({ timeoutMs: this.timeoutMs, method, path });
394
+ throw err;
317
395
  } finally {
318
396
  clearTimeout(timer);
319
397
  }
@@ -325,6 +403,7 @@ var CartSessionClient = class {
325
403
  CartNotFoundError,
326
404
  CartNotOpenError,
327
405
  CartSessionClient,
406
+ CartTimeoutError,
328
407
  StorefrontQuoteClient,
329
408
  ThrottleApiError,
330
409
  ThrottleError,
package/dist/index.d.cts CHANGED
@@ -11,6 +11,8 @@ export { ThrottleError, isThrottleError } from '@usethrottle/errors';
11
11
  interface CartAddress {
12
12
  firstName?: string;
13
13
  lastName?: string;
14
+ /** Business name. Optional — B2B addresses use it, B2C addresses omit it. */
15
+ company?: string;
14
16
  addressLine1: string;
15
17
  addressLine2?: string;
16
18
  city: string;
@@ -22,8 +24,14 @@ interface CartAddress {
22
24
  email?: string;
23
25
  }
24
26
  interface Cart {
27
+ /**
28
+ * `getById`/`create`/etc. (the `carts` resource) return this shape. See
29
+ * {@link CartSessionCart} for the browser-facing `CartSessionClient` shape,
30
+ * which omits `workspaceId` and adds `cartId`.
31
+ */
25
32
  id: string;
26
33
  workspaceId: string;
34
+ cartId?: string;
27
35
  applicationId: string;
28
36
  customerId: string | null;
29
37
  /** Lightweight captured email (no full customer record), or null. */
@@ -39,6 +47,7 @@ interface Cart {
39
47
  billingAddress: CartAddress | null;
40
48
  shippingAddress: CartAddress | null;
41
49
  notes: string | null;
50
+ metadata: Record<string, unknown>;
42
51
  expiresAt: string | null;
43
52
  externalRef?: {
44
53
  connectionId: string;
@@ -52,6 +61,24 @@ interface Cart {
52
61
  createdAt: string;
53
62
  updatedAt: string;
54
63
  }
64
+ /**
65
+ * The cart as the session (browser, opaque-token) surface returns it —
66
+ * `CartSessionClient`'s methods. There is no `workspaceId` on this surface
67
+ * (the browser never needs, and shouldn't see, the internal workspace id);
68
+ * `cartId` — the same value as `id` — is always present, kept for backward
69
+ * compatibility with `@usethrottle/cart`'s original (order-snapshot-shaped)
70
+ * cart-session response.
71
+ */
72
+ interface CartSessionCart extends Omit<Cart, 'workspaceId'> {
73
+ cartId: string;
74
+ /**
75
+ * Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
76
+ * `4`, not `3`. Present only on this surface; the merchant `carts` resource
77
+ * does not return it. Named for what it counts because `itemCount` in the
78
+ * `cart.abandoned` webhook payload means the number of distinct lines.
79
+ */
80
+ unitCount: number;
81
+ }
55
82
  interface LineItem {
56
83
  id: string;
57
84
  cartId: string;
@@ -98,6 +125,12 @@ interface TaxLine {
98
125
  jurisdictionCode: string;
99
126
  jurisdictionName: string | null;
100
127
  taxType: 'sales' | 'vat' | 'gst' | 'pst' | 'hst' | 'service' | 'excise' | 'other';
128
+ /** The VAT code the engine returned, verbatim — AvaTax's `vatCode`. */
129
+ vatCode?: string | null;
130
+ /** The engine's own name for the tax ('Standard'); `taxType` is our slug for it. */
131
+ taxName?: string | null;
132
+ /** Engine rate classification: 'S' standard, 'R' reduced, 'Z' zero. */
133
+ rateTypeCode?: string | null;
101
134
  rate: number;
102
135
  amount: number;
103
136
  currency: string;
@@ -307,6 +340,20 @@ interface ShippingTaxCalculationResponse {
307
340
  taxTotal: number;
308
341
  total: number;
309
342
  };
343
+ /**
344
+ * Verbatim messaging the tax engine returned about this transaction. In
345
+ * several EU member states the engine's own wording is what makes the
346
+ * invoice compliant, so it is carried through rather than paraphrased.
347
+ */
348
+ providerMessages?: Array<{
349
+ summary: string;
350
+ details?: string;
351
+ /** The engine's identifier for the citation, e.g. 'Article138.1'. */
352
+ citationCode?: string;
353
+ refersTo?: string;
354
+ severity?: string;
355
+ source?: string;
356
+ }>;
310
357
  prompts: Array<{
311
358
  code: 'missing_info';
312
359
  field: string;
@@ -441,17 +488,63 @@ interface CreateCartSessionInput {
441
488
  netN?: number;
442
489
  metadata?: Record<string, unknown>;
443
490
  }
491
+ interface CartBranding {
492
+ logoUrl: string | null;
493
+ primaryColor: string | null;
494
+ merchantName: string | null;
495
+ }
496
+ /** Buyer-editable cart fields. `null` clears a value. */
497
+ interface UpdateCartSessionInput {
498
+ customerEmail?: string | null;
499
+ notes?: string | null;
500
+ }
444
501
  interface CartSession {
445
502
  cartSessionId: string;
446
503
  status: string;
447
504
  expiresAt: string;
448
- cart: Cart;
505
+ branding: CartBranding;
506
+ /**
507
+ * Whatever the storefront passed to `create()`. Write-once — unlike the copy
508
+ * on `cart.metadata`, which the merchant API can change — so this is the one
509
+ * to read the buyer's return/cancel/storefront URLs from.
510
+ */
511
+ metadata: Record<string, unknown>;
512
+ cart: CartSessionCart;
513
+ }
514
+ /**
515
+ * The badge-sized read: what a storefront header needs to draw a cart count,
516
+ * and nothing more. No line items, no addresses, no buyer email or notes.
517
+ */
518
+ interface CartSessionSummary {
519
+ cartSessionId: string;
520
+ /**
521
+ * `active` while the cart can still be edited. `expired` (past its TTL) or
522
+ * `converted` (handed off to checkout) once the session is finished — those
523
+ * come back as a normal 200 with a zeroed summary, not an error, so a badge
524
+ * can render a zero without a try/catch.
525
+ */
526
+ status: 'active' | 'expired' | 'converted';
527
+ /**
528
+ * Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
529
+ * `4`, not `3`. Named for what it counts because `itemCount` elsewhere in
530
+ * Throttle (the `cart.abandoned` payload) means the number of distinct lines.
531
+ */
532
+ unitCount: number;
533
+ /** Cart total in minor units, or `0` on a finished session. */
534
+ total: number;
535
+ /** `null` on a finished session — there is no live cart to price. */
536
+ currency: string | null;
449
537
  }
450
538
  interface CheckoutHandoffInput {
451
539
  returnUrl: string;
452
540
  cancelUrl: string;
453
541
  customerEmail?: string;
454
542
  allowedMethods?: string[];
543
+ /**
544
+ * Leave the cart session open after handing off, so a buyer who backs out of
545
+ * checkout returns to a live cart. Throttle's own hosted cart page sets this.
546
+ */
547
+ keepOpen?: boolean;
455
548
  }
456
549
  interface CheckoutHandoff {
457
550
  cartSessionId: string;
@@ -468,6 +561,7 @@ declare class CartSessionClient {
468
561
  private readonly timeoutMs;
469
562
  private readonly origin?;
470
563
  private sessionId;
564
+ private isForSessionClient;
471
565
  constructor(opts: CartSessionClientOptions);
472
566
  /** The current bound session id (after `create()`/`resume()`), or null. */
473
567
  get cartSessionId(): string | null;
@@ -477,13 +571,42 @@ declare class CartSessionClient {
477
571
  create(input?: CreateCartSessionInput): Promise<CartSession>;
478
572
  /** Read the current session + cart snapshot. */
479
573
  get(): Promise<CartSession>;
480
- addItem(input: AddLineItemInput): Promise<Cart>;
481
- updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
482
- removeItem(itemId: string): Promise<Cart>;
483
- selectShipping(input: SelectShippingInput): Promise<Cart>;
484
- clearShipping(): Promise<Cart>;
485
- applyDiscount(code: string): Promise<Cart>;
486
- removeDiscount(): Promise<Cart>;
574
+ /**
575
+ * Unit count + total for a cart badge, without pulling the whole cart.
576
+ *
577
+ * Works on a client built with {@link CartSessionClient.forSession} — a
578
+ * storefront that restored a `cart_…` id from `localStorage` and holds no
579
+ * publishable token in the browser.
580
+ */
581
+ summary(): Promise<CartSessionSummary>;
582
+ addItem(input: AddLineItemInput): Promise<CartSessionCart>;
583
+ updateItem(itemId: string, input: UpdateLineItemInput): Promise<CartSessionCart>;
584
+ removeItem(itemId: string): Promise<CartSessionCart>;
585
+ selectShipping(input: SelectShippingInput): Promise<CartSessionCart>;
586
+ clearShipping(): Promise<CartSessionCart>;
587
+ applyDiscount(code: string): Promise<CartSessionCart>;
588
+ removeDiscount(): Promise<CartSessionCart>;
589
+ /** Set buyer-editable cart fields. Pass `null` to clear one. */
590
+ update(input: UpdateCartSessionInput): Promise<CartSessionCart>;
591
+ /**
592
+ * Bind to an existing session without a publishable token.
593
+ *
594
+ * `create()` needs a `pk_` token; every other route is authorized by the
595
+ * opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
596
+ * hosted cart page, or a storefront restoring from localStorage — would
597
+ * otherwise have to invent a token to satisfy the constructor.
598
+ */
599
+ static forSession(cartSessionId: string, opts?: {
600
+ baseUrl?: string;
601
+ fetch?: typeof globalThis.fetch;
602
+ timeoutMs?: number;
603
+ origin?: string;
604
+ }): CartSessionClient;
605
+ /**
606
+ * The Throttle-hosted cart page for this session. Send a buyer here to let
607
+ * Throttle run the cart UI instead of building one.
608
+ */
609
+ hostedCartUrl(baseUrl?: string): string;
487
610
  /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
488
611
  checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
489
612
  private basePath;
@@ -534,6 +657,25 @@ declare class CartNotFoundError extends ThrottleApiError {
534
657
  details?: Record<string, unknown>;
535
658
  });
536
659
  }
660
+ /**
661
+ * Thrown when a request is aborted by the client's own timeout — the server
662
+ * never answered within `timeoutMs`.
663
+ *
664
+ * It exists because the alternative is worse than useless: aborting an
665
+ * AbortController without a reason produces a DOMException whose message is
666
+ * "signal is aborted without reason", and that string was reaching shoppers as
667
+ * a page-level alert on the hosted cart.
668
+ *
669
+ * `statusCode` is 408 (Request Timeout) so the error keeps the same shape as
670
+ * every other Cart SDK error, even though no server sent it.
671
+ */
672
+ declare class CartTimeoutError extends ThrottleError {
673
+ constructor(args: {
674
+ timeoutMs: number;
675
+ method?: string;
676
+ path?: string;
677
+ });
678
+ }
537
679
  /**
538
680
  * Map a raw API error envelope to the most specific Cart SDK error class.
539
681
  * Falls back to {@link ThrottleApiError} for everything unrecognised.
@@ -545,4 +687,4 @@ declare function cartErrorFromResponse(args: {
545
687
  details?: Record<string, unknown>;
546
688
  }): ThrottleApiError;
547
689
 
548
- export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, type CartSession, CartSessionClient, type CartSessionClientOptions, type CheckoutHandoff, type CheckoutHandoffInput, type CheckoutInput, type CreateCartInput, type CreateCartSessionInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateLineItemInput, cartErrorFromResponse };
690
+ export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, type CartBranding, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, type CartSession, type CartSessionCart, CartSessionClient, type CartSessionClientOptions, type CartSessionSummary, CartTimeoutError, type CheckoutHandoff, type CheckoutHandoffInput, type CheckoutInput, type CreateCartInput, type CreateCartSessionInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateCartSessionInput, type UpdateLineItemInput, cartErrorFromResponse };
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export { ThrottleError, isThrottleError } from '@usethrottle/errors';
11
11
  interface CartAddress {
12
12
  firstName?: string;
13
13
  lastName?: string;
14
+ /** Business name. Optional — B2B addresses use it, B2C addresses omit it. */
15
+ company?: string;
14
16
  addressLine1: string;
15
17
  addressLine2?: string;
16
18
  city: string;
@@ -22,8 +24,14 @@ interface CartAddress {
22
24
  email?: string;
23
25
  }
24
26
  interface Cart {
27
+ /**
28
+ * `getById`/`create`/etc. (the `carts` resource) return this shape. See
29
+ * {@link CartSessionCart} for the browser-facing `CartSessionClient` shape,
30
+ * which omits `workspaceId` and adds `cartId`.
31
+ */
25
32
  id: string;
26
33
  workspaceId: string;
34
+ cartId?: string;
27
35
  applicationId: string;
28
36
  customerId: string | null;
29
37
  /** Lightweight captured email (no full customer record), or null. */
@@ -39,6 +47,7 @@ interface Cart {
39
47
  billingAddress: CartAddress | null;
40
48
  shippingAddress: CartAddress | null;
41
49
  notes: string | null;
50
+ metadata: Record<string, unknown>;
42
51
  expiresAt: string | null;
43
52
  externalRef?: {
44
53
  connectionId: string;
@@ -52,6 +61,24 @@ interface Cart {
52
61
  createdAt: string;
53
62
  updatedAt: string;
54
63
  }
64
+ /**
65
+ * The cart as the session (browser, opaque-token) surface returns it —
66
+ * `CartSessionClient`'s methods. There is no `workspaceId` on this surface
67
+ * (the browser never needs, and shouldn't see, the internal workspace id);
68
+ * `cartId` — the same value as `id` — is always present, kept for backward
69
+ * compatibility with `@usethrottle/cart`'s original (order-snapshot-shaped)
70
+ * cart-session response.
71
+ */
72
+ interface CartSessionCart extends Omit<Cart, 'workspaceId'> {
73
+ cartId: string;
74
+ /**
75
+ * Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
76
+ * `4`, not `3`. Present only on this surface; the merchant `carts` resource
77
+ * does not return it. Named for what it counts because `itemCount` in the
78
+ * `cart.abandoned` webhook payload means the number of distinct lines.
79
+ */
80
+ unitCount: number;
81
+ }
55
82
  interface LineItem {
56
83
  id: string;
57
84
  cartId: string;
@@ -98,6 +125,12 @@ interface TaxLine {
98
125
  jurisdictionCode: string;
99
126
  jurisdictionName: string | null;
100
127
  taxType: 'sales' | 'vat' | 'gst' | 'pst' | 'hst' | 'service' | 'excise' | 'other';
128
+ /** The VAT code the engine returned, verbatim — AvaTax's `vatCode`. */
129
+ vatCode?: string | null;
130
+ /** The engine's own name for the tax ('Standard'); `taxType` is our slug for it. */
131
+ taxName?: string | null;
132
+ /** Engine rate classification: 'S' standard, 'R' reduced, 'Z' zero. */
133
+ rateTypeCode?: string | null;
101
134
  rate: number;
102
135
  amount: number;
103
136
  currency: string;
@@ -307,6 +340,20 @@ interface ShippingTaxCalculationResponse {
307
340
  taxTotal: number;
308
341
  total: number;
309
342
  };
343
+ /**
344
+ * Verbatim messaging the tax engine returned about this transaction. In
345
+ * several EU member states the engine's own wording is what makes the
346
+ * invoice compliant, so it is carried through rather than paraphrased.
347
+ */
348
+ providerMessages?: Array<{
349
+ summary: string;
350
+ details?: string;
351
+ /** The engine's identifier for the citation, e.g. 'Article138.1'. */
352
+ citationCode?: string;
353
+ refersTo?: string;
354
+ severity?: string;
355
+ source?: string;
356
+ }>;
310
357
  prompts: Array<{
311
358
  code: 'missing_info';
312
359
  field: string;
@@ -441,17 +488,63 @@ interface CreateCartSessionInput {
441
488
  netN?: number;
442
489
  metadata?: Record<string, unknown>;
443
490
  }
491
+ interface CartBranding {
492
+ logoUrl: string | null;
493
+ primaryColor: string | null;
494
+ merchantName: string | null;
495
+ }
496
+ /** Buyer-editable cart fields. `null` clears a value. */
497
+ interface UpdateCartSessionInput {
498
+ customerEmail?: string | null;
499
+ notes?: string | null;
500
+ }
444
501
  interface CartSession {
445
502
  cartSessionId: string;
446
503
  status: string;
447
504
  expiresAt: string;
448
- cart: Cart;
505
+ branding: CartBranding;
506
+ /**
507
+ * Whatever the storefront passed to `create()`. Write-once — unlike the copy
508
+ * on `cart.metadata`, which the merchant API can change — so this is the one
509
+ * to read the buyer's return/cancel/storefront URLs from.
510
+ */
511
+ metadata: Record<string, unknown>;
512
+ cart: CartSessionCart;
513
+ }
514
+ /**
515
+ * The badge-sized read: what a storefront header needs to draw a cart count,
516
+ * and nothing more. No line items, no addresses, no buyer email or notes.
517
+ */
518
+ interface CartSessionSummary {
519
+ cartSessionId: string;
520
+ /**
521
+ * `active` while the cart can still be edited. `expired` (past its TTL) or
522
+ * `converted` (handed off to checkout) once the session is finished — those
523
+ * come back as a normal 200 with a zeroed summary, not an error, so a badge
524
+ * can render a zero without a try/catch.
525
+ */
526
+ status: 'active' | 'expired' | 'converted';
527
+ /**
528
+ * Sum of the line-item quantities — 1 stand + 2 notebooks + 1 keyboard is
529
+ * `4`, not `3`. Named for what it counts because `itemCount` elsewhere in
530
+ * Throttle (the `cart.abandoned` payload) means the number of distinct lines.
531
+ */
532
+ unitCount: number;
533
+ /** Cart total in minor units, or `0` on a finished session. */
534
+ total: number;
535
+ /** `null` on a finished session — there is no live cart to price. */
536
+ currency: string | null;
449
537
  }
450
538
  interface CheckoutHandoffInput {
451
539
  returnUrl: string;
452
540
  cancelUrl: string;
453
541
  customerEmail?: string;
454
542
  allowedMethods?: string[];
543
+ /**
544
+ * Leave the cart session open after handing off, so a buyer who backs out of
545
+ * checkout returns to a live cart. Throttle's own hosted cart page sets this.
546
+ */
547
+ keepOpen?: boolean;
455
548
  }
456
549
  interface CheckoutHandoff {
457
550
  cartSessionId: string;
@@ -468,6 +561,7 @@ declare class CartSessionClient {
468
561
  private readonly timeoutMs;
469
562
  private readonly origin?;
470
563
  private sessionId;
564
+ private isForSessionClient;
471
565
  constructor(opts: CartSessionClientOptions);
472
566
  /** The current bound session id (after `create()`/`resume()`), or null. */
473
567
  get cartSessionId(): string | null;
@@ -477,13 +571,42 @@ declare class CartSessionClient {
477
571
  create(input?: CreateCartSessionInput): Promise<CartSession>;
478
572
  /** Read the current session + cart snapshot. */
479
573
  get(): Promise<CartSession>;
480
- addItem(input: AddLineItemInput): Promise<Cart>;
481
- updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
482
- removeItem(itemId: string): Promise<Cart>;
483
- selectShipping(input: SelectShippingInput): Promise<Cart>;
484
- clearShipping(): Promise<Cart>;
485
- applyDiscount(code: string): Promise<Cart>;
486
- removeDiscount(): Promise<Cart>;
574
+ /**
575
+ * Unit count + total for a cart badge, without pulling the whole cart.
576
+ *
577
+ * Works on a client built with {@link CartSessionClient.forSession} — a
578
+ * storefront that restored a `cart_…` id from `localStorage` and holds no
579
+ * publishable token in the browser.
580
+ */
581
+ summary(): Promise<CartSessionSummary>;
582
+ addItem(input: AddLineItemInput): Promise<CartSessionCart>;
583
+ updateItem(itemId: string, input: UpdateLineItemInput): Promise<CartSessionCart>;
584
+ removeItem(itemId: string): Promise<CartSessionCart>;
585
+ selectShipping(input: SelectShippingInput): Promise<CartSessionCart>;
586
+ clearShipping(): Promise<CartSessionCart>;
587
+ applyDiscount(code: string): Promise<CartSessionCart>;
588
+ removeDiscount(): Promise<CartSessionCart>;
589
+ /** Set buyer-editable cart fields. Pass `null` to clear one. */
590
+ update(input: UpdateCartSessionInput): Promise<CartSessionCart>;
591
+ /**
592
+ * Bind to an existing session without a publishable token.
593
+ *
594
+ * `create()` needs a `pk_` token; every other route is authorized by the
595
+ * opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
596
+ * hosted cart page, or a storefront restoring from localStorage — would
597
+ * otherwise have to invent a token to satisfy the constructor.
598
+ */
599
+ static forSession(cartSessionId: string, opts?: {
600
+ baseUrl?: string;
601
+ fetch?: typeof globalThis.fetch;
602
+ timeoutMs?: number;
603
+ origin?: string;
604
+ }): CartSessionClient;
605
+ /**
606
+ * The Throttle-hosted cart page for this session. Send a buyer here to let
607
+ * Throttle run the cart UI instead of building one.
608
+ */
609
+ hostedCartUrl(baseUrl?: string): string;
487
610
  /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
488
611
  checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
489
612
  private basePath;
@@ -534,6 +657,25 @@ declare class CartNotFoundError extends ThrottleApiError {
534
657
  details?: Record<string, unknown>;
535
658
  });
536
659
  }
660
+ /**
661
+ * Thrown when a request is aborted by the client's own timeout — the server
662
+ * never answered within `timeoutMs`.
663
+ *
664
+ * It exists because the alternative is worse than useless: aborting an
665
+ * AbortController without a reason produces a DOMException whose message is
666
+ * "signal is aborted without reason", and that string was reaching shoppers as
667
+ * a page-level alert on the hosted cart.
668
+ *
669
+ * `statusCode` is 408 (Request Timeout) so the error keeps the same shape as
670
+ * every other Cart SDK error, even though no server sent it.
671
+ */
672
+ declare class CartTimeoutError extends ThrottleError {
673
+ constructor(args: {
674
+ timeoutMs: number;
675
+ method?: string;
676
+ path?: string;
677
+ });
678
+ }
537
679
  /**
538
680
  * Map a raw API error envelope to the most specific Cart SDK error class.
539
681
  * Falls back to {@link ThrottleApiError} for everything unrecognised.
@@ -545,4 +687,4 @@ declare function cartErrorFromResponse(args: {
545
687
  details?: Record<string, unknown>;
546
688
  }): ThrottleApiError;
547
689
 
548
- export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, type CartSession, CartSessionClient, type CartSessionClientOptions, type CheckoutHandoff, type CheckoutHandoffInput, type CheckoutInput, type CreateCartInput, type CreateCartSessionInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateLineItemInput, cartErrorFromResponse };
690
+ export { type AddLineItemInput, type AppliedDiscount, type Cart, type CartAddress, type CartBranding, CartClient, type CartEvent, CartNotFoundError, CartNotOpenError, type CartSession, type CartSessionCart, CartSessionClient, type CartSessionClientOptions, type CartSessionSummary, CartTimeoutError, type CheckoutHandoff, type CheckoutHandoffInput, type CheckoutInput, type CreateCartInput, type CreateCartSessionInput, type ExternalShippingTaxSnapshotInput, type LineItem, type Order, type SelectShippingInput, type SelectedShipping, type ShippingTaxAddress, type ShippingTaxCalculationResponse, type ShippingTaxCartCalculateInput, type ShippingTaxMethod, type ShippingTaxQuoteInput, type ShippingTaxQuoteItem, StorefrontQuoteClient, type TaxLine, type TaxLineInput, ThrottleApiError, type UpdateCartInput, type UpdateCartSessionInput, type UpdateLineItemInput, cartErrorFromResponse };
package/dist/index.js CHANGED
@@ -19,6 +19,17 @@ var CartNotFoundError = class extends ThrottleApiError {
19
19
  this.name = "CartNotFoundError";
20
20
  }
21
21
  };
22
+ var CartTimeoutError = class extends ThrottleError {
23
+ constructor(args) {
24
+ super({
25
+ code: "request_timeout",
26
+ statusCode: 408,
27
+ message: `Request timed out after ${args.timeoutMs}ms`,
28
+ details: { timeoutMs: args.timeoutMs, method: args.method, path: args.path }
29
+ });
30
+ this.name = "CartTimeoutError";
31
+ }
32
+ };
22
33
  function cartErrorFromResponse(args) {
23
34
  if (args.statusCode === 409 && args.code === "cart_not_open") {
24
35
  return new CartNotOpenError({ message: args.message, statusCode: args.statusCode, details: args.details });
@@ -42,7 +53,7 @@ var CartClient = class {
42
53
  if (!opts.apiKey) throw new Error("CartClient: apiKey is required");
43
54
  this.apiKey = opts.apiKey;
44
55
  this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
45
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
56
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
46
57
  this.timeoutMs = opts.timeoutMs ?? 3e4;
47
58
  }
48
59
  async request(method, path, body) {
@@ -137,13 +148,13 @@ var StorefrontQuoteClient = class {
137
148
  origin;
138
149
  constructor(opts) {
139
150
  if (!opts.applicationId) throw new Error("StorefrontQuoteClient: applicationId is required");
140
- if (!opts.quoteToken?.startsWith("pk_")) {
141
- throw new Error("StorefrontQuoteClient: quoteToken must be a publishable pk_ token");
151
+ if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
152
+ throw new Error("StorefrontQuoteClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
142
153
  }
143
154
  this.applicationId = opts.applicationId;
144
155
  this.quoteToken = opts.quoteToken;
145
156
  this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
146
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
157
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
147
158
  this.timeoutMs = opts.timeoutMs ?? 15e3;
148
159
  this.origin = opts.origin;
149
160
  }
@@ -180,7 +191,7 @@ var StorefrontQuoteClient = class {
180
191
  };
181
192
 
182
193
  // src/cart-session.ts
183
- var CartSessionClient = class {
194
+ var CartSessionClient = class _CartSessionClient {
184
195
  applicationId;
185
196
  environmentId;
186
197
  quoteToken;
@@ -189,17 +200,18 @@ var CartSessionClient = class {
189
200
  timeoutMs;
190
201
  origin;
191
202
  sessionId = null;
203
+ isForSessionClient = false;
192
204
  constructor(opts) {
193
205
  if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
194
206
  if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
195
- if (!opts.quoteToken?.startsWith("pk_")) {
196
- throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
207
+ if (!/^(pk|qt)_/.test(opts.quoteToken ?? "")) {
208
+ throw new Error("CartSessionClient: quoteToken must be a publishable qt_ (or legacy pk_) token");
197
209
  }
198
210
  this.applicationId = opts.applicationId;
199
211
  this.environmentId = opts.environmentId;
200
212
  this.quoteToken = opts.quoteToken;
201
213
  this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
202
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
214
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
203
215
  this.timeoutMs = opts.timeoutMs ?? 15e3;
204
216
  this.origin = opts.origin;
205
217
  }
@@ -214,6 +226,11 @@ var CartSessionClient = class {
214
226
  }
215
227
  /** Create a new cart session and bind this client to it. */
216
228
  async create(input = {}) {
229
+ if (this.isForSessionClient) {
230
+ throw new Error(
231
+ "CartSessionClient: this client was created with forSession() and cannot call create(). Construct a CartSessionClient with a real pk_ publishable token instead."
232
+ );
233
+ }
217
234
  const data = await this.request("POST", "/api/v1/cart-sessions", {
218
235
  applicationId: this.applicationId,
219
236
  environmentId: this.environmentId,
@@ -221,17 +238,38 @@ var CartSessionClient = class {
221
238
  ...input
222
239
  });
223
240
  this.sessionId = data.cartSessionId;
224
- return data;
241
+ return {
242
+ ...data,
243
+ branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
244
+ };
225
245
  }
226
246
  /** Read the current session + cart snapshot. */
227
- get() {
228
- return this.request("GET", this.basePath());
247
+ async get() {
248
+ const data = await this.request("GET", this.basePath());
249
+ return {
250
+ ...data,
251
+ branding: data.branding ?? { logoUrl: null, primaryColor: null, merchantName: null }
252
+ };
253
+ }
254
+ /**
255
+ * Unit count + total for a cart badge, without pulling the whole cart.
256
+ *
257
+ * Works on a client built with {@link CartSessionClient.forSession} — a
258
+ * storefront that restored a `cart_…` id from `localStorage` and holds no
259
+ * publishable token in the browser.
260
+ */
261
+ async summary() {
262
+ return this.request("GET", `${this.basePath()}/summary`);
229
263
  }
230
264
  async addItem(input) {
231
265
  return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
232
266
  }
233
267
  async updateItem(itemId, input) {
234
- return (await this.request("PATCH", `${this.basePath()}/items/${encodeURIComponent(itemId)}`, input)).cart;
268
+ return (await this.request(
269
+ "PATCH",
270
+ `${this.basePath()}/items/${encodeURIComponent(itemId)}`,
271
+ input
272
+ )).cart;
235
273
  }
236
274
  async removeItem(itemId) {
237
275
  return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
@@ -248,6 +286,38 @@ var CartSessionClient = class {
248
286
  async removeDiscount() {
249
287
  return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
250
288
  }
289
+ /** Set buyer-editable cart fields. Pass `null` to clear one. */
290
+ async update(input) {
291
+ return (await this.request("PATCH", this.basePath(), input)).cart;
292
+ }
293
+ /**
294
+ * Bind to an existing session without a publishable token.
295
+ *
296
+ * `create()` needs a `pk_` token; every other route is authorized by the
297
+ * opaque `cart_…` id alone. A caller that only ever resumes — Throttle's own
298
+ * hosted cart page, or a storefront restoring from localStorage — would
299
+ * otherwise have to invent a token to satisfy the constructor.
300
+ */
301
+ static forSession(cartSessionId, opts = {}) {
302
+ const client = new _CartSessionClient({
303
+ applicationId: "session",
304
+ environmentId: "session",
305
+ quoteToken: "pk_session",
306
+ ...opts
307
+ });
308
+ client.isForSessionClient = true;
309
+ return client.resume(cartSessionId);
310
+ }
311
+ /**
312
+ * The Throttle-hosted cart page for this session. Send a buyer here to let
313
+ * Throttle run the cart UI instead of building one.
314
+ */
315
+ hostedCartUrl(baseUrl = "https://checkout.usethrottle.dev") {
316
+ if (!this.sessionId) {
317
+ throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
318
+ }
319
+ return `${baseUrl.replace(/\/+$/, "")}/cart/${encodeURIComponent(this.sessionId)}`;
320
+ }
251
321
  /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
252
322
  checkout(input) {
253
323
  return this.request("POST", `${this.basePath()}/checkout-session`, input);
@@ -260,7 +330,11 @@ var CartSessionClient = class {
260
330
  }
261
331
  async request(method, path, body) {
262
332
  const ctrl = new AbortController();
263
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
333
+ let timedOut = false;
334
+ const timer = setTimeout(() => {
335
+ timedOut = true;
336
+ ctrl.abort();
337
+ }, this.timeoutMs);
264
338
  try {
265
339
  const headers = { "content-type": "application/json" };
266
340
  if (this.origin) headers.origin = this.origin;
@@ -280,6 +354,9 @@ var CartSessionClient = class {
280
354
  });
281
355
  }
282
356
  return json.data;
357
+ } catch (err) {
358
+ if (timedOut) throw new CartTimeoutError({ timeoutMs: this.timeoutMs, method, path });
359
+ throw err;
283
360
  } finally {
284
361
  clearTimeout(timer);
285
362
  }
@@ -290,6 +367,7 @@ export {
290
367
  CartNotFoundError,
291
368
  CartNotOpenError,
292
369
  CartSessionClient,
370
+ CartTimeoutError,
293
371
  StorefrontQuoteClient,
294
372
  ThrottleApiError,
295
373
  ThrottleError2 as ThrottleError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usethrottle/cart",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
4
  "description": "Typed REST client for the Throttle Cart API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -17,8 +17,13 @@
17
17
  "dist",
18
18
  "README.md"
19
19
  ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "typecheck": "tsc --noEmit"
24
+ },
20
25
  "dependencies": {
21
- "@usethrottle/errors": "^1.0.0"
26
+ "@usethrottle/errors": "workspace:^"
22
27
  },
23
28
  "devDependencies": {
24
29
  "tsup": "^8.0.0",
@@ -27,10 +32,5 @@
27
32
  },
28
33
  "publishConfig": {
29
34
  "access": "public"
30
- },
31
- "scripts": {
32
- "build": "tsup",
33
- "test": "vitest run",
34
- "typecheck": "tsc --noEmit"
35
35
  }
36
- }
36
+ }