@bosonprotocol/x402-server 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.
@@ -60,6 +60,21 @@ function emitNextActions(input, registry, facilitatorUrl) {
60
60
  };
61
61
  }
62
62
 
63
+ // src/handlers/fulfillment-result.ts
64
+ function serializeFulfillmentResult(result) {
65
+ if (result.kind === "inline") {
66
+ return {
67
+ kind: "inline",
68
+ body: Buffer.from(result.body).toString("base64"),
69
+ contentType: result.contentType
70
+ };
71
+ }
72
+ return {
73
+ kind: "async",
74
+ ...result.pointer !== void 0 ? { pointer: result.pointer } : {}
75
+ };
76
+ }
77
+
63
78
  // src/handlers/types.ts
64
79
  function handlerOk(body) {
65
80
  return { ok: true, status: 200, body };
@@ -578,6 +593,18 @@ var FacilitatorHttpError = class extends Error {
578
593
  }
579
594
  };
580
595
 
596
+ // src/logger.ts
597
+ var noopLogger = {
598
+ debug: () => {
599
+ },
600
+ info: () => {
601
+ },
602
+ warn: () => {
603
+ },
604
+ error: () => {
605
+ }
606
+ };
607
+
581
608
  // src/handlers/commit-and-redeem.ts
582
609
  async function handleCommit(input, ctx) {
583
610
  return await handleCommitImpl(input, ctx, {
@@ -592,6 +619,7 @@ async function handleCommitAndRedeem(input, ctx) {
592
619
  });
593
620
  }
594
621
  async function handleCommitImpl(input, ctx, expected) {
622
+ const logger = ctx.logger ?? noopLogger;
595
623
  const decoded = decodeXPaymentHeader(input.paymentHeader);
596
624
  if (!decoded.ok) {
597
625
  const status = decoded.code === "MISSING_HEADER" ? 402 : 400;
@@ -680,28 +708,38 @@ async function handleCommitImpl(input, ctx, expected) {
680
708
  );
681
709
  }
682
710
  if (expected.expectedState === x402Actions.ExchangeState.COMMITTED) {
683
- ctx.exchangeFulfillmentOptionStore.set(
711
+ await ctx.exchangeFulfillmentOptionStore.set(
684
712
  settleResult.exchangeId,
685
713
  input.requirements.fulfillment?.options.map((option) => option.id) ?? []
686
714
  );
687
715
  }
688
716
  const warnings = [];
717
+ let delivery;
689
718
  if (expected.expectedState === x402Actions.ExchangeState.REDEEMED && decoded.payload.fulfillment !== void 0 && decoded.payload.fulfillment.data !== void 0) {
690
719
  const pending = {
691
720
  exchangeId: settleResult.exchangeId,
692
721
  option: decoded.payload.fulfillment.option,
693
722
  data: decoded.payload.fulfillment.data,
694
723
  redeemer: decoded.payload.payload.buyer,
695
- recordedAt: Date.now()
724
+ recordedAt: Date.now(),
725
+ phase: "commit"
696
726
  };
697
- ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, pending);
727
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, pending);
728
+ logger.debug("x402-server: fulfillment recovery entry recorded (Flow B)", {
729
+ exchangeId: settleResult.exchangeId,
730
+ option: decoded.payload.fulfillment.option
731
+ });
698
732
  const channel = channelById.get(decoded.payload.fulfillment.option);
699
733
  if (channel === void 0) {
700
734
  const reason = "no channel adapter is registered";
701
- ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
735
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
702
736
  ...pending,
703
737
  error: reason
704
738
  });
739
+ logger.error("x402-server: Flow B channel adapter missing post-settle", {
740
+ exchangeId: settleResult.exchangeId,
741
+ option: decoded.payload.fulfillment.option
742
+ });
705
743
  warnings.push({
706
744
  code: "FULFILLMENT_COMMIT_DEFERRED",
707
745
  reason: "atomic redeem succeeded on-chain, but no channel adapter is registered",
@@ -714,13 +752,54 @@ async function handleCommitImpl(input, ctx, expected) {
714
752
  } else {
715
753
  try {
716
754
  await channel.onCommit(settleResult.exchangeId, decoded.payload.fulfillment.data);
717
- ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
755
+ if (channel.onFulfill !== void 0) {
756
+ const deliveryPending = {
757
+ ...pending,
758
+ phase: "delivery",
759
+ recordedAt: Date.now()
760
+ };
761
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, deliveryPending);
762
+ try {
763
+ delivery = serializeFulfillmentResult(await channel.onFulfill(settleResult.exchangeId));
764
+ await ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
765
+ logger.debug("x402-server: Flow B channel onCommit succeeded", {
766
+ exchangeId: settleResult.exchangeId,
767
+ option: decoded.payload.fulfillment.option
768
+ });
769
+ } catch (e) {
770
+ const reason = e instanceof Error ? e.message : String(e);
771
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
772
+ ...deliveryPending,
773
+ error: reason
774
+ });
775
+ warnings.push({
776
+ code: "FULFILLMENT_DELIVERY_DEFERRED",
777
+ reason: "atomic redeem succeeded on-chain, but the channel's delivery dispatch failed",
778
+ details: {
779
+ exchangeId: settleResult.exchangeId,
780
+ option: decoded.payload.fulfillment.option,
781
+ error: reason
782
+ }
783
+ });
784
+ }
785
+ } else {
786
+ await ctx.fulfillmentRecoveryStore.delete(settleResult.exchangeId);
787
+ logger.debug("x402-server: Flow B channel onCommit succeeded", {
788
+ exchangeId: settleResult.exchangeId,
789
+ option: decoded.payload.fulfillment.option
790
+ });
791
+ }
718
792
  } catch (e) {
719
793
  const reason = e instanceof Error ? e.message : String(e);
720
- ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
794
+ await ctx.fulfillmentRecoveryStore.set(settleResult.exchangeId, {
721
795
  ...pending,
722
796
  error: reason
723
797
  });
798
+ logger.warn("x402-server: Flow B channel onCommit failed; recovery entry retained", {
799
+ exchangeId: settleResult.exchangeId,
800
+ option: decoded.payload.fulfillment.option,
801
+ error: reason
802
+ });
724
803
  warnings.push({
725
804
  code: "FULFILLMENT_COMMIT_DEFERRED",
726
805
  reason: "atomic redeem succeeded on-chain, but the channel adapter rejected the data",
@@ -745,7 +824,8 @@ async function handleCommitImpl(input, ctx, expected) {
745
824
  exchangeId: settleResult.exchangeId,
746
825
  txHash: settleResult.txHash,
747
826
  nextActions,
748
- ...warnings.length > 0 ? { warnings } : {}
827
+ ...warnings.length > 0 ? { warnings } : {},
828
+ ...delivery !== void 0 ? { fulfillment: delivery } : {}
749
829
  });
750
830
  }
751
831
  function buildExpectedFromRequirements(requirements, state) {
@@ -845,7 +925,7 @@ async function handleRedeem(input, ctx) {
845
925
  }
846
926
  let resolvedChannel;
847
927
  if (input.fulfillment !== void 0) {
848
- const advertisedOptions = ctx.exchangeFulfillmentOptionStore.get(input.exchangeId);
928
+ const advertisedOptions = await ctx.exchangeFulfillmentOptionStore.get(input.exchangeId);
849
929
  if (advertisedOptions !== void 0 && !advertisedOptions.includes(input.fulfillment.option)) {
850
930
  return handlerErr(
851
931
  400,
@@ -888,23 +968,40 @@ async function handleRedeem(input, ctx) {
888
968
  }
889
969
  const result = await handlePerformAction("boson-redeem", input, ctx);
890
970
  if (!result.ok) return result;
891
- let warning;
971
+ const warnings = [];
972
+ let delivery;
892
973
  if (resolvedChannel !== void 0 && input.fulfillment !== void 0) {
893
974
  const pending = {
894
975
  exchangeId: input.exchangeId,
895
976
  option: input.fulfillment.option,
896
977
  data: input.fulfillment.data,
897
978
  redeemer,
898
- recordedAt: Date.now()
979
+ recordedAt: Date.now(),
980
+ phase: "commit"
899
981
  };
900
- ctx.fulfillmentRecoveryStore.set(input.exchangeId, pending);
982
+ const logger = ctx.logger ?? noopLogger;
983
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, pending);
984
+ logger.debug("x402-server: fulfillment recovery entry recorded (Flow A redeem)", {
985
+ exchangeId: input.exchangeId,
986
+ option: input.fulfillment.option
987
+ });
988
+ let onCommitOk = false;
901
989
  try {
902
990
  await resolvedChannel.onCommit(input.exchangeId, input.fulfillment.data);
903
- ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
991
+ onCommitOk = true;
992
+ logger.debug("x402-server: Flow A channel onCommit succeeded", {
993
+ exchangeId: input.exchangeId,
994
+ option: input.fulfillment.option
995
+ });
904
996
  } catch (e) {
905
997
  const reason = errorMessage(e);
906
- ctx.fulfillmentRecoveryStore.set(input.exchangeId, { ...pending, error: reason });
907
- warning = {
998
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, { ...pending, error: reason });
999
+ logger.warn("x402-server: Flow A channel onCommit failed; recovery entry retained", {
1000
+ exchangeId: input.exchangeId,
1001
+ option: input.fulfillment.option,
1002
+ error: reason
1003
+ });
1004
+ warnings.push({
908
1005
  code: "FULFILLMENT_UPDATE_DEFERRED",
909
1006
  reason: "redeem succeeded on-chain, but the server could not persist the fulfillment update",
910
1007
  details: {
@@ -912,16 +1009,48 @@ async function handleRedeem(input, ctx) {
912
1009
  option: input.fulfillment.option,
913
1010
  error: reason
914
1011
  }
915
- };
1012
+ });
1013
+ }
1014
+ if (onCommitOk) {
1015
+ if (resolvedChannel.onFulfill !== void 0) {
1016
+ const deliveryPending = {
1017
+ ...pending,
1018
+ phase: "delivery",
1019
+ recordedAt: Date.now()
1020
+ };
1021
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, deliveryPending);
1022
+ try {
1023
+ delivery = serializeFulfillmentResult(await resolvedChannel.onFulfill(input.exchangeId));
1024
+ await ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
1025
+ } catch (e) {
1026
+ const reason = errorMessage(e);
1027
+ await ctx.fulfillmentRecoveryStore.set(input.exchangeId, {
1028
+ ...deliveryPending,
1029
+ error: reason
1030
+ });
1031
+ warnings.push({
1032
+ code: "FULFILLMENT_DELIVERY_DEFERRED",
1033
+ reason: "redeem succeeded on-chain, but the fulfillment channel's delivery dispatch failed",
1034
+ details: {
1035
+ exchangeId: input.exchangeId,
1036
+ option: input.fulfillment.option,
1037
+ error: reason
1038
+ }
1039
+ });
1040
+ }
1041
+ } else {
1042
+ await ctx.fulfillmentRecoveryStore.delete(input.exchangeId);
1043
+ }
916
1044
  }
917
1045
  }
918
- ctx.exchangeFulfillmentOptionStore.delete(input.exchangeId);
919
- if (warning !== void 0) {
1046
+ await ctx.exchangeFulfillmentOptionStore.delete(input.exchangeId);
1047
+ if (warnings.length > 0 || delivery !== void 0) {
920
1048
  return {
921
1049
  ...result,
922
1050
  body: {
923
1051
  ...result.body,
924
- warnings: [...result.body.warnings ?? [], warning]
1052
+ ...delivery !== void 0 ? { fulfillment: delivery } : {},
1053
+ ...warnings.length > 0 ? { warnings: [...result.body.warnings ?? [], ...warnings] } : {}
925
1054
  }
926
1055
  };
927
1056
  }