cope-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  `cope-sdk` is the new-architecture copy of `@copecart/sdk` 0.3.0 (`svcs/cart-sdk`, whose changelog holds the history
9
9
  before the copy). `@copecart/sdk` is unchanged and keeps serving the pages that already load it.
10
10
 
11
- ## [Unreleased] — 0.1.0
11
+ ## [0.1.1] — 2026-10-05
12
+
13
+ 0.2.0 is reserved for the release that reads only the consistent integrator fields (plan 72 S12). A `^0.1.0` range
14
+ takes 0.1.1 on its next install, so everything a 0.1.0 caller can notice is listed under **Behaviour changes**.
15
+
16
+ ### Behaviour changes
17
+
18
+ Measured against the published 0.1.0 (`cope-sdk@0.1.0` on npm, published from `4b0bd1abad`), not against
19
+ `@copecart/sdk`. Retries, `baseUrl` validation and every request path are unchanged from 0.1.0: 0.1.0 already
20
+ retried only reads and `createPrefilledCheckout`, and already refused a `baseUrl` that is not a bare origin (a query,
21
+ a fragment, a path, or a doubled trailing slash, whose path is `//`).
22
+
23
+ - **`CartLine.id` is typed `number`.** The API always sent a number, so nothing changes at runtime, but code that
24
+ stores a line id in a `string`-typed variable stops compiling. `updateLine` and `removeLine` still accept a string.
25
+ - **An asctime `Retry-After` (`Sun Nov 6 08:49:37 1994`) is read as GMT**, where 0.1.0 read it in the host's time
26
+ zone, so `CopeApiError.retryAfter` changes by the host's UTC offset. A value that is neither delay-seconds nor an
27
+ HTTP-date (an ISO 8601 string, say) is now `null`, where 0.1.0 passed it to `Date.parse`.
28
+ - **The insecure-page error reads `COPE SDK requires HTTPS...`** (was `CopeCart SDK requires HTTPS...`). Code that
29
+ matches the message text needs the new wording.
30
+ - **`createPrefilledCheckout` returns `fieldErrors`** when the API attaches any, and sends
31
+ `collect_buyer_identity_at_checkout` when `collectBuyerIdentityAtCheckout` is set. Both are absent otherwise.
32
+ - **The MCP endpoint (`cope-sdk/mcp`):** `update_line` and `remove_line` take the number `get_cart` returns (0.1.0
33
+ refused it); `get_cart` returns the lines with `totalsPending` instead of failing when the cart has no buyer
34
+ location; `start_checkout` returns `buyerNext`; the handshake reads its version from `package.json` (it was hard-coded).
35
+
36
+ ### Changed
37
+
38
+ - **The class is `Cope`**, after the brand: `import { Cope } from 'cope-sdk'` and `new Cope({ publishableKey })`. Its
39
+ options type is `CopeOptions`.
40
+ - **The CDN build defines the global `Cope`**, the class itself, so a script tag reads `new Cope({...})`. The global
41
+ `CopeCart` namespace is unchanged and still carries every export.
42
+ - The insecure-page error reads `COPE SDK requires HTTPS...` (was `CopeCart SDK requires HTTPS...`).
43
+ - `CheckoutPayload.consents` is optional: creating a checkout does not require them. The hosted checkout page requires
44
+ `buyer_tos` when the buyer starts paying.
45
+
46
+ ### Added
47
+
48
+ - `UpdateBuyerIdentityPayload` takes `phone` and `locale`, which the API already accepted. A malformed phone is dropped,
49
+ not refused, and the cart carries `field_errors`.
50
+ - `createPrefilledCheckout` takes `collectBuyerIdentityAtCheckout` (hosted checkout only) and returns `fieldErrors`
51
+ when the API attaches any.
52
+ - The MCP `start_checkout` result carries `buyerNext`: the buyer accepts the terms, and for a digital product waives
53
+ the right of withdrawal, on the checkout page; an agent cannot do either. `ApiErrorCode` gains
54
+ `missing_withdrawal_waiver`, and `CheckoutPayload.consents` and the README say a digital product needs it.
55
+ - `ApiErrorCode` declares every code the endpoints this SDK calls can answer — 21 were missing, among them
56
+ `invalid_sdk_key`, `price_changed`, `mixed_seller_cart` and `idempotency_conflict` — and the README table lists them.
57
+
58
+ ### Fixed
59
+
60
+ - **A cart line's `id` is a number.** The API renders it as an integer and `CartLine.id` was typed `string`, so the
61
+ MCP tools refused the line id they had just returned (`expected string, received number`): `update_line` and
62
+ `remove_line` could not act on any line. `CartLine.id` is `number`; `updateLine`/`removeLine` take a number (a
63
+ string of its digits is still accepted), and the MCP tools return it as a number and take a number or a string of
64
+ its digits.
65
+ - **The MCP `get_cart` failed on a cart with no buyer location** (`missing_tax_location`), so an agent could not show
66
+ the cart without first asking for the buyer's country and postal code. It now returns the lines with
67
+ `totalsPending`.
68
+ - **An asctime `Retry-After` was read in the host's time zone.** `CopeApiError.retryAfter` now reads it as GMT, as
69
+ RFC 9110 defines it, and ignores a value that is neither delay-seconds nor an HTTP-date.
70
+ - The MCP endpoint's handshake reports the package's version (it said `0.1.0`), and tells an agent to set the buyer's
71
+ country and postal code on `missing_tax_location`.
72
+
73
+ ### Removed
74
+
75
+ - `missing_code` from `ApiErrorCode`: no endpoint answers it (an empty promo code is `not_found`).
76
+
77
+ ### Deprecated
78
+
79
+ - **`CopeCart` and `CopeCartOptions`**: aliases of `Cope` and `CopeOptions`, kept so code written against 0.1.0 —
80
+ `new CopeCart({...})`, and `new CopeCart.CopeCart({...})` on a script tag — runs unchanged. `CopeCart` is the same
81
+ class, so `instanceof` agrees across both names. `CopeCartExpiredError` is not deprecated: it names an expired
82
+ cart.
83
+
84
+ ## [0.1.0] — 2026-09-28
12
85
 
13
86
  ### Added
14
87
 
package/README.md CHANGED
@@ -42,27 +42,32 @@ npm install cope-sdk
42
42
  ```
43
43
 
44
44
  ```typescript
45
- import { CopeCart } from 'cope-sdk'
45
+ import { Cope } from 'cope-sdk'
46
46
  ```
47
47
 
48
48
  ### CDN (no build step)
49
49
 
50
50
  ```html
51
- <script src="https://unpkg.com/cope-sdk@0.1.0/dist/index.global.js"></script>
51
+ <script src="https://unpkg.com/cope-sdk@0.1.1/dist/index.global.js"></script>
52
52
  <script>
53
- const cope = new CopeCart.CopeCart({ publishableKey: 'cope_pk_...' })
53
+ const cope = new Cope({ publishableKey: 'cope_pk_...' })
54
54
  </script>
55
55
  ```
56
56
 
57
- > When loaded via CDN, the SDK is available as `window.CopeCart`. The main class is `CopeCart.CopeCart`.
57
+ > When loaded via CDN, the global `Cope` is the class. The global `CopeCart` namespace carries every export too, for
58
+ > example `CopeCart.CopeApiError`, and `CopeCart.CopeCart` is the same class.
59
+
60
+ > These examples use `cope-sdk` 0.1.1, whose class is `Cope` (options type `CopeOptions`). `CopeCart` and
61
+ > `CopeCartOptions` stay as deprecated aliases, so code written against 0.1.0 still compiles; read the 0.1.1 CHANGELOG
62
+ > before upgrading, because some behaviour changed.
58
63
 
59
64
  ## Quick Start
60
65
 
61
66
  ```typescript
62
- import { CopeCart, CopeApiError } from 'cope-sdk'
67
+ import { Cope, CopeApiError } from 'cope-sdk'
63
68
 
64
69
  // 1. Initialize
65
- const cope = new CopeCart({
70
+ const cope = new Cope({
66
71
  publishableKey: 'cope_pk_your_key_here',
67
72
  })
68
73
 
@@ -93,7 +98,7 @@ console.log(repriced.totals!) // { subtotal_minor_units, tax_minor_units, total_
93
98
  const checkout = await cope.checkout(cart.id, {
94
99
  success_url: 'https://your-site.com/thank-you',
95
100
  cancel_url: 'https://your-site.com/cart',
96
- consents: [{ type: 'buyer_tos' }],
101
+ consents: [{ type: 'buyer_tos', version: 'your-terms-v3' }], // only if the buyer accepted your terms on your page
97
102
  })
98
103
 
99
104
  // 8. Redirect to payment page
@@ -113,9 +118,9 @@ COPE-1260 adds a single helper for server-side TypeScript adapters that already
113
118
  5. create hosted checkout.
114
119
 
115
120
  ```typescript
116
- import { CopeCart } from 'cope-sdk'
121
+ import { Cope } from 'cope-sdk'
117
122
 
118
- const cope = new CopeCart({
123
+ const cope = new Cope({
119
124
  publishableKey: process.env.COPE_PUBLISHABLE_KEY!,
120
125
  })
121
126
 
@@ -158,7 +163,7 @@ const handoff = await cope.createPrefilledCheckout({
158
163
  checkout: {
159
164
  success_url: 'https://example.com/thank-you',
160
165
  cancel_url: 'https://example.com/cancelled',
161
- consents: [{ type: 'buyer_tos' }],
166
+ consents: [{ type: 'buyer_tos', version: 'your-terms-v3' }], // only if the buyer accepted your terms on your page
162
167
  },
163
168
  })
164
169
 
@@ -191,9 +196,9 @@ npm install cope-sdk
191
196
  ```
192
197
 
193
198
  ```typescript
194
- import { CopeCart } from 'cope-sdk'
199
+ import { Cope } from 'cope-sdk'
195
200
 
196
- const cope = new CopeCart({
201
+ const cope = new Cope({
197
202
  publishableKey: 'cope_pk_your_live_key',
198
203
  })
199
204
  ```
@@ -201,9 +206,9 @@ const cope = new CopeCart({
201
206
  **Option B — CDN (script tag):**
202
207
 
203
208
  ```html
204
- <script src="https://unpkg.com/cope-sdk@0.1.0/dist/index.global.js"></script>
209
+ <script src="https://unpkg.com/cope-sdk@0.1.1/dist/index.global.js"></script>
205
210
  <script>
206
- const cope = new CopeCart.CopeCart({
211
+ const cope = new Cope({
207
212
  publishableKey: 'cope_pk_your_live_key',
208
213
  })
209
214
  </script>
@@ -214,9 +219,9 @@ const cope = new CopeCart({
214
219
  Typical integration:
215
220
 
216
221
  ```typescript
217
- import { CopeCart, CopeApiError, CopeCartExpiredError } from 'cope-sdk'
222
+ import { Cope, CopeApiError, CopeCartExpiredError } from 'cope-sdk'
218
223
 
219
- const cope = new CopeCart({ publishableKey: 'cope_pk_live_key' })
224
+ const cope = new Cope({ publishableKey: 'cope_pk_live_key' })
220
225
 
221
226
  // Display product info on your page
222
227
  const product = await cope.getProduct('prod_YourProductUUID')
@@ -259,7 +264,7 @@ async function handleCheckout(cartId: string) {
259
264
  const checkout = await cope.checkout(cartId, {
260
265
  success_url: 'https://your-site.com/order-confirmation',
261
266
  cancel_url: 'https://your-site.com/cart',
262
- consents: [{ type: 'buyer_tos' }],
267
+ consents: [{ type: 'buyer_tos', version: 'your-terms-v3' }], // only if the buyer accepted your terms on your page
263
268
  })
264
269
 
265
270
  // 4. Redirect to Stripe
@@ -291,7 +296,7 @@ In single-page applications, destroy the SDK instance on route change to abort i
291
296
  ```typescript
292
297
  // React example
293
298
  useEffect(() => {
294
- const cope = new CopeCart({ publishableKey: 'cope_pk_...' })
299
+ const cope = new Cope({ publishableKey: 'cope_pk_...' })
295
300
  // ... use cope
296
301
  return () => cope.destroy()
297
302
  }, [])
@@ -299,7 +304,7 @@ useEffect(() => {
299
304
 
300
305
  ## Configuration
301
306
 
302
- ### `new CopeCart(options)`
307
+ ### `new Cope(options)`
303
308
 
304
309
  | Option | Type | Required | Default | Description |
305
310
  |--------|------|----------|---------|-------------|
@@ -369,6 +374,8 @@ const cart = await cope.createCart({
369
374
 
370
375
  Retrieve an existing cart. The `cart_secret` is automatically loaded from `localStorage`.
371
376
 
377
+ The buyer's email comes back masked (`j***n@example.com`); the `setBuyerIdentity()` response carries it as saved.
378
+
372
379
  Throws `CopeCartExpiredError` if the cart is in a terminal state (`ordered`, `expired`, `abandoned`, `merged`) and clears `localStorage`.
373
380
 
374
381
  ```typescript
@@ -398,7 +405,8 @@ const cart = await cope.addLine(cartId, {
398
405
 
399
406
  #### `cope.updateLine(cartId, lineId, payload): Promise<Cart>`
400
407
 
401
- Change quantity or switch payment plan for an existing line item.
408
+ Change quantity or switch payment plan for an existing line item. `lineId` is the line's `id`, an integer (a string
409
+ of its digits is accepted too).
402
410
 
403
411
  ```typescript
404
412
  // Change quantity
@@ -419,7 +427,7 @@ const cart = await cope.removeLine(cartId, lineId)
419
427
  **Typical quantity control pattern:**
420
428
 
421
429
  ```typescript
422
- async function changeQuantity(cartId: string, lineId: string, newQty: number) {
430
+ async function changeQuantity(cartId: string, lineId: number, newQty: number) {
423
431
  if (newQty <= 0) {
424
432
  return cope.removeLine(cartId, lineId)
425
433
  }
@@ -431,7 +439,10 @@ async function changeQuantity(cartId: string, lineId: string, newQty: number) {
431
439
 
432
440
  #### `cope.setBuyerIdentity(cartId, payload): Promise<Cart>`
433
441
 
434
- Set or update buyer information. `email`, `country`, and `postal_code` are required before calling `reprice()` or `checkout()`.
442
+ Set or update buyer information. `reprice()` taxes the buyer's `country` and `postal_code`; with an EU country alone,
443
+ or with no country and a buyer whose IP is in the EU, it prices an estimate, and it answers `missing_tax_location`
444
+ when it has neither. `checkout()` does not require a buyer identity: the hosted checkout page collects whatever is
445
+ missing before the buyer pays.
435
446
 
436
447
  ```typescript
437
448
  await cope.setBuyerIdentity(cartId, {
@@ -449,10 +460,15 @@ await cope.setBuyerIdentity(cartId, {
449
460
  country: 'DE',
450
461
  },
451
462
  billing_address: { ... }, // optional — same structure as shipping (all fields optional)
463
+ phone: '+49 30 1234567', // optional — a malformed phone is dropped, not refused (see field_errors)
464
+ locale: 'de', // optional — the checkout language; one the business has not enabled is ignored
452
465
  consents: [{ type: 'marketing' }], // optional — additional consents
453
466
  })
454
467
  ```
455
468
 
469
+ A malformed `phone` does not fail the call: the rest of the identity is saved and the returned cart carries
470
+ `field_errors: [{ code: 'validation_error', field: 'phone', ... }]`.
471
+
456
472
  ### Pricing
457
473
 
458
474
  #### `cope.reprice(cartId): Promise<Cart>`
@@ -509,7 +525,7 @@ const checkout = await cope.checkout(cartId, {
509
525
  success_url: 'https://your-site.com/thank-you', // optional — redirect after payment
510
526
  cancel_url: 'https://your-site.com/cart', // optional — redirect on cancel
511
527
  embed_origin: window.location.origin, // optional — required before mounting in an iframe
512
- consents: [{ type: 'buyer_tos', version: '1.0' }], // required — buyer ToS (version optional)
528
+ consents: [{ type: 'buyer_tos', version: 'your-terms-v3' }], // optional — only what the buyer accepted on your page
513
529
  })
514
530
 
515
531
  checkout.checkoutId // 'co-001'
@@ -520,6 +536,22 @@ checkout.expiresAt // '2026-03-16T10:30:00Z' (30-minute expiry)
520
536
  checkout.totals // final CartTotals snapshot
521
537
  ```
522
538
 
539
+ **Consents** are not required to create a checkout; any you send are recorded on it. Before the payment starts the
540
+ buyer must have accepted the terms (`buyer_tos`) and, when the cart holds a product that does not ship (a digital
541
+ product), waived their right of withdrawal (`withdrawal_waiver`); without it the payment is refused with
542
+ `missing_withdrawal_waiver`. The hosted checkout page asks the buyer for both, so send them here only when the buyer
543
+ already gave them on your own page:
544
+
545
+ ```typescript
546
+ consents: [
547
+ { type: 'buyer_tos', version: 'your-terms-v3' },
548
+ { type: 'withdrawal_waiver', version: 'your-waiver-v1' }, // only for a cart with a product that does not ship
549
+ ],
550
+ ```
551
+
552
+ Each is recorded as the buyer's own acceptance, so send one only when the buyer gave it, with a `version` naming the
553
+ text they saw; otherwise leave `consents` out and let the checkout page ask.
554
+
523
555
  **Redirect URLs:** If `success_url` is provided, the buyer is redirected there after payment with `?order_id=<uuid>` appended. If omitted, the default COPE thank-you page is shown.
524
556
 
525
557
  **HTTPS required** for redirect URLs in production. `http://localhost` is allowed in development.
@@ -542,7 +574,7 @@ Mount the hosted checkout iframe into your page. Pass the `CheckoutResult` retur
542
574
  ```typescript
543
575
  const checkout = await cope.checkout(cartId, {
544
576
  embed_origin: window.location.origin,
545
- consents: [{ type: 'buyer_tos' }],
577
+ consents: [{ type: 'buyer_tos', version: 'your-terms-v3' }], // only if the buyer accepted your terms on your page
546
578
  })
547
579
 
548
580
  const mounted = cope.mountCheckout('#checkout-frame', checkout, {
@@ -721,7 +753,7 @@ import type {
721
753
  PostPurchaseTerminalPayload,
722
754
 
723
755
  // Payloads (input types)
724
- CopeCartOptions,
756
+ CopeOptions,
725
757
  CreateCartPayload,
726
758
  CartAttribution,
727
759
  AddLinePayload,
@@ -758,7 +790,7 @@ interface Cart {
758
790
 
759
791
  ```typescript
760
792
  interface CartLine {
761
- id: string // use for updateLine/removeLine
793
+ id: number // an integer; use for updateLine/removeLine
762
794
  cart_id: string
763
795
  product_id: number // internal integer FK
764
796
  plan_id: number | null
@@ -918,33 +950,52 @@ try {
918
950
 
919
951
  ### Error Codes Reference
920
952
 
953
+ The codes the cart-API calls this SDK makes can answer. `tests/error-codes-agreement.test.ts` holds this list to the
954
+ literal codes in the cope-core sources those calls render errors from — a listed set of controllers, concerns and
955
+ services — and fails when either side has one the other lacks. A code built at runtime, or emitted by a source outside
956
+ that set, is not seen.
957
+
921
958
  | Code | HTTP Status | When |
922
959
  |------|-------------|------|
923
- | `unauthorized` | 401 | Invalid/missing publishable key or cart secret |
924
- | `not_found` | 404 | Product or cart not found |
960
+ | `invalid_sdk_key` | 401 | The publishable key is unknown or inactive |
961
+ | `unauthorized` | 401 | The cart secret is missing or wrong |
962
+ | `cart_id_mismatch` | 403 | Cart ID in URL doesn't match cart secret |
963
+ | `business_checkout_hold` | 403 | The business cannot take checkout payments right now |
964
+ | `not_found` | 404 | Product, cart, line or checkout not found |
965
+ | `validation_error` | 422 | A field failed validation; `field` names it |
966
+ | `forbidden_field` | 422 | `createPrefilledCheckout` was sent a field only a private service may set |
967
+ | `unsupported_currency` | 422 | `createCart` with a currency the business does not sell in |
968
+ | `unsupported_checkout_locale`, `checkout_locale_not_allowed` | 422 | The checkout locale is unknown, or not enabled for the business |
925
969
  | `cart_not_active` | 422 | Cart is not in `active` state |
926
970
  | `duplicate_line` | 422 | Same product already in cart |
927
971
  | `not_saleable` | 422 | Product is not available for purchase |
972
+ | `mixed_seller_cart`, `mixed_vat_mode_cart` | 422 | The product cannot share a cart with the lines already in it |
973
+ | `product_unavailable`, `insufficient_stock` | 422 | At checkout: a product went off sale, or there is not enough stock |
928
974
  | `empty_cart` | 422 | Attempted checkout with no line items |
929
- | `incomplete_buyer_identity` | 422 | Missing email, country, or postal code |
930
- | `incomplete_shipping_address` | 422 | Physical product requires complete shipping address |
931
- | `missing_consent` | 422 | Checkout requires `buyer_tos` consent |
932
- | `below_minimum_charge` | 422 | Order total below Stripe's minimum charge amount |
933
- | `invalid_redirect_url` | 422 | success_url or cancel_url is malformed |
975
+ | `below_minimum_charge`, `above_maximum_charge` | 422 | The total is outside what a single charge may be |
976
+ | `missing_tax_location` | 422 | `reprice()` with no location to tax: no country set, and no EU IP country |
977
+ | `cart_not_repriceable` | 422 | `reprice()` on a cart that can no longer be priced |
978
+ | `tax_address_invalid`, `tax_request_rejected` | 422 | Tax could not be calculated for the buyer's address or details |
979
+ | `tax_service_unavailable` | 503 | Tax calculation service temporarily down |
934
980
  | `not_found`, `inactive`, `not_started`, `expired`, `exhausted`, `no_applicable_items`, `currency_not_supported`, `promo_not_supported_for_recurring` | 422 | The promo code was refused; the code says why (`not_found`: no such code) |
935
- | `missing_code` | 422 | Promo code request missing the `code` field |
936
- | `missing_tax_location` | 422 | Buyer country/postal code needed for reprice |
981
+ | `offer_sourced_no_promo` | 422 | The cart comes from an offer; promo codes do not apply |
982
+ | `promo_locked_during_payment` | 409 | The promo code cannot change while a payment is in progress |
983
+ | `invalid_redirect_url` | 422 | success_url or cancel_url is malformed or not registered |
984
+ | `embed_origin_not_allowed` | 422 | `embed_origin` is not a registered embed domain |
985
+ | `price_changed` | 409 | The total changed since it was shown; reprice and try again. Only when the checkout request carries `expected_total_minor_units`, which `CheckoutPayload` does not declare |
986
+ | `idempotency_conflict` | 409 | The same checkout is still being created; repeat shortly |
987
+ | `idempotency_body_mismatch` | 422 | An `Idempotency-Key` was reused with a different payload |
937
988
  | `checkout_expired` | 410 | Checkout session expired (30-minute limit) |
938
- | `checkout_not_open` | 422 | Checkout is not in `open` state |
939
989
  | `checkout_not_cancellable` | 422 | Cannot cancel a completed/expired checkout |
940
- | `cart_id_mismatch` | 403 | Cart ID in URL doesn't match cart secret |
990
+ | `checkout_payment_in_flight` | 409 | Cannot cancel: a payment for the checkout is still being processed |
941
991
  | `payload_too_large` | 413 | Request body over edge's cap (answered by edge) |
992
+ | `rate_limited` | 429 | Too many requests — retry after delay (answered by edge) |
942
993
  | `upstream_unavailable` | 502 | Edge could not reach the cart API |
943
994
  | `gateway_timeout` | 504 | Edge's deadline passed; a write may still have been applied |
944
- | `validation_error` | 422 | Generic field validation failure |
945
- | `rate_limited` | 429 | Too many requests — retry after delay |
946
- | `tax_service_unavailable` | 503 | Tax calculation service temporarily down |
947
- | `payment_provider_error` | 422 | Stripe returned an error |
995
+
996
+ `incomplete_buyer_identity`, `incomplete_shipping_address`, `missing_consent`, `missing_withdrawal_waiver`,
997
+ `checkout_not_open` and `payment_provider_error` are declared in `ApiErrorCode` so code can switch on them, but no SDK
998
+ method returns them: the hosted checkout page answers them when the buyer starts paying.
948
999
 
949
1000
  ## MCP endpoint for AI shopping agents (`cope-sdk/mcp`)
950
1001
 
@@ -1010,6 +1061,8 @@ The SDK retries failed requests **once** with exponential backoff + jitter:
1010
1061
  - Backoff: 500ms base, doubles per attempt, max 5s, with random factor between 0.5x and 1.0x
1011
1062
  - Request timeout: 8 seconds per attempt
1012
1063
  - 4xx errors (except 429) are never retried
1064
+ - The SDK's own retry waits by this backoff alone: it does not honour a `Retry-After` header. The value is reported on
1065
+ the error it finally throws, as `retryAfter` (seconds), for your own scheduling
1013
1066
  - A 429 is refused before the request is processed, so resending it cannot apply a write twice
1014
1067
 
1015
1068
  ### Idempotency Keys
package/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict';var i=class extends Error{constructor(e){super(e),this.name="CopeError";}},b=class extends i{code;status;field;requestId;retryAfter;errors;constructor(e,r,n,o){let a=r[0];super(a?.message??`API error ${e}`),this.name="CopeApiError",this.status=e,this.code=a?.code??"unknown",this.field=a?.field,this.requestId=n??null,this.retryAfter=o??null,this.errors=r;}},E=class extends i{constructor(e){super(e),this.name="CopeNetworkError";}},S=class extends i{state;constructor(e){super(`Cart is ${e} and cannot be used`),this.name="CopeCartExpiredError",this.state=e;}},U=["not_found","load_failed","embed_not_allowed","confirm_failed"],v=["expired","completed","already_completed","cancelled","processing"];var $=new Set(["ordered","expired","abandoned","merged"]),H="https://app.cope.com",x="cope_sdk_cart",N=8e3,B=500,z=5e3,V="cope.checkout",G=1,F="copecart.checkout.mount",j=1,Y=250,W=20,X=8e3,J=new Set(["oto-v1"]);function d(t){return encodeURIComponent(t)}function u(t){return typeof t=="object"&&t!==null}function Q(t){try{let e=new URL(t);return e.pathname==="/"&&!e.search&&!e.hash&&!e.username&&!e.password?e.origin:null}catch{return null}}var Z={payload_too_large:"Request body is too large",rate_limited:"Too many requests",upstream_unavailable:"The checkout service is unavailable",gateway_timeout:"The checkout service did not answer in time",not_found:"Not found"};function ee(t){return u(t)?Array.isArray(t.errors)&&t.errors.length>0&&t.errors.every(te)?t.errors:typeof t.error=="string"&&t.error.length>0?/^[a-z][a-z0-9_]*$/.test(t.error)?[{code:t.error,message:Z[t.error]??t.error}]:[{code:"unknown",message:t.error}]:null:null}function te(t){return u(t)&&typeof t.code=="string"&&typeof t.message=="string"}function re(t){if(t===null||t.trim()==="")return null;if(/^\d+$/.test(t.trim()))return Number(t.trim());let e=Date.parse(t);return Number.isNaN(e)?null:Math.max(0,Math.ceil((e-Date.now())/1e3))}function ne(t){return u(t)&&typeof t.request_id=="string"?t.request_id:null}function oe(t){if(!u(t))return {applicable:false,terms_version:null};let e=t.terms_version;return t.applicable!==true||typeof e!="string"||!J.has(e)?{applicable:false,terms_version:null}:{applicable:true,terms_version:e}}function g(t){return {...t,one_click_charge_consent:oe(t.one_click_charge_consent)}}var ae=new Set(U),ie=new Set(v);function se(t){return ae.has(t)}function le(t){return ie.has(t)}function ce(t){return t==="upsell"||t==="downsell"}function ue(t){return t==="accepted"||t==="declined"||t==="failed_not_charged"||t==="requires_action"}function de(t){return t==="not_charged"||t==="charged"||t==="requires_action"||t==="failed"}function ye(t){return t==="completed"||t==="declined"||t==="expired"||t==="unavailable"}function pe(){let t=window.location;if(t.origin)return t.origin;let e=t.host||[t.hostname,t.port].filter(Boolean).join(":");return new URL(`${t.protocol}//${e}`).origin}function he(t){let e=new URL(t),r=new URL(t),n=e.pathname.match(/^\/checkout\/embed\/([^/]+)$/),o=e.pathname.match(/^\/checkout\/([^/]+)$/);if(n)return e.pathname=`/checkout/${n[1]}`,r.pathname=`/checkout/embed/${n[1]}`,{checkoutUrl:e.toString(),embedCheckoutUrl:r.toString()};if(o)return e.pathname=`/checkout/${o[1]}`,r.pathname=`/checkout/embed/${o[1]}`,{checkoutUrl:e.toString(),embedCheckoutUrl:r.toString()};throw new i("Checkout URL path cannot be mapped to COPE checkout routes")}function D(t){let r=new URL(t).pathname.match(/^\/checkout\/embed\/([^/]+)$/);if(!r)throw new i("Checkout embed URL path cannot be mapped to COPE checkout embed route");return decodeURIComponent(r[1])}function me(t){let e=t.embed;if(u(e)&&e.allowed===true&&typeof e.parent_origin=="string")try{return new URL(e.parent_origin).origin===e.parent_origin?e.parent_origin:void 0}catch{return}}function ge(t){if(!u(t)||t.source!==V||t.version!==G)return null;let e={...typeof t.messageId=="string"?{messageId:t.messageId}:{},...typeof t.timestamp=="string"?{timestamp:t.timestamp}:{}};if(t.type==="ready")return {type:"ready",...e};if(t.type==="resize"){if(!u(t.payload))return null;let{height:r}=t.payload;return typeof r!="number"||!Number.isFinite(r)||r<=0?null:{type:"resize",payload:{height:Math.ceil(r)},...e}}if(t.type==="error"){if(!u(t.payload))return null;let{code:r,retryable:n}=t.payload;return typeof r!="string"||!se(r)||typeof n!="boolean"?null:{type:"error",payload:{code:r,retryable:n},...e}}if(t.type==="terminal"){if(!u(t.payload))return null;let{status:r}=t.payload;return typeof r!="string"||!le(r)?null:{type:"terminal",payload:{status:r},...e}}if(t.type==="post_purchase_offer"){if(!u(t.payload))return null;let{offerRef:r,stepKind:n,title:o,priceGrossMinorUnits:a,currency:y}=t.payload;return typeof r!="string"||!r||typeof n!="string"||!ce(n)||typeof o!="string"||!o||typeof a!="number"||!Number.isFinite(a)||a<0||typeof y!="string"||!y?null:{type:"post_purchase_offer",payload:{offerRef:r,stepKind:n,title:o,priceGrossMinorUnits:a,currency:y},...e}}if(t.type==="post_purchase_decision"){if(!u(t.payload))return null;let{offerRef:r,status:n,chargeStatus:o}=t.payload;return typeof r!="string"||!r||typeof n!="string"||!ue(n)||typeof o!="string"||!de(o)?null:{type:"post_purchase_decision",payload:{offerRef:r,status:n,chargeStatus:o},...e}}if(t.type==="post_purchase_terminal"){if(!u(t.payload))return null;let{status:r}=t.payload;return typeof r!="string"||!ye(r)?null:{type:"post_purchase_terminal",payload:{status:r},...e}}return null}var A=class{publishableKey;baseUrl;checkoutBaseUrl;persistence;abortController;mountCleanups=new Set;cachedSecret=null;cachedCartId=null;get currentCartId(){return this.cachedCartId}constructor(e){if(!e.publishableKey?.startsWith("cope_pk_"))throw new i('Invalid publishableKey. Must start with "cope_pk_".');if(typeof window<"u"&&window.location?.protocol==="http:"&&window.location.hostname!=="localhost")throw new i("CopeCart SDK requires HTTPS. Refusing to operate on an insecure page.");this.publishableKey=e.publishableKey;let r=Q(e.baseUrl??H);if(r===null)throw new i('Invalid baseUrl. Pass an origin such as "https://app.cope.com"; the SDK adds /api/cart/v1 itself.');this.baseUrl=r,this.checkoutBaseUrl=e.checkoutBaseUrl?e.checkoutBaseUrl.replace(/\/+$/,""):new URL(this.baseUrl).origin,this.abortController=new AbortController,this.persistence=e.cart?"none":e.persistence??"localStorage",e.cart?(this.cachedCartId=e.cart.id,this.cachedSecret=e.cart.secret):this.hydrateFromStorage();}async createCart(e){return this.createCartRequest(e,{skipAuth:true})}async getProduct(e){return this.get(`/products/${d(e)}`,{skipAuth:true})}async getCart(e){let r=g(await this.get(`/carts/${d(e)}`));if($.has(r.state))throw this.clearStorage(),new S(r.state);return r}async addLine(e,r){return g(await this.post(`/carts/${d(e)}/lines`,r))}async updateLine(e,r,n){return g(await this.patch(`/carts/${d(e)}/lines/${d(r)}`,n))}async removeLine(e,r){return g(await this.del(`/carts/${d(e)}/lines/${d(r)}`))}async setBuyerIdentity(e,r){return g(await this.patch(`/carts/${d(e)}/buyer-identity`,r))}async reprice(e){return g(await this.post(`/carts/${d(e)}/reprice`,{}))}async applyPromoCode(e,r){return g(await this.post(`/carts/${d(e)}/promo_code`,{code:r}))}async removePromoCode(e){return g(await this.del(`/carts/${d(e)}/promo_code`))}async checkout(e,r,n={}){return this.createCheckoutResult(e,r,n.idempotencyKey?{idempotencyKey:n.idempotencyKey}:void 0)}async createPrefilledCheckout(e){let r=this.normalizePrefilledLines(e),n=await this.post("/prefilled-checkouts",{external_reference:e.externalReference,currency:e.currency,...e.locale!=null?{locale:e.locale}:{},...e.attribution?{attribution:e.attribution}:{},...e.intendedPaymentMethod!=null?{intended_payment_method:e.intendedPaymentMethod}:{},...e.metadata?{metadata:e.metadata}:{},lines:r.map(a=>({product_id:a.productId,...a.planId!==void 0?{plan_id:a.planId}:{},...a.quantity!==void 0?{quantity:a.quantity}:{}})),buyer_identity:e.buyerIdentity,checkout:e.checkout},{skipAuth:true,idempotencyKey:await this.prefilledIdempotencyKey(e.externalReference)}),o=this.normalizeCheckoutResult(n);return {cartId:n.cart_id,cartVersion:n.cart_version,checkoutId:o.checkoutId,checkoutToken:n.checkout_token??D(o.embedCheckoutUrl),checkoutUrl:o.checkoutUrl,embedCheckoutUrl:o.embedCheckoutUrl,...o.embedOrigin?{embedOrigin:o.embedOrigin}:{},status:o.status,expiresAt:o.expiresAt,totals:o.totals,...e.intendedPaymentMethod!=null?{intendedPaymentMethod:e.intendedPaymentMethod}:{}}}async cancelCheckout(e){await this.requestVoid("POST",`/checkouts/${d(e)}/cancel`);}redirectToCheckout(e){this.abortController.signal.aborted||typeof window<"u"&&(window.location.href=e.checkoutUrl);}mountCheckout(e,r,n={}){if(this.abortController.signal.aborted)throw new i("SDK destroyed");if(typeof window>"u"||typeof document>"u")throw new i("mountCheckout requires a browser environment");let{checkout:o,options:a}=this.normalizeMountCheckoutArgs(e,r,n),y=this.checkoutOrigin();if(this.assertCopeOrigin(o.embedCheckoutUrl,y,"Checkout embed URL"),!o.embedOrigin)throw new i("mountCheckout requires checkout.embedOrigin \u2014 send embed_origin on checkout() or on create-offer, and register the origin in dashboard Settings");let w;if(a.fallback==="redirect"){if(!o.checkoutUrl)throw new i("mountCheckout with fallback: 'redirect' requires checkout.checkoutUrl \u2014 for an offer, pass its public_url");this.assertCopeOrigin(o.checkoutUrl,y,"Checkout URL"),w=o.checkoutUrl;}let T=this.resolveMountTarget(a.target),l=document.createElement("iframe"),f=new URL(o.embedCheckoutUrl).origin,C={type:F,version:j,token:D(o.embedCheckoutUrl)},R=pe(),p=false,P=false,s=0,h,k;if(o.embedOrigin!==R)throw new i(`Checkout embed origin ${o.embedOrigin} does not match current origin ${R}`);l.src=o.embedCheckoutUrl,l.title=a.title??"COPE checkout",l.setAttribute("allow","payment *"),l.referrerPolicy="no-referrer",l.style.border="0",l.style.width="100%",l.style.minHeight="720px",a.className&&(l.className=a.className);let m=()=>{h!==void 0&&(window.clearInterval(h),h=void 0);},_=()=>{k!==void 0&&(window.clearTimeout(k),k=void 0);},O=()=>{p||(p=true,m(),_(),window.removeEventListener("message",q),l.removeEventListener("load",K),l.remove(),this.mountCleanups.delete(O));},I=()=>{if(!(p||P)){if(s>=W){m();return}s+=1,l.contentWindow?.postMessage(C,f);}},L=()=>{p||(s=0,m(),I(),h=window.setInterval(I,Y));},K=()=>{p||(P=false,L());},q=M=>{if(p||M.origin!==f||M.source!==l.contentWindow)return;let c=ge(M.data);c&&(a.onMessage?.(c),c.type==="ready"?(P=true,m(),_(),a.onReady?.()):c.type==="resize"?(l.style.height=`${c.payload.height}px`,a.onResize?.(c.payload)):c.type==="error"?(m(),_(),a.onError?.(c.payload)):c.type==="terminal"?(m(),_(),a.onTerminal?.(c.payload),c.payload.status==="completed"?a.onSuccess?.():c.payload.status==="cancelled"&&a.onCancel?.()):c.type==="post_purchase_offer"?a.onPostPurchaseOffer?.(c.payload):c.type==="post_purchase_decision"?a.onPostPurchaseDecision?.(c.payload):c.type==="post_purchase_terminal"&&a.onPostPurchaseTerminal?.(c.payload));};window.addEventListener("message",q),l.addEventListener("load",K),this.mountCleanups.add(O);try{T.appendChild(l),L(),k=window.setTimeout(()=>{if(!(p||P)){if(a.fallback==="redirect"){a.onFallbackRedirect?.(),window.location.href=w;return}a.onError?.({code:"load_failed",retryable:!0});}},a.readyTimeoutMs??X);}catch(M){throw O(),M}return {iframe:l,destroy:O}}destroy(){for(let e of Array.from(this.mountCleanups))e();this.abortController.abort(),this.clearStorage();}async createCartRequest(e,r){let n=await this.post("/carts",e,r),o=g({...n,lines:n.lines??[],buyer_identity:n.buyer_identity??null});return o.cart_secret&&(this.cachedCartId=o.id,this.cachedSecret=o.cart_secret,this.persistToStorage()),o}async createCheckoutResult(e,r,n){let o=await this.post(`/carts/${d(e)}/checkout`,r,n);return this.normalizeCheckoutResult(o)}normalizeCheckoutResult(e){let r=e.checkout_url.startsWith("http")?e.checkout_url:`${this.checkoutBaseUrl}${e.checkout_url}`;this.assertCopeOrigin(r,this.checkoutOrigin(),"Checkout URL");let n=he(r);return {checkoutId:e.checkout_id,checkoutUrl:n.checkoutUrl,embedCheckoutUrl:n.embedCheckoutUrl,embedOrigin:me(e),status:e.status,expiresAt:e.expires_at,totals:e.totals}}async requestVoid(e,r,n){await this.request(e,r,n);}async get(e,r){return this.request("GET",e,void 0,r)}async post(e,r,n){return this.request("POST",e,r,n)}async patch(e,r){return this.request("PATCH",e,r)}async del(e){return this.request("DELETE",e)}async request(e,r,n,o){if(this.abortController.signal.aborted)throw new i("SDK destroyed");let a=`${this.baseUrl}/api/cart/v1${r}`,y={Accept:"application/json","X-Cope-Key":this.publishableKey};n!==void 0&&(y["Content-Type"]="application/json"),!o?.skipAuth&&this.cachedSecret&&(y.Authorization=`CartSecret ${this.cachedSecret}`);let w=e==="POST"?o?.idempotencyKey??crypto.randomUUID():void 0;w&&(y["Idempotency-Key"]=w);let T=e==="GET"||e==="POST"&&this.isRetryablePath(r)?2:1,l=this.isReplaySafe(e,r),f;for(let C=0;C<T;C++){if(C>0){let s=this.calculateRetryDelay(C-1);await this.sleep(s);}let R=new AbortController,p=setTimeout(()=>R.abort(),N),P=AbortSignal.any([this.abortController.signal,R.signal]);try{let s=await fetch(a,{method:e,headers:y,body:n!==void 0?JSON.stringify(n):void 0,signal:P});if(s.ok)return s.status===204?void 0:await s.json();let h=await s.json().catch(()=>null),k=ee(h)??[{code:"unknown",message:s.statusText}],m=s.headers.get("X-Request-Id")??ne(h),_=re(s.headers.get("Retry-After"));if(this.shouldRetry(s.status)&&C<T-1&&(l||s.status===429)){f=new b(s.status,k,m,_);continue}throw new b(s.status,k,m,_)}catch(s){if(s instanceof b||s instanceof S)throw s;if(this.abortController.signal.aborted)throw new i("SDK destroyed");if(C<T-1&&l){f=s;continue}let h=s instanceof Error?s.message:"Network error";throw new E(h)}finally{clearTimeout(p);}}throw f instanceof Error?f:new E("Request failed")}shouldRetry(e){return e===429||e>=500}isRetryablePath(e){return !e.includes("/checkout")}isReplaySafe(e,r){return e==="GET"||e==="POST"&&r==="/prefilled-checkouts"}calculateRetryDelay(e){let r=B*Math.pow(2,e);return Math.min(r,z)*(.5+Math.random()*.5)}sleep(e){return new Promise(r=>setTimeout(r,e))}normalizePrefilledLines(e){if(!u(e))throw new i("createPrefilledCheckout requires an input object");if(!e.externalReference)throw new i("createPrefilledCheckout requires externalReference for backend idempotency");if(e.line!=null&&e.lines!=null)throw new i("Use either line or lines for createPrefilledCheckout, not both");if(e.line!=null)return [e.line];if(e.lines==null||e.lines.length===0)throw new i("createPrefilledCheckout requires at least one line");return e.lines}async prefilledIdempotencyKey(e){if(/^[A-Za-z0-9._:-]{1,120}$/.test(e))return `prefilled:${e}:cart`;let r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return `prefilled#${Array.from(new Uint8Array(r),o=>o.toString(16).padStart(2,"0")).join("")}:cart`}checkoutOrigin(){return new URL(this.checkoutBaseUrl).origin}assertCopeOrigin(e,r,n){let o=new URL(e).origin;if(o!==r)throw new i(`${n} origin ${o} does not match expected ${r}`)}resolveMountTarget(e){if(e==null)throw new i("mountCheckout requires options.target");if(typeof e!="string")return e;let r=document.querySelector(e);if(r instanceof HTMLElement)return r;throw new i(`mountCheckout target "${e}" was not found`)}normalizeMountCheckoutArgs(e,r,n){if(this.isMountableCheckout(e))return {checkout:e,options:r};if(!this.isMountableCheckout(r))throw new i("mountCheckout requires a checkout with embedCheckoutUrl");return {checkout:r,options:{...n,target:e}}}isMountableCheckout(e){return u(e)?typeof e.embedCheckoutUrl=="string":false}get storageEnabled(){return this.persistence==="localStorage"&&typeof localStorage<"u"}hydrateFromStorage(){if(this.storageEnabled)try{let e=localStorage.getItem(x);if(e){let r=JSON.parse(e);this.cachedCartId=r.cartId??null,this.cachedSecret=r.secret??null;}}catch{}}persistToStorage(){if(this.storageEnabled)try{localStorage.setItem(x,JSON.stringify({cartId:this.cachedCartId,secret:this.cachedSecret}));}catch{}}clearStorage(){if(this.cachedCartId=null,this.cachedSecret=null,!!this.storageEnabled)try{localStorage.removeItem(x);}catch{}}};
2
- exports.CHECKOUT_EMBED_ERROR_CODES=U;exports.CHECKOUT_EMBED_TERMINAL_STATUSES=v;exports.CopeApiError=b;exports.CopeCart=A;exports.CopeCartExpiredError=S;exports.CopeError=i;exports.CopeNetworkError=E;//# sourceMappingURL=index.cjs.map
1
+ 'use strict';var i=class extends Error{constructor(e){super(e),this.name="CopeError";}},E=class extends i{code;status;field;requestId;retryAfter;errors;constructor(e,r,n,o){let a=r[0];super(a?.message??`API error ${e}`),this.name="CopeApiError",this.status=e,this.code=a?.code??"unknown",this.field=a?.field,this.requestId=n??null,this.retryAfter=o??null,this.errors=r;}},w=class extends i{constructor(e){super(e),this.name="CopeNetworkError";}},T=class extends i{state;constructor(e){super(`Cart is ${e} and cannot be used`),this.name="CopeCartExpiredError",this.state=e;}},U=["not_found","load_failed","embed_not_allowed","confirm_failed"],x=["expired","completed","already_completed","cancelled","processing"];var N=new Set(["ordered","expired","abandoned","merged"]),$="https://app.cope.com",A="cope_sdk_cart",H=8e3,B=500,z=5e3,F="cope.checkout",V=1,G="copecart.checkout.mount",j=1,Y=250,J=20,W=8e3,X=new Set(["oto-v1"]);function y(t){return encodeURIComponent(String(t))}function u(t){return typeof t=="object"&&t!==null}function Z(t){try{let e=new URL(t);return e.pathname==="/"&&!e.search&&!e.hash&&!e.username&&!e.password?e.origin:null}catch{return null}}var Q={payload_too_large:"Request body is too large",rate_limited:"Too many requests",upstream_unavailable:"The checkout service is unavailable",gateway_timeout:"The checkout service did not answer in time",not_found:"Not found"};function ee(t){return u(t)?Array.isArray(t.errors)&&t.errors.length>0&&t.errors.every(te)?t.errors:typeof t.error=="string"&&t.error.length>0?/^[a-z][a-z0-9_]*$/.test(t.error)?[{code:t.error,message:Q[t.error]??t.error}]:[{code:"unknown",message:t.error}]:null:null}function te(t){return u(t)&&typeof t.code=="string"&&typeof t.message=="string"}var re=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],ne=/^[A-Z][a-z]{2} ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;function oe([,t,e,r,n,o,a]){let c=[Number(a),re.indexOf(t??""),Number((e??"").trim()),Number(r),Number(n),Number(o)],k=Date.UTC(c[0],c[1],c[2],c[3],c[4],c[5]),p=new Date(k);return [p.getUTCFullYear(),p.getUTCMonth(),p.getUTCDate(),p.getUTCHours(),p.getUTCMinutes(),p.getUTCSeconds()].every((m,h)=>m===c[h])?k:Number.NaN}function ae(t){if(t===null)return null;let e=t.trim();if(/^\d+$/.test(e))return Number(e);let r=ne.exec(e),n=r?oe(r):/ GMT$/.test(e)?Date.parse(e):Number.NaN;return Number.isNaN(n)?null:Math.max(0,Math.ceil((n-Date.now())/1e3))}function ie(t){return u(t)&&typeof t.request_id=="string"?t.request_id:null}function se(t){if(!u(t))return {applicable:false,terms_version:null};let e=t.terms_version;return t.applicable!==true||typeof e!="string"||!X.has(e)?{applicable:false,terms_version:null}:{applicable:true,terms_version:e}}function _(t){return {...t,one_click_charge_consent:se(t.one_click_charge_consent)}}var le=new Set(U),ce=new Set(x);function de(t){return le.has(t)}function ue(t){return ce.has(t)}function ye(t){return t==="upsell"||t==="downsell"}function pe(t){return t==="accepted"||t==="declined"||t==="failed_not_charged"||t==="requires_action"}function he(t){return t==="not_charged"||t==="charged"||t==="requires_action"||t==="failed"}function me(t){return t==="completed"||t==="declined"||t==="expired"||t==="unavailable"}function ge(){let t=window.location;if(t.origin)return t.origin;let e=t.host||[t.hostname,t.port].filter(Boolean).join(":");return new URL(`${t.protocol}//${e}`).origin}function Ce(t){let e=new URL(t),r=new URL(t),n=e.pathname.match(/^\/checkout\/embed\/([^/]+)$/),o=e.pathname.match(/^\/checkout\/([^/]+)$/);if(n)return e.pathname=`/checkout/${n[1]}`,r.pathname=`/checkout/embed/${n[1]}`,{checkoutUrl:e.toString(),embedCheckoutUrl:r.toString()};if(o)return e.pathname=`/checkout/${o[1]}`,r.pathname=`/checkout/embed/${o[1]}`,{checkoutUrl:e.toString(),embedCheckoutUrl:r.toString()};throw new i("Checkout URL path cannot be mapped to COPE checkout routes")}function D(t){let r=new URL(t).pathname.match(/^\/checkout\/embed\/([^/]+)$/);if(!r)throw new i("Checkout embed URL path cannot be mapped to COPE checkout embed route");return decodeURIComponent(r[1])}function fe(t){let e=t.embed;if(u(e)&&e.allowed===true&&typeof e.parent_origin=="string")try{return new URL(e.parent_origin).origin===e.parent_origin?e.parent_origin:void 0}catch{return}}function _e(t){if(!u(t)||t.source!==F||t.version!==V)return null;let e={...typeof t.messageId=="string"?{messageId:t.messageId}:{},...typeof t.timestamp=="string"?{timestamp:t.timestamp}:{}};if(t.type==="ready")return {type:"ready",...e};if(t.type==="resize"){if(!u(t.payload))return null;let{height:r}=t.payload;return typeof r!="number"||!Number.isFinite(r)||r<=0?null:{type:"resize",payload:{height:Math.ceil(r)},...e}}if(t.type==="error"){if(!u(t.payload))return null;let{code:r,retryable:n}=t.payload;return typeof r!="string"||!de(r)||typeof n!="boolean"?null:{type:"error",payload:{code:r,retryable:n},...e}}if(t.type==="terminal"){if(!u(t.payload))return null;let{status:r}=t.payload;return typeof r!="string"||!ue(r)?null:{type:"terminal",payload:{status:r},...e}}if(t.type==="post_purchase_offer"){if(!u(t.payload))return null;let{offerRef:r,stepKind:n,title:o,priceGrossMinorUnits:a,currency:c}=t.payload;return typeof r!="string"||!r||typeof n!="string"||!ye(n)||typeof o!="string"||!o||typeof a!="number"||!Number.isFinite(a)||a<0||typeof c!="string"||!c?null:{type:"post_purchase_offer",payload:{offerRef:r,stepKind:n,title:o,priceGrossMinorUnits:a,currency:c},...e}}if(t.type==="post_purchase_decision"){if(!u(t.payload))return null;let{offerRef:r,status:n,chargeStatus:o}=t.payload;return typeof r!="string"||!r||typeof n!="string"||!pe(n)||typeof o!="string"||!he(o)?null:{type:"post_purchase_decision",payload:{offerRef:r,status:n,chargeStatus:o},...e}}if(t.type==="post_purchase_terminal"){if(!u(t.payload))return null;let{status:r}=t.payload;return typeof r!="string"||!me(r)?null:{type:"post_purchase_terminal",payload:{status:r},...e}}return null}var v=class{publishableKey;baseUrl;checkoutBaseUrl;persistence;abortController;mountCleanups=new Set;cachedSecret=null;cachedCartId=null;get currentCartId(){return this.cachedCartId}constructor(e){if(!e.publishableKey?.startsWith("cope_pk_"))throw new i('Invalid publishableKey. Must start with "cope_pk_".');if(typeof window<"u"&&window.location?.protocol==="http:"&&window.location.hostname!=="localhost")throw new i("COPE SDK requires HTTPS. Refusing to operate on an insecure page.");this.publishableKey=e.publishableKey;let r=Z(e.baseUrl??$);if(r===null)throw new i('Invalid baseUrl. Pass an origin such as "https://app.cope.com"; the SDK adds /api/cart/v1 itself.');this.baseUrl=r,this.checkoutBaseUrl=e.checkoutBaseUrl?e.checkoutBaseUrl.replace(/\/+$/,""):new URL(this.baseUrl).origin,this.abortController=new AbortController,this.persistence=e.cart?"none":e.persistence??"localStorage",e.cart?(this.cachedCartId=e.cart.id,this.cachedSecret=e.cart.secret):this.hydrateFromStorage();}async createCart(e){return this.createCartRequest(e,{skipAuth:true})}async getProduct(e){return this.get(`/products/${y(e)}`,{skipAuth:true})}async getCart(e){let r=_(await this.get(`/carts/${y(e)}`));if(N.has(r.state))throw this.clearStorage(),new T(r.state);return r}async addLine(e,r){return _(await this.post(`/carts/${y(e)}/lines`,r))}async updateLine(e,r,n){return _(await this.patch(`/carts/${y(e)}/lines/${y(r)}`,n))}async removeLine(e,r){return _(await this.del(`/carts/${y(e)}/lines/${y(r)}`))}async setBuyerIdentity(e,r){return _(await this.patch(`/carts/${y(e)}/buyer-identity`,r))}async reprice(e){return _(await this.post(`/carts/${y(e)}/reprice`,{}))}async applyPromoCode(e,r){return _(await this.post(`/carts/${y(e)}/promo_code`,{code:r}))}async removePromoCode(e){return _(await this.del(`/carts/${y(e)}/promo_code`))}async checkout(e,r,n={}){return this.createCheckoutResult(e,r,n.idempotencyKey?{idempotencyKey:n.idempotencyKey}:void 0)}async createPrefilledCheckout(e){let r=this.normalizePrefilledLines(e),n=await this.post("/prefilled-checkouts",{external_reference:e.externalReference,currency:e.currency,...e.locale!=null?{locale:e.locale}:{},...e.attribution?{attribution:e.attribution}:{},...e.intendedPaymentMethod!=null?{intended_payment_method:e.intendedPaymentMethod}:{},...e.metadata?{metadata:e.metadata}:{},...e.collectBuyerIdentityAtCheckout!=null?{collect_buyer_identity_at_checkout:e.collectBuyerIdentityAtCheckout}:{},lines:r.map(a=>({product_id:a.productId,...a.planId!==void 0?{plan_id:a.planId}:{},...a.quantity!==void 0?{quantity:a.quantity}:{}})),buyer_identity:e.buyerIdentity,checkout:e.checkout},{skipAuth:true,idempotencyKey:await this.prefilledIdempotencyKey(e.externalReference)}),o=this.normalizeCheckoutResult(n);return {cartId:n.cart_id,cartVersion:n.cart_version,checkoutId:o.checkoutId,checkoutToken:n.checkout_token??D(o.embedCheckoutUrl),checkoutUrl:o.checkoutUrl,embedCheckoutUrl:o.embedCheckoutUrl,...o.embedOrigin?{embedOrigin:o.embedOrigin}:{},status:o.status,expiresAt:o.expiresAt,totals:o.totals,...e.intendedPaymentMethod!=null?{intendedPaymentMethod:e.intendedPaymentMethod}:{},...n.field_errors?.length?{fieldErrors:n.field_errors}:{}}}async cancelCheckout(e){await this.requestVoid("POST",`/checkouts/${y(e)}/cancel`);}redirectToCheckout(e){this.abortController.signal.aborted||typeof window<"u"&&(window.location.href=e.checkoutUrl);}mountCheckout(e,r,n={}){if(this.abortController.signal.aborted)throw new i("SDK destroyed");if(typeof window>"u"||typeof document>"u")throw new i("mountCheckout requires a browser environment");let{checkout:o,options:a}=this.normalizeMountCheckoutArgs(e,r,n),c=this.checkoutOrigin();if(this.assertCopeOrigin(o.embedCheckoutUrl,c,"Checkout embed URL"),!o.embedOrigin)throw new i("mountCheckout requires checkout.embedOrigin \u2014 send embed_origin on checkout() or on create-offer, and register the origin in dashboard Settings");let k;if(a.fallback==="redirect"){if(!o.checkoutUrl)throw new i("mountCheckout with fallback: 'redirect' requires checkout.checkoutUrl \u2014 for an offer, pass its public_url");this.assertCopeOrigin(o.checkoutUrl,c,"Checkout URL"),k=o.checkoutUrl;}let p=this.resolveMountTarget(a.target),l=document.createElement("iframe"),m=new URL(o.embedCheckoutUrl).origin,h={type:G,version:j,token:D(o.embedCheckoutUrl)},M=ge(),g=false,S=false,s=0,C,b;if(o.embedOrigin!==M)throw new i(`Checkout embed origin ${o.embedOrigin} does not match current origin ${M}`);l.src=o.embedCheckoutUrl,l.title=a.title??"COPE checkout",l.setAttribute("allow","payment *"),l.referrerPolicy="no-referrer",l.style.border="0",l.style.width="100%",l.style.minHeight="720px",a.className&&(l.className=a.className);let f=()=>{C!==void 0&&(window.clearInterval(C),C=void 0);},P=()=>{b!==void 0&&(window.clearTimeout(b),b=void 0);},O=()=>{g||(g=true,f(),P(),window.removeEventListener("message",q),l.removeEventListener("load",K),l.remove(),this.mountCleanups.delete(O));},I=()=>{if(!(g||S)){if(s>=J){f();return}s+=1,l.contentWindow?.postMessage(h,m);}},L=()=>{g||(s=0,f(),I(),C=window.setInterval(I,Y));},K=()=>{g||(S=false,L());},q=R=>{if(g||R.origin!==m||R.source!==l.contentWindow)return;let d=_e(R.data);d&&(a.onMessage?.(d),d.type==="ready"?(S=true,f(),P(),a.onReady?.()):d.type==="resize"?(l.style.height=`${d.payload.height}px`,a.onResize?.(d.payload)):d.type==="error"?(f(),P(),a.onError?.(d.payload)):d.type==="terminal"?(f(),P(),a.onTerminal?.(d.payload),d.payload.status==="completed"?a.onSuccess?.():d.payload.status==="cancelled"&&a.onCancel?.()):d.type==="post_purchase_offer"?a.onPostPurchaseOffer?.(d.payload):d.type==="post_purchase_decision"?a.onPostPurchaseDecision?.(d.payload):d.type==="post_purchase_terminal"&&a.onPostPurchaseTerminal?.(d.payload));};window.addEventListener("message",q),l.addEventListener("load",K),this.mountCleanups.add(O);try{p.appendChild(l),L(),b=window.setTimeout(()=>{if(!(g||S)){if(a.fallback==="redirect"){a.onFallbackRedirect?.(),window.location.href=k;return}a.onError?.({code:"load_failed",retryable:!0});}},a.readyTimeoutMs??W);}catch(R){throw O(),R}return {iframe:l,destroy:O}}destroy(){for(let e of Array.from(this.mountCleanups))e();this.abortController.abort(),this.clearStorage();}async createCartRequest(e,r){let n=await this.post("/carts",e,r),o=_({...n,lines:n.lines??[],buyer_identity:n.buyer_identity??null});return o.cart_secret&&(this.cachedCartId=o.id,this.cachedSecret=o.cart_secret,this.persistToStorage()),o}async createCheckoutResult(e,r,n){let o=await this.post(`/carts/${y(e)}/checkout`,r,n);return this.normalizeCheckoutResult(o)}normalizeCheckoutResult(e){let r=e.checkout_url.startsWith("http")?e.checkout_url:`${this.checkoutBaseUrl}${e.checkout_url}`;this.assertCopeOrigin(r,this.checkoutOrigin(),"Checkout URL");let n=Ce(r);return {checkoutId:e.checkout_id,checkoutUrl:n.checkoutUrl,embedCheckoutUrl:n.embedCheckoutUrl,embedOrigin:fe(e),status:e.status,expiresAt:e.expires_at,totals:e.totals}}async requestVoid(e,r,n){await this.request(e,r,n);}async get(e,r){return this.request("GET",e,void 0,r)}async post(e,r,n){return this.request("POST",e,r,n)}async patch(e,r){return this.request("PATCH",e,r)}async del(e){return this.request("DELETE",e)}async request(e,r,n,o){if(this.abortController.signal.aborted)throw new i("SDK destroyed");let a=`${this.baseUrl}/api/cart/v1${r}`,c={Accept:"application/json","X-Cope-Key":this.publishableKey};n!==void 0&&(c["Content-Type"]="application/json"),!o?.skipAuth&&this.cachedSecret&&(c.Authorization=`CartSecret ${this.cachedSecret}`);let k=e==="POST"?o?.idempotencyKey??crypto.randomUUID():void 0;k&&(c["Idempotency-Key"]=k);let p=e==="GET"||e==="POST"&&this.isRetryablePath(r)?2:1,l=this.isReplaySafe(e,r),m;for(let h=0;h<p;h++){if(h>0){let s=this.calculateRetryDelay(h-1);await this.sleep(s);}let M=new AbortController,g=setTimeout(()=>M.abort(),H),S=AbortSignal.any([this.abortController.signal,M.signal]);try{let s=await fetch(a,{method:e,headers:c,body:n!==void 0?JSON.stringify(n):void 0,signal:S});if(s.ok)return s.status===204?void 0:await s.json();let C=await s.json().catch(()=>null),b=ee(C)??[{code:"unknown",message:s.statusText}],f=s.headers.get("X-Request-Id")??ie(C),P=ae(s.headers.get("Retry-After"));if(this.shouldRetry(s.status)&&h<p-1&&(l||s.status===429)){m=new E(s.status,b,f,P);continue}throw new E(s.status,b,f,P)}catch(s){if(s instanceof E||s instanceof T)throw s;if(this.abortController.signal.aborted)throw new i("SDK destroyed");if(h<p-1&&l){m=s;continue}let C=s instanceof Error?s.message:"Network error";throw new w(C)}finally{clearTimeout(g);}}throw m instanceof Error?m:new w("Request failed")}shouldRetry(e){return e===429||e>=500}isRetryablePath(e){return !e.includes("/checkout")}isReplaySafe(e,r){return e==="GET"||e==="POST"&&r==="/prefilled-checkouts"}calculateRetryDelay(e){let r=B*Math.pow(2,e);return Math.min(r,z)*(.5+Math.random()*.5)}sleep(e){return new Promise(r=>setTimeout(r,e))}normalizePrefilledLines(e){if(!u(e))throw new i("createPrefilledCheckout requires an input object");if(!e.externalReference)throw new i("createPrefilledCheckout requires externalReference for backend idempotency");if(e.line!=null&&e.lines!=null)throw new i("Use either line or lines for createPrefilledCheckout, not both");if(e.line!=null)return [e.line];if(e.lines==null||e.lines.length===0)throw new i("createPrefilledCheckout requires at least one line");return e.lines}async prefilledIdempotencyKey(e){if(/^[A-Za-z0-9._:-]{1,120}$/.test(e))return `prefilled:${e}:cart`;let r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return `prefilled#${Array.from(new Uint8Array(r),o=>o.toString(16).padStart(2,"0")).join("")}:cart`}checkoutOrigin(){return new URL(this.checkoutBaseUrl).origin}assertCopeOrigin(e,r,n){let o=new URL(e).origin;if(o!==r)throw new i(`${n} origin ${o} does not match expected ${r}`)}resolveMountTarget(e){if(e==null)throw new i("mountCheckout requires options.target");if(typeof e!="string")return e;let r=document.querySelector(e);if(r instanceof HTMLElement)return r;throw new i(`mountCheckout target "${e}" was not found`)}normalizeMountCheckoutArgs(e,r,n){if(this.isMountableCheckout(e))return {checkout:e,options:r};if(!this.isMountableCheckout(r))throw new i("mountCheckout requires a checkout with embedCheckoutUrl");return {checkout:r,options:{...n,target:e}}}isMountableCheckout(e){return u(e)?typeof e.embedCheckoutUrl=="string":false}get storageEnabled(){return this.persistence==="localStorage"&&typeof localStorage<"u"}hydrateFromStorage(){if(this.storageEnabled)try{let e=localStorage.getItem(A);if(e){let r=JSON.parse(e);this.cachedCartId=r.cartId??null,this.cachedSecret=r.secret??null;}}catch{}}persistToStorage(){if(this.storageEnabled)try{localStorage.setItem(A,JSON.stringify({cartId:this.cachedCartId,secret:this.cachedSecret}));}catch{}}clearStorage(){if(this.cachedCartId=null,this.cachedSecret=null,!!this.storageEnabled)try{localStorage.removeItem(A);}catch{}}},ke=v;
2
+ exports.CHECKOUT_EMBED_ERROR_CODES=U;exports.CHECKOUT_EMBED_TERMINAL_STATUSES=x;exports.Cope=v;exports.CopeApiError=E;exports.CopeCart=ke;exports.CopeCartExpiredError=T;exports.CopeError=i;exports.CopeNetworkError=w;//# sourceMappingURL=index.cjs.map
3
3
  //# sourceMappingURL=index.cjs.map