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 +74 -1
- package/README.md +94 -41
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -10
- package/dist/index.d.ts +55 -10
- package/dist/index.global.js +1 -2
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/node.cjs +59 -20
- package/dist/mcp/node.cjs.map +1 -1
- package/dist/mcp/node.js +59 -20
- package/dist/mcp/node.js.map +1 -1
- package/dist/mcp.cjs +59 -20
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +59 -20
- package/dist/mcp.js.map +1 -1
- package/package.json +2 -1
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
|
-
## [
|
|
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 {
|
|
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.
|
|
51
|
+
<script src="https://unpkg.com/cope-sdk@0.1.1/dist/index.global.js"></script>
|
|
52
52
|
<script>
|
|
53
|
-
const cope = new
|
|
53
|
+
const cope = new Cope({ publishableKey: 'cope_pk_...' })
|
|
54
54
|
</script>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
> When loaded via CDN, the
|
|
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 {
|
|
67
|
+
import { Cope, CopeApiError } from 'cope-sdk'
|
|
63
68
|
|
|
64
69
|
// 1. Initialize
|
|
65
|
-
const cope = new
|
|
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 {
|
|
121
|
+
import { Cope } from 'cope-sdk'
|
|
117
122
|
|
|
118
|
-
const cope = new
|
|
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 {
|
|
199
|
+
import { Cope } from 'cope-sdk'
|
|
195
200
|
|
|
196
|
-
const cope = new
|
|
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.
|
|
209
|
+
<script src="https://unpkg.com/cope-sdk@0.1.1/dist/index.global.js"></script>
|
|
205
210
|
<script>
|
|
206
|
-
const cope = new
|
|
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 {
|
|
222
|
+
import { Cope, CopeApiError, CopeCartExpiredError } from 'cope-sdk'
|
|
218
223
|
|
|
219
|
-
const cope = new
|
|
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
|
|
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
|
|
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:
|
|
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. `
|
|
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: '
|
|
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
|
-
|
|
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:
|
|
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
|
-
| `
|
|
924
|
-
| `
|
|
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
|
-
| `
|
|
930
|
-
| `
|
|
931
|
-
| `
|
|
932
|
-
| `
|
|
933
|
-
| `
|
|
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
|
-
| `
|
|
936
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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=
|
|
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
|