@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 +403 -19
- package/dist/cd.js +2 -2
- package/dist/cd.js.sri +1 -1
- package/dist/checkout.d.ts +5 -1
- package/dist/checkout.js +1 -0
- package/dist/index.js +2 -1
- package/dist/verify.d.ts +120 -3
- package/dist/verify.js +246 -0
- package/package.json +1 -1
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).
|
|
991
|
-
*
|
|
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
|
-
*
|
|
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
|
|
1058
|
-
*
|
|
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
|
|
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": "
|
|
2817
|
-
* "code": "
|
|
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
|
|
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`,
|
|
5158
|
-
*
|
|
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
|
|
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.
|
|
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-
|
|
1
|
+
sha384-yNIDKP+1edth+ChOSlYVUWmXdlE3j2HVn66BpXQ0x1J4Bs87EUmozo6w2bxKCXFZ
|
package/dist/checkout.d.ts
CHANGED
|
@@ -13,13 +13,17 @@ export interface CheckoutOptions {
|
|
|
13
13
|
successUrl?: string | null;
|
|
14
14
|
}
|
|
15
15
|
export type CheckoutResult =
|
|
16
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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
|
+
"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": {
|