@bosonprotocol/x402-client 0.1.0-alpha-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.
@@ -0,0 +1,923 @@
1
+ 'use strict';
2
+
3
+ var Ajv = require('ajv');
4
+ var coreSdk = require('@bosonprotocol/core-sdk');
5
+ var escrow = require('@bosonprotocol/x402-core/schemes/escrow');
6
+ var codec = require('@bosonprotocol/x402-evm/codec');
7
+ var viem = require('viem');
8
+ var tokenAuth = require('@bosonprotocol/x402-core/eip712/token-auth');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var Ajv__default = /*#__PURE__*/_interopDefault(Ajv);
13
+
14
+ // src/errors.ts
15
+ var UnsupportedSchemeError = class extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "UnsupportedSchemeError";
19
+ }
20
+ };
21
+ var UnsupportedTokenAuthError = class extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "UnsupportedTokenAuthError";
25
+ }
26
+ };
27
+ var NoCompatibleActionError = class extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "NoCompatibleActionError";
31
+ }
32
+ };
33
+ var FulfillmentValidationError = class extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "FulfillmentValidationError";
37
+ }
38
+ };
39
+ var MaxAmountExceededError = class extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "MaxAmountExceededError";
43
+ }
44
+ };
45
+
46
+ // src/action.ts
47
+ var FLOW_A = "boson-createOfferAndCommit";
48
+ var FLOW_B = "boson-createOfferCommitAndRedeem";
49
+ function pickAction(requirements, policy) {
50
+ const redeemMode = policy?.redeemMode ?? "auto";
51
+ const flowAOffered = requirements.actions.next.some(
52
+ (a) => a.id === FLOW_A && a.channels.includes("server")
53
+ );
54
+ const flowBOffered = requirements.actions.next.some(
55
+ (a) => a.id === FLOW_B && a.channels.includes("server")
56
+ );
57
+ if (redeemMode === "commit-and-redeem") {
58
+ if (flowBOffered) return FLOW_B;
59
+ throw new NoCompatibleActionError(
60
+ `policy.redeemMode='commit-and-redeem' requires '${FLOW_B}' on the server channel; requirements only advertise [${requirements.actions.next.map((a) => a.id).join(", ")}]`
61
+ );
62
+ }
63
+ if (redeemMode === "commit-only") {
64
+ if (flowAOffered) return FLOW_A;
65
+ throw new NoCompatibleActionError(
66
+ `policy.redeemMode='commit-only' requires '${FLOW_A}' on the server channel; requirements only advertise [${requirements.actions.next.map((a) => a.id).join(", ")}]`
67
+ );
68
+ }
69
+ if (flowAOffered) return FLOW_A;
70
+ if (flowBOffered) return FLOW_B;
71
+ throw new NoCompatibleActionError(
72
+ `no commit-time action ('${FLOW_A}' or '${FLOW_B}') with 'server' channel found in requirements.actions.next`
73
+ );
74
+ }
75
+ function resolveFulfillment(requirements, config) {
76
+ const required = requirements.fulfillment?.required ?? false;
77
+ if (!required) {
78
+ return void 0;
79
+ }
80
+ const fulfillmentConfig = config.fulfillment;
81
+ if (!fulfillmentConfig) {
82
+ throw new FulfillmentValidationError(
83
+ "requirements.fulfillment.required is true but client config did not supply a fulfillment selection"
84
+ );
85
+ }
86
+ const option = requirements.fulfillment?.options.find((o) => o.id === fulfillmentConfig.option);
87
+ if (!option) {
88
+ const advertised = requirements.fulfillment?.options.map((o) => o.id).join(", ") ?? "<none>";
89
+ throw new FulfillmentValidationError(
90
+ `fulfillment option '${fulfillmentConfig.option}' not advertised by requirements (advertised: ${advertised})`
91
+ );
92
+ }
93
+ if (option.schema === null) {
94
+ if (fulfillmentConfig.data !== null) {
95
+ throw new FulfillmentValidationError(
96
+ `fulfillment option '${option.id}' accepts no buyer data; use null`
97
+ );
98
+ }
99
+ } else {
100
+ const ajv = new Ajv__default.default({ allErrors: true, strict: false });
101
+ const validate = ajv.compile(option.schema);
102
+ const ok = validate(fulfillmentConfig.data);
103
+ if (!ok) {
104
+ throw new FulfillmentValidationError(
105
+ `fulfillment data does not match option '${option.id}' schema: ${ajv.errorsText(validate.errors)}`
106
+ );
107
+ }
108
+ }
109
+ return { option: fulfillmentConfig.option, data: fulfillmentConfig.data };
110
+ }
111
+
112
+ // src/internal/web3lib-adapter.ts
113
+ function signerToWeb3LibAdapter(signer, chainId, publicClient) {
114
+ return {
115
+ uuid: "x402-client:signer-adapter",
116
+ getSignerAddress: () => signer.getAddress(),
117
+ isSignerContract: async () => false,
118
+ getChainId: async () => chainId,
119
+ send: async (method, params) => {
120
+ if (method !== "eth_signTypedData_v4") {
121
+ throw new Error(
122
+ `x402-client: signer adapter does not support RPC method '${method}'; only eth_signTypedData_v4 is implemented`
123
+ );
124
+ }
125
+ const raw = params[1];
126
+ if (typeof raw !== "string") {
127
+ throw new Error(
128
+ "x402-client: eth_signTypedData_v4 payload[1] is not a JSON string \u2014 core-sdk internals may have changed"
129
+ );
130
+ }
131
+ const typedData = JSON.parse(raw);
132
+ return signer.signTypedData(typedData);
133
+ },
134
+ call: async (req) => {
135
+ if (!publicClient) {
136
+ throw new Error(
137
+ "x402-client: signer adapter has no PublicClient configured; eth_call cannot be forwarded. Pass `publicClients` in X402bClientConfig to enable the Permit token-auth strategy."
138
+ );
139
+ }
140
+ const result = await publicClient.call({
141
+ to: req.to,
142
+ data: req.data
143
+ });
144
+ return result.data ?? "0x";
145
+ },
146
+ getBalance: () => Promise.reject(unreachable("getBalance")),
147
+ estimateGas: () => Promise.reject(unreachable("estimateGas")),
148
+ sendTransaction: () => Promise.reject(unreachable("sendTransaction")),
149
+ getTransactionReceipt: () => Promise.reject(unreachable("getTransactionReceipt")),
150
+ getCurrentTimeMs: () => Promise.reject(unreachable("getCurrentTimeMs"))
151
+ };
152
+ }
153
+ function unreachable(method) {
154
+ return new Error(
155
+ `x402-client: stub Web3LibAdapter.${method}() is not implemented \u2014 the client never goes on-chain in MVP`
156
+ );
157
+ }
158
+
159
+ // src/core-sdk-factory.ts
160
+ var PLACEHOLDER_SUBGRAPH_URL = "https://x402-client.placeholder.invalid/subgraph";
161
+ function parseChainId(network) {
162
+ const match = network.match(/^eip155:(\d+)$/);
163
+ if (!match) {
164
+ throw new Error(
165
+ `x402-client: unsupported network '${network}' (expected CAIP-2 'eip155:<chainId>')`
166
+ );
167
+ }
168
+ const chainId = Number(match[1]);
169
+ if (!Number.isSafeInteger(chainId) || chainId <= 0) {
170
+ throw new Error(
171
+ `x402-client: unsupported network '${network}' (chainId must be a positive safe integer)`
172
+ );
173
+ }
174
+ return chainId;
175
+ }
176
+ function createCoreSdkFactory(signer, config) {
177
+ const cache = /* @__PURE__ */ new Map();
178
+ return function buildCoreSdk(network, escrowAddress) {
179
+ const chainId = parseChainId(network);
180
+ const cacheKey = `${chainId}:${escrowAddress.toLowerCase()}`;
181
+ const cached = cache.get(cacheKey);
182
+ if (cached) {
183
+ return { coreSdk: cached, chainId };
184
+ }
185
+ const subgraphUrl = config.subgraphUrls?.[chainId] ?? PLACEHOLDER_SUBGRAPH_URL;
186
+ const publicClient = config.publicClients?.[chainId];
187
+ const web3Lib = signerToWeb3LibAdapter(signer, chainId, publicClient);
188
+ const coreSdk$1 = new coreSdk.CoreSDK({
189
+ web3Lib,
190
+ subgraphUrl,
191
+ protocolDiamond: escrowAddress,
192
+ chainId
193
+ });
194
+ cache.set(cacheKey, coreSdk$1);
195
+ return { coreSdk: coreSdk$1, chainId };
196
+ };
197
+ }
198
+
199
+ // src/response.ts
200
+ var HEADER_NAME = "X-PAYMENT-RESPONSE";
201
+ function parsePaymentResponse(response) {
202
+ const raw = response.headers.get(HEADER_NAME);
203
+ if (!raw) {
204
+ return void 0;
205
+ }
206
+ const json = decodeBase64(raw);
207
+ const parsed = JSON.parse(json);
208
+ const summary = { raw: parsed };
209
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
210
+ const obj = parsed;
211
+ const exchangeId = pickString(obj, ["exchangeId", "exchange_id"]);
212
+ if (exchangeId !== void 0) {
213
+ summary.exchangeId = exchangeId;
214
+ }
215
+ if (isClientStateShape(obj.state)) {
216
+ summary.state = obj.state;
217
+ }
218
+ if (summary.state === void 0 && typeof obj.nextActions === "object" && obj.nextActions !== null && !Array.isArray(obj.nextActions)) {
219
+ const na = obj.nextActions;
220
+ const exchange = pickString(na, ["exchangeState"]);
221
+ const dispute = pickString(na, ["disputeState"]);
222
+ if (exchange !== void 0) {
223
+ let candidate;
224
+ if (exchange === "DISPUTED") {
225
+ candidate = dispute !== void 0 ? { exchange, dispute } : void 0;
226
+ } else {
227
+ candidate = { exchange };
228
+ }
229
+ if (isClientStateShape(candidate)) {
230
+ summary.state = candidate;
231
+ }
232
+ }
233
+ }
234
+ }
235
+ return summary;
236
+ }
237
+ function isClientStateShape(v) {
238
+ if (typeof v === "string") return v.length > 0;
239
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
240
+ const rec = v;
241
+ if (typeof rec.exchange !== "string" || rec.exchange.length === 0) return false;
242
+ if ("dispute" in rec && rec.dispute !== void 0 && typeof rec.dispute !== "string") {
243
+ return false;
244
+ }
245
+ return true;
246
+ }
247
+ function decodeBase64(value) {
248
+ if (typeof Buffer !== "undefined") {
249
+ return Buffer.from(value, "base64").toString("utf8");
250
+ }
251
+ const binary = atob(value);
252
+ const bytes = new Uint8Array(binary.length);
253
+ for (let i = 0; i < binary.length; i++) {
254
+ bytes[i] = binary.charCodeAt(i);
255
+ }
256
+ return new TextDecoder().decode(bytes);
257
+ }
258
+ function pickString(obj, keys) {
259
+ for (const key of keys) {
260
+ const v = obj[key];
261
+ if (typeof v === "string" && v.length > 0) {
262
+ return v;
263
+ }
264
+ }
265
+ return void 0;
266
+ }
267
+ var X402_VERSION = 2;
268
+ function assemblePayload({
269
+ requirements,
270
+ action,
271
+ tokenAuthStrategy,
272
+ metaTx,
273
+ tokenAuth,
274
+ fulfillment,
275
+ buyer
276
+ }) {
277
+ const fulfillmentSlot = fulfillment === void 0 ? void 0 : action === "boson-createOfferCommitAndRedeem" ? { option: fulfillment.option, data: fulfillment.data } : { option: fulfillment.option };
278
+ const payload = {
279
+ x402Version: X402_VERSION,
280
+ scheme: "escrow",
281
+ network: requirements.network,
282
+ payload: {
283
+ action,
284
+ tokenAuthStrategy,
285
+ offerRef: {
286
+ fullOffer: requirements.offer.fullOffer,
287
+ sellerSig: requirements.offer.sellerSig
288
+ },
289
+ buyer,
290
+ metaTx,
291
+ ...tokenAuth ? { tokenAuth } : {}
292
+ },
293
+ ...fulfillmentSlot ? { fulfillment: fulfillmentSlot } : {}
294
+ };
295
+ escrow.parseEscrowPaymentPayload(payload);
296
+ return payload;
297
+ }
298
+ function assembleAndEncodePayload(args) {
299
+ const payload = assemblePayload(args);
300
+ const json = JSON.stringify(payload);
301
+ if (typeof Buffer !== "undefined") {
302
+ return Buffer.from(json, "utf8").toString("base64");
303
+ }
304
+ const bytes = new TextEncoder().encode(json);
305
+ let binary = "";
306
+ for (const b of bytes) {
307
+ binary += String.fromCharCode(b);
308
+ }
309
+ return btoa(binary);
310
+ }
311
+
312
+ // src/utils/crypto.ts
313
+ function randomBytes(size) {
314
+ const bytes = new Uint8Array(size);
315
+ if (typeof globalThis.crypto?.getRandomValues !== "function") {
316
+ throw new Error(
317
+ "x402-client: globalThis.crypto.getRandomValues is unavailable \u2014 environment lacks the Web Crypto API"
318
+ );
319
+ }
320
+ globalThis.crypto.getRandomValues(bytes);
321
+ return bytes;
322
+ }
323
+ function randomUint256() {
324
+ const bytes = randomBytes(32);
325
+ let n = 0n;
326
+ for (const byte of bytes) {
327
+ n = n << 8n | BigInt(byte);
328
+ }
329
+ return n;
330
+ }
331
+
332
+ // src/pre-commit.ts
333
+ async function signCreateOfferAndCommit({
334
+ requirements,
335
+ coreSdk,
336
+ buyer
337
+ }) {
338
+ const nonce = randomUint256();
339
+ const createOfferAndCommitArgs = buildCreateOfferAndCommitArgs(requirements, buyer);
340
+ const signed = await coreSdk.signMetaTxCreateOfferAndCommit({
341
+ nonce: nonce.toString(),
342
+ createOfferAndCommitArgs
343
+ });
344
+ return reshape(signed, buyer, nonce);
345
+ }
346
+ async function signCreateOfferCommitAndRedeem({
347
+ requirements,
348
+ coreSdk,
349
+ buyer
350
+ }) {
351
+ const nonce = randomUint256();
352
+ const createOfferAndCommitArgs = buildCreateOfferAndCommitArgs(requirements, buyer);
353
+ const signed = await coreSdk.signMetaTxCreateOfferCommitAndRedeem({
354
+ nonce: nonce.toString(),
355
+ createOfferAndCommitArgs
356
+ });
357
+ return reshape(signed, buyer, nonce);
358
+ }
359
+ function buildCreateOfferAndCommitArgs(requirements, buyer) {
360
+ return {
361
+ ...requirements.offer.fullOffer,
362
+ signature: requirements.offer.sellerSig,
363
+ committer: buyer
364
+ };
365
+ }
366
+ function reshape(signed, buyer, nonce) {
367
+ return {
368
+ from: buyer,
369
+ nonce: nonce.toString(),
370
+ functionName: signed.functionName,
371
+ functionSignature: signed.functionSignature,
372
+ sig: {
373
+ v: Number(signed.v),
374
+ r: signed.r,
375
+ s: signed.s
376
+ }
377
+ };
378
+ }
379
+ async function signPostCommitAction(args, deps) {
380
+ const nonce = randomUint256();
381
+ const buyer = await deps.getBuyerAddress();
382
+ const { coreSdk } = deps.buildCoreSdk(args.network, args.escrowAddress);
383
+ const signed = await callSignMetaTx(coreSdk, args, nonce);
384
+ const metaTx = {
385
+ from: buyer,
386
+ nonce: nonce.toString(),
387
+ functionName: signed.functionName,
388
+ functionSignature: signed.functionSignature,
389
+ sig: {
390
+ v: Number(signed.v),
391
+ r: signed.r,
392
+ s: signed.s
393
+ }
394
+ };
395
+ return { metaTx, signedPayload: codec.encodeSignedPayload(metaTx) };
396
+ }
397
+ async function callSignMetaTx(coreSdk, args, nonce) {
398
+ const nonceStr = nonce.toString();
399
+ const exchangeId = args.exchangeId;
400
+ switch (args.actionId) {
401
+ case "boson-redeem":
402
+ return coreSdk.signMetaTxRedeemVoucher({ nonce: nonceStr, exchangeId });
403
+ case "boson-cancelVoucher":
404
+ return coreSdk.signMetaTxCancelVoucher({ nonce: nonceStr, exchangeId });
405
+ case "boson-completeExchange":
406
+ return coreSdk.signMetaTxCompleteExchange({ nonce: nonceStr, exchangeId });
407
+ case "boson-raiseDispute":
408
+ return coreSdk.signMetaTxRaiseDispute({ nonce: nonceStr, exchangeId });
409
+ case "boson-retractDispute":
410
+ return coreSdk.signMetaTxRetractDispute({ nonce: nonceStr, exchangeId });
411
+ case "boson-escalateDispute":
412
+ return coreSdk.signMetaTxEscalateDispute({ nonce: nonceStr, exchangeId });
413
+ case "boson-resolveDispute":
414
+ return coreSdk.signMetaTxResolveDispute({
415
+ nonce: nonceStr,
416
+ exchangeId,
417
+ buyerPercent: args.buyerPercent,
418
+ counterpartySig: args.counterpartySig
419
+ });
420
+ default: {
421
+ const _exhaustive = args;
422
+ throw new Error(
423
+ `x402-client: unsupported post-commit action '${_exhaustive.actionId}'`
424
+ );
425
+ }
426
+ }
427
+ }
428
+ function normalizeEntityId(value) {
429
+ if (typeof value === "bigint") {
430
+ if (value < 0n) throw new Error(`entityId must be non-negative, got ${value.toString()}`);
431
+ return value.toString();
432
+ }
433
+ if (typeof value === "number") {
434
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
435
+ throw new Error(`entityId must be a non-negative integer, got ${String(value)}`);
436
+ }
437
+ return value.toString();
438
+ }
439
+ const trimmed = value.trim();
440
+ if (trimmed.length === 0 || !escrow.DECIMAL_UINT.test(trimmed)) {
441
+ throw new Error(`entityId must be a decimal non-negative integer string, got "${value}"`);
442
+ }
443
+ return trimmed;
444
+ }
445
+ async function signWithdrawFunds(args, deps) {
446
+ const normalised = {
447
+ ...args,
448
+ entityId: normalizeEntityId(args.entityId)
449
+ };
450
+ const { coreSdk } = deps.buildCoreSdk(normalised.network, normalised.escrowAddress);
451
+ const from = await deps.getSignerAddress();
452
+ return signWithdrawFundsInternal(normalised, coreSdk, from);
453
+ }
454
+ async function signWithdrawAllAvailableFunds(args, deps) {
455
+ const { coreSdk } = deps.buildCoreSdk(args.network, args.escrowAddress);
456
+ const sdk = coreSdk;
457
+ const from = await deps.getSignerAddress();
458
+ const entityId = await resolveEntityIdClientSide(sdk, args);
459
+ const funds = await sdk.getFunds({ fundsFilter: { accountId: entityId } });
460
+ const nonZero = funds.filter((f) => f.availableAmount !== "0" && BigInt(f.availableAmount) > 0n);
461
+ if (nonZero.length === 0) {
462
+ throw new Error(
463
+ `signWithdrawAllAvailableFunds: entity ${entityId} has no available funds to withdraw`
464
+ );
465
+ }
466
+ return signWithdrawFundsInternal(
467
+ {
468
+ entityId,
469
+ network: args.network,
470
+ escrowAddress: args.escrowAddress,
471
+ tokenList: nonZero.map((f) => f.token.address),
472
+ tokenAmounts: nonZero.map((f) => f.availableAmount)
473
+ },
474
+ sdk,
475
+ from
476
+ );
477
+ }
478
+ async function signWithdrawFundsInternal(args, coreSdk, from) {
479
+ if (args.tokenList.length !== args.tokenAmounts.length) {
480
+ throw new Error(
481
+ `signWithdrawFunds: tokenList (${args.tokenList.length}) and tokenAmounts (${args.tokenAmounts.length}) must be the same length`
482
+ );
483
+ }
484
+ const nonce = randomUint256();
485
+ const nonceStr = nonce.toString();
486
+ const signed = await coreSdk.signMetaTxWithdrawFunds({
487
+ nonce: nonceStr,
488
+ entityId: args.entityId,
489
+ tokenList: args.tokenList.map((t) => t),
490
+ tokenAmounts: args.tokenAmounts.map((a) => a)
491
+ });
492
+ const metaTx = {
493
+ from,
494
+ nonce: nonceStr,
495
+ functionName: signed.functionName,
496
+ functionSignature: signed.functionSignature,
497
+ sig: {
498
+ v: Number(signed.v),
499
+ r: signed.r,
500
+ s: signed.s
501
+ }
502
+ };
503
+ return {
504
+ metaTx,
505
+ signedPayload: codec.encodeSignedPayload(metaTx),
506
+ entityId: normalizeEntityId(args.entityId),
507
+ tokenList: args.tokenList,
508
+ tokenAmounts: args.tokenAmounts.map((a) => String(a))
509
+ };
510
+ }
511
+ async function resolveEntityIdClientSide(coreSdk, selector) {
512
+ if ("entityId" in selector) return normalizeEntityId(selector.entityId);
513
+ const address = selector.address.toLowerCase();
514
+ const role = selector.role;
515
+ const [sellers, buyers] = await Promise.all([
516
+ role === "buyer" ? Promise.resolve([]) : coreSdk.getSellersByAddress(address),
517
+ role === "seller" ? Promise.resolve([]) : coreSdk.getBuyers({ buyersFilter: { wallet: address } })
518
+ ]);
519
+ const sellerIds = sellers.map((s) => s.id);
520
+ const buyerIds = buyers.map((b) => b.id);
521
+ if (role === "seller") {
522
+ if (sellerIds.length === 0) {
523
+ throw new Error(
524
+ `signWithdrawAllAvailableFunds: no seller entity found for address ${address}`
525
+ );
526
+ }
527
+ if (sellerIds.length > 1) {
528
+ throw new Error(
529
+ `signWithdrawAllAvailableFunds: address ${address} resolves to multiple seller entities (ids ${sellerIds.join(", ")}); pass entityId to disambiguate`
530
+ );
531
+ }
532
+ return sellerIds[0];
533
+ }
534
+ if (role === "buyer") {
535
+ if (buyerIds.length === 0) {
536
+ throw new Error(
537
+ `signWithdrawAllAvailableFunds: no buyer entity found for address ${address}`
538
+ );
539
+ }
540
+ if (buyerIds.length > 1) {
541
+ throw new Error(
542
+ `signWithdrawAllAvailableFunds: address ${address} resolves to multiple buyer entities (ids ${buyerIds.join(", ")}); pass entityId to disambiguate`
543
+ );
544
+ }
545
+ return buyerIds[0];
546
+ }
547
+ const totalMatches = sellerIds.length + buyerIds.length;
548
+ if (totalMatches === 0) {
549
+ throw new Error(
550
+ `signWithdrawAllAvailableFunds: no seller or buyer entity found for address ${address}`
551
+ );
552
+ }
553
+ if (totalMatches > 1) {
554
+ throw new Error(
555
+ `signWithdrawAllAvailableFunds: address ${address} resolves to ${describeClientMatches(sellerIds, buyerIds)}; pass role and/or entityId to disambiguate`
556
+ );
557
+ }
558
+ if (sellerIds.length === 1) return sellerIds[0];
559
+ return buyerIds[0];
560
+ }
561
+ function describeClientMatches(sellerIds, buyerIds) {
562
+ const parts = [];
563
+ if (sellerIds.length > 0) {
564
+ parts.push(
565
+ sellerIds.length === 1 ? `seller (id ${sellerIds[0]})` : `${sellerIds.length} sellers (ids ${sellerIds.join(", ")})`
566
+ );
567
+ }
568
+ if (buyerIds.length > 0) {
569
+ parts.push(
570
+ buyerIds.length === 1 ? `buyer (id ${buyerIds[0]})` : `${buyerIds.length} buyers (ids ${buyerIds.join(", ")})`
571
+ );
572
+ }
573
+ return parts.join(" and ");
574
+ }
575
+
576
+ // src/token-auth/deadline.ts
577
+ var TOKEN_AUTH_DEADLINE_SAFETY_MARGIN_SECONDS = 60;
578
+ function computeTokenAuthDeadline(maxTimeoutSeconds, now = Date.now) {
579
+ const timeoutSeconds = Math.max(1, Math.floor(maxTimeoutSeconds));
580
+ const safetyMarginSeconds = Math.min(
581
+ TOKEN_AUTH_DEADLINE_SAFETY_MARGIN_SECONDS,
582
+ Math.max(0, timeoutSeconds - 1)
583
+ );
584
+ return Math.floor(now() / 1e3) + timeoutSeconds - safetyMarginSeconds;
585
+ }
586
+
587
+ // src/token-auth/erc3009.ts
588
+ async function signErc3009({
589
+ requirements,
590
+ buyer,
591
+ coreSdk,
592
+ tokenDomainResolver,
593
+ now = Date.now
594
+ }) {
595
+ const chainId = parseChainId(requirements.network);
596
+ const domain = await tokenDomainResolver(requirements.asset, chainId);
597
+ const validAfter = 0;
598
+ const validBefore = computeTokenAuthDeadline(requirements.maxTimeoutSeconds, now);
599
+ const result = await coreSdk.signReceiveWithErc3009Authorization(
600
+ requirements.asset,
601
+ { name: domain.name, version: domain.version },
602
+ requirements.amount,
603
+ validAfter,
604
+ validBefore,
605
+ { spender: requirements.escrowAddress }
606
+ );
607
+ return {
608
+ from: buyer,
609
+ to: requirements.escrowAddress,
610
+ value: requirements.amount,
611
+ validAfter: Number(result.data.validAfter),
612
+ validBefore: Number(result.data.validBefore),
613
+ nonce: result.data.nonce,
614
+ v: result.v,
615
+ r: result.r,
616
+ s: result.s
617
+ };
618
+ }
619
+ var NONCES_ABI = [
620
+ {
621
+ type: "function",
622
+ name: "nonces",
623
+ stateMutability: "view",
624
+ inputs: [{ name: "owner", type: "address" }],
625
+ outputs: [{ type: "uint256" }]
626
+ }
627
+ ];
628
+ async function signPermit({
629
+ requirements,
630
+ buyer,
631
+ coreSdk,
632
+ tokenDomainResolver,
633
+ publicClient,
634
+ now = Date.now
635
+ }) {
636
+ const chainId = parseChainId(requirements.network);
637
+ const domain = await tokenDomainResolver(requirements.asset, chainId);
638
+ const noncePreSign = await readNonce(publicClient, requirements.asset, buyer);
639
+ const deadline = computeTokenAuthDeadline(requirements.maxTimeoutSeconds, now);
640
+ const result = await coreSdk.signReceiveWithErc2612Permit(
641
+ requirements.asset,
642
+ { name: domain.name, version: domain.version },
643
+ requirements.amount,
644
+ deadline,
645
+ { spender: requirements.escrowAddress }
646
+ );
647
+ const noncePostSign = await readNonce(publicClient, requirements.asset, buyer);
648
+ if (noncePreSign !== noncePostSign) {
649
+ throw new UnsupportedTokenAuthError(
650
+ `x402-client: token nonce shifted during permit signing (was ${noncePreSign}, now ${noncePostSign}); retry`
651
+ );
652
+ }
653
+ return {
654
+ owner: buyer,
655
+ spender: requirements.escrowAddress,
656
+ value: requirements.amount,
657
+ deadline,
658
+ nonce: noncePreSign.toString(),
659
+ v: result.v,
660
+ r: result.r,
661
+ s: result.s
662
+ };
663
+ }
664
+ async function readNonce(publicClient, token, owner) {
665
+ const result = await publicClient.call({
666
+ to: token,
667
+ data: viem.encodeFunctionData({
668
+ abi: NONCES_ABI,
669
+ functionName: "nonces",
670
+ args: [owner]
671
+ })
672
+ });
673
+ if (!result.data) {
674
+ throw new UnsupportedTokenAuthError(
675
+ `x402-client: token ${token} did not return data for nonces(${owner}) \u2014 does it implement EIP-2612?`
676
+ );
677
+ }
678
+ return viem.decodeFunctionResult({
679
+ abi: NONCES_ABI,
680
+ functionName: "nonces",
681
+ data: result.data
682
+ });
683
+ }
684
+ async function signPermit2({
685
+ requirements,
686
+ buyer,
687
+ coreSdk,
688
+ now = Date.now
689
+ }) {
690
+ const deadline = computeTokenAuthDeadline(requirements.maxTimeoutSeconds, now);
691
+ const permit2Nonce = randomUint256();
692
+ const result = await coreSdk.signReceiveWithPermit2(
693
+ requirements.asset,
694
+ requirements.amount,
695
+ deadline,
696
+ {
697
+ spender: requirements.escrowAddress,
698
+ permit2Address: tokenAuth.PERMIT2_ADDRESS,
699
+ permit2Nonce
700
+ }
701
+ );
702
+ return {
703
+ permitted: { token: requirements.asset, amount: requirements.amount },
704
+ spender: requirements.escrowAddress,
705
+ nonce: permit2Nonce.toString(),
706
+ deadline,
707
+ signature: result.signature
708
+ };
709
+ }
710
+
711
+ // src/token-auth/index.ts
712
+ var STRATEGY_PREFERENCE = ["erc3009", "permit2", "permit"];
713
+ function strategyIsAvailable(s, args) {
714
+ switch (s) {
715
+ case "erc3009":
716
+ return args.tokenDomainResolver !== void 0;
717
+ case "permit":
718
+ return args.tokenDomainResolver !== void 0 && args.publicClient !== void 0;
719
+ case "permit2":
720
+ return true;
721
+ case "none":
722
+ return false;
723
+ default: {
724
+ return false;
725
+ }
726
+ }
727
+ }
728
+ async function buildAndSignTokenAuth(args) {
729
+ const advertised = args.requirements.tokenAuthStrategies;
730
+ const chosen = STRATEGY_PREFERENCE.find(
731
+ (s) => advertised.includes(s) && strategyIsAvailable(s, args)
732
+ );
733
+ if (!chosen) {
734
+ throw new UnsupportedTokenAuthError(
735
+ `server advertises tokenAuthStrategies=[${advertised.join(", ")}]; client supports [${STRATEGY_PREFERENCE.join(", ")}] but none are usable with the current X402bClientConfig (ERC-3009 and Permit require tokenDomainResolver; Permit also requires a PublicClient for the requirements' chain via publicClients)`
736
+ );
737
+ }
738
+ switch (chosen) {
739
+ case "erc3009": {
740
+ if (!args.tokenDomainResolver) {
741
+ throw new UnsupportedTokenAuthError(
742
+ "server advertises 'erc3009' but no tokenDomainResolver is configured \u2014 pass tokenDomainResolver in X402bClientConfig"
743
+ );
744
+ }
745
+ const data = await signErc3009({
746
+ ...args,
747
+ tokenDomainResolver: args.tokenDomainResolver
748
+ });
749
+ return { strategy: "erc3009", tokenAuth: { kind: "erc3009", data } };
750
+ }
751
+ case "permit": {
752
+ if (!args.tokenDomainResolver) {
753
+ throw new UnsupportedTokenAuthError(
754
+ "server advertises 'permit' but no tokenDomainResolver is configured \u2014 pass tokenDomainResolver in X402bClientConfig"
755
+ );
756
+ }
757
+ if (!args.publicClient) {
758
+ throw new UnsupportedTokenAuthError(
759
+ "server advertises 'permit' but no PublicClient is configured for this chain \u2014 pass publicClients in X402bClientConfig"
760
+ );
761
+ }
762
+ const data = await signPermit({
763
+ ...args,
764
+ tokenDomainResolver: args.tokenDomainResolver,
765
+ publicClient: args.publicClient
766
+ });
767
+ return { strategy: "permit", tokenAuth: { kind: "permit", data } };
768
+ }
769
+ case "permit2": {
770
+ const data = await signPermit2(args);
771
+ return { strategy: "permit2", tokenAuth: { kind: "permit2", data } };
772
+ }
773
+ case "none": {
774
+ throw new UnsupportedTokenAuthError(
775
+ "tokenAuthStrategy='none' has no client-side signing \u2014 callers handle it without invoking this dispatcher"
776
+ );
777
+ }
778
+ default: {
779
+ const _exhaustive = chosen;
780
+ throw new UnsupportedTokenAuthError(
781
+ `unrecognised tokenAuthStrategy '${_exhaustive.toString()}'`
782
+ );
783
+ }
784
+ }
785
+ }
786
+
787
+ // src/client.ts
788
+ function createX402bClient(config) {
789
+ const buildCoreSdk = createCoreSdkFactory(config.signer, config);
790
+ const getBuyerAddress = async () => await config.signer.getAddress();
791
+ return {
792
+ async handle402(rawRequirements) {
793
+ const requirements = escrow.parseEscrowPaymentRequirements(rawRequirements);
794
+ const action = pickAction(requirements, config.policy);
795
+ const fulfillment = resolveFulfillment(requirements, config);
796
+ enforceMaxAmount(requirements.amount, config.policy?.maxAmount);
797
+ const buyer = await getBuyerAddress();
798
+ const { coreSdk, chainId } = buildCoreSdk(
799
+ requirements.network,
800
+ requirements.escrowAddress
801
+ );
802
+ let tokenAuth;
803
+ let strategy;
804
+ const forcedStrategy = config.policy?.tokenAuthStrategy;
805
+ if (forcedStrategy !== void 0) {
806
+ if (!requirements.tokenAuthStrategies.includes(forcedStrategy)) {
807
+ throw new UnsupportedTokenAuthError(
808
+ `policy.tokenAuthStrategy "${forcedStrategy}" is not in requirements.tokenAuthStrategies (${requirements.tokenAuthStrategies.join(", ")})`
809
+ );
810
+ }
811
+ if (forcedStrategy === "none") {
812
+ strategy = "none";
813
+ tokenAuth = void 0;
814
+ } else {
815
+ const built = await buildAndSignTokenAuth({
816
+ requirements: { ...requirements, tokenAuthStrategies: [forcedStrategy] },
817
+ buyer,
818
+ coreSdk,
819
+ tokenDomainResolver: config.tokenDomainResolver,
820
+ publicClient: config.publicClients?.[chainId]
821
+ });
822
+ strategy = built.strategy;
823
+ tokenAuth = built.tokenAuth;
824
+ }
825
+ } else {
826
+ const built = await buildAndSignTokenAuth({
827
+ requirements,
828
+ buyer,
829
+ coreSdk,
830
+ tokenDomainResolver: config.tokenDomainResolver,
831
+ publicClient: config.publicClients?.[chainId]
832
+ });
833
+ strategy = built.strategy;
834
+ tokenAuth = built.tokenAuth;
835
+ }
836
+ const signMetaTx = action === "boson-createOfferCommitAndRedeem" ? signCreateOfferCommitAndRedeem : signCreateOfferAndCommit;
837
+ const metaTx = await signMetaTx({ requirements, coreSdk, buyer });
838
+ return assembleAndEncodePayload({
839
+ requirements,
840
+ action,
841
+ tokenAuthStrategy: strategy,
842
+ metaTx,
843
+ tokenAuth,
844
+ fulfillment,
845
+ buyer
846
+ });
847
+ },
848
+ signAction(args) {
849
+ return signPostCommitAction(args, { buildCoreSdk, getBuyerAddress });
850
+ },
851
+ signWithdrawFunds(args) {
852
+ return signWithdrawFunds(args, { buildCoreSdk, getSignerAddress: getBuyerAddress });
853
+ },
854
+ signWithdrawAllAvailableFunds(args) {
855
+ return signWithdrawAllAvailableFunds(args, {
856
+ buildCoreSdk,
857
+ getSignerAddress: getBuyerAddress
858
+ });
859
+ },
860
+ parsePaymentResponse(response) {
861
+ return parsePaymentResponse(response);
862
+ }
863
+ };
864
+ }
865
+ function enforceMaxAmount(amount, maxAmount) {
866
+ if (maxAmount === void 0) {
867
+ return;
868
+ }
869
+ const amountBigInt = BigInt(amount);
870
+ const maxAmountBigInt = BigInt(maxAmount);
871
+ if (amountBigInt > maxAmountBigInt) {
872
+ throw new MaxAmountExceededError(
873
+ `x402-client: requirements.amount ${amount} exceeds policy.maxAmount ${maxAmount}`
874
+ );
875
+ }
876
+ }
877
+ function signerFromEthersAdapter(adapter) {
878
+ const resolveAddress = async () => viem.getAddress(await adapter.getSignerAddress());
879
+ return {
880
+ getAddress: resolveAddress,
881
+ signTypedData: async ({ domain, types, primaryType, message }) => {
882
+ const from = await resolveAddress();
883
+ const typedData = {
884
+ domain,
885
+ types: { ...types, EIP712Domain: deriveEip712DomainType(domain) },
886
+ primaryType,
887
+ message
888
+ };
889
+ const json = viem.serializeTypedData(typedData);
890
+ const sig = await adapter.send("eth_signTypedData_v4", [from, json]);
891
+ if (typeof sig !== "string" || !/^0x[0-9a-fA-F]+$/.test(sig)) {
892
+ throw new Error(
893
+ "signerFromEthersAdapter: adapter.send did not return a hex signature string"
894
+ );
895
+ }
896
+ return sig;
897
+ }
898
+ };
899
+ }
900
+ function deriveEip712DomainType(domain) {
901
+ const fields = [];
902
+ if (domain.name !== void 0) fields.push({ name: "name", type: "string" });
903
+ if (domain.version !== void 0) fields.push({ name: "version", type: "string" });
904
+ if (domain.chainId !== void 0) fields.push({ name: "chainId", type: "uint256" });
905
+ if (domain.verifyingContract !== void 0)
906
+ fields.push({ name: "verifyingContract", type: "address" });
907
+ if (domain.salt !== void 0) fields.push({ name: "salt", type: "bytes32" });
908
+ return fields;
909
+ }
910
+
911
+ exports.FulfillmentValidationError = FulfillmentValidationError;
912
+ exports.MaxAmountExceededError = MaxAmountExceededError;
913
+ exports.NoCompatibleActionError = NoCompatibleActionError;
914
+ exports.UnsupportedSchemeError = UnsupportedSchemeError;
915
+ exports.UnsupportedTokenAuthError = UnsupportedTokenAuthError;
916
+ exports.createX402bClient = createX402bClient;
917
+ exports.parseChainId = parseChainId;
918
+ exports.parsePaymentResponse = parsePaymentResponse;
919
+ exports.pickAction = pickAction;
920
+ exports.resolveFulfillment = resolveFulfillment;
921
+ exports.signerFromEthersAdapter = signerFromEthersAdapter;
922
+ //# sourceMappingURL=index.js.map
923
+ //# sourceMappingURL=index.js.map