@agentokratia/x402-escrow 2.0.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.js ADDED
@@ -0,0 +1,568 @@
1
+ // src/client/session-wrapper.ts
2
+ import { isAddress, getAddress as getAddress2 } from "viem";
3
+ import { x402Client } from "@x402/core/client";
4
+ import { wrapFetchWithPayment } from "@x402/fetch";
5
+
6
+ // src/client/escrow.ts
7
+ import { toHex as toHex2, getAddress } from "viem";
8
+
9
+ // src/types.ts
10
+ var X402_HEADERS = {
11
+ /** Server → Client: Payment options (402 response) - base64 JSON */
12
+ PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
13
+ /** Client → Server: Payment payload - base64 JSON */
14
+ PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
15
+ /** Server → Client: Settlement result - base64 JSON (includes session data) */
16
+ PAYMENT_RESPONSE: "PAYMENT-RESPONSE"
17
+ };
18
+ function fromBase64(str) {
19
+ try {
20
+ if (typeof atob !== "undefined") {
21
+ return decodeURIComponent(escape(atob(str)));
22
+ }
23
+ return Buffer.from(str, "base64").toString("utf-8");
24
+ } catch {
25
+ return Buffer.from(str, "base64").toString("utf-8");
26
+ }
27
+ }
28
+ function generateRequestId() {
29
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
30
+ return crypto.randomUUID();
31
+ }
32
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
33
+ }
34
+ function generateRandomBytes(length) {
35
+ const bytes = new Uint8Array(length);
36
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
37
+ crypto.getRandomValues(bytes);
38
+ } else {
39
+ for (let i = 0; i < length; i++) {
40
+ bytes[i] = Math.floor(Math.random() * 256);
41
+ }
42
+ }
43
+ return bytes;
44
+ }
45
+ function parsePaymentResponseHeader(header) {
46
+ try {
47
+ return JSON.parse(fromBase64(header));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ // src/constants.ts
54
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
55
+ var DEFAULT_SESSION_DURATION = 3600;
56
+ var DEFAULT_REFUND_WINDOW = 86400;
57
+
58
+ // src/client/storage.ts
59
+ var BaseStorage = class {
60
+ constructor() {
61
+ this.sessions = /* @__PURE__ */ new Map();
62
+ }
63
+ get(network, receiver) {
64
+ const now = Date.now() / 1e3;
65
+ for (const session of this.sessions.values()) {
66
+ if (session.network === network && session.receiver.toLowerCase() === receiver.toLowerCase() && session.authorizationExpiry > now) {
67
+ return session;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ findBest(network, receiver, minAmount) {
73
+ const now = Date.now() / 1e3;
74
+ let best = null;
75
+ let bestBalance = 0n;
76
+ for (const session of this.sessions.values()) {
77
+ if (session.network === network && session.receiver.toLowerCase() === receiver.toLowerCase() && session.authorizationExpiry > now) {
78
+ const balance = BigInt(session.balance);
79
+ if (balance >= minAmount && balance > bestBalance) {
80
+ best = session;
81
+ bestBalance = balance;
82
+ }
83
+ }
84
+ }
85
+ return best;
86
+ }
87
+ set(session) {
88
+ this.sessions.set(session.sessionId, session);
89
+ this.onUpdate();
90
+ }
91
+ update(sessionId, balance) {
92
+ const session = this.sessions.get(sessionId);
93
+ if (session) {
94
+ this.sessions.set(sessionId, { ...session, balance });
95
+ this.onUpdate();
96
+ }
97
+ }
98
+ list() {
99
+ return Array.from(this.sessions.values());
100
+ }
101
+ remove(sessionId) {
102
+ this.sessions.delete(sessionId);
103
+ this.onUpdate();
104
+ }
105
+ /** Override in subclasses for persistence */
106
+ onUpdate() {
107
+ }
108
+ };
109
+ var InMemoryStorage = class extends BaseStorage {
110
+ // No persistence needed
111
+ };
112
+ var BrowserLocalStorage = class extends BaseStorage {
113
+ constructor(key = "x402-sessions") {
114
+ super();
115
+ this.key = key;
116
+ this.load();
117
+ }
118
+ onUpdate() {
119
+ this.save();
120
+ }
121
+ load() {
122
+ if (typeof localStorage === "undefined") return;
123
+ try {
124
+ const data = localStorage.getItem(this.key);
125
+ if (data) {
126
+ const sessions = JSON.parse(data);
127
+ for (const s of sessions) this.sessions.set(s.sessionId, s);
128
+ }
129
+ } catch {
130
+ if (process.env.NODE_ENV !== "production") {
131
+ console.warn("[x402] Failed to load sessions from localStorage");
132
+ }
133
+ }
134
+ }
135
+ save() {
136
+ if (typeof localStorage === "undefined") return;
137
+ try {
138
+ localStorage.setItem(this.key, JSON.stringify(Array.from(this.sessions.values())));
139
+ } catch {
140
+ if (process.env.NODE_ENV !== "production") {
141
+ console.warn("[x402] Failed to save sessions to localStorage");
142
+ }
143
+ }
144
+ }
145
+ };
146
+ function createStorage(type, storageKey) {
147
+ return type === "localStorage" ? new BrowserLocalStorage(storageKey) : new InMemoryStorage();
148
+ }
149
+
150
+ // src/client/session-manager.ts
151
+ var SessionManager = class {
152
+ constructor(network, options = {}) {
153
+ this.network = network;
154
+ this.storage = createStorage(options.storage ?? "memory", options.storageKey);
155
+ }
156
+ /**
157
+ * Store a session from escrow settlement response.
158
+ */
159
+ store(session) {
160
+ this.storage.set({ ...session, createdAt: Date.now() });
161
+ }
162
+ /**
163
+ * Get session for a specific receiver.
164
+ */
165
+ getForReceiver(receiver) {
166
+ return this.storage.get(this.network, receiver);
167
+ }
168
+ /**
169
+ * Find best session for receiver with minimum balance.
170
+ */
171
+ findBest(receiver, minAmount) {
172
+ return this.storage.findBest(this.network, receiver, minAmount);
173
+ }
174
+ /**
175
+ * Check if valid session exists for receiver.
176
+ */
177
+ hasValid(receiver, minAmount) {
178
+ const session = minAmount ? this.storage.findBest(this.network, receiver, BigInt(minAmount)) : this.storage.get(this.network, receiver);
179
+ return session !== null;
180
+ }
181
+ /**
182
+ * Update session balance after debit.
183
+ */
184
+ updateBalance(sessionId, newBalance) {
185
+ this.storage.update(sessionId, newBalance);
186
+ }
187
+ /**
188
+ * Get all stored sessions.
189
+ */
190
+ getAll() {
191
+ return this.storage.list();
192
+ }
193
+ /**
194
+ * Remove a specific session.
195
+ */
196
+ remove(sessionId) {
197
+ this.storage.remove(sessionId);
198
+ }
199
+ /**
200
+ * Clear all sessions.
201
+ */
202
+ clear() {
203
+ for (const session of this.storage.list()) {
204
+ this.storage.remove(session.sessionId);
205
+ }
206
+ }
207
+ };
208
+
209
+ // src/client/eip712.ts
210
+ import {
211
+ keccak256,
212
+ encodeAbiParameters,
213
+ parseAbiParameters,
214
+ toHex
215
+ } from "viem";
216
+ var PAYMENT_INFO_TYPE = "PaymentInfo(address operator,address payer,address receiver,address token,uint120 maxAmount,uint48 preApprovalExpiry,uint48 authorizationExpiry,uint48 refundExpiry,uint16 minFeeBps,uint16 maxFeeBps,address feeReceiver,uint256 salt)";
217
+ var PAYMENT_INFO_TYPEHASH = keccak256(toHex(new TextEncoder().encode(PAYMENT_INFO_TYPE)));
218
+ var PAYMENT_INFO_ABI_PARAMS = "bytes32, address, address, address, address, uint120, uint48, uint48, uint48, uint16, uint16, address, uint256";
219
+ var NONCE_ABI_PARAMS = "uint256, address, bytes32";
220
+ var ERC3009_TYPES = {
221
+ ReceiveWithAuthorization: [
222
+ { name: "from", type: "address" },
223
+ { name: "to", type: "address" },
224
+ { name: "value", type: "uint256" },
225
+ { name: "validAfter", type: "uint256" },
226
+ { name: "validBefore", type: "uint256" },
227
+ { name: "nonce", type: "bytes32" }
228
+ ]
229
+ };
230
+ function computeEscrowNonce(chainId, escrowContract, paymentInfo) {
231
+ const paymentInfoHash = keccak256(
232
+ encodeAbiParameters(parseAbiParameters(PAYMENT_INFO_ABI_PARAMS), [
233
+ PAYMENT_INFO_TYPEHASH,
234
+ paymentInfo.operator,
235
+ ZERO_ADDRESS,
236
+ // payer = 0 for payer-agnostic
237
+ paymentInfo.receiver,
238
+ paymentInfo.token,
239
+ paymentInfo.maxAmount,
240
+ paymentInfo.preApprovalExpiry,
241
+ paymentInfo.authorizationExpiry,
242
+ paymentInfo.refundExpiry,
243
+ paymentInfo.minFeeBps,
244
+ paymentInfo.maxFeeBps,
245
+ paymentInfo.feeReceiver,
246
+ paymentInfo.salt
247
+ ])
248
+ );
249
+ return keccak256(
250
+ encodeAbiParameters(parseAbiParameters(NONCE_ABI_PARAMS), [
251
+ BigInt(chainId),
252
+ escrowContract,
253
+ paymentInfoHash
254
+ ])
255
+ );
256
+ }
257
+ async function signERC3009(wallet, authorization, domain) {
258
+ if (!wallet.account) {
259
+ throw new Error("WalletClient must have an account");
260
+ }
261
+ return wallet.signTypedData({
262
+ account: wallet.account,
263
+ domain,
264
+ types: ERC3009_TYPES,
265
+ primaryType: "ReceiveWithAuthorization",
266
+ message: {
267
+ from: authorization.from,
268
+ to: authorization.to,
269
+ value: authorization.value,
270
+ validAfter: authorization.validAfter,
271
+ validBefore: authorization.validBefore,
272
+ nonce: authorization.nonce
273
+ }
274
+ });
275
+ }
276
+
277
+ // src/client/escrow.ts
278
+ var EscrowScheme = class {
279
+ constructor(walletClient, options = {}) {
280
+ this.scheme = "escrow";
281
+ if (!walletClient.account) {
282
+ throw new Error("WalletClient must have an account");
283
+ }
284
+ if (!walletClient.chain) {
285
+ throw new Error("WalletClient must have a chain");
286
+ }
287
+ this.wallet = walletClient;
288
+ this.chainId = walletClient.chain.id;
289
+ this.network = `eip155:${walletClient.chain.id}`;
290
+ this.sessionDuration = options.sessionDuration ?? DEFAULT_SESSION_DURATION;
291
+ this.refundWindow = options.refundWindow ?? DEFAULT_REFUND_WINDOW;
292
+ this.customDepositAmount = options.depositAmount ? BigInt(options.depositAmount) : void 0;
293
+ this.sessions = new SessionManager(this.network, {
294
+ storage: options.storage,
295
+ storageKey: options.storageKey
296
+ });
297
+ }
298
+ get address() {
299
+ return this.wallet.account.address;
300
+ }
301
+ // ========== Payment Payload Creation ==========
302
+ /**
303
+ * Creates payment payload for escrow scheme.
304
+ * Auto-detects whether to create new session or use existing one.
305
+ */
306
+ async createPaymentPayload(x402Version, paymentRequirements) {
307
+ const receiver = getAddress(paymentRequirements.payTo);
308
+ const amount = BigInt(paymentRequirements.amount);
309
+ const existingSession = this.sessions.findBest(receiver, amount);
310
+ if (existingSession) {
311
+ return this.createUsagePayload(x402Version, existingSession, paymentRequirements.amount);
312
+ }
313
+ return this.createCreationPayload(x402Version, paymentRequirements);
314
+ }
315
+ // ========== Private: Payload Builders ==========
316
+ /**
317
+ * Session USAGE payload - uses existing session (no signature).
318
+ */
319
+ createUsagePayload(x402Version, session, amount) {
320
+ return {
321
+ x402Version,
322
+ payload: {
323
+ session: {
324
+ id: session.sessionId,
325
+ token: session.sessionToken
326
+ },
327
+ amount,
328
+ requestId: generateRequestId()
329
+ }
330
+ };
331
+ }
332
+ /**
333
+ * Session CREATION payload - requires wallet signature.
334
+ */
335
+ async createCreationPayload(x402Version, paymentRequirements) {
336
+ const extra = paymentRequirements.extra;
337
+ if (!extra.escrowContract || !extra.facilitator || !extra.tokenCollector) {
338
+ throw new Error("Missing required escrow configuration in payment requirements");
339
+ }
340
+ const escrowContract = getAddress(extra.escrowContract);
341
+ const facilitator = getAddress(extra.facilitator);
342
+ const tokenCollector = getAddress(extra.tokenCollector);
343
+ const receiver = getAddress(paymentRequirements.payTo);
344
+ const token = getAddress(paymentRequirements.asset);
345
+ const now = Math.floor(Date.now() / 1e3);
346
+ const salt = this.generateSalt();
347
+ const authorizationExpiry = now + this.sessionDuration;
348
+ const refundExpiry = authorizationExpiry + this.refundWindow;
349
+ const minDeposit = extra.minDeposit ? BigInt(extra.minDeposit) : BigInt(paymentRequirements.amount);
350
+ const maxDeposit = extra.maxDeposit ? BigInt(extra.maxDeposit) : minDeposit;
351
+ let amount;
352
+ if (this.customDepositAmount !== void 0) {
353
+ if (this.customDepositAmount < minDeposit) {
354
+ throw new Error(
355
+ `Deposit amount ${this.customDepositAmount} is below minimum ${minDeposit}`
356
+ );
357
+ }
358
+ if (this.customDepositAmount > maxDeposit) {
359
+ throw new Error(`Deposit amount ${this.customDepositAmount} exceeds maximum ${maxDeposit}`);
360
+ }
361
+ amount = this.customDepositAmount;
362
+ } else {
363
+ amount = maxDeposit;
364
+ }
365
+ const validAfter = 0n;
366
+ const validBefore = BigInt(authorizationExpiry);
367
+ const nonce = computeEscrowNonce(this.chainId, escrowContract, {
368
+ operator: facilitator,
369
+ payer: this.address,
370
+ receiver,
371
+ token,
372
+ maxAmount: amount,
373
+ preApprovalExpiry: authorizationExpiry,
374
+ authorizationExpiry,
375
+ refundExpiry,
376
+ minFeeBps: 0,
377
+ maxFeeBps: 0,
378
+ feeReceiver: ZERO_ADDRESS,
379
+ salt: BigInt(salt)
380
+ });
381
+ const domain = {
382
+ name: extra.name,
383
+ version: extra.version,
384
+ chainId: this.chainId,
385
+ verifyingContract: token
386
+ };
387
+ const signature = await signERC3009(
388
+ this.wallet,
389
+ { from: this.address, to: tokenCollector, value: amount, validAfter, validBefore, nonce },
390
+ domain
391
+ );
392
+ const payload = {
393
+ signature,
394
+ authorization: {
395
+ from: this.address,
396
+ to: tokenCollector,
397
+ value: amount.toString(),
398
+ validAfter: validAfter.toString(),
399
+ validBefore: validBefore.toString(),
400
+ nonce
401
+ },
402
+ sessionParams: {
403
+ salt,
404
+ authorizationExpiry,
405
+ refundExpiry
406
+ }
407
+ };
408
+ if (paymentRequirements.scheme === "escrow") {
409
+ payload.requestId = generateRequestId();
410
+ }
411
+ const accepted = {
412
+ scheme: paymentRequirements.scheme,
413
+ network: paymentRequirements.network,
414
+ asset: paymentRequirements.asset,
415
+ amount: paymentRequirements.amount,
416
+ payTo: paymentRequirements.payTo,
417
+ maxTimeoutSeconds: paymentRequirements.maxTimeoutSeconds,
418
+ extra: { ...paymentRequirements.extra, facilitator, escrowContract, tokenCollector }
419
+ };
420
+ return { x402Version, accepted, payload };
421
+ }
422
+ generateSalt() {
423
+ return toHex2(generateRandomBytes(32));
424
+ }
425
+ };
426
+
427
+ // src/client/session-wrapper.ts
428
+ function createEscrowFetch(walletClient, options) {
429
+ const scheme = new EscrowScheme(walletClient, options);
430
+ const x402 = new x402Client().register(scheme.network, scheme);
431
+ const baseFetch = options?.fetch ?? globalThis.fetch;
432
+ const paidFetch = wrapFetchWithPayment(baseFetch, x402);
433
+ return {
434
+ fetch: withSessionExtraction(paidFetch, scheme),
435
+ scheme,
436
+ x402
437
+ // Expose for adding hooks
438
+ };
439
+ }
440
+ function extractSession(getHeader, escrowScheme) {
441
+ const paymentResponseHeader = getHeader("PAYMENT-RESPONSE") || getHeader("payment-response");
442
+ if (!paymentResponseHeader) return;
443
+ try {
444
+ const data = JSON.parse(fromBase64(paymentResponseHeader));
445
+ if (!data.session?.id) return;
446
+ if (!data.session.token) {
447
+ if (data.session.balance !== void 0) {
448
+ escrowScheme.sessions.updateBalance(data.session.id, data.session.balance);
449
+ }
450
+ return;
451
+ }
452
+ const receiver = data.requirements?.payTo || data.receiver;
453
+ if (!receiver) {
454
+ if (process.env.NODE_ENV !== "production") {
455
+ console.warn("[x402] Session missing receiver - cannot store");
456
+ }
457
+ return;
458
+ }
459
+ if (!isAddress(receiver)) {
460
+ if (process.env.NODE_ENV !== "production") {
461
+ console.warn("[x402] Invalid receiver address in session:", receiver);
462
+ }
463
+ return;
464
+ }
465
+ escrowScheme.sessions.store({
466
+ sessionId: data.session.id,
467
+ sessionToken: data.session.token,
468
+ network: escrowScheme.network,
469
+ payer: escrowScheme.address,
470
+ receiver: getAddress2(receiver),
471
+ balance: data.session.balance || "0",
472
+ authorizationExpiry: data.session.expiresAt || 0
473
+ });
474
+ } catch (error) {
475
+ if (process.env.NODE_ENV !== "production") {
476
+ console.warn("[x402] Failed to parse PAYMENT-RESPONSE:", error);
477
+ }
478
+ }
479
+ }
480
+ function withSessionExtraction(paidFetch, escrowScheme) {
481
+ return async (input, init) => {
482
+ const response = await paidFetch(input, init);
483
+ extractSession((name) => response.headers.get(name), escrowScheme);
484
+ return response;
485
+ };
486
+ }
487
+ function withAxiosSessionExtraction(escrowScheme) {
488
+ return (response) => {
489
+ extractSession((name) => response.headers[name.toLowerCase()], escrowScheme);
490
+ return response;
491
+ };
492
+ }
493
+
494
+ // src/server/utils.ts
495
+ function parseUsdcPrice(price, usdcAddress, decimals = 6) {
496
+ const defaultAsset = usdcAddress || "";
497
+ if (typeof price === "object" && "amount" in price && "asset" in price) {
498
+ return {
499
+ amount: price.amount,
500
+ asset: price.asset || defaultAsset,
501
+ extra: price.extra
502
+ };
503
+ }
504
+ let usdAmount;
505
+ if (typeof price === "number") {
506
+ usdAmount = price;
507
+ } else {
508
+ const cleanPrice = price.toString().replace(/^\$/, "").trim();
509
+ usdAmount = parseFloat(cleanPrice);
510
+ }
511
+ if (isNaN(usdAmount) || usdAmount < 0) {
512
+ throw new Error(`Invalid price: ${price}`);
513
+ }
514
+ const multiplier = Math.pow(10, decimals);
515
+ const amount = Math.round(usdAmount * multiplier).toString();
516
+ return {
517
+ amount,
518
+ asset: defaultAsset
519
+ };
520
+ }
521
+
522
+ // src/server/escrow.ts
523
+ var EscrowScheme2 = class {
524
+ constructor(config) {
525
+ this.scheme = "escrow";
526
+ this.usdcDecimals = config?.usdcDecimals ?? 6;
527
+ }
528
+ /**
529
+ * Parse a user-friendly price to USDC amount
530
+ *
531
+ * Supports:
532
+ * - Number: 0.10 -> 100000 (assuming USD)
533
+ * - String: "$0.10", "0.10" -> 100000
534
+ * - AssetAmount: { amount: "100000", asset: "0x..." } -> passthrough
535
+ */
536
+ async parsePrice(price, _network) {
537
+ return parseUsdcPrice(price, void 0, this.usdcDecimals);
538
+ }
539
+ /**
540
+ * Enhance payment requirements with escrow-specific extra data.
541
+ * Config comes from facilitator's supportedKind.extra.
542
+ */
543
+ async enhancePaymentRequirements(paymentRequirements, supportedKind, _facilitatorExtensions) {
544
+ const facilitatorExtra = supportedKind.extra || {};
545
+ const asset = paymentRequirements.asset || facilitatorExtra.asset || "";
546
+ return {
547
+ ...paymentRequirements,
548
+ asset,
549
+ extra: {
550
+ ...paymentRequirements.extra,
551
+ ...facilitatorExtra
552
+ }
553
+ };
554
+ }
555
+ };
556
+
557
+ // src/server/index.ts
558
+ import { HTTPFacilitatorClient } from "@x402/core/server";
559
+ export {
560
+ EscrowScheme,
561
+ EscrowScheme2 as EscrowServerScheme,
562
+ X402_HEADERS,
563
+ createEscrowFetch,
564
+ parsePaymentResponseHeader,
565
+ withAxiosSessionExtraction,
566
+ withSessionExtraction
567
+ };
568
+ //# sourceMappingURL=index.js.map