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.d.mts +169 -1
- package/dist/index.d.ts +169 -1
- package/dist/index.js +273 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +258 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
- package/src/dx402.ts +484 -0
- package/src/dx402.vectors.json +15 -0
- package/src/index.ts +22 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,264 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var aes = require('@noble/ciphers/aes');
|
|
4
|
+
var ed25519 = require('@noble/curves/ed25519');
|
|
5
|
+
var secp256k1 = require('@noble/curves/secp256k1');
|
|
6
|
+
var hkdf = require('@noble/hashes/hkdf');
|
|
7
|
+
var sha3 = require('@noble/hashes/sha3');
|
|
8
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
9
|
+
var utils = require('@noble/hashes/utils');
|
|
3
10
|
var ethers = require('ethers');
|
|
4
11
|
|
|
5
|
-
// src/
|
|
12
|
+
// src/dx402.ts
|
|
13
|
+
var EVIDENCE_HEADER = "X-Durable-Evidence";
|
|
14
|
+
var MAGIC = new Uint8Array([68, 88, 52, 48, 50]);
|
|
15
|
+
var FORMAT_VERSION = 1;
|
|
16
|
+
var NONCE_LEN = 12;
|
|
17
|
+
var CEK_LEN = 32;
|
|
18
|
+
var HKDF_INFO = new TextEncoder().encode("DX402-v1-wrap");
|
|
19
|
+
var DX402Error = class extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "DX402Error";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var EvidenceSkipped = class extends DX402Error {
|
|
26
|
+
constructor(reason) {
|
|
27
|
+
super(`no durable evidence was anchored: ${reason}`);
|
|
28
|
+
this.reason = reason;
|
|
29
|
+
this.name = "EvidenceSkipped";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ContentHashMismatch = class extends DX402Error {
|
|
33
|
+
constructor(anchored, actual) {
|
|
34
|
+
super(`content hash mismatch: anchored ${anchored}, decrypted ${actual}`);
|
|
35
|
+
this.anchored = anchored;
|
|
36
|
+
this.actual = actual;
|
|
37
|
+
this.name = "ContentHashMismatch";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function isEndToEnd(evidence) {
|
|
41
|
+
return evidence.mode === "direct";
|
|
42
|
+
}
|
|
43
|
+
function b64urlDecode(value) {
|
|
44
|
+
const normalized = value.trim().replace(/-/g, "+").replace(/_/g, "/");
|
|
45
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
46
|
+
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
47
|
+
}
|
|
48
|
+
function hexToBytes(hex) {
|
|
49
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
50
|
+
if (clean.length % 2 !== 0) throw new DX402Error("odd-length hex string");
|
|
51
|
+
const out = new Uint8Array(clean.length / 2);
|
|
52
|
+
for (let i = 0; i < out.length; i++) {
|
|
53
|
+
out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function bytesToHex(bytes) {
|
|
58
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
59
|
+
}
|
|
60
|
+
function contentHash(body) {
|
|
61
|
+
return "0x" + bytesToHex(sha3.keccak_256(body));
|
|
62
|
+
}
|
|
63
|
+
function paymentId(caip2Network, txHash) {
|
|
64
|
+
const enc = new TextEncoder();
|
|
65
|
+
const clean = txHash.startsWith("0x") ? txHash.slice(2) : txHash;
|
|
66
|
+
const preimage = new Uint8Array([...enc.encode(caip2Network), ...enc.encode(clean)]);
|
|
67
|
+
return "0x" + bytesToHex(sha3.keccak_256(preimage));
|
|
68
|
+
}
|
|
69
|
+
function parseEvidenceHeader(value) {
|
|
70
|
+
let payload;
|
|
71
|
+
try {
|
|
72
|
+
payload = JSON.parse(new TextDecoder().decode(b64urlDecode(value)));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new DX402Error(`malformed ${EVIDENCE_HEADER} header: ${String(err)}`);
|
|
75
|
+
}
|
|
76
|
+
if ("skipped" in payload) {
|
|
77
|
+
throw new EvidenceSkipped(String(payload.skipped));
|
|
78
|
+
}
|
|
79
|
+
if (!payload.paymentId || !payload.pointer || !payload.contentHash) {
|
|
80
|
+
throw new DX402Error(`malformed ${EVIDENCE_HEADER} header: missing required fields`);
|
|
81
|
+
}
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
84
|
+
function evidenceFromHeaders(headers) {
|
|
85
|
+
let raw;
|
|
86
|
+
if (typeof headers.get === "function") {
|
|
87
|
+
raw = headers.get(EVIDENCE_HEADER);
|
|
88
|
+
} else {
|
|
89
|
+
const target = EVIDENCE_HEADER.toLowerCase();
|
|
90
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
91
|
+
if (key.toLowerCase() === target) {
|
|
92
|
+
raw = value;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!raw) throw new DX402Error(`no ${EVIDENCE_HEADER} header on this response`);
|
|
98
|
+
return parseEvidenceHeader(raw);
|
|
99
|
+
}
|
|
100
|
+
function dereferencePointer(pointer) {
|
|
101
|
+
if (pointer.startsWith("s3+")) return pointer.slice(3);
|
|
102
|
+
if (pointer.startsWith("ipfs://")) return `https://ipfs.io/ipfs/${pointer.slice(7)}`;
|
|
103
|
+
if (pointer.startsWith("ar://")) return `https://arweave.net/${pointer.slice(5)}`;
|
|
104
|
+
return pointer;
|
|
105
|
+
}
|
|
106
|
+
function parseSealed(raw) {
|
|
107
|
+
let pos = 0;
|
|
108
|
+
const take = (n, what) => {
|
|
109
|
+
if (raw.length < pos + n) throw new DX402Error(`truncated DX402 sealed blob at ${what}`);
|
|
110
|
+
const chunk = raw.subarray(pos, pos + n);
|
|
111
|
+
pos += n;
|
|
112
|
+
return chunk;
|
|
113
|
+
};
|
|
114
|
+
const magic = take(MAGIC.length, "magic");
|
|
115
|
+
for (let i = 0; i < MAGIC.length; i++) {
|
|
116
|
+
if (magic[i] !== MAGIC[i]) throw new DX402Error("not a DX402 sealed blob");
|
|
117
|
+
}
|
|
118
|
+
const version = take(1, "version")[0];
|
|
119
|
+
if (version !== FORMAT_VERSION) {
|
|
120
|
+
throw new DX402Error(`unsupported sealed-blob version ${version}`);
|
|
121
|
+
}
|
|
122
|
+
const algByte = take(1, "algorithm")[0];
|
|
123
|
+
if (algByte !== 1 && algByte !== 2) {
|
|
124
|
+
throw new DX402Error(`unknown key algorithm ${algByte}`);
|
|
125
|
+
}
|
|
126
|
+
const ephLen = take(1, "ephemeral key length")[0];
|
|
127
|
+
const ephemeral = take(ephLen, "ephemeral key");
|
|
128
|
+
const cekNonce = take(NONCE_LEN, "cek nonce");
|
|
129
|
+
const lenBytes = take(2, "wrapped key length");
|
|
130
|
+
const wrappedLen = lenBytes[0] << 8 | lenBytes[1];
|
|
131
|
+
const wrappedCek = take(wrappedLen, "wrapped cek");
|
|
132
|
+
const bodyNonce = take(NONCE_LEN, "body nonce");
|
|
133
|
+
const ciphertext = raw.subarray(pos);
|
|
134
|
+
return {
|
|
135
|
+
alg: algByte === 1 ? "secp256k1" : "x25519",
|
|
136
|
+
ephemeral,
|
|
137
|
+
cekNonce,
|
|
138
|
+
wrappedCek,
|
|
139
|
+
bodyNonce,
|
|
140
|
+
ciphertext
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function sharedSecret(sealed, privateKey) {
|
|
144
|
+
if (sealed.alg === "secp256k1") {
|
|
145
|
+
const full = secp256k1.secp256k1.getSharedSecret(privateKey, sealed.ephemeral, true);
|
|
146
|
+
return full.subarray(1);
|
|
147
|
+
}
|
|
148
|
+
const scalar = sha2.sha512(privateKey).subarray(0, 32);
|
|
149
|
+
const shared = ed25519.x25519.getSharedSecret(scalar, sealed.ephemeral);
|
|
150
|
+
if (shared.every((b) => b === 0)) {
|
|
151
|
+
throw new DX402Error("degenerate ECDH result (small-order public key)");
|
|
152
|
+
}
|
|
153
|
+
return shared;
|
|
154
|
+
}
|
|
155
|
+
function unseal(sealed, privateKey, aad) {
|
|
156
|
+
const shared = sharedSecret(sealed, privateKey);
|
|
157
|
+
const wrapKey = hkdf.hkdf(sha2.sha256, shared, aad, HKDF_INFO, 32);
|
|
158
|
+
const cek = aes.gcm(wrapKey, sealed.cekNonce, aad).decrypt(sealed.wrappedCek);
|
|
159
|
+
if (cek.length !== CEK_LEN) {
|
|
160
|
+
throw new DX402Error(`unwrapped CEK is ${cek.length} bytes, expected ${CEK_LEN}`);
|
|
161
|
+
}
|
|
162
|
+
return aes.gcm(cek, sealed.bodyNonce, aad).decrypt(sealed.ciphertext);
|
|
163
|
+
}
|
|
164
|
+
async function recoverEvidence(evidence, privateKey, options = {}) {
|
|
165
|
+
const key = typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey;
|
|
166
|
+
if (key.length !== 32) {
|
|
167
|
+
throw new DX402Error(`private key must be 32 bytes, got ${key.length}`);
|
|
168
|
+
}
|
|
169
|
+
const doFetch = options.fetch ?? fetch;
|
|
170
|
+
const url = dereferencePointer(evidence.pointer);
|
|
171
|
+
const res = await doFetch(url);
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
throw new DX402Error(`could not fetch sealed evidence: HTTP ${res.status}`);
|
|
174
|
+
}
|
|
175
|
+
const blob = new Uint8Array(await res.arrayBuffer());
|
|
176
|
+
const sealed = parseSealed(blob);
|
|
177
|
+
const aad = new TextEncoder().encode(evidence.paymentId);
|
|
178
|
+
let plaintext;
|
|
179
|
+
try {
|
|
180
|
+
plaintext = unseal(sealed, key, aad);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err instanceof DX402Error) throw err;
|
|
183
|
+
throw new DX402Error(
|
|
184
|
+
"decryption failed -- wrong key, or the blob belongs to another payment"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const actual = contentHash(plaintext);
|
|
188
|
+
if (actual.toLowerCase() !== evidence.contentHash.toLowerCase()) {
|
|
189
|
+
throw new ContentHashMismatch(evidence.contentHash, actual);
|
|
190
|
+
}
|
|
191
|
+
return plaintext;
|
|
192
|
+
}
|
|
193
|
+
function ed25519ToX25519(pubkey) {
|
|
194
|
+
if (pubkey.length !== 32) {
|
|
195
|
+
throw new DX402Error(`ed25519 public key must be 32 bytes, got ${pubkey.length}`);
|
|
196
|
+
}
|
|
197
|
+
return ed25519.edwardsToMontgomeryPub(pubkey);
|
|
198
|
+
}
|
|
199
|
+
function payerKeyFromEvmSignature(signature, digest) {
|
|
200
|
+
const sig = typeof signature === "string" ? hexToBytes(signature) : signature;
|
|
201
|
+
if (sig.length !== 65) {
|
|
202
|
+
throw new DX402Error(`signature must be 65 bytes, got ${sig.length}`);
|
|
203
|
+
}
|
|
204
|
+
let v = sig[65 - 1];
|
|
205
|
+
if (v === 27 || v === 28) v -= 27;
|
|
206
|
+
else if (v >= 35) v = (v - 35) % 2;
|
|
207
|
+
if (v !== 0 && v !== 1) throw new DX402Error(`invalid recovery id ${sig[64]}`);
|
|
208
|
+
const parsed = secp256k1.secp256k1.Signature.fromCompact(sig.subarray(0, 64)).addRecoveryBit(v);
|
|
209
|
+
return parsed.recoverPublicKey(digest).toRawBytes(true);
|
|
210
|
+
}
|
|
211
|
+
function sealEvidence(body, payerKey, paymentIdValue) {
|
|
212
|
+
const aad = new TextEncoder().encode(paymentIdValue);
|
|
213
|
+
const cek = utils.randomBytes(32);
|
|
214
|
+
const bodyNonce = utils.randomBytes(NONCE_LEN);
|
|
215
|
+
const cekNonce = utils.randomBytes(NONCE_LEN);
|
|
216
|
+
const ciphertext = aes.gcm(cek, bodyNonce, aad).encrypt(body);
|
|
217
|
+
let algByte;
|
|
218
|
+
let ephemeral;
|
|
219
|
+
let shared;
|
|
220
|
+
if (payerKey.length === 33) {
|
|
221
|
+
algByte = 1;
|
|
222
|
+
const ephPriv = secp256k1.secp256k1.utils.randomPrivateKey();
|
|
223
|
+
ephemeral = secp256k1.secp256k1.getPublicKey(ephPriv, true);
|
|
224
|
+
shared = secp256k1.secp256k1.getSharedSecret(ephPriv, payerKey, true).subarray(1);
|
|
225
|
+
} else if (payerKey.length === 32) {
|
|
226
|
+
algByte = 2;
|
|
227
|
+
const ephPriv = utils.randomBytes(32);
|
|
228
|
+
ephemeral = ed25519.x25519.getPublicKey(ephPriv);
|
|
229
|
+
shared = ed25519.x25519.getSharedSecret(ephPriv, payerKey);
|
|
230
|
+
if (shared.every((b) => b === 0)) {
|
|
231
|
+
throw new DX402Error("degenerate ECDH result (small-order public key)");
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
throw new DX402Error(
|
|
235
|
+
`payer key must be 33 bytes (secp256k1) or 32 (X25519), got ${payerKey.length}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const wrapKey = hkdf.hkdf(sha2.sha256, shared, aad, HKDF_INFO, 32);
|
|
239
|
+
const wrappedCek = aes.gcm(wrapKey, cekNonce, aad).encrypt(cek);
|
|
240
|
+
const out = new Uint8Array(
|
|
241
|
+
MAGIC.length + 3 + ephemeral.length + NONCE_LEN + 2 + wrappedCek.length + NONCE_LEN + ciphertext.length
|
|
242
|
+
);
|
|
243
|
+
let pos = 0;
|
|
244
|
+
out.set(MAGIC, pos);
|
|
245
|
+
pos += MAGIC.length;
|
|
246
|
+
out[pos++] = FORMAT_VERSION;
|
|
247
|
+
out[pos++] = algByte;
|
|
248
|
+
out[pos++] = ephemeral.length;
|
|
249
|
+
out.set(ephemeral, pos);
|
|
250
|
+
pos += ephemeral.length;
|
|
251
|
+
out.set(cekNonce, pos);
|
|
252
|
+
pos += NONCE_LEN;
|
|
253
|
+
out[pos++] = wrappedCek.length >> 8 & 255;
|
|
254
|
+
out[pos++] = wrappedCek.length & 255;
|
|
255
|
+
out.set(wrappedCek, pos);
|
|
256
|
+
pos += wrappedCek.length;
|
|
257
|
+
out.set(bodyNonce, pos);
|
|
258
|
+
pos += NONCE_LEN;
|
|
259
|
+
out.set(ciphertext, pos);
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
6
262
|
|
|
7
263
|
// src/types/index.ts
|
|
8
264
|
var CAIP2_IDENTIFIERS = {
|
|
@@ -3781,14 +4037,18 @@ var ESCROW_TIMEOUT_MS = {
|
|
|
3781
4037
|
|
|
3782
4038
|
exports.CAIP2_IDENTIFIERS = CAIP2_IDENTIFIERS;
|
|
3783
4039
|
exports.CAIP2_TO_CHAIN = CAIP2_TO_CHAIN;
|
|
4040
|
+
exports.ContentHashMismatch = ContentHashMismatch;
|
|
3784
4041
|
exports.DEFAULT_CHAIN = DEFAULT_CHAIN;
|
|
3785
4042
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
3786
4043
|
exports.DEFAULT_FACILITATOR_URL = DEFAULT_FACILITATOR_URL;
|
|
3787
4044
|
exports.DEFAULT_PAYMENT_HEADER = DEFAULT_PAYMENT_HEADER;
|
|
4045
|
+
exports.DX402Error = DX402Error;
|
|
3788
4046
|
exports.ESCROW_DEPOSIT_LIMIT_USD = ESCROW_DEPOSIT_LIMIT_USD;
|
|
3789
4047
|
exports.ESCROW_TIER_WINDOWS = ESCROW_TIER_WINDOWS;
|
|
3790
4048
|
exports.EVENT_KINDS = EVENT_KINDS;
|
|
4049
|
+
exports.EVIDENCE_HEADER = EVIDENCE_HEADER;
|
|
3791
4050
|
exports.EnvKeyAdapter = EnvKeyAdapter;
|
|
4051
|
+
exports.EvidenceSkipped = EvidenceSkipped;
|
|
3792
4052
|
exports.FACILITATOR_ADDRESSES = FACILITATOR_ADDRESSES;
|
|
3793
4053
|
exports.FacilitatorClient = FacilitatorClient;
|
|
3794
4054
|
exports.KEEPALIVE_INTERVAL_MS = KEEPALIVE_INTERVAL_MS;
|
|
@@ -3814,6 +4074,7 @@ exports.buildVerifyRequestV2 = buildVerifyRequestV2;
|
|
|
3814
4074
|
exports.caip2ToChain = caip2ToChain;
|
|
3815
4075
|
exports.chainToCAIP2 = chainToCAIP2;
|
|
3816
4076
|
exports.computeEscrowNonce = computeEscrowNonce;
|
|
4077
|
+
exports.contentHash = contentHash;
|
|
3817
4078
|
exports.convertX402Header = convertX402Header;
|
|
3818
4079
|
exports.create402Response = create402Response;
|
|
3819
4080
|
exports.createHonoMiddleware = createHonoMiddleware;
|
|
@@ -3825,10 +4086,14 @@ exports.createX402V1Header = createX402V1Header;
|
|
|
3825
4086
|
exports.createX402V2Header = createX402V2Header;
|
|
3826
4087
|
exports.decodeBase64Utf8 = decodeBase64Utf8;
|
|
3827
4088
|
exports.decodeX402Header = decodeX402Header;
|
|
4089
|
+
exports.dereferencePointer = dereferencePointer;
|
|
3828
4090
|
exports.detectX402Version = detectX402Version;
|
|
4091
|
+
exports.dx402PaymentId = paymentId;
|
|
4092
|
+
exports.ed25519ToX25519 = ed25519ToX25519;
|
|
3829
4093
|
exports.encodeBase64Json = encodeBase64Json;
|
|
3830
4094
|
exports.encodeBase64Utf8 = encodeBase64Utf8;
|
|
3831
4095
|
exports.encodeX402Header = encodeX402Header;
|
|
4096
|
+
exports.evidenceFromHeaders = evidenceFromHeaders;
|
|
3832
4097
|
exports.extractPaymentFromHeaders = extractPaymentFromHeaders;
|
|
3833
4098
|
exports.fetchNonce = fetchNonce;
|
|
3834
4099
|
exports.generatePaymentOptions = generatePaymentOptions;
|
|
@@ -3854,17 +4119,24 @@ exports.getXRPLChains = getXRPLChains;
|
|
|
3854
4119
|
exports.isAlgorandChain = isAlgorandChain;
|
|
3855
4120
|
exports.isCAIP2Format = isCAIP2Format;
|
|
3856
4121
|
exports.isChainSupported = isChainSupported;
|
|
4122
|
+
exports.isEndToEnd = isEndToEnd;
|
|
3857
4123
|
exports.isSVMChain = isSVMChain;
|
|
3858
4124
|
exports.isSuiChain = isSuiChain;
|
|
3859
4125
|
exports.isTokenSupported = isTokenSupported;
|
|
3860
4126
|
exports.isXRPLChain = isXRPLChain;
|
|
3861
4127
|
exports.matchesFilters = matchesFilters;
|
|
4128
|
+
exports.parseEvidenceHeader = parseEvidenceHeader;
|
|
3862
4129
|
exports.parseNetworkIdentifier = parseNetworkIdentifier;
|
|
4130
|
+
exports.parseSealed = parseSealed;
|
|
3863
4131
|
exports.parseTrafficEvent = parseTrafficEvent;
|
|
4132
|
+
exports.payerKeyFromEvmSignature = payerKeyFromEvmSignature;
|
|
4133
|
+
exports.recoverEvidence = recoverEvidence;
|
|
4134
|
+
exports.sealEvidence = sealEvidence;
|
|
3864
4135
|
exports.signRequest = signRequest;
|
|
3865
4136
|
exports.signRequestWithSigner = signRequestWithSigner;
|
|
3866
4137
|
exports.signRequestWithWallet = signRequestWithWallet;
|
|
3867
4138
|
exports.streamTrafficEvents = streamTrafficEvents;
|
|
4139
|
+
exports.unseal = unseal;
|
|
3868
4140
|
exports.validateAmount = validateAmount;
|
|
3869
4141
|
exports.validateRecipient = validateRecipient;
|
|
3870
4142
|
//# sourceMappingURL=index.js.map
|