@bosonprotocol/x402-client 0.2.0 → 0.3.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.
package/dist/esm/index.js CHANGED
@@ -3,6 +3,7 @@ export { FulfillmentValidationError, MaxAmountExceededError, NoCompatibleActionE
3
3
  import Ajv from 'ajv';
4
4
  import { parseEscrowPaymentRequirements, DECIMAL_UINT, parseEscrowPaymentPayload } from '@bosonprotocol/x402-core/schemes/escrow';
5
5
  import { encodeSignedPayload } from '@bosonprotocol/x402-evm/codec';
6
+ import { ExchangeState, DisputeState } from '@bosonprotocol/x402-core/state-machine';
6
7
  import { serializeTypedData, getAddress } from 'viem';
7
8
 
8
9
  // src/action.ts
@@ -34,6 +35,30 @@ function pickAction(requirements, policy) {
34
35
  `no commit-time action ('${FLOW_A}' or '${FLOW_B}') with 'server' channel found in requirements.actions.next`
35
36
  );
36
37
  }
38
+
39
+ // src/base64.ts
40
+ function decodeBase64(value) {
41
+ if (typeof Buffer !== "undefined") {
42
+ return Buffer.from(value, "base64").toString("utf8");
43
+ }
44
+ const binary = atob(value);
45
+ const bytes = new Uint8Array(binary.length);
46
+ for (let i = 0; i < binary.length; i++) {
47
+ bytes[i] = binary.charCodeAt(i);
48
+ }
49
+ return new TextDecoder().decode(bytes);
50
+ }
51
+ function encodeBase64(value) {
52
+ if (typeof Buffer !== "undefined") {
53
+ return Buffer.from(value, "utf8").toString("base64");
54
+ }
55
+ const bytes = new TextEncoder().encode(value);
56
+ let binary = "";
57
+ for (const b of bytes) {
58
+ binary += String.fromCharCode(b);
59
+ }
60
+ return btoa(binary);
61
+ }
37
62
  function resolveFulfillment(requirements, config) {
38
63
  const required = requirements.fulfillment?.required ?? false;
39
64
  if (!required) {
@@ -119,17 +144,6 @@ function isClientStateShape(v) {
119
144
  }
120
145
  return true;
121
146
  }
122
- function decodeBase64(value) {
123
- if (typeof Buffer !== "undefined") {
124
- return Buffer.from(value, "base64").toString("utf8");
125
- }
126
- const binary = atob(value);
127
- const bytes = new Uint8Array(binary.length);
128
- for (let i = 0; i < binary.length; i++) {
129
- bytes[i] = binary.charCodeAt(i);
130
- }
131
- return new TextDecoder().decode(bytes);
132
- }
133
147
  function pickString(obj, keys) {
134
148
  for (const key of keys) {
135
149
  const v = obj[key];
@@ -172,16 +186,7 @@ function assemblePayload({
172
186
  }
173
187
  function assembleAndEncodePayload(args) {
174
188
  const payload = assemblePayload(args);
175
- const json = JSON.stringify(payload);
176
- if (typeof Buffer !== "undefined") {
177
- return Buffer.from(json, "utf8").toString("base64");
178
- }
179
- const bytes = new TextEncoder().encode(json);
180
- let binary = "";
181
- for (const b of bytes) {
182
- binary += String.fromCharCode(b);
183
- }
184
- return btoa(binary);
189
+ return encodeBase64(JSON.stringify(payload));
185
190
  }
186
191
 
187
192
  // src/pre-commit.ts
@@ -280,6 +285,235 @@ async function callSignMetaTx(coreSdk, args, nonce) {
280
285
  }
281
286
  }
282
287
  }
288
+ var SUBMIT_CHANNELS = ["server", "facilitator"];
289
+ var NoCompatibleChannelError = class extends Error {
290
+ constructor(actionId, advertisedChannels) {
291
+ super(
292
+ `x402-client: action '${actionId}' advertises channels [${advertisedChannels.join(", ")}] but none are submittable over HTTP (server / facilitator).`
293
+ );
294
+ this.name = "NoCompatibleChannelError";
295
+ this.actionId = actionId;
296
+ this.advertisedChannels = advertisedChannels;
297
+ }
298
+ };
299
+ var AllChannelsFailedError = class extends Error {
300
+ constructor(actionId, attempts) {
301
+ super(
302
+ `x402-client: action '${actionId}' failed on every attempted channel \u2014 ${attempts.map((a) => formatAttempt(a)).join("; ")}`
303
+ );
304
+ this.name = "AllChannelsFailedError";
305
+ this.attempts = attempts;
306
+ }
307
+ };
308
+ function formatAttempt(a) {
309
+ if (a.ok) return `${a.channel}=ok(${a.status})`;
310
+ const tail = a.status !== void 0 ? `(${a.status})` : "";
311
+ return `${a.channel}=${a.reason}${tail}`;
312
+ }
313
+ async function submitAction(args) {
314
+ const fetcher = args.fetch ?? globalThis.fetch.bind(globalThis);
315
+ const timeoutMs = args.timeoutMs ?? 1e4;
316
+ const ordered = orderedChannels(args.action);
317
+ if (ordered.length === 0) {
318
+ throw new NoCompatibleChannelError(args.action.id, args.action.channels);
319
+ }
320
+ const attempts = [];
321
+ for (const channel of ordered) {
322
+ const endpoint = args.action.endpoints?.[channel];
323
+ if (endpoint === void 0) {
324
+ attempts.push({
325
+ channel,
326
+ ok: false,
327
+ reason: "no-endpoint",
328
+ message: `no endpoint advertised for channel '${channel}'`
329
+ });
330
+ continue;
331
+ }
332
+ const outcome = await attemptChannel({
333
+ channel,
334
+ endpoint,
335
+ args,
336
+ fetcher,
337
+ timeoutMs
338
+ });
339
+ attempts.push(outcome.attempt);
340
+ if (outcome.attempt.ok && outcome.result !== void 0) {
341
+ return { ...outcome.result, attempts };
342
+ }
343
+ if (!outcome.attempt.ok && outcome.attempt.reason === "4xx") {
344
+ throw new AllChannelsFailedError(args.action.id, attempts);
345
+ }
346
+ }
347
+ throw new AllChannelsFailedError(args.action.id, attempts);
348
+ }
349
+ function orderedChannels(action) {
350
+ const seen = /* @__PURE__ */ new Set();
351
+ const out = [];
352
+ for (const c of action.channels) {
353
+ if (SUBMIT_CHANNELS.includes(c) && !seen.has(c)) {
354
+ seen.add(c);
355
+ out.push(c);
356
+ }
357
+ }
358
+ return out;
359
+ }
360
+ async function attemptChannel(input) {
361
+ const { channel, endpoint, args, fetcher, timeoutMs } = input;
362
+ const body = channel === "server" ? buildServerBody(args) : buildFacilitatorBody(args);
363
+ let res;
364
+ try {
365
+ res = await fetchWithTimeout(fetcher, endpoint, body, timeoutMs);
366
+ } catch (e) {
367
+ const reason = isTimeout(e) ? "timeout" : "network";
368
+ return {
369
+ attempt: {
370
+ channel,
371
+ ok: false,
372
+ reason,
373
+ message: e instanceof Error ? e.message : String(e)
374
+ }
375
+ };
376
+ }
377
+ const parsed = await res.json().catch(() => null);
378
+ if (res.status >= 500) {
379
+ return {
380
+ attempt: { channel, ok: false, reason: "5xx", status: res.status }
381
+ };
382
+ }
383
+ if (!res.ok) {
384
+ return {
385
+ attempt: { channel, ok: false, reason: "4xx", status: res.status }
386
+ };
387
+ }
388
+ if (parsed === null || typeof parsed !== "object") {
389
+ return {
390
+ attempt: {
391
+ channel,
392
+ ok: false,
393
+ reason: "invalid-response",
394
+ status: res.status,
395
+ message: "response body is not a JSON object"
396
+ }
397
+ };
398
+ }
399
+ try {
400
+ const result = channel === "server" ? parseServerResult(parsed) : parseFacilitatorResult(parsed);
401
+ return {
402
+ attempt: { channel, ok: true, status: res.status },
403
+ result: { ...result, channelUsed: channel }
404
+ };
405
+ } catch (e) {
406
+ return {
407
+ attempt: {
408
+ channel,
409
+ ok: false,
410
+ reason: "invalid-response",
411
+ status: res.status,
412
+ message: e instanceof Error ? e.message : String(e)
413
+ }
414
+ };
415
+ }
416
+ }
417
+ function buildServerBody(args) {
418
+ const body = {
419
+ exchangeId: args.exchangeId,
420
+ signedPayload: args.signed.signedPayload
421
+ };
422
+ if (args.action.id === "boson-redeem" && args.fulfillment !== void 0) {
423
+ body.fulfillment = args.fulfillment;
424
+ }
425
+ return body;
426
+ }
427
+ function buildFacilitatorBody(args) {
428
+ return {
429
+ action: args.action.id,
430
+ exchangeId: args.exchangeId,
431
+ network: args.network,
432
+ escrowAddress: args.escrowAddress,
433
+ signedPayload: args.signed.signedPayload
434
+ };
435
+ }
436
+ async function fetchWithTimeout(fetcher, endpoint, body, timeoutMs) {
437
+ const controller = new AbortController();
438
+ const timer = setTimeout(() => controller.abort(new TimeoutError(timeoutMs)), timeoutMs);
439
+ try {
440
+ return await fetcher(endpoint, {
441
+ method: "POST",
442
+ headers: { "content-type": "application/json" },
443
+ body: JSON.stringify(body),
444
+ signal: controller.signal
445
+ });
446
+ } finally {
447
+ clearTimeout(timer);
448
+ }
449
+ }
450
+ var TimeoutError = class extends Error {
451
+ constructor(timeoutMs) {
452
+ super(`x402-client/submit: channel attempt timed out after ${timeoutMs}ms`);
453
+ this.isTimeout = true;
454
+ this.name = "TimeoutError";
455
+ }
456
+ };
457
+ function isTimeout(e) {
458
+ if (e instanceof TimeoutError) return true;
459
+ if (typeof e !== "object" || e === null) return false;
460
+ const obj = e;
461
+ if (obj.isTimeout === true) return true;
462
+ if (obj.name === "AbortError" || obj.name === "TimeoutError") return true;
463
+ if (typeof obj.cause === "object" && obj.cause !== null) {
464
+ const cause = obj.cause;
465
+ if (cause.isTimeout === true) return true;
466
+ if (cause.name === "TimeoutError") return true;
467
+ }
468
+ return false;
469
+ }
470
+ function parseServerResult(body) {
471
+ if (typeof body !== "object" || body === null) {
472
+ throw new Error("server response body is not an object");
473
+ }
474
+ const raw = body;
475
+ const txHash = raw.txHash;
476
+ const exchangeState = raw.nextActions?.exchangeState;
477
+ if (!isHexHash(txHash) || !isExchangeState(exchangeState)) {
478
+ throw new Error("server response missing txHash or nextActions.exchangeState");
479
+ }
480
+ const out = {
481
+ txHash,
482
+ newExchangeState: exchangeState,
483
+ nextActions: raw.nextActions
484
+ };
485
+ if (isDisputeState(raw.nextActions?.disputeState)) {
486
+ out.newDisputeState = raw.nextActions.disputeState;
487
+ }
488
+ return out;
489
+ }
490
+ function parseFacilitatorResult(body) {
491
+ if (typeof body !== "object" || body === null) {
492
+ throw new Error("facilitator response body is not an object");
493
+ }
494
+ const raw = body;
495
+ if (raw.ok !== true || !isHexHash(raw.txHash) || !isExchangeState(raw.newExchangeState)) {
496
+ throw new Error("facilitator response missing ok/txHash/newExchangeState");
497
+ }
498
+ const out = {
499
+ txHash: raw.txHash,
500
+ newExchangeState: raw.newExchangeState
501
+ };
502
+ if (isDisputeState(raw.newDisputeState)) {
503
+ out.newDisputeState = raw.newDisputeState;
504
+ }
505
+ return out;
506
+ }
507
+ var TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
508
+ function isHexHash(v) {
509
+ return typeof v === "string" && TX_HASH_RE.test(v);
510
+ }
511
+ function isExchangeState(v) {
512
+ return typeof v === "string" && Object.values(ExchangeState).includes(v);
513
+ }
514
+ function isDisputeState(v) {
515
+ return typeof v === "string" && Object.values(DisputeState).includes(v);
516
+ }
283
517
  function normalizeEntityId(value) {
284
518
  if (typeof value === "bigint") {
285
519
  if (value < 0n) throw new Error(`entityId must be non-negative, got ${value.toString()}`);
@@ -492,6 +726,26 @@ function createX402bClient(config) {
492
726
  signAction(args) {
493
727
  return signPostCommitAction(args, { buildCoreSdk, getBuyerAddress });
494
728
  },
729
+ async submitAction(args) {
730
+ const signed = await signPostCommitAction(args, { buildCoreSdk, getBuyerAddress });
731
+ const action = args.priorNextActions.next.find((entry) => entry.id === args.actionId);
732
+ if (action === void 0) {
733
+ throw new Error(
734
+ `x402-client/submitAction: actionId '${args.actionId}' is not present in priorNextActions.next[]`
735
+ );
736
+ }
737
+ const submitArgs = {
738
+ action,
739
+ signed,
740
+ exchangeId: String(args.exchangeId),
741
+ network: args.network,
742
+ escrowAddress: args.escrowAddress,
743
+ fulfillment: args.fulfillment,
744
+ fetch: args.fetch,
745
+ timeoutMs: args.timeoutMs
746
+ };
747
+ return submitAction(submitArgs);
748
+ },
495
749
  signWithdrawFunds(args) {
496
750
  return signWithdrawFunds(args, { buildCoreSdk, getSignerAddress: getBuyerAddress });
497
751
  },
@@ -552,6 +806,6 @@ function deriveEip712DomainType(domain) {
552
806
  return fields;
553
807
  }
554
808
 
555
- export { createX402bClient, parsePaymentResponse, pickAction, resolveFulfillment, signerFromEthersAdapter };
809
+ export { AllChannelsFailedError, NoCompatibleChannelError, createX402bClient, decodeBase64, encodeBase64, parsePaymentResponse, pickAction, resolveFulfillment, signerFromEthersAdapter, submitAction };
556
810
  //# sourceMappingURL=index.js.map
557
811
  //# sourceMappingURL=index.js.map