uvd-x402-sdk 2.53.0 → 2.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,262 @@
1
+ import { gcm } from '@noble/ciphers/aes';
2
+ import { edwardsToMontgomeryPub, x25519 } from '@noble/curves/ed25519';
3
+ import { secp256k1 } from '@noble/curves/secp256k1';
4
+ import { hkdf } from '@noble/hashes/hkdf';
5
+ import { keccak_256 } from '@noble/hashes/sha3';
6
+ import { sha256, sha512 } from '@noble/hashes/sha2';
7
+ import { randomBytes } from '@noble/hashes/utils';
1
8
  import { ethers } from 'ethers';
2
9
 
3
- // src/client/X402Client.ts
10
+ // src/dx402.ts
11
+ var EVIDENCE_HEADER = "X-Durable-Evidence";
12
+ var MAGIC = new Uint8Array([68, 88, 52, 48, 50]);
13
+ var FORMAT_VERSION = 1;
14
+ var NONCE_LEN = 12;
15
+ var CEK_LEN = 32;
16
+ var HKDF_INFO = new TextEncoder().encode("DX402-v1-wrap");
17
+ var DX402Error = class extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "DX402Error";
21
+ }
22
+ };
23
+ var EvidenceSkipped = class extends DX402Error {
24
+ constructor(reason) {
25
+ super(`no durable evidence was anchored: ${reason}`);
26
+ this.reason = reason;
27
+ this.name = "EvidenceSkipped";
28
+ }
29
+ };
30
+ var ContentHashMismatch = class extends DX402Error {
31
+ constructor(anchored, actual) {
32
+ super(`content hash mismatch: anchored ${anchored}, decrypted ${actual}`);
33
+ this.anchored = anchored;
34
+ this.actual = actual;
35
+ this.name = "ContentHashMismatch";
36
+ }
37
+ };
38
+ function isEndToEnd(evidence) {
39
+ return evidence.mode === "direct";
40
+ }
41
+ function b64urlDecode(value) {
42
+ const normalized = value.trim().replace(/-/g, "+").replace(/_/g, "/");
43
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
44
+ return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
45
+ }
46
+ function hexToBytes(hex) {
47
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
48
+ if (clean.length % 2 !== 0) throw new DX402Error("odd-length hex string");
49
+ const out = new Uint8Array(clean.length / 2);
50
+ for (let i = 0; i < out.length; i++) {
51
+ out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
52
+ }
53
+ return out;
54
+ }
55
+ function bytesToHex(bytes) {
56
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
57
+ }
58
+ function contentHash(body) {
59
+ return "0x" + bytesToHex(keccak_256(body));
60
+ }
61
+ function paymentId(caip2Network, txHash) {
62
+ const enc = new TextEncoder();
63
+ const clean = txHash.startsWith("0x") ? txHash.slice(2) : txHash;
64
+ const preimage = new Uint8Array([...enc.encode(caip2Network), ...enc.encode(clean)]);
65
+ return "0x" + bytesToHex(keccak_256(preimage));
66
+ }
67
+ function parseEvidenceHeader(value) {
68
+ let payload;
69
+ try {
70
+ payload = JSON.parse(new TextDecoder().decode(b64urlDecode(value)));
71
+ } catch (err) {
72
+ throw new DX402Error(`malformed ${EVIDENCE_HEADER} header: ${String(err)}`);
73
+ }
74
+ if ("skipped" in payload) {
75
+ throw new EvidenceSkipped(String(payload.skipped));
76
+ }
77
+ if (!payload.paymentId || !payload.pointer || !payload.contentHash) {
78
+ throw new DX402Error(`malformed ${EVIDENCE_HEADER} header: missing required fields`);
79
+ }
80
+ return payload;
81
+ }
82
+ function evidenceFromHeaders(headers) {
83
+ let raw;
84
+ if (typeof headers.get === "function") {
85
+ raw = headers.get(EVIDENCE_HEADER);
86
+ } else {
87
+ const target = EVIDENCE_HEADER.toLowerCase();
88
+ for (const [key, value] of Object.entries(headers)) {
89
+ if (key.toLowerCase() === target) {
90
+ raw = value;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ if (!raw) throw new DX402Error(`no ${EVIDENCE_HEADER} header on this response`);
96
+ return parseEvidenceHeader(raw);
97
+ }
98
+ function dereferencePointer(pointer) {
99
+ if (pointer.startsWith("s3+")) return pointer.slice(3);
100
+ if (pointer.startsWith("ipfs://")) return `https://ipfs.io/ipfs/${pointer.slice(7)}`;
101
+ if (pointer.startsWith("ar://")) return `https://arweave.net/${pointer.slice(5)}`;
102
+ return pointer;
103
+ }
104
+ function parseSealed(raw) {
105
+ let pos = 0;
106
+ const take = (n, what) => {
107
+ if (raw.length < pos + n) throw new DX402Error(`truncated DX402 sealed blob at ${what}`);
108
+ const chunk = raw.subarray(pos, pos + n);
109
+ pos += n;
110
+ return chunk;
111
+ };
112
+ const magic = take(MAGIC.length, "magic");
113
+ for (let i = 0; i < MAGIC.length; i++) {
114
+ if (magic[i] !== MAGIC[i]) throw new DX402Error("not a DX402 sealed blob");
115
+ }
116
+ const version = take(1, "version")[0];
117
+ if (version !== FORMAT_VERSION) {
118
+ throw new DX402Error(`unsupported sealed-blob version ${version}`);
119
+ }
120
+ const algByte = take(1, "algorithm")[0];
121
+ if (algByte !== 1 && algByte !== 2) {
122
+ throw new DX402Error(`unknown key algorithm ${algByte}`);
123
+ }
124
+ const ephLen = take(1, "ephemeral key length")[0];
125
+ const ephemeral = take(ephLen, "ephemeral key");
126
+ const cekNonce = take(NONCE_LEN, "cek nonce");
127
+ const lenBytes = take(2, "wrapped key length");
128
+ const wrappedLen = lenBytes[0] << 8 | lenBytes[1];
129
+ const wrappedCek = take(wrappedLen, "wrapped cek");
130
+ const bodyNonce = take(NONCE_LEN, "body nonce");
131
+ const ciphertext = raw.subarray(pos);
132
+ return {
133
+ alg: algByte === 1 ? "secp256k1" : "x25519",
134
+ ephemeral,
135
+ cekNonce,
136
+ wrappedCek,
137
+ bodyNonce,
138
+ ciphertext
139
+ };
140
+ }
141
+ function sharedSecret(sealed, privateKey) {
142
+ if (sealed.alg === "secp256k1") {
143
+ const full = secp256k1.getSharedSecret(privateKey, sealed.ephemeral, true);
144
+ return full.subarray(1);
145
+ }
146
+ const scalar = sha512(privateKey).subarray(0, 32);
147
+ const shared = x25519.getSharedSecret(scalar, sealed.ephemeral);
148
+ if (shared.every((b) => b === 0)) {
149
+ throw new DX402Error("degenerate ECDH result (small-order public key)");
150
+ }
151
+ return shared;
152
+ }
153
+ function unseal(sealed, privateKey, aad) {
154
+ const shared = sharedSecret(sealed, privateKey);
155
+ const wrapKey = hkdf(sha256, shared, aad, HKDF_INFO, 32);
156
+ const cek = gcm(wrapKey, sealed.cekNonce, aad).decrypt(sealed.wrappedCek);
157
+ if (cek.length !== CEK_LEN) {
158
+ throw new DX402Error(`unwrapped CEK is ${cek.length} bytes, expected ${CEK_LEN}`);
159
+ }
160
+ return gcm(cek, sealed.bodyNonce, aad).decrypt(sealed.ciphertext);
161
+ }
162
+ async function recoverEvidence(evidence, privateKey, options = {}) {
163
+ const key = typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey;
164
+ if (key.length !== 32) {
165
+ throw new DX402Error(`private key must be 32 bytes, got ${key.length}`);
166
+ }
167
+ const doFetch = options.fetch ?? fetch;
168
+ const url = dereferencePointer(evidence.pointer);
169
+ const res = await doFetch(url);
170
+ if (!res.ok) {
171
+ throw new DX402Error(`could not fetch sealed evidence: HTTP ${res.status}`);
172
+ }
173
+ const blob = new Uint8Array(await res.arrayBuffer());
174
+ const sealed = parseSealed(blob);
175
+ const aad = new TextEncoder().encode(evidence.paymentId);
176
+ let plaintext;
177
+ try {
178
+ plaintext = unseal(sealed, key, aad);
179
+ } catch (err) {
180
+ if (err instanceof DX402Error) throw err;
181
+ throw new DX402Error(
182
+ "decryption failed -- wrong key, or the blob belongs to another payment"
183
+ );
184
+ }
185
+ const actual = contentHash(plaintext);
186
+ if (actual.toLowerCase() !== evidence.contentHash.toLowerCase()) {
187
+ throw new ContentHashMismatch(evidence.contentHash, actual);
188
+ }
189
+ return plaintext;
190
+ }
191
+ function ed25519ToX25519(pubkey) {
192
+ if (pubkey.length !== 32) {
193
+ throw new DX402Error(`ed25519 public key must be 32 bytes, got ${pubkey.length}`);
194
+ }
195
+ return edwardsToMontgomeryPub(pubkey);
196
+ }
197
+ function payerKeyFromEvmSignature(signature, digest) {
198
+ const sig = typeof signature === "string" ? hexToBytes(signature) : signature;
199
+ if (sig.length !== 65) {
200
+ throw new DX402Error(`signature must be 65 bytes, got ${sig.length}`);
201
+ }
202
+ let v = sig[65 - 1];
203
+ if (v === 27 || v === 28) v -= 27;
204
+ else if (v >= 35) v = (v - 35) % 2;
205
+ if (v !== 0 && v !== 1) throw new DX402Error(`invalid recovery id ${sig[64]}`);
206
+ const parsed = secp256k1.Signature.fromCompact(sig.subarray(0, 64)).addRecoveryBit(v);
207
+ return parsed.recoverPublicKey(digest).toRawBytes(true);
208
+ }
209
+ function sealEvidence(body, payerKey, paymentIdValue) {
210
+ const aad = new TextEncoder().encode(paymentIdValue);
211
+ const cek = randomBytes(32);
212
+ const bodyNonce = randomBytes(NONCE_LEN);
213
+ const cekNonce = randomBytes(NONCE_LEN);
214
+ const ciphertext = gcm(cek, bodyNonce, aad).encrypt(body);
215
+ let algByte;
216
+ let ephemeral;
217
+ let shared;
218
+ if (payerKey.length === 33) {
219
+ algByte = 1;
220
+ const ephPriv = secp256k1.utils.randomPrivateKey();
221
+ ephemeral = secp256k1.getPublicKey(ephPriv, true);
222
+ shared = secp256k1.getSharedSecret(ephPriv, payerKey, true).subarray(1);
223
+ } else if (payerKey.length === 32) {
224
+ algByte = 2;
225
+ const ephPriv = randomBytes(32);
226
+ ephemeral = x25519.getPublicKey(ephPriv);
227
+ shared = x25519.getSharedSecret(ephPriv, payerKey);
228
+ if (shared.every((b) => b === 0)) {
229
+ throw new DX402Error("degenerate ECDH result (small-order public key)");
230
+ }
231
+ } else {
232
+ throw new DX402Error(
233
+ `payer key must be 33 bytes (secp256k1) or 32 (X25519), got ${payerKey.length}`
234
+ );
235
+ }
236
+ const wrapKey = hkdf(sha256, shared, aad, HKDF_INFO, 32);
237
+ const wrappedCek = gcm(wrapKey, cekNonce, aad).encrypt(cek);
238
+ const out = new Uint8Array(
239
+ MAGIC.length + 3 + ephemeral.length + NONCE_LEN + 2 + wrappedCek.length + NONCE_LEN + ciphertext.length
240
+ );
241
+ let pos = 0;
242
+ out.set(MAGIC, pos);
243
+ pos += MAGIC.length;
244
+ out[pos++] = FORMAT_VERSION;
245
+ out[pos++] = algByte;
246
+ out[pos++] = ephemeral.length;
247
+ out.set(ephemeral, pos);
248
+ pos += ephemeral.length;
249
+ out.set(cekNonce, pos);
250
+ pos += NONCE_LEN;
251
+ out[pos++] = wrappedCek.length >> 8 & 255;
252
+ out[pos++] = wrappedCek.length & 255;
253
+ out.set(wrappedCek, pos);
254
+ pos += wrappedCek.length;
255
+ out.set(bodyNonce, pos);
256
+ pos += NONCE_LEN;
257
+ out.set(ciphertext, pos);
258
+ return out;
259
+ }
4
260
 
5
261
  // src/types/index.ts
6
262
  var CAIP2_IDENTIFIERS = {
@@ -3777,6 +4033,6 @@ var ESCROW_TIMEOUT_MS = {
3777
4033
  // Monad: 90s
3778
4034
  };
3779
4035
 
3780
- export { CAIP2_IDENTIFIERS, CAIP2_TO_CHAIN, DEFAULT_CHAIN, DEFAULT_CONFIG, DEFAULT_FACILITATOR_URL, DEFAULT_PAYMENT_HEADER, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EnvKeyAdapter, FACILITATOR_ADDRESSES, FacilitatorClient, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, OWSWalletAdapter, PAYMENT_HEADER_NAMES, SSEParser, SUPPORTED_CHAINS, TrafficStreamError, X402Client, X402Error, X402_CORS_HEADERS, X402_HEADER_NAMES, buildEscrowPreAuth, buildPaymentRequirements, buildSettleRequest, buildSettleRequestV2, buildSignatureBase, buildSignatureParams, buildTokenMetadata, buildVerifyRequest, buildVerifyRequestV2, caip2ToChain, chainToCAIP2, computeEscrowNonce, convertX402Header, create402Response, createHonoMiddleware, createPaymentHeaders, createPaymentMiddleware, createSignedFetch, createX402Header, createX402V1Header, createX402V2Header, decodeBase64Utf8, decodeX402Header, detectX402Version, encodeBase64Json, encodeBase64Utf8, encodeX402Header, extractPaymentFromHeaders, fetchNonce, generatePaymentOptions, getAlgorandChains, getChainById, getChainByName, getChainsByNetworkType, getChainsByToken, getCorsHeaders, getEVMChainIds, getEnabledChains, getExplorerAddressUrl, getExplorerTxUrl, getFacilitatorAddress, getNetworkType, getPaymentHeader, getSVMChains, getSuiChains, getSupportedTokens, getTokenByAddress, getTokenConfig, getXRPLChains, isAlgorandChain, isCAIP2Format, isChainSupported, isSVMChain, isSuiChain, isTokenSupported, isXRPLChain, matchesFilters, parseNetworkIdentifier, parseTrafficEvent, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, validateAmount, validateRecipient };
4036
+ export { CAIP2_IDENTIFIERS, CAIP2_TO_CHAIN, ContentHashMismatch, DEFAULT_CHAIN, DEFAULT_CONFIG, DEFAULT_FACILITATOR_URL, DEFAULT_PAYMENT_HEADER, DX402Error, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, EnvKeyAdapter, EvidenceSkipped, FACILITATOR_ADDRESSES, FacilitatorClient, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, OWSWalletAdapter, PAYMENT_HEADER_NAMES, SSEParser, SUPPORTED_CHAINS, TrafficStreamError, X402Client, X402Error, X402_CORS_HEADERS, X402_HEADER_NAMES, buildEscrowPreAuth, buildPaymentRequirements, buildSettleRequest, buildSettleRequestV2, buildSignatureBase, buildSignatureParams, buildTokenMetadata, buildVerifyRequest, buildVerifyRequestV2, caip2ToChain, chainToCAIP2, computeEscrowNonce, contentHash, convertX402Header, create402Response, createHonoMiddleware, createPaymentHeaders, createPaymentMiddleware, createSignedFetch, createX402Header, createX402V1Header, createX402V2Header, decodeBase64Utf8, decodeX402Header, dereferencePointer, detectX402Version, paymentId as dx402PaymentId, ed25519ToX25519, encodeBase64Json, encodeBase64Utf8, encodeX402Header, evidenceFromHeaders, extractPaymentFromHeaders, fetchNonce, generatePaymentOptions, getAlgorandChains, getChainById, getChainByName, getChainsByNetworkType, getChainsByToken, getCorsHeaders, getEVMChainIds, getEnabledChains, getExplorerAddressUrl, getExplorerTxUrl, getFacilitatorAddress, getNetworkType, getPaymentHeader, getSVMChains, getSuiChains, getSupportedTokens, getTokenByAddress, getTokenConfig, getXRPLChains, isAlgorandChain, isCAIP2Format, isChainSupported, isEndToEnd, isSVMChain, isSuiChain, isTokenSupported, isXRPLChain, matchesFilters, parseEvidenceHeader, parseNetworkIdentifier, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, recoverEvidence, sealEvidence, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal, validateAmount, validateRecipient };
3781
4037
  //# sourceMappingURL=index.mjs.map
3782
4038
  //# sourceMappingURL=index.mjs.map