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