@usethrottle/cart 3.2.0 → 3.4.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
@@ -2,7 +2,6 @@
2
2
  <img src="https://raw.githubusercontent.com/Epic-Design-Labs/app-throttle/main/packages/brand/assets/throttle-logo.png" alt="Throttle" height="56" />
3
3
  </p>
4
4
 
5
-
6
5
  # @usethrottle/cart
7
6
 
8
7
  Typed Node.js REST client for the Throttle Cart API.
@@ -62,6 +61,43 @@ are rejected by the API.
62
61
 
63
62
  All monetary values are integers in the smallest currency unit (cents for USD).
64
63
 
64
+ ## Browser carts (backend-less)
65
+
66
+ `CartClient` carries a secret `sk_` key and must run on your server. For
67
+ JAMstack / frontend-only storefronts that have no backend, use
68
+ `CartSessionClient` instead — it creates and owns a Throttle cart directly from
69
+ the browser, authorized by a publishable `pk_` quote token (the same token used
70
+ by `StorefrontQuoteClient`) plus your application's origin allowlist.
71
+
72
+ Set your allowed origins first (`PUT /api/v1/embed-config`), then:
73
+
74
+ ```ts
75
+ import { CartSessionClient } from '@usethrottle/cart';
76
+
77
+ const cart = new CartSessionClient({
78
+ applicationId: '7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40',
79
+ environmentId: 'a1b2c3d4-...',
80
+ quoteToken: 'pk_live_...',
81
+ });
82
+
83
+ // Create a new session (store cart.cartSessionId in localStorage),
84
+ // or resume one you saved: cart.resume(savedId)
85
+ await cart.create({ currency: 'USD' });
86
+
87
+ await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 2 });
88
+ await cart.applyDiscount('SAVE10');
89
+ await cart.selectShipping({ methodId: 'fedex_ground', displayName: 'FedEx Ground', rateAmount: 599 });
90
+
91
+ // Hand off to hosted/embedded checkout and redirect the buyer.
92
+ const { checkoutUrl } = await cart.checkout({
93
+ returnUrl: 'https://shop.example.com/thanks',
94
+ cancelUrl: 'https://shop.example.com/cart',
95
+ });
96
+ ```
97
+
98
+ The opaque `cart_…` session id is scoped to exactly one cart. See the
99
+ [cart sessions guide](https://docs.usethrottle.dev/developers/cart-sessions).
100
+
65
101
  ## Net-N invoice terms
66
102
 
67
103
  Cart-level invoice terms are optional. Pass `netN` on cart create or update to
@@ -194,14 +230,41 @@ try {
194
230
  }
195
231
  ```
196
232
 
233
+ `ThrottleApiError` extends the shared **`ThrottleError`** from
234
+ [`@usethrottle/errors`](https://www.npmjs.com/package/@usethrottle/errors),
235
+ re-exported here. If you use more than one Throttle SDK you can catch all of
236
+ their errors with a single check — `instanceof ThrottleApiError` keeps working:
237
+
238
+ ```ts
239
+ import { ThrottleError } from '@usethrottle/cart'; // same class across every SDK
240
+
241
+ try {
242
+ await cart.items.add(cartId, item);
243
+ await checkout.completeSession(sessionId, payment);
244
+ } catch (e) {
245
+ if (e instanceof ThrottleError) {
246
+ console.error(e.statusCode, e.code, e.message);
247
+ }
248
+ }
249
+ ```
250
+
251
+ ### Line item `imageUrl`
252
+
253
+ `imageUrl` accepts an absolute `http(s)` URL **or** a relative path like
254
+ `/images/x.png`. Relative paths are resolved to an absolute URL against your
255
+ application's storefront base URL so the image renders on the hosted checkout —
256
+ set it with `PUT /v1/embed-config { "storefrontBaseUrl": "https://yourstore.com" }`
257
+ (the first `allowedOrigin` is used as a fallback). A relative path with no
258
+ resolvable base returns `400 image_url_unresolvable`.
259
+
197
260
  ## Client options
198
261
 
199
- | Option | Default | Description |
200
- | ----------- | ----------------------------- | ----------------------------------------------------------- |
201
- | `apiKey` | — | **Required.** Your Throttle secret key (`sk_…`). |
202
- | `baseUrl` | `https://api.usethrottle.dev` | Optional override for a dedicated Throttle API environment. |
203
- | `timeoutMs` | `30000` | Per-request abort timeout in ms. |
204
- | `fetch` | `globalThis.fetch` | Custom fetch implementation (e.g. `node-fetch`). |
262
+ | Option | Default | Description |
263
+ | ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
264
+ | `apiKey` | — | **Required.** Your Throttle secret key (`sk_…`). |
265
+ | `baseUrl` | `https://api.usethrottle.dev` | Optional Throttle API host override for self-hosted/proxy setups. Workspace environment selection comes from the API key. |
266
+ | `timeoutMs` | `30000` | Per-request abort timeout in ms. |
267
+ | `fetch` | `globalThis.fetch` | Custom fetch implementation (e.g. `node-fetch`). |
205
268
 
206
269
  ## See also
207
270
 
package/dist/index.cjs CHANGED
@@ -21,22 +21,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  CartClient: () => CartClient,
24
+ CartSessionClient: () => CartSessionClient,
24
25
  StorefrontQuoteClient: () => StorefrontQuoteClient,
25
- ThrottleApiError: () => ThrottleApiError
26
+ ThrottleApiError: () => ThrottleApiError,
27
+ ThrottleError: () => import_errors2.ThrottleError,
28
+ isThrottleError: () => import_errors2.isThrottleError
26
29
  });
27
30
  module.exports = __toCommonJS(index_exports);
28
31
 
29
32
  // src/errors.ts
30
- var ThrottleApiError = class extends Error {
31
- code;
32
- statusCode;
33
- details;
33
+ var import_errors = require("@usethrottle/errors");
34
+ var import_errors2 = require("@usethrottle/errors");
35
+ var ThrottleApiError = class extends import_errors.ThrottleError {
34
36
  constructor(args) {
35
- super(args.message);
37
+ super(args);
36
38
  this.name = "ThrottleApiError";
37
- this.code = args.code;
38
- this.statusCode = args.statusCode;
39
- this.details = args.details;
40
39
  }
41
40
  };
42
41
 
@@ -189,9 +188,119 @@ var StorefrontQuoteClient = class {
189
188
  }
190
189
  }
191
190
  };
191
+
192
+ // src/cart-session.ts
193
+ var CartSessionClient = class {
194
+ applicationId;
195
+ environmentId;
196
+ quoteToken;
197
+ baseUrl;
198
+ fetchImpl;
199
+ timeoutMs;
200
+ origin;
201
+ sessionId = null;
202
+ constructor(opts) {
203
+ if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
204
+ if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
205
+ if (!opts.quoteToken?.startsWith("pk_")) {
206
+ throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
207
+ }
208
+ this.applicationId = opts.applicationId;
209
+ this.environmentId = opts.environmentId;
210
+ this.quoteToken = opts.quoteToken;
211
+ this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
212
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
213
+ this.timeoutMs = opts.timeoutMs ?? 15e3;
214
+ this.origin = opts.origin;
215
+ }
216
+ /** The current bound session id (after `create()`/`resume()`), or null. */
217
+ get cartSessionId() {
218
+ return this.sessionId;
219
+ }
220
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
221
+ resume(cartSessionId) {
222
+ this.sessionId = cartSessionId;
223
+ return this;
224
+ }
225
+ /** Create a new cart session and bind this client to it. */
226
+ async create(input = {}) {
227
+ const data = await this.request("POST", "/api/v1/cart-sessions", {
228
+ applicationId: this.applicationId,
229
+ environmentId: this.environmentId,
230
+ quoteToken: this.quoteToken,
231
+ ...input
232
+ });
233
+ this.sessionId = data.cartSessionId;
234
+ return data;
235
+ }
236
+ /** Read the current session + cart snapshot. */
237
+ get() {
238
+ return this.request("GET", this.basePath());
239
+ }
240
+ async addItem(input) {
241
+ return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
242
+ }
243
+ async updateItem(itemId, input) {
244
+ return (await this.request("PATCH", `${this.basePath()}/items/${encodeURIComponent(itemId)}`, input)).cart;
245
+ }
246
+ async removeItem(itemId) {
247
+ return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
248
+ }
249
+ async selectShipping(input) {
250
+ return (await this.request("POST", `${this.basePath()}/shipping`, input)).cart;
251
+ }
252
+ async clearShipping() {
253
+ return (await this.request("DELETE", `${this.basePath()}/shipping`)).cart;
254
+ }
255
+ async applyDiscount(code) {
256
+ return (await this.request("POST", `${this.basePath()}/discount`, { code })).cart;
257
+ }
258
+ async removeDiscount() {
259
+ return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
260
+ }
261
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
262
+ checkout(input) {
263
+ return this.request("POST", `${this.basePath()}/checkout-session`, input);
264
+ }
265
+ basePath() {
266
+ if (!this.sessionId) {
267
+ throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
268
+ }
269
+ return `/api/v1/cart-sessions/${encodeURIComponent(this.sessionId)}`;
270
+ }
271
+ async request(method, path, body) {
272
+ const ctrl = new AbortController();
273
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
274
+ try {
275
+ const headers = { "content-type": "application/json" };
276
+ if (this.origin) headers.origin = this.origin;
277
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
278
+ method,
279
+ headers,
280
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
281
+ signal: ctrl.signal
282
+ });
283
+ const json = await res.json().catch(() => ({}));
284
+ if (!res.ok) {
285
+ throw new ThrottleApiError({
286
+ code: json?.error?.code ?? "unknown_error",
287
+ message: json?.error?.message ?? `HTTP ${res.status}`,
288
+ statusCode: res.status,
289
+ details: json?.error?.details
290
+ });
291
+ }
292
+ return json.data;
293
+ } finally {
294
+ clearTimeout(timer);
295
+ }
296
+ }
297
+ };
192
298
  // Annotate the CommonJS export names for ESM import in node:
193
299
  0 && (module.exports = {
194
300
  CartClient,
301
+ CartSessionClient,
195
302
  StorefrontQuoteClient,
196
- ThrottleApiError
303
+ ThrottleApiError,
304
+ ThrottleError,
305
+ isThrottleError
197
306
  });
package/dist/index.d.cts CHANGED
@@ -1,3 +1,6 @@
1
+ import { ThrottleError } from '@usethrottle/errors';
2
+ export { ThrottleError, isThrottleError } from '@usethrottle/errors';
3
+
1
4
  interface Cart {
2
5
  id: string;
3
6
  workspaceId: string;
@@ -116,6 +119,13 @@ interface AddLineItemInput {
116
119
  count: number | null;
117
120
  } | null;
118
121
  imageUrl?: string;
122
+ /**
123
+ * Whether this item is physically shippable. For external/ad-hoc catalogs
124
+ * with no Throttle product behind the item. Omit to infer from `type`
125
+ * (everything except `service` ships); set explicitly to force-include or
126
+ * force-exclude the item from `calculated` shipping.
127
+ */
128
+ requiresShipping?: boolean;
119
129
  metadata?: Record<string, unknown>;
120
130
  }
121
131
  interface UpdateLineItemInput {
@@ -359,9 +369,101 @@ declare class StorefrontQuoteClient {
359
369
  quote(input: ShippingTaxQuoteInput): Promise<ShippingTaxCalculationResponse>;
360
370
  }
361
371
 
362
- declare class ThrottleApiError extends Error {
363
- readonly code: string;
364
- readonly statusCode: number;
372
+ /**
373
+ * Browser client for backend-less cart ownership. Unlike {@link CartClient}
374
+ * (which needs a server-side `sk_` key), `CartSessionClient` runs in the browser:
375
+ * it creates and mutates a Throttle cart authorized by a publishable quote token
376
+ * (`pk_…`, the same token used by {@link StorefrontQuoteClient}) plus the
377
+ * application origin allowlist, and an opaque `cart_…` session id scoped to that
378
+ * one cart.
379
+ *
380
+ * Typical use:
381
+ * ```ts
382
+ * const cart = new CartSessionClient({ applicationId, environmentId, quoteToken });
383
+ * await cart.create(); // or cart.resume(savedId)
384
+ * await cart.addItem({ name: 'Widget', unitPrice: 2999, quantity: 1 });
385
+ * const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
386
+ * ```
387
+ */
388
+ interface CartSessionClientOptions {
389
+ /** Application UUID. */
390
+ applicationId: string;
391
+ /** Workspace environment UUID the publishable token was minted for. */
392
+ environmentId: string;
393
+ /** Publishable storefront quote token (`pk_…`). */
394
+ quoteToken: string;
395
+ baseUrl?: string;
396
+ fetch?: typeof globalThis.fetch;
397
+ timeoutMs?: number;
398
+ /**
399
+ * Origin to send. Browsers set the `Origin` header automatically (and it can't
400
+ * be overridden from JS), so this is only needed for non-browser callers
401
+ * (SSR, tests). It must be on the application's allowlist.
402
+ */
403
+ origin?: string;
404
+ }
405
+ interface CreateCartSessionInput {
406
+ currency?: string;
407
+ netN?: number;
408
+ metadata?: Record<string, unknown>;
409
+ }
410
+ interface CartSession {
411
+ cartSessionId: string;
412
+ status: string;
413
+ expiresAt: string;
414
+ cart: Cart;
415
+ }
416
+ interface CheckoutHandoffInput {
417
+ returnUrl: string;
418
+ cancelUrl: string;
419
+ customerEmail?: string;
420
+ allowedMethods?: string[];
421
+ }
422
+ interface CheckoutHandoff {
423
+ cartSessionId: string;
424
+ checkoutSessionId: string;
425
+ checkoutUrl: string;
426
+ expiresAt: string;
427
+ }
428
+ declare class CartSessionClient {
429
+ private readonly applicationId;
430
+ private readonly environmentId;
431
+ private readonly quoteToken;
432
+ private readonly baseUrl;
433
+ private readonly fetchImpl;
434
+ private readonly timeoutMs;
435
+ private readonly origin?;
436
+ private sessionId;
437
+ constructor(opts: CartSessionClientOptions);
438
+ /** The current bound session id (after `create()`/`resume()`), or null. */
439
+ get cartSessionId(): string | null;
440
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
441
+ resume(cartSessionId: string): this;
442
+ /** Create a new cart session and bind this client to it. */
443
+ create(input?: CreateCartSessionInput): Promise<CartSession>;
444
+ /** Read the current session + cart snapshot. */
445
+ get(): Promise<CartSession>;
446
+ addItem(input: AddLineItemInput): Promise<Cart>;
447
+ updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
448
+ removeItem(itemId: string): Promise<Cart>;
449
+ selectShipping(input: SelectShippingInput): Promise<Cart>;
450
+ clearShipping(): Promise<Cart>;
451
+ applyDiscount(code: string): Promise<Cart>;
452
+ removeDiscount(): Promise<Cart>;
453
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
454
+ checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
455
+ private basePath;
456
+ private request;
457
+ }
458
+
459
+ /**
460
+ * Thrown on a non-2xx response from the Throttle Cart API.
461
+ *
462
+ * Extends the shared {@link ThrottleError} so a single
463
+ * `catch (e) { if (e instanceof ThrottleError) … }` covers errors from every
464
+ * Throttle SDK. `instanceof ThrottleApiError` keeps working for existing code.
465
+ */
466
+ declare class ThrottleApiError extends ThrottleError {
365
467
  readonly details?: Record<string, unknown>;
366
468
  constructor(args: {
367
469
  code: string;
@@ -371,4 +473,4 @@ declare class ThrottleApiError extends Error {
371
473
  });
372
474
  }
373
475
 
374
- export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, type CheckoutInput, type CreateCartInput, 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 };
476
+ export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, 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 };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { ThrottleError } from '@usethrottle/errors';
2
+ export { ThrottleError, isThrottleError } from '@usethrottle/errors';
3
+
1
4
  interface Cart {
2
5
  id: string;
3
6
  workspaceId: string;
@@ -116,6 +119,13 @@ interface AddLineItemInput {
116
119
  count: number | null;
117
120
  } | null;
118
121
  imageUrl?: string;
122
+ /**
123
+ * Whether this item is physically shippable. For external/ad-hoc catalogs
124
+ * with no Throttle product behind the item. Omit to infer from `type`
125
+ * (everything except `service` ships); set explicitly to force-include or
126
+ * force-exclude the item from `calculated` shipping.
127
+ */
128
+ requiresShipping?: boolean;
119
129
  metadata?: Record<string, unknown>;
120
130
  }
121
131
  interface UpdateLineItemInput {
@@ -359,9 +369,101 @@ declare class StorefrontQuoteClient {
359
369
  quote(input: ShippingTaxQuoteInput): Promise<ShippingTaxCalculationResponse>;
360
370
  }
361
371
 
362
- declare class ThrottleApiError extends Error {
363
- readonly code: string;
364
- readonly statusCode: number;
372
+ /**
373
+ * Browser client for backend-less cart ownership. Unlike {@link CartClient}
374
+ * (which needs a server-side `sk_` key), `CartSessionClient` runs in the browser:
375
+ * it creates and mutates a Throttle cart authorized by a publishable quote token
376
+ * (`pk_…`, the same token used by {@link StorefrontQuoteClient}) plus the
377
+ * application origin allowlist, and an opaque `cart_…` session id scoped to that
378
+ * one cart.
379
+ *
380
+ * Typical use:
381
+ * ```ts
382
+ * const cart = new CartSessionClient({ applicationId, environmentId, quoteToken });
383
+ * await cart.create(); // or cart.resume(savedId)
384
+ * await cart.addItem({ name: 'Widget', unitPrice: 2999, quantity: 1 });
385
+ * const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
386
+ * ```
387
+ */
388
+ interface CartSessionClientOptions {
389
+ /** Application UUID. */
390
+ applicationId: string;
391
+ /** Workspace environment UUID the publishable token was minted for. */
392
+ environmentId: string;
393
+ /** Publishable storefront quote token (`pk_…`). */
394
+ quoteToken: string;
395
+ baseUrl?: string;
396
+ fetch?: typeof globalThis.fetch;
397
+ timeoutMs?: number;
398
+ /**
399
+ * Origin to send. Browsers set the `Origin` header automatically (and it can't
400
+ * be overridden from JS), so this is only needed for non-browser callers
401
+ * (SSR, tests). It must be on the application's allowlist.
402
+ */
403
+ origin?: string;
404
+ }
405
+ interface CreateCartSessionInput {
406
+ currency?: string;
407
+ netN?: number;
408
+ metadata?: Record<string, unknown>;
409
+ }
410
+ interface CartSession {
411
+ cartSessionId: string;
412
+ status: string;
413
+ expiresAt: string;
414
+ cart: Cart;
415
+ }
416
+ interface CheckoutHandoffInput {
417
+ returnUrl: string;
418
+ cancelUrl: string;
419
+ customerEmail?: string;
420
+ allowedMethods?: string[];
421
+ }
422
+ interface CheckoutHandoff {
423
+ cartSessionId: string;
424
+ checkoutSessionId: string;
425
+ checkoutUrl: string;
426
+ expiresAt: string;
427
+ }
428
+ declare class CartSessionClient {
429
+ private readonly applicationId;
430
+ private readonly environmentId;
431
+ private readonly quoteToken;
432
+ private readonly baseUrl;
433
+ private readonly fetchImpl;
434
+ private readonly timeoutMs;
435
+ private readonly origin?;
436
+ private sessionId;
437
+ constructor(opts: CartSessionClientOptions);
438
+ /** The current bound session id (after `create()`/`resume()`), or null. */
439
+ get cartSessionId(): string | null;
440
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
441
+ resume(cartSessionId: string): this;
442
+ /** Create a new cart session and bind this client to it. */
443
+ create(input?: CreateCartSessionInput): Promise<CartSession>;
444
+ /** Read the current session + cart snapshot. */
445
+ get(): Promise<CartSession>;
446
+ addItem(input: AddLineItemInput): Promise<Cart>;
447
+ updateItem(itemId: string, input: UpdateLineItemInput): Promise<Cart>;
448
+ removeItem(itemId: string): Promise<Cart>;
449
+ selectShipping(input: SelectShippingInput): Promise<Cart>;
450
+ clearShipping(): Promise<Cart>;
451
+ applyDiscount(code: string): Promise<Cart>;
452
+ removeDiscount(): Promise<Cart>;
453
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
454
+ checkout(input: CheckoutHandoffInput): Promise<CheckoutHandoff>;
455
+ private basePath;
456
+ private request;
457
+ }
458
+
459
+ /**
460
+ * Thrown on a non-2xx response from the Throttle Cart API.
461
+ *
462
+ * Extends the shared {@link ThrottleError} so a single
463
+ * `catch (e) { if (e instanceof ThrottleError) … }` covers errors from every
464
+ * Throttle SDK. `instanceof ThrottleApiError` keeps working for existing code.
465
+ */
466
+ declare class ThrottleApiError extends ThrottleError {
365
467
  readonly details?: Record<string, unknown>;
366
468
  constructor(args: {
367
469
  code: string;
@@ -371,4 +473,4 @@ declare class ThrottleApiError extends Error {
371
473
  });
372
474
  }
373
475
 
374
- export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, type CheckoutInput, type CreateCartInput, 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 };
476
+ export { type AddLineItemInput, type AppliedDiscount, type Cart, CartClient, type CartEvent, 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 };
package/dist/index.js CHANGED
@@ -1,14 +1,10 @@
1
1
  // src/errors.ts
2
- var ThrottleApiError = class extends Error {
3
- code;
4
- statusCode;
5
- details;
2
+ import { ThrottleError } from "@usethrottle/errors";
3
+ import { ThrottleError as ThrottleError2, isThrottleError } from "@usethrottle/errors";
4
+ var ThrottleApiError = class extends ThrottleError {
6
5
  constructor(args) {
7
- super(args.message);
6
+ super(args);
8
7
  this.name = "ThrottleApiError";
9
- this.code = args.code;
10
- this.statusCode = args.statusCode;
11
- this.details = args.details;
12
8
  }
13
9
  };
14
10
 
@@ -161,8 +157,118 @@ var StorefrontQuoteClient = class {
161
157
  }
162
158
  }
163
159
  };
160
+
161
+ // src/cart-session.ts
162
+ var CartSessionClient = class {
163
+ applicationId;
164
+ environmentId;
165
+ quoteToken;
166
+ baseUrl;
167
+ fetchImpl;
168
+ timeoutMs;
169
+ origin;
170
+ sessionId = null;
171
+ constructor(opts) {
172
+ if (!opts.applicationId) throw new Error("CartSessionClient: applicationId is required");
173
+ if (!opts.environmentId) throw new Error("CartSessionClient: environmentId is required");
174
+ if (!opts.quoteToken?.startsWith("pk_")) {
175
+ throw new Error("CartSessionClient: quoteToken must be a publishable pk_ token");
176
+ }
177
+ this.applicationId = opts.applicationId;
178
+ this.environmentId = opts.environmentId;
179
+ this.quoteToken = opts.quoteToken;
180
+ this.baseUrl = (opts.baseUrl ?? "https://api.usethrottle.dev").replace(/\/+$/, "");
181
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
182
+ this.timeoutMs = opts.timeoutMs ?? 15e3;
183
+ this.origin = opts.origin;
184
+ }
185
+ /** The current bound session id (after `create()`/`resume()`), or null. */
186
+ get cartSessionId() {
187
+ return this.sessionId;
188
+ }
189
+ /** Bind to an existing session id — e.g. one restored from `localStorage`. */
190
+ resume(cartSessionId) {
191
+ this.sessionId = cartSessionId;
192
+ return this;
193
+ }
194
+ /** Create a new cart session and bind this client to it. */
195
+ async create(input = {}) {
196
+ const data = await this.request("POST", "/api/v1/cart-sessions", {
197
+ applicationId: this.applicationId,
198
+ environmentId: this.environmentId,
199
+ quoteToken: this.quoteToken,
200
+ ...input
201
+ });
202
+ this.sessionId = data.cartSessionId;
203
+ return data;
204
+ }
205
+ /** Read the current session + cart snapshot. */
206
+ get() {
207
+ return this.request("GET", this.basePath());
208
+ }
209
+ async addItem(input) {
210
+ return (await this.request("POST", `${this.basePath()}/items`, input)).cart;
211
+ }
212
+ async updateItem(itemId, input) {
213
+ return (await this.request("PATCH", `${this.basePath()}/items/${encodeURIComponent(itemId)}`, input)).cart;
214
+ }
215
+ async removeItem(itemId) {
216
+ return (await this.request("DELETE", `${this.basePath()}/items/${encodeURIComponent(itemId)}`)).cart;
217
+ }
218
+ async selectShipping(input) {
219
+ return (await this.request("POST", `${this.basePath()}/shipping`, input)).cart;
220
+ }
221
+ async clearShipping() {
222
+ return (await this.request("DELETE", `${this.basePath()}/shipping`)).cart;
223
+ }
224
+ async applyDiscount(code) {
225
+ return (await this.request("POST", `${this.basePath()}/discount`, { code })).cart;
226
+ }
227
+ async removeDiscount() {
228
+ return (await this.request("DELETE", `${this.basePath()}/discount`)).cart;
229
+ }
230
+ /** Hand off to hosted/embedded checkout. Marks the session converted (no more edits). */
231
+ checkout(input) {
232
+ return this.request("POST", `${this.basePath()}/checkout-session`, input);
233
+ }
234
+ basePath() {
235
+ if (!this.sessionId) {
236
+ throw new Error("CartSessionClient: no active session \u2014 call create() or resume(id) first");
237
+ }
238
+ return `/api/v1/cart-sessions/${encodeURIComponent(this.sessionId)}`;
239
+ }
240
+ async request(method, path, body) {
241
+ const ctrl = new AbortController();
242
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
243
+ try {
244
+ const headers = { "content-type": "application/json" };
245
+ if (this.origin) headers.origin = this.origin;
246
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
247
+ method,
248
+ headers,
249
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
250
+ signal: ctrl.signal
251
+ });
252
+ const json = await res.json().catch(() => ({}));
253
+ if (!res.ok) {
254
+ throw new ThrottleApiError({
255
+ code: json?.error?.code ?? "unknown_error",
256
+ message: json?.error?.message ?? `HTTP ${res.status}`,
257
+ statusCode: res.status,
258
+ details: json?.error?.details
259
+ });
260
+ }
261
+ return json.data;
262
+ } finally {
263
+ clearTimeout(timer);
264
+ }
265
+ }
266
+ };
164
267
  export {
165
268
  CartClient,
269
+ CartSessionClient,
166
270
  StorefrontQuoteClient,
167
- ThrottleApiError
271
+ ThrottleApiError,
272
+ ThrottleError2 as ThrottleError,
273
+ isThrottleError
168
274
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usethrottle/cart",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Typed REST client for the Throttle Cart API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -17,6 +17,9 @@
17
17
  "dist",
18
18
  "README.md"
19
19
  ],
20
+ "dependencies": {
21
+ "@usethrottle/errors": "^1.0.0"
22
+ },
20
23
  "devDependencies": {
21
24
  "tsup": "^8.0.0",
22
25
  "vitest": "^2.1.9",