@chaindaddy/js 0.3.3 → 0.3.5

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/dist/api.d.ts CHANGED
@@ -27,6 +27,50 @@ export interface paths {
27
27
  patch?: never;
28
28
  trace?: never;
29
29
  };
30
+ "/api/v2/public/signed/coins/{chain}/{symbol}": {
31
+ parameters: {
32
+ query?: never;
33
+ header?: never;
34
+ path?: never;
35
+ cookie?: never;
36
+ };
37
+ /**
38
+ * Get a coin's registration, signed
39
+ * @description **Not available yet.** It answers 404 until Chain Daddy turns it on; the record on
40
+ * `chaindaddy.eth` and `verifyAnswer` arrive with it.
41
+ *
42
+ * Whether a ticker is registered on a chain and which wallet holds the registration, signed by
43
+ * Chain Daddy so you can check it wherever you got it from: a cache, a gateway, or another site's
44
+ * server. `jws` is the signed answer (a compact JWS); `claims` repeats what it says so you can read
45
+ * it, but trust only what you get out of `jws` after verifying it.
46
+ *
47
+ * `status` is `registered` (the ticker has a verified page on that chain), `not_registered` (it
48
+ * has none) or `removed` (it has one and Chain Daddy took it down). All three answer 200 and are
49
+ * signed, so nobody between you and Chain Daddy can make up a missing registration. Only
50
+ * `registered` carries `tokenAddress`, `crownId`, `holder`, `pageUrl` and, when known,
51
+ * `registeredAt`.
52
+ *
53
+ * To verify: split `jws` at the dots; the first part is the header (`alg` must be `EdDSA`, and
54
+ * `kid` names the key), the second the claims. Check the Ed25519 signature over the first two
55
+ * parts against the key with that `kid`, then check `typ` is `cd-answer+v1`, `iss` is
56
+ * `https://chaindaddy.io` and `exp` has not passed. `verifyAnswer` in `@chaindaddy/js/verify` does
57
+ * all of it. The keys are at `GET /api/v2/iap/jwks` (the keys receipts are signed with). They are
58
+ * also named on `chaindaddy.eth`, in the text record `io.chaindaddy.signing-keys`: `ed25519:<x>`
59
+ * entries separated by a space, the current key first, where `<x>` is the key's `x` in the key
60
+ * set. Read the record yourself and you do not depend on this API for the keys either.
61
+ *
62
+ * An answer is valid for 15 minutes (`exp`), and caches keep it for less than that. Keyless, the
63
+ * same for every caller.
64
+ */
65
+ get: operations["getPublicSignedCoin"];
66
+ put?: never;
67
+ post?: never;
68
+ delete?: never;
69
+ options?: never;
70
+ head?: never;
71
+ patch?: never;
72
+ trace?: never;
73
+ };
30
74
  "/api/v2/public/coins/{chain}/{symbol}/apps": {
31
75
  parameters: {
32
76
  query?: never;
@@ -138,7 +182,8 @@ export interface paths {
138
182
  /**
139
183
  * Receipt signing keys (JWKS)
140
184
  * @description The Ed25519 public keys receipts are signed with (the current key and the previous one), as a JSON Web Key Set.
141
- * Match a receipt's `kid` header to a key and verify its EdDSA signature offline.
185
+ * Match a receipt's `kid` header to a key and verify its EdDSA signature offline. Signed answers
186
+ * (`GET /api/v2/public/signed/coins/{chain}/{symbol}`) are signed with the same keys.
142
187
  */
143
188
  get: operations["getIapJwks"];
144
189
  put?: never;
@@ -987,16 +1032,18 @@ export interface paths {
987
1032
  * goes are the item's own, exactly as when it is bought on chaindaddy.io: a session changes none of them.
988
1033
  *
989
1034
  * `successUrl` and `cancelUrl` must be `https` pages on one of the coin's verified sites, or a subdomain of
990
- * one (400 `RETURN_URL_NOT_VERIFIED`, which lists the sites that pass). The wallet that opened the store
991
- * must have signed the Seller Terms (403 `SELLER_TERMS_REQUIRED`).
1035
+ * one (400 `RETURN_URL_NOT_VERIFIED`, which lists the sites that pass). A test store's key
1036
+ * (`cd_iap_test_…`) may also use `http://localhost` or `http://127.0.0.1`, on any port: test mode works on
1037
+ * localhost, live needs a verified site. The wallet that opened the store must have signed the Seller
1038
+ * Terms (403 `SELLER_TERMS_REQUIRED`).
992
1039
  *
993
1040
  * An item of an app installed on the coin's page (its `sku` starts with `app.`) is sold through a session
994
1041
  * only when the app's developer has allowed that item to be sold outside the app; otherwise the call answers
995
1042
  * 409 `APP_ITEM_NOT_SOLD_HERE`, with a `reason`. The sale pays the same wallets as inside the app: the page,
996
1043
  * the fee and the developer's share. Such a session takes at most the app's per-order quantity.
997
1044
  *
998
- * Send an `Idempotency-Key`. A retry of the same request returns the session it opened, with status 200
999
- * and the header `Idempotent-Replayed: true`.
1045
+ * An `Idempotency-Key` is required. A retry of the same request returns the session it opened, with
1046
+ * status 200 and the header `Idempotent-Replayed: true`.
1000
1047
  *
1001
1048
  * Learn that the session was paid from the `iap.order.confirmed` webhook (its payload carries
1002
1049
  * `checkoutSessionId`), or by reading the session. Never from the buyer arriving on `successUrl`.
@@ -1054,12 +1101,13 @@ export interface paths {
1054
1101
  * needs no credential (the session id in the buyer's link is enough) and never carries the session's
1055
1102
  * `metadata` or `clientReferenceId`. The answer is never cached.
1056
1103
  *
1057
- * When the item belongs to an app installed on the coin's page, `sku` names the app (`appId`) and carries
1058
- * nothing else of the app's: no `appBps`, `appTermsVersion`, `appItemKey` or `appRole`, and an empty
1059
- * `metadata`.
1104
+ * When the item belongs to an app installed on the coin's page, `sku` names the app (`appId`) and the
1105
+ * share of the price its developer gets (`appBps`, basis points of the price after the fee), and carries
1106
+ * nothing else of the app's: no `appTermsVersion`, `appItemKey` or `appRole`, and an empty `metadata`.
1060
1107
  *
1061
1108
  * Pass `origin` to learn whether a page on that origin may open this checkout in a popup
1062
- * (`openerAllowed`): it must be `https` on one of the coin's verified sites.
1109
+ * (`openerAllowed`): it must be `https` on one of the coin's verified sites. A test store's session
1110
+ * (`livemode: false`) may also be opened from `http://localhost` or `http://127.0.0.1`, on any port.
1063
1111
  *
1064
1112
  * `returnAllowed` says whether the buyer may still be sent to `successUrl` and `cancelUrl`. It is checked
1065
1113
  * again on every read, so a coin whose site stopped being verified after the session was opened no longer
@@ -1074,6 +1122,48 @@ export interface paths {
1074
1122
  patch?: never;
1075
1123
  trace?: never;
1076
1124
  };
1125
+ "/api/v2/iap/checkout/sessions/{sessionId}/test-pay": {
1126
+ parameters: {
1127
+ query?: never;
1128
+ header?: never;
1129
+ path: {
1130
+ /** @description The checkout session's id, `cs_` followed by 48 hex characters */
1131
+ sessionId: components["parameters"]["IapCheckoutSessionId"];
1132
+ };
1133
+ cookie?: never;
1134
+ };
1135
+ get?: never;
1136
+ put?: never;
1137
+ /**
1138
+ * Complete a test checkout session
1139
+ * @description Completes a test store's checkout session from your own server or terminal, with no buyer signed in
1140
+ * and no wallet: the whole loop (open a session, pay it, get the webhook, read it `complete`) with
1141
+ * nothing but a test store's key. It makes the session's order for `wallet` and pays it as
1142
+ * `POST /api/v2/iap/orders/{orderId}/test-pay` does: the items are granted, the receipt is signed and the
1143
+ * test store's webhook gets `iap.order.confirmed` with `checkoutSessionId` and `"livemode": false`. No
1144
+ * chain is read or written and no money moves. A pay page left open on the session notices within a
1145
+ * few seconds and takes the buyer back.
1146
+ *
1147
+ * Call it with a key of the session's test store (`cd_iap_test_…`, scope `orders:write`). `wallet` is the
1148
+ * wallet the test items go to: required unless the session names one, and then it must be that wallet
1149
+ * (403 `CHECKOUT_SESSION_WALLET`). The answer is the session as `GET` returns it: `status: complete`,
1150
+ * the order and its receipt.
1151
+ *
1152
+ * `Idempotency-Key` is required. A retry of the same request returns the same order, with the header
1153
+ * `Idempotent-Replayed: true`; the key used for a different session or wallet is a 422.
1154
+ *
1155
+ * Test stores only. A live store's session is only ever paid by the buyer on chaindaddy.io: a live
1156
+ * store's key gets 409 `LIVE_ORDER` and nothing is created. A key of any other store gets 404. An item
1157
+ * of an installed app cannot be bought with a store key (403 `SESSION_REQUIRED`); pay that session on
1158
+ * its page. Guide: https://docs.chaindaddy.io/developer/take-payments-on-your-site#test-mode
1159
+ */
1160
+ post: operations["testPayIapCheckoutSession"];
1161
+ delete?: never;
1162
+ options?: never;
1163
+ head?: never;
1164
+ patch?: never;
1165
+ trace?: never;
1166
+ };
1077
1167
  "/api/v2/webhooks": {
1078
1168
  parameters: {
1079
1169
  query?: never;
@@ -1451,6 +1541,73 @@ export interface components {
1451
1541
  /** @description Machine-readable error code */
1452
1542
  code?: string;
1453
1543
  };
1544
+ /** @description A signed answer and what it says. Trust `claims` only as read out of `jws` after verifying it. */
1545
+ PublicSignedAnswer: {
1546
+ /** @description The signed answer: a compact JWS (header, claims and signature, base64url, joined by dots), signed with Ed25519 (`alg` `EdDSA`) by the key its header's `kid` names */
1547
+ jws: string;
1548
+ claims: components["schemas"]["SignedAnswerClaims"];
1549
+ };
1550
+ /** @description What a signed answer says. The same object is the payload of `jws`. */
1551
+ SignedAnswerClaims: {
1552
+ /**
1553
+ * @description Who signed it. Always `https://chaindaddy.io`.
1554
+ * @example https://chaindaddy.io
1555
+ */
1556
+ iss: string;
1557
+ /**
1558
+ * @description What kind of signed document this is. Always `cd-answer+v1`; a store receipt, signed by the same keys, is `iap-receipt+v1`.
1559
+ * @enum {string}
1560
+ */
1561
+ typ: "cd-answer+v1";
1562
+ /**
1563
+ * Format: int64
1564
+ * @description When it was signed, in Unix seconds
1565
+ */
1566
+ iat: number;
1567
+ /**
1568
+ * Format: int64
1569
+ * @description When it stops being valid, in Unix seconds, 15 minutes after `iat`. Do not act on it after this.
1570
+ */
1571
+ exp: number;
1572
+ /**
1573
+ * @description What the answer is about. `registration` is a ticker's registration on a chain.
1574
+ * @enum {string}
1575
+ */
1576
+ kind: "registration";
1577
+ /**
1578
+ * @description CAIP-2 chain ID
1579
+ * @example eip155:8453
1580
+ */
1581
+ chainId: string;
1582
+ /**
1583
+ * @description Chain key
1584
+ * @example base
1585
+ */
1586
+ chain: string;
1587
+ /** @description The ticker: as registered when `status` is `registered`, otherwise the one asked about, in upper case. Tickers match in any case, so compare without regard to case. */
1588
+ symbol: string;
1589
+ /**
1590
+ * @description `registered`: the ticker has a verified page on the chain. `not_registered`: it has none. `removed`: it has one, and Chain Daddy took it down.
1591
+ * @enum {string}
1592
+ */
1593
+ status: "registered" | "not_registered" | "removed";
1594
+ /** @description The registered token's address (lowercase on EVM chains). Only when `status` is `registered`. */
1595
+ tokenAddress?: string;
1596
+ /**
1597
+ * Format: int64
1598
+ * @description The registration's ID on that chain. Only when `status` is `registered`.
1599
+ */
1600
+ crownId?: number;
1601
+ /** @description The wallet that holds the registration (lowercase on EVM chains). Only when `status` is `registered`. */
1602
+ holder?: string;
1603
+ /**
1604
+ * Format: date-time
1605
+ * @description When the registration was made. Only when `status` is `registered`, and left out when not known.
1606
+ */
1607
+ registeredAt?: string;
1608
+ /** @description The coin's page on chaindaddy.io. Only when `status` is `registered`. */
1609
+ pageUrl?: string;
1610
+ };
1454
1611
  IapError: {
1455
1612
  /** @description Human-readable description */
1456
1613
  error: string;
@@ -2228,7 +2385,8 @@ export interface components {
2228
2385
  /** @description Whether a page on the `origin` passed may open this checkout in a popup. False when no `origin` is passed */
2229
2386
  openerAllowed?: boolean;
2230
2387
  /**
2231
- * @description Whether `successUrl` and `cancelUrl` are both still on the coin's verified sites, as of this read.
2388
+ * @description Whether `successUrl` and `cancelUrl` are both still on the coin's verified sites, as of this read
2389
+ * (for a test store's session, a URL on `http://localhost` or `http://127.0.0.1` also counts).
2232
2390
  * When false the buyer is not sent to either
2233
2391
  */
2234
2392
  returnAllowed?: boolean;
@@ -2805,7 +2963,7 @@ export interface components {
2805
2963
  "application/json": components["schemas"]["IapError"];
2806
2964
  };
2807
2965
  };
2808
- /** @description No wallet session */
2966
+ /** @description `AUTHENTICATION_REQUIRED`: no credential at all. Sign in, or send the credential the operation names */
2809
2967
  IapUnauthorized: {
2810
2968
  headers: {
2811
2969
  [name: string]: unknown;
@@ -2813,8 +2971,8 @@ export interface components {
2813
2971
  content: {
2814
2972
  /**
2815
2973
  * @example {
2816
- * "error": "sign in first",
2817
- * "code": "UNAUTHORIZED"
2974
+ * "error": "authentication required",
2975
+ * "code": "AUTHENTICATION_REQUIRED"
2818
2976
  * }
2819
2977
  */
2820
2978
  "application/json": components["schemas"]["IapError"];
@@ -3101,6 +3259,96 @@ export interface operations {
3101
3259
  };
3102
3260
  };
3103
3261
  };
3262
+ getPublicSignedCoin: {
3263
+ parameters: {
3264
+ query?: never;
3265
+ header?: never;
3266
+ path: {
3267
+ /** @description Chain key, lowercase, as in the page's own address (e.g. `base`, `solana`). A numeric chain ID answers 404. */
3268
+ chain: string;
3269
+ /** @description The coin's ticker (letters and digits, any case) */
3270
+ symbol: string;
3271
+ };
3272
+ cookie?: never;
3273
+ };
3274
+ requestBody?: never;
3275
+ responses: {
3276
+ /** @description The signed answer, whatever the status */
3277
+ 200: {
3278
+ headers: {
3279
+ [name: string]: unknown;
3280
+ };
3281
+ content: {
3282
+ "application/json": components["schemas"]["PublicSignedAnswer"];
3283
+ };
3284
+ };
3285
+ /** @description The request came with the user agent of an AI or LLM crawler, and no cached answer was there to serve instead. Never sent to a browser or to a server using its own user agent. */
3286
+ 403: {
3287
+ headers: {
3288
+ [name: string]: unknown;
3289
+ };
3290
+ content: {
3291
+ /**
3292
+ * @example {
3293
+ * "error": "ai_bot_blocked",
3294
+ * "message": "AI/LLM scraping is not permitted. Use our API with a developer-tier API key for programmatic access."
3295
+ * }
3296
+ */
3297
+ "application/json": components["schemas"]["Error"];
3298
+ };
3299
+ };
3300
+ /** @description An unknown chain, or a ticker that is not letters and digits. Not signed; there is nothing to attest. */
3301
+ 404: {
3302
+ headers: {
3303
+ [name: string]: unknown;
3304
+ };
3305
+ content: {
3306
+ /**
3307
+ * @example {
3308
+ * "error": "not_found",
3309
+ * "code": "not_found",
3310
+ * "message": "Not found."
3311
+ * }
3312
+ */
3313
+ "application/json": components["schemas"]["Error"];
3314
+ };
3315
+ };
3316
+ /** @description More than 400 requests in a minute from one IP address, across the whole API. Never cached. */
3317
+ 429: {
3318
+ headers: {
3319
+ /** @description Seconds to wait before trying again */
3320
+ "Retry-After"?: number;
3321
+ [name: string]: unknown;
3322
+ };
3323
+ content: {
3324
+ /**
3325
+ * @example {
3326
+ * "error": "Too many requests. Please try again later.",
3327
+ * "code": "rate_limit_exceeded",
3328
+ * "message": "Too many requests. Please try again later."
3329
+ * }
3330
+ */
3331
+ "application/json": components["schemas"]["Error"];
3332
+ };
3333
+ };
3334
+ /** @description The answer could not be read or signed right now. Never cached, never signed; try again. */
3335
+ 503: {
3336
+ headers: {
3337
+ [name: string]: unknown;
3338
+ };
3339
+ content: {
3340
+ /**
3341
+ * @example {
3342
+ * "error": "unavailable",
3343
+ * "code": "unavailable",
3344
+ * "message": "Unavailable right now. Try again."
3345
+ * }
3346
+ */
3347
+ "application/json": components["schemas"]["Error"];
3348
+ };
3349
+ };
3350
+ };
3351
+ };
3104
3352
  listPublicCoinApps: {
3105
3353
  parameters: {
3106
3354
  query?: never;
@@ -4873,7 +5121,8 @@ export interface operations {
4873
5121
  appAccountToken?: string;
4874
5122
  /**
4875
5123
  * @description The checkout session the buyer is paying. A buyer's session only: a store key gets 403
4876
- * `SESSION_REQUIRED`. 409 `CHECKOUT_SESSION_EXPIRED` or `CHECKOUT_SESSION_COMPLETE` when the
5124
+ * `SESSION_REQUIRED` (a test store's key completes its own test session with
5125
+ * `POST /api/v2/iap/checkout/sessions/{sessionId}/test-pay` instead). 409 `CHECKOUT_SESSION_EXPIRED` or `CHECKOUT_SESSION_COMPLETE` when the
4877
5126
  * session can no longer be paid; 403 `CHECKOUT_SESSION_WALLET` when it names another wallet.
4878
5127
  * A session takes one buyer at a time: 409 `CHECKOUT_SESSION_IN_USE` while another wallet has
4879
5128
  * an order for it that can still be paid, or is paid and being confirmed
@@ -5106,7 +5355,9 @@ export interface operations {
5106
5355
  }[];
5107
5356
  /**
5108
5357
  * Format: uri
5109
- * @description Where the buyer returns after paying. `https`, on one of the coin's verified sites
5358
+ * @description Where the buyer returns after paying. `https`, on one of the coin's verified sites or a
5359
+ * subdomain of one. With a test store's key it may also be `http://localhost` or
5360
+ * `http://127.0.0.1`, on any port
5110
5361
  */
5111
5362
  successUrl: string;
5112
5363
  /**
@@ -5154,8 +5405,10 @@ export interface operations {
5154
5405
  };
5155
5406
  };
5156
5407
  /**
5157
- * @description `INVALID_REQUEST`, `IDEMPOTENCY_KEY_REQUIRED`, or `RETURN_URL_NOT_VERIFIED`: a return URL is not an
5158
- * `https` page on one of the coin's verified sites
5408
+ * @description `INVALID_REQUEST`, `IDEMPOTENCY_KEY_REQUIRED`, `IDEMPOTENCY_KEY_INVALID` (a key longer than 255
5409
+ * characters), or `RETURN_URL_NOT_VERIFIED`: a return URL is not an
5410
+ * `https` page on one of the coin's verified sites (nor, with a test store's key, `http://localhost` or
5411
+ * `http://127.0.0.1`). A live store's key is always refused a localhost URL
5159
5412
  */
5160
5413
  400: {
5161
5414
  headers: {
@@ -5180,7 +5433,8 @@ export interface operations {
5180
5433
  };
5181
5434
  /**
5182
5435
  * @description `STORE_KEY_REQUIRED`: called with a signed-in session instead of a store key. `SCOPE_REQUIRED`: the key
5183
- * lacks `orders:write`. `STORE_NOT_LIVE`: the store is paused or its plan lapsed. `SELLER_TERMS_REQUIRED`:
5436
+ * lacks `orders:write`. `STORE_NOT_LIVE`: the store is paused or its plan lapsed, or (`reason`
5437
+ * `owner_changed`) its page has a new owner who has not opened it yet. `SELLER_TERMS_REQUIRED`:
5184
5438
  * the wallet that opened the store has not signed the Seller Terms
5185
5439
  */
5186
5440
  403: {
@@ -5289,7 +5543,7 @@ export interface operations {
5289
5543
  getIapPublicCheckoutSession: {
5290
5544
  parameters: {
5291
5545
  query?: {
5292
- /** @description The origin of the page that opened the checkout, e.g. `https://example.com` */
5546
+ /** @description The origin of the page that opened the checkout, e.g. `https://example.com` (or `http://localhost:3000` for a test store's session) */
5293
5547
  origin?: string;
5294
5548
  };
5295
5549
  header?: never;
@@ -5324,6 +5578,136 @@ export interface operations {
5324
5578
  };
5325
5579
  };
5326
5580
  };
5581
+ testPayIapCheckoutSession: {
5582
+ parameters: {
5583
+ query?: never;
5584
+ header: {
5585
+ /**
5586
+ * @description Required: a call without it answers 400 `IDEMPOTENCY_KEY_REQUIRED`. Send one value per attempt (a UUID,
5587
+ * or an id of your own such as `cart-8842-attempt-1`), up to 255 characters. Retrying with the same key
5588
+ * and the same request returns the first result and never sells, grants or consumes twice.
5589
+ */
5590
+ "Idempotency-Key": components["parameters"]["IdempotencyKeyRequired"];
5591
+ };
5592
+ path: {
5593
+ /** @description The checkout session's id, `cs_` followed by 48 hex characters */
5594
+ sessionId: components["parameters"]["IapCheckoutSessionId"];
5595
+ };
5596
+ cookie?: never;
5597
+ };
5598
+ requestBody?: {
5599
+ content: {
5600
+ /**
5601
+ * @example {
5602
+ * "wallet": "0x00000000000000000000000000000000000000d4"
5603
+ * }
5604
+ */
5605
+ "application/json": {
5606
+ /**
5607
+ * @description The wallet the test items go to: an address on the store's chain. Required unless the
5608
+ * session names a wallet; when it does, leave this out or send the same wallet
5609
+ */
5610
+ wallet?: string;
5611
+ };
5612
+ };
5613
+ };
5614
+ responses: {
5615
+ /** @description The session, complete, with the order that paid it and its receipt */
5616
+ 200: {
5617
+ headers: {
5618
+ /** @description Present, and `true`, on a retry of a request already made under this `Idempotency-Key` */
5619
+ "Idempotent-Replayed"?: "true";
5620
+ [name: string]: unknown;
5621
+ };
5622
+ content: {
5623
+ "application/json": components["schemas"]["IapCheckoutSession"];
5624
+ };
5625
+ };
5626
+ /**
5627
+ * @description `IDEMPOTENCY_KEY_REQUIRED`, or `INVALID_REQUEST`: no `wallet` and the session names none, a `wallet`
5628
+ * that is not an address on the store's chain, or a body that is not the JSON above
5629
+ */
5630
+ 400: {
5631
+ headers: {
5632
+ [name: string]: unknown;
5633
+ };
5634
+ content: {
5635
+ "application/json": components["schemas"]["IapError"];
5636
+ };
5637
+ };
5638
+ /**
5639
+ * @description `AUTHENTICATION_REQUIRED`: no credential at all; send the test store's key as
5640
+ * `Authorization: Bearer cd_iap_test_…`. `INVALID_STORE_KEY`: the key is unknown or revoked
5641
+ */
5642
+ 401: {
5643
+ headers: {
5644
+ [name: string]: unknown;
5645
+ };
5646
+ content: {
5647
+ "application/json": components["schemas"]["IapError"];
5648
+ };
5649
+ };
5650
+ /**
5651
+ * @description `STORE_KEY_REQUIRED`: called with a signed-in session instead of a store key. `SCOPE_REQUIRED`: the
5652
+ * key lacks `orders:write`. `CHECKOUT_SESSION_WALLET`: the session names another wallet.
5653
+ * `IAP_PLAN_REQUIRED` or `STORE_NOT_LIVE`: the store's plan lapsed or the store is paused, or (`reason`
5654
+ * `owner_changed`) its page has a new owner who has not opened it yet.
5655
+ * `SESSION_REQUIRED`: the session's item belongs to an installed app
5656
+ */
5657
+ 403: {
5658
+ headers: {
5659
+ [name: string]: unknown;
5660
+ };
5661
+ content: {
5662
+ "application/json": components["schemas"]["IapError"];
5663
+ };
5664
+ };
5665
+ /**
5666
+ * @description `NOT_FOUND`: no such session, or it belongs to another store. `TEST_MODE_DISABLED`: test stores are
5667
+ * not available here. `CHECKOUT_SESSIONS_DISABLED`: checkout sessions are not switched on here
5668
+ */
5669
+ 404: {
5670
+ headers: {
5671
+ [name: string]: unknown;
5672
+ };
5673
+ content: {
5674
+ "application/json": components["schemas"]["IapError"];
5675
+ };
5676
+ };
5677
+ /**
5678
+ * @description `LIVE_ORDER`: the session belongs to a live store. `CHECKOUT_SESSION_COMPLETE`: it is already paid.
5679
+ * `CHECKOUT_SESSION_EXPIRED`: it expired; open a new one. `CHECKOUT_SESSION_IN_USE`: another wallet
5680
+ * has an unpaid order for it; use that wallet, or wait a few minutes past that order's deadline.
5681
+ * `ORDER_EXPIRED`: the order this `Idempotency-Key` made has lapsed; use a new key
5682
+ */
5683
+ 409: {
5684
+ headers: {
5685
+ [name: string]: unknown;
5686
+ };
5687
+ content: {
5688
+ /**
5689
+ * @example {
5690
+ * "error": "this is a live checkout session: it is paid by the buyer on chaindaddy.io, and test-pay pays a test store's sessions only",
5691
+ * "code": "LIVE_ORDER"
5692
+ * }
5693
+ */
5694
+ "application/json": components["schemas"]["IapError"];
5695
+ };
5696
+ };
5697
+ /**
5698
+ * @description `IDEMPOTENCY_KEY_REUSED`: this `Idempotency-Key` already made an order for a different session or
5699
+ * wallet. Nothing is returned or created; use a new key
5700
+ */
5701
+ 422: {
5702
+ headers: {
5703
+ [name: string]: unknown;
5704
+ };
5705
+ content: {
5706
+ "application/json": components["schemas"]["IapError"];
5707
+ };
5708
+ };
5709
+ };
5710
+ };
5327
5711
  listWebhooks: {
5328
5712
  parameters: {
5329
5713
  query?: never;
package/dist/cd.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";(()=>{var R="0.3.3";function C(t){let e="";for(let i of t)e+=String.fromCharCode(i);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function w(){return C(crypto.getRandomValues(new Uint8Array(32)))}function T(t,e,i,o,r){let u=n=>{if(n&&typeof n=="object"&&n.type===t)try{i(n)}catch{}},l=n=>{n.origin===e&&(!r||n.source===r)&&u(n.data)};window.addEventListener("message",l);let a=null;if(o&&typeof BroadcastChannel=="function"&&e===location.origin)try{a=new BroadcastChannel(o),a.onmessage=n=>u(n.data)}catch{a=null}return()=>{window.removeEventListener("message",l),a?.close()}}var y={get(t){try{return sessionStorage.getItem(t)}catch{return null}},set(t,e){try{sessionStorage.setItem(t,e)}catch{}},remove(t){try{sessionStorage.removeItem(t)}catch{}}};var F="https://api.chaindaddy.io",G={"https://api.chaindaddy.io":"https://chaindaddy.io/_oauth/authorize","https://staging-api.chaindaddy.io":"https://staging.chaindaddy.io/_oauth/authorize"},b="cd-signin",U="cd:signin",v="cd.signin",_=300;async function K(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return C(new Uint8Array(e))}function W(t="openid"){let e=t.split(/\s+/).filter(Boolean);return(e.includes("openid")?e:["openid",...e]).join(" ")}async function Y(t,e,i){let o=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:W(t.scope),state:e.state,nonce:e.nonce,code_challenge:await K(e.verifier),code_challenge_method:"S256"});return t.claims&&o.set("claims",typeof t.claims=="string"?t.claims:JSON.stringify(t.claims)),`${i}?${o}`}function A(t,e){return{code:e,codeVerifier:t.verifier,state:t.state,nonce:t.nonce,issuer:t.issuer,clientId:t.clientId,redirectUri:t.redirectUri}}var m;function k(t){let e=(t.issuer??F).replace(/\/$/,""),i=t.authorizeUrl??G[e];if(!i)return Promise.reject(new Error("authorizeUrl is required for this issuer"));if(!t.clientId||!t.redirectUri)return Promise.reject(new Error("clientId and redirectUri are required"));if(m&&!m.popup.closed)return m.popup.focus(),m.waiting;let o={state:w(),nonce:w(),verifier:w(),issuer:e,clientId:t.clientId,redirectUri:t.redirectUri};y.set(v,JSON.stringify(o));let r=window.open("",b,"popup,width=480,height=720"),u=(async()=>{let l=await Y(t,o,i);if(!r)return location.assign(l),null;r.location.href=l;let a=new URL(t.redirectUri,location.href).origin;return new Promise((n,c)=>{let s,d=()=>{g(),clearInterval(h),clearTimeout(s)},g=T(U,a,p=>{p.state===o.state&&(d(),y.remove(v),typeof p.error=="string"&&p.error?c(new Error(p.error)):p.iss!==e?c(new Error("the answer came from another issuer")):typeof p.code!="string"||!p.code?c(new Error("the answer carried no code")):n(A(o,p.code)))},b,r),h=setInterval(()=>{r.closed&&(clearInterval(h),s=setTimeout(()=>{d(),c(new Error("closed"))},_))},_);t.signal?.addEventListener("abort",()=>{d(),c(new Error("aborted"))})})})();if(r){m={popup:r,waiting:u};let l=()=>{m&&m.popup===r&&(m=void 0)};u.then(l,l)}return u}async function L(){let t=new URLSearchParams(location.search),e={type:U,code:t.get("code"),state:t.get("state"),iss:t.get("iss"),error:t.get("error")};if(window.name===b){try{window.opener?.postMessage(e,location.origin)}catch{}try{let r=new BroadcastChannel(b);r.postMessage(e),r.close()}catch{}return window.close(),null}let i=y.get(v);y.remove(v);let o=i?JSON.parse(i):null;if(!o||e.state!==o.state)throw new Error("the sign-in state does not match: start again");if(e.error)throw new Error(e.error);if(e.iss!==o.issuer)throw new Error("the answer came from another issuer");if(!e.code)throw new Error("the answer carried no code");return A(o,e.code)}async function M(t){let e=await fetch(`${t.issuer}/api/v2/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:t.codeVerifier})}),i=await e.json();if(!e.ok)throw new Error(`${i.error??e.status}: ${i.error_description??""}`.trim());return i}var J='<svg viewBox="0 0 24 24" fill="#FFD700" width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z" stroke="#B8860B" stroke-width="0.5"/><path d="M5 18h14v2H5v-2z" stroke="#B8860B" stroke-width="0.5"/></svg>',X=`
1
+ "use strict";(()=>{var R="0.3.5";function C(t){let e="";for(let i of t)e+=String.fromCharCode(i);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function w(){return C(crypto.getRandomValues(new Uint8Array(32)))}function T(t,e,i,o,r){let u=n=>{if(n&&typeof n=="object"&&n.type===t)try{i(n)}catch{}},l=n=>{n.origin===e&&(!r||n.source===r)&&u(n.data)};window.addEventListener("message",l);let a=null;if(o&&typeof BroadcastChannel=="function"&&e===location.origin)try{a=new BroadcastChannel(o),a.onmessage=n=>u(n.data)}catch{a=null}return()=>{window.removeEventListener("message",l),a?.close()}}var y={get(t){try{return sessionStorage.getItem(t)}catch{return null}},set(t,e){try{sessionStorage.setItem(t,e)}catch{}},remove(t){try{sessionStorage.removeItem(t)}catch{}}};var F="https://api.chaindaddy.io",G={"https://api.chaindaddy.io":"https://chaindaddy.io/_oauth/authorize","https://staging-api.chaindaddy.io":"https://staging.chaindaddy.io/_oauth/authorize"},b="cd-signin",U="cd:signin",v="cd.signin",_=300;async function K(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return C(new Uint8Array(e))}function W(t="openid"){let e=t.split(/\s+/).filter(Boolean);return(e.includes("openid")?e:["openid",...e]).join(" ")}async function Y(t,e,i){let o=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:W(t.scope),state:e.state,nonce:e.nonce,code_challenge:await K(e.verifier),code_challenge_method:"S256"});return t.claims&&o.set("claims",typeof t.claims=="string"?t.claims:JSON.stringify(t.claims)),`${i}?${o}`}function A(t,e){return{code:e,codeVerifier:t.verifier,state:t.state,nonce:t.nonce,issuer:t.issuer,clientId:t.clientId,redirectUri:t.redirectUri}}var m;function k(t){let e=(t.issuer??F).replace(/\/$/,""),i=t.authorizeUrl??G[e];if(!i)return Promise.reject(new Error("authorizeUrl is required for this issuer"));if(!t.clientId||!t.redirectUri)return Promise.reject(new Error("clientId and redirectUri are required"));if(m&&!m.popup.closed)return m.popup.focus(),m.waiting;let o={state:w(),nonce:w(),verifier:w(),issuer:e,clientId:t.clientId,redirectUri:t.redirectUri};y.set(v,JSON.stringify(o));let r=window.open("",b,"popup,width=480,height=720"),u=(async()=>{let l=await Y(t,o,i);if(!r)return location.assign(l),null;r.location.href=l;let a=new URL(t.redirectUri,location.href).origin;return new Promise((n,c)=>{let s,d=()=>{g(),clearInterval(h),clearTimeout(s)},g=T(U,a,p=>{p.state===o.state&&(d(),y.remove(v),typeof p.error=="string"&&p.error?c(new Error(p.error)):p.iss!==e?c(new Error("the answer came from another issuer")):typeof p.code!="string"||!p.code?c(new Error("the answer carried no code")):n(A(o,p.code)))},b,r),h=setInterval(()=>{r.closed&&(clearInterval(h),s=setTimeout(()=>{d(),c(new Error("closed"))},_))},_);t.signal?.addEventListener("abort",()=>{d(),c(new Error("aborted"))})})})();if(r){m={popup:r,waiting:u};let l=()=>{m&&m.popup===r&&(m=void 0)};u.then(l,l)}return u}async function L(){let t=new URLSearchParams(location.search),e={type:U,code:t.get("code"),state:t.get("state"),iss:t.get("iss"),error:t.get("error")};if(window.name===b){try{window.opener?.postMessage(e,location.origin)}catch{}try{let r=new BroadcastChannel(b);r.postMessage(e),r.close()}catch{}return window.close(),null}let i=y.get(v);y.remove(v);let o=i?JSON.parse(i):null;if(!o||e.state!==o.state)throw new Error("the sign-in state does not match: start again");if(e.error)throw new Error(e.error);if(e.iss!==o.issuer)throw new Error("the answer came from another issuer");if(!e.code)throw new Error("the answer carried no code");return A(o,e.code)}async function M(t){let e=await fetch(`${t.issuer}/api/v2/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:t.codeVerifier})}),i=await e.json();if(!e.ok)throw new Error(`${i.error??e.status}: ${i.error_description??""}`.trim());return i}var J='<svg viewBox="0 0 24 24" fill="#FFD700" width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z" stroke="#B8860B" stroke-width="0.5"/><path d="M5 18h14v2H5v-2z" stroke="#B8860B" stroke-width="0.5"/></svg>',X=`
2
2
  :host { display: inline-block; }
3
3
  button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; box-sizing: border-box;
4
4
  min-height: 40px; padding: 0 16px; border-radius: 10px; font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
@@ -19,4 +19,4 @@ button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radiu
19
19
  font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
20
20
  border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
21
21
  button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
22
- `;function D(t={}){try{if(typeof customElements>"u"||typeof HTMLElement>"u")return;let e=t.origin===E?E:O;class i extends HTMLElement{constructor(){super(...arguments);this.kind="app";this.frame=null;this.onMessage=n=>this.receive(n)}connectedCallback(){try{this.shadowRoot||this.attachShadow({mode:"open"}),this.mount()}catch{this.unavailable()}}disconnectedCallback(){try{window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.watch?.disconnect()}catch{}}attributeChangedCallback(){try{this.isConnected&&this.shadowRoot&&this.mount()}catch{this.unavailable()}}src(){let n=(this.getAttribute("crown")??"").trim(),c=(this.getAttribute("chain")??"").trim(),s=(this.getAttribute("app")??"").trim();if(!/^[0-9]{1,18}$/.test(n)||!c||this.kind==="app"&&!s)return null;let d=new URLSearchParams({crown:n,chain:c});return this.kind==="app"?d.set("app",s):d.set("kind","coin"),d.set("mode",this.mode()),d.set("v","1"),`${e}/v1/frame?${d.toString()}`}mode(){let n=this.getAttribute("mode");if(n==="light"||n==="dark")return n;try{return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch{return"light"}}mount(){let n=this.shadowRoot;window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.watch?.disconnect(),this.removeAttribute("data-cd-ready"),this.removeAttribute("data-cd-unavailable");let c=this.src();if(!c){n.innerHTML="<slot></slot>",this.frame=null;return}n.innerHTML=`<style>${et}</style><slot></slot>`;let s=document.createElement("iframe");s.setAttribute("referrerpolicy","strict-origin-when-cross-origin"),s.setAttribute("allow","clipboard-write; fullscreen"),s.setAttribute("title",this.kind==="coin"?"Coin on Chain Daddy":"App on Chain Daddy"),n.appendChild(s),this.frame=s,window.addEventListener("message",this.onMessage);let d=()=>{s.src=c,this.timer=setTimeout(()=>{this.hasAttribute("data-cd-ready")||this.unavailable()},8e3)};if(typeof IntersectionObserver>"u")return d();this.watch=new IntersectionObserver(g=>{g.some(h=>h.isIntersecting)&&(this.watch?.disconnect(),d())},{rootMargin:Q}),this.watch.observe(this)}why(){try{let n=new URL(this.frame.src).searchParams,c=n.get("app");n.delete("mode"),n.delete("v"),fetch(`https://${e===E?"staging-api":"api"}.chaindaddy.io/api/v2/embed/status?${n}`,{credentials:"omit"}).then(s=>s.json()).then(s=>{let d=s.embeddable?s.interactive?"not a verified site of the coin":"not https, or the app did not start":s.reason;console.info(`[Chain Daddy] cd-${this.kind} ${c??""} cannot show here: ${d}. ${Z}`)}).catch(()=>{})}catch{}}receive(n){try{if(n.origin!==e||!this.frame||!n.source||n.source!==this.frame.contentWindow)return;let c=n.data;if(!c||typeof c!="object")return;if(c.type==="cd:ready")clearTimeout(this.timer),this.removeAttribute("data-cd-unavailable"),this.setAttribute("data-cd-ready",""),this.sendTheme(),this.dispatchEvent(new CustomEvent("cd:ready"));else if(c.type==="cd:resize"){let s=Number(c.height);Number.isFinite(s)&&(this.frame.style.height=`${Math.max(1,Math.min(4e3,Math.ceil(s)))}px`)}else c.type==="cd:unavailable"&&this.unavailable()}catch{}}sendTheme(){let n={},c=getComputedStyle(this);for(let s of tt){let d=c.getPropertyValue(s).trim();d&&(n[s]=d)}this.frame?.contentWindow?.postMessage({type:"cd:theme",tokens:n},e)}unavailable(){try{if(clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.hasAttribute("data-cd-unavailable"))return;this.setAttribute("data-cd-unavailable",""),this.dispatchEvent(new CustomEvent("cd:unavailable")),this.why()}catch{}}}i.observedAttributes=["crown","chain","app","mode"];class o extends i{}class r extends i{constructor(){super(...arguments);this.kind="coin"}}class u extends HTMLElement{connectedCallback(){try{let a=this.querySelector("a[href]"),n=a?new URL(a.getAttribute("href")??"",document.baseURI):null;if(!n||n.protocol!=="https:")return;let c=this.shadowRoot??this.attachShadow({mode:"open"});c.innerHTML=`<style>${nt}</style><button type="button" part="button"></button>`;let s=c.querySelector("button");s.textContent=(a.textContent??"").trim()||"Buy on Chain Daddy",s.addEventListener("click",()=>{try{window.open(n.toString(),"cd-buy","popup,width=460,height=760")||window.location.assign(n.toString())}catch{}})}catch{}}}for(let[l,a]of[["cd-app",o],["cd-coin",r],["cd-buy",u]])customElements.get(l)||customElements.define(l,a)}catch{}}var it=["https://chaindaddy.io","https://staging.chaindaddy.io"],H=()=>"cd-checkout-"+w(),N="popup,width=480,height=820",B=300,rt=7;function I(t){if(typeof t!="string")return null;let e;try{e=new URL(t)}catch{return null}let i=/^\/_pay\/([\w-]+)$/.exec(e.pathname);return i&&it.includes(e.origin)&&!e.username&&!e.password?{href:e.origin+e.pathname,origin:e.origin,id:i[1]}:null}function x(t,e){if(typeof t!="string")return null;let i;try{i=new URL(t,location.href)}catch{return null}if(!/^https?:$/.test(i.protocol)||i.username||i.password)return null;let o=u=>{let l=u.split("=")[0];try{return decodeURIComponent(l.replace(/\+/g," "))}catch{return l}},r=i.search.slice(1).split("&").filter(u=>u&&o(u)!=="session_id");return r.push("session_id="+encodeURIComponent(e)),i.origin+i.pathname+"?"+r.join("&")+i.hash}var f;function z(t,e,i){return new Promise(o=>{let r=!1,u=0,l,a=d=>{clearInterval(s),clearTimeout(l),window.removeEventListener("message",n),i&&d.status==="completed"&&location.assign(i),o(d)},n=d=>{if(d.origin!==e.origin||d.source!==t)return;let g=d.data;if(!g||typeof g!="object")return;g.type==="cd:checkout.ready"&&(r=!0);let h=g.session;if(!(!h||typeof h!="object"||h.id!==e.id)){if(g.type==="cd:checkout.cancelled")a({status:"cancelled",session:{id:e.id}});else if(g.type==="cd:checkout.completed"&&(h.status==="complete"||h.status==="paid")){let p={id:e.id,status:h.status};typeof h.orderId=="string"&&/^[\w-]{1,128}$/.test(h.orderId)&&(p.orderId=h.orderId),a({status:"completed",session:p})}}},c=()=>{try{t.postMessage({type:"cd:checkout.hello"},e.origin)}catch{}},s=setInterval(()=>{t.closed?(clearInterval(s),l=setTimeout(()=>a({status:"closed"}),B)):(!r||++u%rt===0)&&c()},B);window.addEventListener("message",n),c()})}function S(t){let e=()=>new Error("checkout: url is not a Chain Daddy checkout session link"),i=(r,u,l)=>{f={popup:r,result:u,href:l};let a=()=>{f&&f.popup===r&&(f=void 0)};return u.then(a,a),u},o=r=>(location.assign(r.href),{status:"redirected"});try{let{url:r,successUrl:u}=t;if(u&&!x(u,""))throw new Error("checkout: successUrl is not a web address");let l=s=>u?x(u,s.id):null,a=typeof r=="function"?null:I(r);if(typeof r!="function"&&!a)throw e();if(f&&!f.popup.closed&&(!a||f.href===a.href))return f.popup.focus(),f.result;if(a){let s=window.open(a.href,H(),N);return s?i(s,z(s,a,l(a)),a.href):Promise.resolve(o(a))}let n=window.open("",H(),N),c=new Promise(s=>s(r())).then(s=>{let d=I(s);if(!d)throw n?.close(),e();return n?(n.location.href=d.href,z(n,d,l(d))):o(d)},s=>{throw n?.close(),s});return n?i(n,c):c}catch(r){return Promise.reject(r)}}var j=!1,q;function V(){j||typeof document>"u"||(j=!0,document.addEventListener("click",t=>{let e=t.target instanceof Element?t.target.closest("[data-cd-checkout]"):null;if(!e||t.defaultPrevented||t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;let i=e.getAttribute("data-cd-checkout")||e.href,o=e.getAttribute("data-cd-success");if(!I(i)||o&&!x(o,""))return;t.preventDefault();let r=S({url:i,successUrl:o});r!==q&&(q=r,r.then(u=>void e.dispatchEvent(new CustomEvent("cd:checkout",{detail:u,bubbles:!0})),()=>{}))}))}var st={version:R,signIn:k,handleCallback:L,exchangeCode:M,checkout:S};window.ChainDaddy=Object.assign(window.ChainDaddy??{},st);P();D({origin:$(document.currentScript?.src)});V();})();
22
+ `;function D(t={}){try{if(typeof customElements>"u"||typeof HTMLElement>"u")return;let e=t.origin===E?E:O;class i extends HTMLElement{constructor(){super(...arguments);this.kind="app";this.frame=null;this.onMessage=n=>this.receive(n)}connectedCallback(){try{this.shadowRoot||this.attachShadow({mode:"open"}),this.mount()}catch{this.unavailable()}}disconnectedCallback(){try{window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.watch?.disconnect()}catch{}}attributeChangedCallback(){try{this.isConnected&&this.shadowRoot&&this.mount()}catch{this.unavailable()}}src(){let n=(this.getAttribute("crown")??"").trim(),c=(this.getAttribute("chain")??"").trim(),s=(this.getAttribute("app")??"").trim();if(!/^[0-9]{1,18}$/.test(n)||!c||this.kind==="app"&&!s)return null;let d=new URLSearchParams({crown:n,chain:c});return this.kind==="app"?d.set("app",s):d.set("kind","coin"),d.set("mode",this.mode()),d.set("v","1"),`${e}/v1/frame?${d.toString()}`}mode(){let n=this.getAttribute("mode");if(n==="light"||n==="dark")return n;try{return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch{return"light"}}mount(){let n=this.shadowRoot;window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.watch?.disconnect(),this.removeAttribute("data-cd-ready"),this.removeAttribute("data-cd-unavailable");let c=this.src();if(!c){n.innerHTML="<slot></slot>",this.frame=null;return}n.innerHTML=`<style>${et}</style><slot></slot>`;let s=document.createElement("iframe");s.setAttribute("referrerpolicy","strict-origin-when-cross-origin"),s.setAttribute("allow","clipboard-write; fullscreen"),s.setAttribute("title",this.kind==="coin"?"Coin on Chain Daddy":"App on Chain Daddy"),n.appendChild(s),this.frame=s,window.addEventListener("message",this.onMessage);let d=()=>{s.src=c,this.timer=setTimeout(()=>{this.hasAttribute("data-cd-ready")||this.unavailable()},8e3)};if(typeof IntersectionObserver>"u")return d();this.watch=new IntersectionObserver(g=>{g.some(h=>h.isIntersecting)&&(this.watch?.disconnect(),d())},{rootMargin:Q}),this.watch.observe(this)}why(){try{let n=new URL(this.frame.src).searchParams,c=n.get("app");n.delete("mode"),n.delete("v"),fetch(`https://${e===E?"staging-api":"api"}.chaindaddy.io/api/v2/embed/status?${n}`,{credentials:"omit"}).then(s=>s.json()).then(s=>{let d=s.embeddable?s.interactive?"not a verified site of the coin":"not https, or the app did not start":s.reason;console.info(`[Chain Daddy] cd-${this.kind} ${c??""} cannot show here: ${d}. ${Z}`)}).catch(()=>{})}catch{}}receive(n){try{if(n.origin!==e||!this.frame||!n.source||n.source!==this.frame.contentWindow)return;let c=n.data;if(!c||typeof c!="object")return;if(c.type==="cd:ready")clearTimeout(this.timer),this.removeAttribute("data-cd-unavailable"),this.setAttribute("data-cd-ready",""),this.sendTheme(),this.dispatchEvent(new CustomEvent("cd:ready"));else if(c.type==="cd:resize"){let s=Number(c.height);Number.isFinite(s)&&(this.frame.style.height=`${Math.max(1,Math.min(4e3,Math.ceil(s)))}px`)}else c.type==="cd:unavailable"&&this.unavailable()}catch{}}sendTheme(){let n={},c=getComputedStyle(this);for(let s of tt){let d=c.getPropertyValue(s).trim();d&&(n[s]=d)}this.frame?.contentWindow?.postMessage({type:"cd:theme",tokens:n},e)}unavailable(){try{if(clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.hasAttribute("data-cd-unavailable"))return;this.setAttribute("data-cd-unavailable",""),this.dispatchEvent(new CustomEvent("cd:unavailable")),this.why()}catch{}}}i.observedAttributes=["crown","chain","app","mode"];class o extends i{}class r extends i{constructor(){super(...arguments);this.kind="coin"}}class u extends HTMLElement{connectedCallback(){try{let a=this.querySelector("a[href]"),n=a?new URL(a.getAttribute("href")??"",document.baseURI):null;if(!n||n.protocol!=="https:")return;let c=this.shadowRoot??this.attachShadow({mode:"open"});c.innerHTML=`<style>${nt}</style><button type="button" part="button"></button>`;let s=c.querySelector("button");s.textContent=(a.textContent??"").trim()||"Buy on Chain Daddy",s.addEventListener("click",()=>{try{window.open(n.toString(),"cd-buy","popup,width=460,height=760")||window.location.assign(n.toString())}catch{}})}catch{}}}for(let[l,a]of[["cd-app",o],["cd-coin",r],["cd-buy",u]])customElements.get(l)||customElements.define(l,a)}catch{}}var it=["https://chaindaddy.io","https://staging.chaindaddy.io"],H=()=>"cd-checkout-"+w(),N="popup,width=480,height=820",B=300,rt=7;function I(t){if(typeof t!="string")return null;let e;try{e=new URL(t)}catch{return null}let i=/^\/_pay\/([\w-]+)$/.exec(e.pathname);return i&&it.includes(e.origin)&&!e.username&&!e.password?{href:e.origin+e.pathname,origin:e.origin,id:i[1]}:null}function x(t,e){if(typeof t!="string")return null;let i;try{i=new URL(t,location.href)}catch{return null}if(!/^https?:$/.test(i.protocol)||i.username||i.password)return null;let o=u=>{let l=u.split("=")[0];try{return decodeURIComponent(l.replace(/\+/g," "))}catch{return l}},r=i.search.slice(1).split("&").filter(u=>u&&o(u)!=="session_id");return r.push("session_id="+encodeURIComponent(e)),i.origin+i.pathname+"?"+r.join("&")+i.hash}var f;function z(t,e,i){return new Promise(o=>{let r=!1,u=0,l,a=d=>{clearInterval(s),clearTimeout(l),window.removeEventListener("message",n),i&&d.status==="completed"&&location.assign(i),o(d)},n=d=>{if(d.origin!==e.origin||d.source!==t)return;let g=d.data;if(!g||typeof g!="object")return;g.type==="cd:checkout.ready"&&(r=!0);let h=g.session;if(!(!h||typeof h!="object"||h.id!==e.id)){if(g.type==="cd:checkout.cancelled")a({status:"cancelled",session:{id:e.id}});else if(g.type==="cd:checkout.completed"&&(h.status==="complete"||h.status==="paid")){let p={id:e.id,status:h.status};typeof h.orderId=="string"&&/^[\w-]{1,128}$/.test(h.orderId)&&(p.orderId=h.orderId),typeof h.livemode=="boolean"&&(p.livemode=h.livemode),a({status:"completed",session:p})}}},c=()=>{try{t.postMessage({type:"cd:checkout.hello"},e.origin)}catch{}},s=setInterval(()=>{t.closed?(clearInterval(s),l=setTimeout(()=>a({status:"closed"}),B)):(!r||++u%rt===0)&&c()},B);window.addEventListener("message",n),c()})}function S(t){let e=()=>new Error("checkout: url is not a Chain Daddy checkout session link"),i=(r,u,l)=>{f={popup:r,result:u,href:l};let a=()=>{f&&f.popup===r&&(f=void 0)};return u.then(a,a),u},o=r=>(location.assign(r.href),{status:"redirected"});try{let{url:r,successUrl:u}=t;if(u&&!x(u,""))throw new Error("checkout: successUrl is not a web address");let l=s=>u?x(u,s.id):null,a=typeof r=="function"?null:I(r);if(typeof r!="function"&&!a)throw e();if(f&&!f.popup.closed&&(!a||f.href===a.href))return f.popup.focus(),f.result;if(a){let s=window.open(a.href,H(),N);return s?i(s,z(s,a,l(a)),a.href):Promise.resolve(o(a))}let n=window.open("",H(),N),c=new Promise(s=>s(r())).then(s=>{let d=I(s);if(!d)throw n?.close(),e();return n?(n.location.href=d.href,z(n,d,l(d))):o(d)},s=>{throw n?.close(),s});return n?i(n,c):c}catch(r){return Promise.reject(r)}}var j=!1,q;function V(){j||typeof document>"u"||(j=!0,document.addEventListener("click",t=>{let e=t.target instanceof Element?t.target.closest("[data-cd-checkout]"):null;if(!e||t.defaultPrevented||t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;let i=e.getAttribute("data-cd-checkout")||e.href,o=e.getAttribute("data-cd-success");if(!I(i)||o&&!x(o,""))return;t.preventDefault();let r=S({url:i,successUrl:o});r!==q&&(q=r,r.then(u=>void e.dispatchEvent(new CustomEvent("cd:checkout",{detail:u,bubbles:!0})),()=>{}))}))}var st={version:R,signIn:k,handleCallback:L,exchangeCode:M,checkout:S};window.ChainDaddy=Object.assign(window.ChainDaddy??{},st);P();D({origin:$(document.currentScript?.src)});V();})();
package/dist/cd.js.sri CHANGED
@@ -1 +1 @@
1
- sha384-x7XiOLUzANtLnqFmv/hWd2uhGI7c+jlhVivakZ7LYHklw9zgK/diic0+fJom/mXU
1
+ sha384-yNIDKP+1edth+ChOSlYVUWmXdlE3j2HVn66BpXQ0x1J4Bs87EUmozo6w2bxKCXFZ
@@ -13,13 +13,17 @@ export interface CheckoutOptions {
13
13
  successUrl?: string | null;
14
14
  }
15
15
  export type CheckoutResult =
16
- /** The buyer paid. `session.status` is "complete" (confirmed) or "paid" (sent, still confirming). */
16
+ /**
17
+ * The buyer paid. `session.status` is "complete" (confirmed) or "paid" (sent, still confirming).
18
+ * `session.livemode` is false for a test store's session, where nothing was charged.
19
+ */
17
20
  {
18
21
  status: 'completed';
19
22
  session: {
20
23
  id: string;
21
24
  status: 'complete' | 'paid';
22
25
  orderId?: string;
26
+ livemode?: boolean;
23
27
  };
24
28
  }
25
29
  /** The buyer left the checkout without paying. */
package/dist/checkout.js CHANGED
@@ -70,6 +70,7 @@ function wait(popup, target, success) {
70
70
  else if (data.type === "cd:checkout.completed" && (s.status === "complete" || s.status === "paid")) {
71
71
  const session = { id: target.id, status: s.status };
72
72
  if (typeof s.orderId === "string" && /^[\w-]{1,128}$/.test(s.orderId)) session.orderId = s.orderId;
73
+ if (typeof s.livemode === "boolean") session.livemode = s.livemode;
73
74
  done({ status: "completed", session });
74
75
  }
75
76
  };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core.ts
2
- var VERSION = true ? "0.3.3" : "0.0.0";
2
+ var VERSION = true ? "0.3.5" : "0.0.0";
3
3
  function b64url(bytes) {
4
4
  let s = "";
5
5
  for (const b of bytes) s += String.fromCharCode(b);
@@ -522,6 +522,7 @@ function wait(popup, target, success) {
522
522
  else if (data.type === "cd:checkout.completed" && (s.status === "complete" || s.status === "paid")) {
523
523
  const session = { id: target.id, status: s.status };
524
524
  if (typeof s.orderId === "string" && /^[\w-]{1,128}$/.test(s.orderId)) session.orderId = s.orderId;
525
+ if (typeof s.livemode === "boolean") session.livemode = s.livemode;
525
526
  done({ status: "completed", session });
526
527
  }
527
528
  };
package/dist/verify.d.ts CHANGED
@@ -13,7 +13,7 @@ export declare const MIN_CONFIRMATIONS: Readonly<Record<string, number>>;
13
13
  /** `token` of an order paid in the chain's own coin (ETH on Base, POL on Polygon). */
14
14
  export declare const NATIVE_TOKEN = "0x0000000000000000000000000000000000000000";
15
15
  export type VerifyErrorCode =
16
- /** A Solana order: follow the manual recipe for now. */
16
+ /** A Solana order: follow the manual recipe for now. Or, from verifyAnswer, a runtime whose Web Crypto has no Ed25519. */
17
17
  'unsupported'
18
18
  /** What you passed (options, expect) is missing or malformed. */
19
19
  | 'bad_options'
@@ -26,8 +26,24 @@ export type VerifyErrorCode =
26
26
  /** The block is not the order, asset, payout address or amount you expect. */
27
27
  | 'expectation_mismatch'
28
28
  /** The RPC endpoint failed or answered something that is not an answer. */
29
- | 'rpc_error';
30
- /** Every failure of verifyPayment. A payment that is simply not there yet is not an error: it resolves `paid: false`. */
29
+ | 'rpc_error'
30
+ /** verifyAnswer: not a compact JWS, not signed with EdDSA, not a signed answer (a receipt, say), not from the issuer you expect, or missing a claim an answer carries. */
31
+ | 'bad_answer'
32
+ /** verifyAnswer: the answer names a key that is not one of the keys. */
33
+ | 'unknown_key'
34
+ /** verifyAnswer: the signature is not that key's signature over this answer. */
35
+ | 'bad_signature'
36
+ /** verifyAnswer: a genuine answer past its `exp`. Fetch a new one. */
37
+ | 'expired'
38
+ /** The keys you passed, or the record value, are not Ed25519 keys. */
39
+ | 'bad_keys'
40
+ /** verifyAnswer: the key set could not be fetched, or holds no Ed25519 key. */
41
+ | 'keys_unavailable';
42
+ /**
43
+ * Every failure of verifyPayment and verifyAnswer. A payment that is simply not
44
+ * there yet is not an error: it resolves `paid: false`. Nor is a coin that is
45
+ * not registered: that is a signed answer with `status: "not_registered"`.
46
+ */
31
47
  export declare class VerifyError extends Error {
32
48
  readonly code: VerifyErrorCode;
33
49
  constructor(code: VerifyErrorCode, message: string);
@@ -124,3 +140,104 @@ export declare function isKnownCheckout(chainId: string | number, checkout: stri
124
140
  * `paid: true` on a doubt.
125
141
  */
126
142
  export declare function verifyPayment(verification: Verification, options: VerifyOptions): Promise<PaymentResult>;
143
+ /** The `typ` claim of a signed answer. A store receipt, signed by the same keys, is "iap-receipt+v1" and is refused. */
144
+ export declare const ANSWER_TYPE = "cd-answer+v1";
145
+ /** The `iss` claim of a signed answer. */
146
+ export declare const ANSWER_ISSUER = "https://chaindaddy.io";
147
+ /** Where the signing keys are published, as a JSON Web Key Set. */
148
+ export declare const SIGNING_KEYS_URL = "https://api.chaindaddy.io/api/v2/iap/jwks";
149
+ /** The text record on chaindaddy.eth that names the signing keys: `ed25519:<x>` entries separated by a space, the current key first. */
150
+ export declare const SIGNING_KEYS_RECORD = "io.chaindaddy.signing-keys";
151
+ /** One Ed25519 signing key. */
152
+ export interface SigningKey {
153
+ /** The first 16 hex characters of the SHA-256 of the raw public key: what a JWS header's `kid` names. */
154
+ kid: string;
155
+ /** The raw 32-byte public key, base64url without padding (a JWK's `x`). */
156
+ x: string;
157
+ }
158
+ /** What a signed answer says. */
159
+ export interface AnswerClaims {
160
+ iss: string;
161
+ typ: typeof ANSWER_TYPE;
162
+ /** Unix seconds. */
163
+ iat: number;
164
+ /** Unix seconds: 15 minutes after `iat`. */
165
+ exp: number;
166
+ /** What the answer is about. More kinds may be added. */
167
+ kind: 'registration' | (string & {});
168
+ /** CAIP-2, e.g. "eip155:8453". */
169
+ chainId: string;
170
+ /** The chain key, e.g. "base". */
171
+ chain: string;
172
+ /** The ticker: as registered when `status` is "registered", otherwise the one asked about, in upper case. */
173
+ symbol: string;
174
+ /** "registered": a verified page. "not_registered": none. "removed": one Chain Daddy took down. More may be added: treat one you do not know as not registered. */
175
+ status: 'registered' | 'not_registered' | 'removed' | (string & {});
176
+ /** The fields below are present only when `status` is "registered". Addresses are lowercase on EVM chains. */
177
+ tokenAddress?: string;
178
+ /** The registration's ID on that chain. */
179
+ crownId?: number;
180
+ /** The wallet that holds the registration. */
181
+ holder?: string;
182
+ /** RFC 3339. Left out when not known. */
183
+ registeredAt?: string;
184
+ pageUrl?: string;
185
+ }
186
+ /** What you asked about. An answer about anything else is refused. */
187
+ export interface AnswerExpectation {
188
+ /** The chain key you asked about, e.g. "base". */
189
+ chain?: string;
190
+ /** Or its CAIP-2 ID, e.g. "eip155:8453". */
191
+ chainId?: string;
192
+ /** The ticker you asked about, in any case. */
193
+ symbol?: string;
194
+ /** The kind of answer, e.g. "registration". */
195
+ kind?: string;
196
+ }
197
+ export interface VerifyAnswerOptions {
198
+ /**
199
+ * The keys to trust: what signingKeysFromRecord returns, or JWKs (the `keys`
200
+ * of the key set). A key's `kid` is always computed from the key itself.
201
+ */
202
+ keys?: readonly (SigningKey | {
203
+ x: string;
204
+ kty?: string;
205
+ crv?: string;
206
+ })[];
207
+ /** Instead of keys: a key set to fetch. Default SIGNING_KEYS_URL, when neither is passed. */
208
+ jwksUrl?: string;
209
+ /**
210
+ * What you asked about. Pass it whenever the answer did not come straight
211
+ * from Chain Daddy: whoever relays answers can hand you a genuine one about
212
+ * a different coin.
213
+ */
214
+ expect?: AnswerExpectation;
215
+ /** The `iss` to accept. Default ANSWER_ISSUER. */
216
+ issuer?: string;
217
+ /** Seconds past `exp` an answer is still accepted, for a clock that runs fast. Default 0. */
218
+ clockToleranceSeconds?: number;
219
+ /** The time to check `exp` against, in Unix seconds. Default: the clock. */
220
+ now?: number;
221
+ /** Used to fetch the key set, in place of the global fetch. */
222
+ fetch?: typeof fetch;
223
+ /** Aborts the key set request. */
224
+ signal?: AbortSignal;
225
+ }
226
+ /**
227
+ * The signing keys named by the text record SIGNING_KEYS_RECORD on
228
+ * chaindaddy.eth: `ed25519:<x>` entries separated by whitespace, the current
229
+ * key first. An entry of another kind is skipped (a new kind of key can be
230
+ * added to the record); an `ed25519:` entry that is not a key throws, so a
231
+ * damaged record is never read as fewer keys.
232
+ */
233
+ export declare function signingKeysFromRecord(value: string): SigningKey[];
234
+ /**
235
+ * Checks a signed answer and resolves what it says. It resolves only when the
236
+ * JWS is signed with EdDSA by one of the keys, is a signed answer (`typ`) from
237
+ * the issuer you expect (`iss`), has not expired (`exp`) and, when you pass
238
+ * `expect`, is about what you asked. Anything else rejects with a VerifyError.
239
+ *
240
+ * A coin that is not registered resolves too, with `status: "not_registered"`:
241
+ * read `status` before anything else.
242
+ */
243
+ export declare function verifyAnswer(jws: string, options?: VerifyAnswerOptions): Promise<AnswerClaims>;
package/dist/verify.js CHANGED
@@ -330,13 +330,259 @@ async function verifyPayment(verification, options) {
330
330
  const paid = payer !== NATIVE_TOKEN;
331
331
  return { paid, payer: paid ? payer : null, blockNumber: Number(at), ...result };
332
332
  }
333
+ var ANSWER_TYPE = "cd-answer+v1";
334
+ var ANSWER_ISSUER = "https://chaindaddy.io";
335
+ var SIGNING_KEYS_URL = "https://api.chaindaddy.io/api/v2/iap/jwks";
336
+ var SIGNING_KEYS_RECORD = "io.chaindaddy.signing-keys";
337
+ function fromBase64Url(v) {
338
+ if (typeof v !== "string" || !/^[A-Za-z0-9_-]*$/.test(v) || v.length % 4 === 1) return null;
339
+ let bin;
340
+ try {
341
+ bin = atob(v.replace(/-/g, "+").replace(/_/g, "/"));
342
+ } catch {
343
+ return null;
344
+ }
345
+ const out = new Uint8Array(bin.length);
346
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
347
+ return toBase64Url(out) === v ? out : null;
348
+ }
349
+ function toBase64Url(bytes) {
350
+ let bin = "";
351
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
352
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
353
+ }
354
+ var SHA256_K = new Uint32Array([
355
+ 1116352408,
356
+ 1899447441,
357
+ 3049323471,
358
+ 3921009573,
359
+ 961987163,
360
+ 1508970993,
361
+ 2453635748,
362
+ 2870763221,
363
+ 3624381080,
364
+ 310598401,
365
+ 607225278,
366
+ 1426881987,
367
+ 1925078388,
368
+ 2162078206,
369
+ 2614888103,
370
+ 3248222580,
371
+ 3835390401,
372
+ 4022224774,
373
+ 264347078,
374
+ 604807628,
375
+ 770255983,
376
+ 1249150122,
377
+ 1555081692,
378
+ 1996064986,
379
+ 2554220882,
380
+ 2821834349,
381
+ 2952996808,
382
+ 3210313671,
383
+ 3336571891,
384
+ 3584528711,
385
+ 113926993,
386
+ 338241895,
387
+ 666307205,
388
+ 773529912,
389
+ 1294757372,
390
+ 1396182291,
391
+ 1695183700,
392
+ 1986661051,
393
+ 2177026350,
394
+ 2456956037,
395
+ 2730485921,
396
+ 2820302411,
397
+ 3259730800,
398
+ 3345764771,
399
+ 3516065817,
400
+ 3600352804,
401
+ 4094571909,
402
+ 275423344,
403
+ 430227734,
404
+ 506948616,
405
+ 659060556,
406
+ 883997877,
407
+ 958139571,
408
+ 1322822218,
409
+ 1537002063,
410
+ 1747873779,
411
+ 1955562222,
412
+ 2024104815,
413
+ 2227730452,
414
+ 2361852424,
415
+ 2428436474,
416
+ 2756734187,
417
+ 3204031479,
418
+ 3329325298
419
+ ]);
420
+ var rotr = (x, n) => x >>> n | x << 32 - n;
421
+ function keyId(key) {
422
+ const block = new Uint8Array(64);
423
+ block.set(key);
424
+ block[32] = 128;
425
+ block[62] = 1;
426
+ const view = new DataView(block.buffer);
427
+ const w = new Uint32Array(64);
428
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(i * 4);
429
+ for (let i = 16; i < 64; i++) {
430
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
431
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
432
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1;
433
+ }
434
+ const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
435
+ let [a, b, c, d, e, f, g, hh] = h;
436
+ for (let i = 0; i < 64; i++) {
437
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + SHA256_K[i] + w[i] >>> 0;
438
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
439
+ hh = g;
440
+ g = f;
441
+ f = e;
442
+ e = d + t1 >>> 0;
443
+ d = c;
444
+ c = b;
445
+ b = a;
446
+ a = t1 + t2 >>> 0;
447
+ }
448
+ return [a, b].map((v, i) => (h[i] + v >>> 0).toString(16).padStart(8, "0")).join("");
449
+ }
450
+ function keyFromX(x) {
451
+ const raw = typeof x === "string" && x.length === 43 ? fromBase64Url(x) : null;
452
+ return raw && raw.length === 32 ? { kid: keyId(raw), x, raw } : null;
453
+ }
454
+ function keyFromObject(v) {
455
+ if (!isObject(v)) return null;
456
+ if (v.kty !== void 0 && v.kty !== "OKP" || v.crv !== void 0 && v.crv !== "Ed25519") return null;
457
+ return keyFromX(v.x);
458
+ }
459
+ function signingKeysFromRecord(value) {
460
+ if (typeof value !== "string") throw new VerifyError("bad_keys", "the record value is not a string");
461
+ const out = [];
462
+ for (const entry of value.split(/\s+/)) {
463
+ if (!entry.startsWith("ed25519:")) continue;
464
+ const key = keyFromX(entry.slice("ed25519:".length));
465
+ if (!key) throw new VerifyError("bad_keys", `"${entry.slice(0, 60)}" is not an Ed25519 key: expected ed25519: and 43 base64url characters`);
466
+ if (!out.some((k) => k.kid === key.kid)) out.push({ kid: key.kid, x: key.x });
467
+ }
468
+ return out;
469
+ }
470
+ async function fetchKeys(url, o) {
471
+ const doFetch = o.fetch ?? globalThis.fetch;
472
+ if (typeof doFetch !== "function") throw new VerifyError("bad_options", "no fetch here: pass options.fetch, or options.keys");
473
+ let body;
474
+ try {
475
+ const res = await doFetch(url, { headers: { accept: "application/json" }, signal: o.signal });
476
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
477
+ body = await res.json();
478
+ } catch (e) {
479
+ throw new VerifyError("keys_unavailable", `the key set at ${url} could not be read: ${e instanceof Error ? e.message : String(e)}`);
480
+ }
481
+ const keys = isObject(body) && Array.isArray(body.keys) ? body.keys.map(keyFromObject).filter((k) => k !== null) : [];
482
+ if (keys.length === 0) throw new VerifyError("keys_unavailable", `the key set at ${url} holds no Ed25519 key`);
483
+ return keys;
484
+ }
485
+ function passedKeys(v) {
486
+ if (!Array.isArray(v) || v.length === 0) throw new VerifyError("bad_keys", "options.keys must be a list of keys, one or more");
487
+ return Array.from(v, (k, i) => {
488
+ const key = keyFromObject(k);
489
+ if (!key) throw new VerifyError("bad_keys", `options.keys[${i}] is not an Ed25519 key (x: 43 base64url characters)`);
490
+ return key;
491
+ });
492
+ }
493
+ function parseAnswerExpectation(v) {
494
+ if (v === void 0) return null;
495
+ const fields = ["chain", "chainId", "symbol", "kind"];
496
+ if (!isObject(v) || Object.keys(v).length === 0 || Object.keys(v).some((k) => !fields.includes(k) || typeof v[k] !== "string" || v[k] === "")) {
497
+ throw new VerifyError("bad_options", "options.expect takes chain or chainId, symbol and kind, each a string: what you asked about");
498
+ }
499
+ return v;
500
+ }
501
+ function jsonObject(bytes) {
502
+ try {
503
+ const v = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
504
+ return isObject(v) ? v : null;
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+ async function ed25519Verify(key, signature, data) {
510
+ const subtle = globalThis.crypto?.subtle;
511
+ if (!subtle) throw new VerifyError("unsupported", "this runtime has no Web Crypto (crypto.subtle) to check an Ed25519 signature with");
512
+ let imported;
513
+ try {
514
+ imported = await subtle.importKey("raw", key, { name: "Ed25519" }, false, ["verify"]);
515
+ } catch (e) {
516
+ throw new VerifyError("unsupported", `this runtime's Web Crypto cannot check an Ed25519 signature: ${e instanceof Error ? e.message : String(e)}`);
517
+ }
518
+ return subtle.verify({ name: "Ed25519" }, imported, signature, data);
519
+ }
520
+ var isText = (v) => typeof v === "string" && v !== "";
521
+ async function verifyAnswer(jws, options = {}) {
522
+ if (!isObject(options)) throw new VerifyError("bad_options", "options must be an object: keys or jwksUrl, and expect");
523
+ const o = options;
524
+ const tolerance = o.clockToleranceSeconds ?? 0;
525
+ if (typeof tolerance !== "number" || !Number.isSafeInteger(tolerance) || tolerance < 0) {
526
+ throw new VerifyError("bad_options", "options.clockToleranceSeconds must be a whole number of seconds, 0 or more");
527
+ }
528
+ const now = o.now ?? Math.floor(Date.now() / 1e3);
529
+ if (typeof now !== "number" || !Number.isFinite(now)) throw new VerifyError("bad_options", "options.now must be a time in Unix seconds");
530
+ const issuer = o.issuer ?? ANSWER_ISSUER;
531
+ if (!isText(issuer)) throw new VerifyError("bad_options", "options.issuer must be a string");
532
+ const expect = parseAnswerExpectation(o.expect);
533
+ if (o.keys !== void 0 && o.jwksUrl !== void 0) throw new VerifyError("bad_options", "pass keys or jwksUrl, one of the two");
534
+ if (o.jwksUrl !== void 0 && !isText(o.jwksUrl)) throw new VerifyError("bad_options", "options.jwksUrl must be a URL");
535
+ const parts = typeof jws === "string" ? jws.trim().split(".") : [];
536
+ if (parts.length !== 3) throw new VerifyError("bad_answer", "not a compact JWS: three base64url parts joined by dots");
537
+ const headerBytes = fromBase64Url(parts[0]);
538
+ const payloadBytes = fromBase64Url(parts[1]);
539
+ const signature = fromBase64Url(parts[2]);
540
+ const header = headerBytes && jsonObject(headerBytes);
541
+ if (!header || !payloadBytes || !signature) throw new VerifyError("bad_answer", "not a compact JWS: a part is not base64url, or the header is not JSON");
542
+ if (header.alg !== "EdDSA") throw new VerifyError("bad_answer", `the answer is signed with ${JSON.stringify(header.alg)}, not EdDSA`);
543
+ if (signature.length !== 64) throw new VerifyError("bad_answer", "the signature is not an Ed25519 signature");
544
+ const keys = o.keys !== void 0 ? passedKeys(o.keys) : await fetchKeys(o.jwksUrl ?? SIGNING_KEYS_URL, o);
545
+ const key = keys.find((k) => k.kid === header.kid);
546
+ if (!key) throw new VerifyError("unknown_key", `the answer is signed by key ${JSON.stringify(header.kid)}, which is not one of the keys`);
547
+ if (!await ed25519Verify(key.raw, signature, new TextEncoder().encode(`${parts[0]}.${parts[1]}`))) {
548
+ throw new VerifyError("bad_signature", "the signature does not match: the answer was changed, or another key signed it");
549
+ }
550
+ const c = jsonObject(payloadBytes);
551
+ if (!c) throw new VerifyError("bad_answer", "the signed payload is not a JSON object");
552
+ if (c.typ !== ANSWER_TYPE) throw new VerifyError("bad_answer", `not a signed answer: typ is ${JSON.stringify(c.typ)}, not "${ANSWER_TYPE}"`);
553
+ if (c.iss !== issuer) throw new VerifyError("bad_answer", `the answer is from ${JSON.stringify(c.iss)}, not ${issuer}`);
554
+ if (!Number.isSafeInteger(c.iat) || !Number.isSafeInteger(c.exp)) throw new VerifyError("bad_answer", "the answer has no iat or no exp");
555
+ for (const name of ["kind", "chainId", "chain", "symbol", "status"]) {
556
+ if (!isText(c[name])) throw new VerifyError("bad_answer", `the answer has no ${name}`);
557
+ }
558
+ if (c.status === "registered" && (!isText(c.tokenAddress) || !isText(c.holder) || !Number.isSafeInteger(c.crownId))) {
559
+ throw new VerifyError("bad_answer", "a registered answer must name the token, the registration and its holder");
560
+ }
561
+ if (now >= c.exp + tolerance) throw new VerifyError("expired", `the answer expired at ${c.exp} (Unix seconds): fetch a new one`);
562
+ const claims = c;
563
+ if (expect) {
564
+ if (expect.chain !== void 0 && claims.chain !== expect.chain) throw new VerifyError("expectation_mismatch", `the answer is about chain ${claims.chain}, not ${expect.chain}`);
565
+ if (expect.chainId !== void 0 && claims.chainId !== expect.chainId) throw new VerifyError("expectation_mismatch", `the answer is about chain ${claims.chainId}, not ${expect.chainId}`);
566
+ if (expect.symbol !== void 0 && claims.symbol.toUpperCase() !== expect.symbol.toUpperCase()) {
567
+ throw new VerifyError("expectation_mismatch", `the answer is about ${claims.symbol}, not ${expect.symbol}`);
568
+ }
569
+ if (expect.kind !== void 0 && claims.kind !== expect.kind) throw new VerifyError("expectation_mismatch", `the answer is a ${claims.kind}, not a ${expect.kind}`);
570
+ }
571
+ return claims;
572
+ }
333
573
  export {
574
+ ANSWER_ISSUER,
575
+ ANSWER_TYPE,
334
576
  CHECKOUTS,
335
577
  MIN_CONFIRMATIONS,
336
578
  NATIVE_TOKEN,
579
+ SIGNING_KEYS_RECORD,
580
+ SIGNING_KEYS_URL,
337
581
  VerifyError,
338
582
  encodeHashOrder,
339
583
  encodePaidBy,
340
584
  isKnownCheckout,
585
+ signingKeysFromRecord,
586
+ verifyAnswer,
341
587
  verifyPayment
342
588
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaindaddy/js",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Chain Daddy for any website: Sign in with Chain Daddy (a popup and a button), a coin's apps on its own site (<cd-app>), checkout for a coin's store in a popup, and a server-side check that a store order was paid on chain",
5
5
  "homepage": "https://docs.chaindaddy.io/developer/javascript-sdk",
6
6
  "repository": {