@devlab.group/agent-commerce 1.6.0 → 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.
Files changed (41) hide show
  1. package/LICENSE +187 -2
  2. package/README.md +6 -6
  3. package/dist/ap2.d.ts +2 -2
  4. package/dist/ap2.js +94 -28
  5. package/dist/ap2.js.map +1 -1
  6. package/dist/{chunk-V2V2E7HX.js → chunk-3LUJHVKX.js} +4 -4
  7. package/dist/{chunk-V2V2E7HX.js.map → chunk-3LUJHVKX.js.map} +1 -1
  8. package/dist/{chunk-IVJC5YLE.js → chunk-53WXXIPR.js} +3 -3
  9. package/dist/{chunk-IVJC5YLE.js.map → chunk-53WXXIPR.js.map} +1 -1
  10. package/dist/chunk-CKJRITWY.js +113 -0
  11. package/dist/chunk-CKJRITWY.js.map +1 -0
  12. package/dist/chunk-HOZA3IWP.js +115 -0
  13. package/dist/chunk-HOZA3IWP.js.map +1 -0
  14. package/dist/{chunk-ATTWSFQP.js → chunk-RKBGCPCK.js} +78 -25
  15. package/dist/chunk-RKBGCPCK.js.map +1 -0
  16. package/dist/{chunk-RAJGPGJG.js → chunk-SYEGCNJZ.js} +3 -3
  17. package/dist/{chunk-RAJGPGJG.js.map → chunk-SYEGCNJZ.js.map} +1 -1
  18. package/dist/{chunk-74BSEXB5.js → chunk-TWRUNWDR.js} +11 -4
  19. package/dist/chunk-TWRUNWDR.js.map +1 -0
  20. package/dist/cli/index.js +143 -34
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/{constants-Bkq04FRZ.d.ts → constants-BSBRlKRR.d.ts} +2 -2
  23. package/dist/index.d.ts +42 -22
  24. package/dist/index.js +1254 -377
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp.d.ts +1 -1
  27. package/dist/mcp.js +247 -94
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/mpp.d.ts +6 -2
  30. package/dist/mpp.js +65 -28
  31. package/dist/mpp.js.map +1 -1
  32. package/dist/{guardrails-71XwtXlT.d.ts → payment-flow-DCS882tR.d.ts} +9 -1
  33. package/dist/{protocol-adapter-DTkrXTev.d.ts → protocol-adapter-DonxfAwa.d.ts} +2 -0
  34. package/dist/{types-O6lg3Q5V.d.ts → types-CVw6Sf46.d.ts} +11 -0
  35. package/dist/x402.d.ts +11 -3
  36. package/dist/x402.js +4 -4
  37. package/package.json +22 -17
  38. package/dist/chunk-74BSEXB5.js.map +0 -1
  39. package/dist/chunk-AFI2CEZS.js +0 -10
  40. package/dist/chunk-AFI2CEZS.js.map +0 -1
  41. package/dist/chunk-ATTWSFQP.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,18 +1,20 @@
1
- import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-RAJGPGJG.js';
2
- import { MCP_TOOL_NAME_PATTERN } from './chunk-AFI2CEZS.js';
3
- import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-V2V2E7HX.js';
4
- import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-74BSEXB5.js';
5
- import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, isRecord, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, isCommerceError, describeError, isHttpProtocolAdapter, toLogInfo, extractReservedInputFields, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, toErrorEnvelope, DELIVERY_SUMMARY_META_KEY, toDeliverySummary } from './chunk-IVJC5YLE.js';
6
- export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_METHOD_NAMES, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-IVJC5YLE.js';
1
+ import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-SYEGCNJZ.js';
2
+ import { MCP_TOOL_NAME_PATTERN, readCappedBody, createBearerCheck, settlementResponse, joinUrl, settlementResponseFromDetails, x402ErrorCode, settlementFailure } from './chunk-HOZA3IWP.js';
3
+ import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-3LUJHVKX.js';
4
+ import { contentDigest, CONTENT_DIGEST_METADATA_KEY, mppProblem, mppProblemDetail } from './chunk-CKJRITWY.js';
5
+ import { X402_PAYMENT_FLOWS, X402_DEFAULT_PAYMENT_FLOW, resolveX402Deployment, SETTLE_AFTER_BACKEND_METADATA_KEY, resolveDeploymentMode, requireNetworkProfile } from './chunk-TWRUNWDR.js';
6
+ import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, isRecord, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, isCommerceError, describeError, isHttpProtocolAdapter, toLogInfo, extractReservedInputFields, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, toErrorEnvelope, DELIVERY_SUMMARY_META_KEY, toDeliverySummary, AUTHORIZATION_INPUT_FIELD, parseAuthorizationSubmission, PAYMENT_INPUT_FIELD } from './chunk-53WXXIPR.js';
7
+ export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_METHOD_NAMES, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-53WXXIPR.js';
7
8
  import { promises, accessSync, constants } from 'fs';
8
9
  import * as path from 'path';
9
10
  import { parse, YAMLError } from 'yaml';
10
11
  import { z } from 'zod';
11
12
  import { isDeepStrictEqual } from 'util';
12
13
  import Fastify from 'fastify';
13
- import { randomUUID, timingSafeEqual, createHash } from 'crypto';
14
+ import { randomUUID, createHash } from 'crypto';
14
15
  import { createRequire } from 'module';
15
16
  import pino from 'pino';
17
+ import { Transform } from 'stream';
16
18
  import Ajv2020 from 'ajv/dist/2020.js';
17
19
  import addFormats from 'ajv-formats';
18
20
 
@@ -85,8 +87,19 @@ var DEFAULT_CONFIG_FILENAME = "config.yaml";
85
87
  // src/core/execution/backend-http.ts
86
88
  var MAX_BODY_SNIPPET_LENGTH = 512;
87
89
  var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
90
+ var NEVER_FORWARDED_HEADER = /^(authorization|cookie|set-cookie|host|content-length|content-type|connection|keep-alive|te|trailer|transfer-encoding|upgrade|idempotency-key|proxy-.*)$/;
88
91
  var PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
89
92
  var MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
93
+ var BackendErrorResponse = class {
94
+ constructor(status, body, headers = {}) {
95
+ this.status = status;
96
+ this.body = body;
97
+ this.headers = headers;
98
+ }
99
+ status;
100
+ body;
101
+ headers;
102
+ };
90
103
  var HttpBackendExecutor = class {
91
104
  fetchImpl;
92
105
  logger;
@@ -117,7 +130,7 @@ var HttpBackendExecutor = class {
117
130
  context
118
131
  );
119
132
  }
120
- const headers = buildHeaders(handler, context);
133
+ const headers = buildHeaders(handler, context, request.backendHeaders);
121
134
  if (request.idempotencyKey !== void 0) {
122
135
  setHeader(headers, IDEMPOTENCY_KEY_HEADER, request.idempotencyKey, context);
123
136
  }
@@ -205,7 +218,8 @@ var HttpBackendExecutor = class {
205
218
  throw new CommerceError("BACKEND_ERROR", `Backend responded with status ${response.status}`, {
206
219
  requestId: request.requestId,
207
220
  resourceId: request.resourceId,
208
- details: { status: response.status }
221
+ details: { status: response.status },
222
+ cause: new BackendErrorResponse(response.status, parsedBody, responseHeaders)
209
223
  });
210
224
  }
211
225
  return {
@@ -226,10 +240,10 @@ function findUnparsedBraceToken(url) {
226
240
  const token = /\{[^{}]*\}?|\}/.exec(residue.slice(index));
227
241
  return token?.[0] ?? residue[index];
228
242
  }
229
- function validateBackendRequestShape(handler, input, context) {
243
+ function validateBackendRequestShape(handler, input, context, backendHeaders) {
230
244
  const inputRecord = isRecord(input) ? input : {};
231
245
  const parts = buildBackendRequestParts(handler, inputRecord, context);
232
- buildHeaders(handler, context);
246
+ buildHeaders(handler, context, backendHeaders);
233
247
  let target;
234
248
  try {
235
249
  target = new URL(parts.url);
@@ -321,12 +335,25 @@ function stringifyPrimitive(value) {
321
335
  return String(value);
322
336
  }
323
337
  }
324
- function buildHeaders(handler, context) {
338
+ function buildHeaders(handler, context, forwarded = {}) {
339
+ let headers;
325
340
  try {
326
- return new Headers(handler.headers);
341
+ headers = new Headers(handler.headers);
327
342
  } catch {
328
343
  throw invalidHeaderError(context);
329
344
  }
345
+ for (const [name, value] of Object.entries(forwarded)) {
346
+ if (NEVER_FORWARDED_HEADER.test(name.toLowerCase())) continue;
347
+ try {
348
+ if (!headers.has(name)) headers.set(name, value);
349
+ } catch {
350
+ throw new CommerceError("INPUT_INVALID", "A forwarded request header is invalid", {
351
+ ...context,
352
+ details: { reason: "invalid-forwarded-header" }
353
+ });
354
+ }
355
+ }
356
+ return headers;
330
357
  }
331
358
  function setHeader(headers, name, value, context) {
332
359
  try {
@@ -645,6 +672,138 @@ function createExecutionPipeline(options) {
645
672
  }
646
673
  };
647
674
  }
675
+ async function settle(args) {
676
+ const { request, resource, provider, requirement, payment, verification, replayKey, hold } = args;
677
+ let settlement;
678
+ try {
679
+ settlement = await provider.settle({
680
+ requestId: request.requestId,
681
+ resource,
682
+ requirement,
683
+ submission: payment,
684
+ verification
685
+ });
686
+ } catch (error) {
687
+ const txHash = settlementTxHash(error);
688
+ await hold?.finalize("markUncertain");
689
+ await safePersist(
690
+ () => options.store.updatePaymentAttempt({
691
+ replayKey,
692
+ status: "settlement-uncertain",
693
+ ...txHash !== void 0 ? { externalReference: txHash } : {},
694
+ ...error instanceof Error ? { rejectionReason: error.message } : {}
695
+ }),
696
+ "updatePaymentAttempt(settlement-uncertain)",
697
+ request.requestId
698
+ );
699
+ await safeEmit(
700
+ buildEvent({
701
+ type: "payment.rejected",
702
+ requestId: request.requestId,
703
+ resourceId: resource.id,
704
+ adapter: request.protocol,
705
+ paymentProvider: provider.name,
706
+ status: "error",
707
+ data: {
708
+ reason: "settlement-uncertain",
709
+ ...txHash !== void 0 ? { transactionHash: txHash } : {}
710
+ }
711
+ })
712
+ );
713
+ throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
714
+ requestId: request.requestId,
715
+ resourceId: resource.id,
716
+ cause: error,
717
+ details: {
718
+ settlementUncertain: true,
719
+ ...txHash !== void 0 ? { transactionHash: txHash } : {},
720
+ ...settlementParties(requirement, verification)
721
+ }
722
+ });
723
+ }
724
+ if (settlement.status !== "settled") {
725
+ await hold?.finalize("release");
726
+ await safePersist(
727
+ () => options.store.updatePaymentAttempt({
728
+ replayKey,
729
+ status: "rejected",
730
+ ...settlement.rejectionReason !== void 0 ? { rejectionReason: settlement.rejectionReason } : {}
731
+ }),
732
+ "updatePaymentAttempt(rejected)",
733
+ request.requestId
734
+ );
735
+ await safeEmit(
736
+ buildEvent({
737
+ type: "payment.rejected",
738
+ requestId: request.requestId,
739
+ resourceId: resource.id,
740
+ adapter: request.protocol,
741
+ paymentProvider: provider.name,
742
+ status: "error",
743
+ data: settlement.rejectionReason ? { reason: settlement.rejectionReason } : {}
744
+ })
745
+ );
746
+ const rejectionDetails = retryChallenge(requirement, settlement.rejectionReason).details;
747
+ throw new CommerceError(
748
+ "PAYMENT_SETTLEMENT_FAILED",
749
+ settlement.rejectionReason ?? "Settlement was rejected",
750
+ {
751
+ requestId: request.requestId,
752
+ resourceId: resource.id,
753
+ httpStatus: 402,
754
+ details: {
755
+ ...rejectionDetails,
756
+ ...settlementParties(requirement, settlement, verification)
757
+ }
758
+ }
759
+ );
760
+ }
761
+ await hold?.finalize("consume");
762
+ await safePersist(
763
+ () => options.store.updatePaymentAttempt({
764
+ replayKey,
765
+ status: "settled",
766
+ ...settlement.externalReference !== void 0 ? { externalReference: settlement.externalReference } : {}
767
+ }),
768
+ "updatePaymentAttempt(settled)",
769
+ request.requestId
770
+ );
771
+ await safeEmit(
772
+ buildEvent({
773
+ type: "payment.settled",
774
+ requestId: request.requestId,
775
+ resourceId: resource.id,
776
+ adapter: request.protocol,
777
+ paymentProvider: provider.name,
778
+ status: "ok"
779
+ })
780
+ );
781
+ return settlement;
782
+ }
783
+ async function abandonSettlement(args, backendErrorCode) {
784
+ const { request, resource, provider, replayKey, hold } = args;
785
+ await hold?.finalize("release");
786
+ await safePersist(
787
+ () => options.store.updatePaymentAttempt({
788
+ replayKey,
789
+ status: "rejected",
790
+ rejectionReason: "backend_failed"
791
+ }),
792
+ "updatePaymentAttempt(backend-failed)",
793
+ request.requestId
794
+ );
795
+ await safeEmit(
796
+ buildEvent({
797
+ type: "payment.rejected",
798
+ requestId: request.requestId,
799
+ resourceId: resource.id,
800
+ adapter: request.protocol,
801
+ paymentProvider: provider.name,
802
+ status: "error",
803
+ data: { reason: "backend-failed", backendErrorCode }
804
+ })
805
+ );
806
+ }
648
807
  async function execute(request) {
649
808
  const pipelineStart = options.clock.monotonicMs();
650
809
  const resource = options.resources.get(request.resourceId);
@@ -680,10 +839,12 @@ function createExecutionPipeline(options) {
680
839
  );
681
840
  }
682
841
  const validInput = validation.value;
683
- validateBackendRequestShape(resource.handler, validInput, {
684
- requestId: request.requestId,
685
- resourceId: resource.id
686
- });
842
+ validateBackendRequestShape(
843
+ resource.handler,
844
+ validInput,
845
+ { requestId: request.requestId, resourceId: resource.id },
846
+ request.backendHeaders
847
+ );
687
848
  if (resource.pricing.type === "dynamic") {
688
849
  throw new CommerceError(
689
850
  "CONFIG_INVALID",
@@ -706,6 +867,7 @@ function createExecutionPipeline(options) {
706
867
  }
707
868
  let paymentResult;
708
869
  let authorization;
870
+ let deferredSettlement;
709
871
  if (resource.pricing.type === "fixed") {
710
872
  const pricing = resource.pricing;
711
873
  const provider = pickProvider(options.paymentProviders, resource.paymentMethods);
@@ -732,7 +894,9 @@ function createExecutionPipeline(options) {
732
894
  resource,
733
895
  amount: pricing.amount,
734
896
  currency: pricing.currency,
735
- requestedAt: options.clock.nowIso()
897
+ requestedAt: options.clock.nowIso(),
898
+ // Transport facts a rail may bind into its challenge, such as a body digest
899
+ ...request.metadata !== void 0 ? { metadata: request.metadata } : {}
736
900
  });
737
901
  } catch (error) {
738
902
  throw isCommerceError(error) ? error : new CommerceError("PAYMENT_PROVIDER_UNAVAILABLE", "Payment provider is unavailable", {
@@ -806,7 +970,7 @@ function createExecutionPipeline(options) {
806
970
  {
807
971
  requestId: request.requestId,
808
972
  resourceId: resource.id,
809
- ...retryChallenge(requirement)
973
+ ...retryChallenge(requirement, verification.rejectionReason)
810
974
  }
811
975
  );
812
976
  }
@@ -865,7 +1029,7 @@ function createExecutionPipeline(options) {
865
1029
  data: { reason: mapped.code }
866
1030
  })
867
1031
  );
868
- throw mapped;
1032
+ throw finishedReplay(mapped, requirement);
869
1033
  }
870
1034
  await safeEmit(
871
1035
  buildEvent({
@@ -877,104 +1041,21 @@ function createExecutionPipeline(options) {
877
1041
  status: "ok"
878
1042
  })
879
1043
  );
880
- let settlement;
881
- try {
882
- settlement = await provider.settle({
883
- requestId: request.requestId,
884
- resource,
885
- requirement,
886
- submission: payment,
887
- verification
888
- });
889
- } catch (error) {
890
- const txHash = settlementTxHash(error);
891
- await hold?.finalize("markUncertain");
892
- await safePersist(
893
- () => options.store.updatePaymentAttempt({
894
- replayKey,
895
- status: "settlement-uncertain",
896
- ...txHash !== void 0 ? { externalReference: txHash } : {},
897
- ...error instanceof Error ? { rejectionReason: error.message } : {}
898
- }),
899
- "updatePaymentAttempt(settlement-uncertain)",
900
- request.requestId
901
- );
902
- await safeEmit(
903
- buildEvent({
904
- type: "payment.rejected",
905
- requestId: request.requestId,
906
- resourceId: resource.id,
907
- adapter: request.protocol,
908
- paymentProvider: provider.name,
909
- status: "error",
910
- data: {
911
- reason: "settlement-uncertain",
912
- ...txHash !== void 0 ? { transactionHash: txHash } : {}
913
- }
914
- })
915
- );
916
- throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
917
- requestId: request.requestId,
918
- resourceId: resource.id,
919
- cause: error,
920
- details: {
921
- settlementUncertain: true,
922
- ...txHash !== void 0 ? { transactionHash: txHash } : {}
923
- }
924
- });
925
- }
926
- if (settlement.status !== "settled") {
927
- await hold?.finalize("release");
928
- await safePersist(
929
- () => options.store.updatePaymentAttempt({
930
- replayKey,
931
- status: "rejected",
932
- ...settlement.rejectionReason !== void 0 ? { rejectionReason: settlement.rejectionReason } : {}
933
- }),
934
- "updatePaymentAttempt(rejected)",
935
- request.requestId
936
- );
937
- await safeEmit(
938
- buildEvent({
939
- type: "payment.rejected",
940
- requestId: request.requestId,
941
- resourceId: resource.id,
942
- adapter: request.protocol,
943
- paymentProvider: provider.name,
944
- status: "error",
945
- data: settlement.rejectionReason ? { reason: settlement.rejectionReason } : {}
946
- })
947
- );
948
- throw new CommerceError(
949
- "PAYMENT_SETTLEMENT_FAILED",
950
- settlement.rejectionReason ?? "Settlement was rejected",
951
- {
952
- requestId: request.requestId,
953
- resourceId: resource.id
954
- }
955
- );
1044
+ const settleArgs = {
1045
+ request,
1046
+ resource,
1047
+ provider,
1048
+ requirement,
1049
+ payment,
1050
+ verification,
1051
+ replayKey,
1052
+ hold
1053
+ };
1054
+ if (requirement.metadata?.[SETTLE_AFTER_BACKEND_METADATA_KEY] === true) {
1055
+ deferredSettlement = settleArgs;
1056
+ } else {
1057
+ paymentResult = await settle(settleArgs);
956
1058
  }
957
- await hold?.finalize("consume");
958
- await safePersist(
959
- () => options.store.updatePaymentAttempt({
960
- replayKey,
961
- status: "settled",
962
- ...settlement.externalReference !== void 0 ? { externalReference: settlement.externalReference } : {}
963
- }),
964
- "updatePaymentAttempt(settled)",
965
- request.requestId
966
- );
967
- await safeEmit(
968
- buildEvent({
969
- type: "payment.settled",
970
- requestId: request.requestId,
971
- resourceId: resource.id,
972
- adapter: request.protocol,
973
- paymentProvider: provider.name,
974
- status: "ok"
975
- })
976
- );
977
- paymentResult = settlement;
978
1059
  }
979
1060
  const backendStart = options.clock.monotonicMs();
980
1061
  let backendResponse;
@@ -983,7 +1064,8 @@ function createExecutionPipeline(options) {
983
1064
  requestId: request.requestId,
984
1065
  resourceId: resource.id,
985
1066
  input: validInput,
986
- ...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {}
1067
+ ...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {},
1068
+ ...request.backendHeaders !== void 0 ? { backendHeaders: request.backendHeaders } : {}
987
1069
  });
988
1070
  } catch (error) {
989
1071
  const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
@@ -998,6 +1080,10 @@ function createExecutionPipeline(options) {
998
1080
  data: { code: commerceError.code }
999
1081
  })
1000
1082
  );
1083
+ if (deferredSettlement !== void 0) {
1084
+ await abandonSettlement(deferredSettlement, commerceError.code);
1085
+ throw commerceError;
1086
+ }
1001
1087
  if (paymentResult !== void 0) {
1002
1088
  const failedReceipt = {
1003
1089
  id: options.ids.next("receipt"),
@@ -1030,6 +1116,8 @@ function createExecutionPipeline(options) {
1030
1116
  currency: paymentResult.currency,
1031
1117
  ...paymentResult.network !== void 0 ? { network: paymentResult.network } : {},
1032
1118
  ...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {},
1119
+ ...paymentResult.payer !== void 0 ? { payer: paymentResult.payer } : {},
1120
+ ...typeof paymentResult.metadata?.["amountBaseUnits"] === "string" ? { amountBaseUnits: paymentResult.metadata["amountBaseUnits"] } : {},
1033
1121
  ...typeof paymentResult.metadata?.["receipt"] === "string" ? { receipt: paymentResult.metadata["receipt"] } : {}
1034
1122
  }
1035
1123
  }
@@ -1049,6 +1137,9 @@ function createExecutionPipeline(options) {
1049
1137
  data: { status: backendResponse.status }
1050
1138
  })
1051
1139
  );
1140
+ if (deferredSettlement !== void 0) {
1141
+ paymentResult = await settle(deferredSettlement);
1142
+ }
1052
1143
  const receipt = {
1053
1144
  id: options.ids.next("receipt"),
1054
1145
  requestId: request.requestId,
@@ -1108,9 +1199,35 @@ function resolveAuthorizationProviders(providers, resource, requestId) {
1108
1199
  return found;
1109
1200
  });
1110
1201
  }
1111
- function retryChallenge(requirement) {
1202
+ function retryChallenge(requirement, reason) {
1112
1203
  const envelope = requirement.challenge.envelope;
1113
- return envelope === void 0 ? {} : { details: { challenge: envelope } };
1204
+ if (envelope === void 0 && reason === void 0) return {};
1205
+ return {
1206
+ details: {
1207
+ ...envelope !== void 0 ? { challenge: envelope } : {},
1208
+ ...reason !== void 0 ? { reason } : {}
1209
+ }
1210
+ };
1211
+ }
1212
+ var FINISHED_ATTEMPT_STATUSES = /* @__PURE__ */ new Set(["settled", "rejected"]);
1213
+ function finishedReplay(error, requirement) {
1214
+ const status = error.details?.["attemptStatus"];
1215
+ if (error.code !== "PAYMENT_REPLAYED" || typeof status !== "string") return error;
1216
+ if (!FINISHED_ATTEMPT_STATUSES.has(status)) return error;
1217
+ return new CommerceError("PAYMENT_REPLAYED", error.message, {
1218
+ ...error.requestId !== void 0 ? { requestId: error.requestId } : {},
1219
+ ...error.resourceId !== void 0 ? { resourceId: error.resourceId } : {},
1220
+ httpStatus: 402,
1221
+ details: { ...error.details, ...retryChallenge(requirement).details }
1222
+ });
1223
+ }
1224
+ function settlementParties(requirement, ...results) {
1225
+ const network = results.find((r) => r.network !== void 0)?.network ?? requirement.network;
1226
+ const payer = results.find((r) => r.payer !== void 0)?.payer;
1227
+ return {
1228
+ ...network !== void 0 ? { network } : {},
1229
+ ...payer !== void 0 ? { payer } : {}
1230
+ };
1114
1231
  }
1115
1232
  function pickProvider(providers, methods) {
1116
1233
  for (const method of methods) {
@@ -1199,6 +1316,7 @@ var ACP_OPERATION_INPUT_KEYS = {
1199
1316
  var ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
1200
1317
  cancelCheckoutSession: ["body"]
1201
1318
  };
1319
+ var ACP_SESSION_ID_INPUT_KEY = "checkout_session_id";
1202
1320
  var ACP_JSON_MEDIA_TYPE = "application/json";
1203
1321
  var ACP_API_VERSION_HEADER = "api-version";
1204
1322
  var ACP_REQUEST_ID_HEADER = "request-id";
@@ -1206,6 +1324,17 @@ var ACP_IDEMPOTENCY_KEY_HEADER = "idempotency-key";
1206
1324
  var ACP_IDEMPOTENT_REPLAYED_HEADER = "idempotent-replayed";
1207
1325
  var ACP_IN_FLIGHT_RETRY_AFTER_SECONDS = 1;
1208
1326
  var ACP_MAX_REQUEST_ID_LENGTH = 128;
1327
+ var ACP_FORWARDED_HEADERS = [
1328
+ "accept-language",
1329
+ "user-agent",
1330
+ ACP_REQUEST_ID_HEADER,
1331
+ ACP_API_VERSION_HEADER,
1332
+ "signature",
1333
+ "timestamp"
1334
+ ];
1335
+ var ACP_MAX_FORWARDED_HEADER_LENGTH = 2048;
1336
+ var ACP_MERCHANT_RETRY_AFTER_DEFAULT_SECONDS = 5;
1337
+ var ACP_MERCHANT_RETRY_AFTER_MAX_SECONDS = 300;
1209
1338
 
1210
1339
  // src/config/schema.ts
1211
1340
  var SUPPORTED_CONFIG_VERSION = 1;
@@ -1272,7 +1401,11 @@ var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"),
1272
1401
  }
1273
1402
  );
1274
1403
  var AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
1275
- var AcpIdempotencySchema = z.object({ path: z.string().min(1), retentionHours: NumberOrString.optional() }).strict();
1404
+ var AcpIdempotencySchema = z.object({
1405
+ path: z.string().min(1),
1406
+ retentionHours: NumberOrString.optional(),
1407
+ merchantIdempotent: z.boolean().optional()
1408
+ }).strict();
1276
1409
  var AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
1277
1410
  var AcpDiscoverySchema = z.object({
1278
1411
  documentationUrl: z.string().min(1).optional(),
@@ -1342,6 +1475,7 @@ var ResourceEntrySchema = z.object({
1342
1475
  pricing: PricingSchema,
1343
1476
  expose: z.array(z.string().min(1)).min(1),
1344
1477
  payments: z.array(z.string().min(1)).optional(),
1478
+ paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
1345
1479
  authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
1346
1480
  }).strict();
1347
1481
  var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
@@ -1379,6 +1513,8 @@ var X402Schema = z.object({
1379
1513
  assetDecimals: NumberOrString,
1380
1514
  payTo: z.string().min(1),
1381
1515
  maxTimeoutSeconds: NumberOrString,
1516
+ // `authorization` (the x402 default) settles after the backend call
1517
+ paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
1382
1518
  facilitator: FacilitatorSchema,
1383
1519
  // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1384
1520
  allowMainnet: BooleanOrString.optional(),
@@ -1396,6 +1532,8 @@ var MppSchema = z.object({
1396
1532
  realm: z.string().min(1),
1397
1533
  challengeSecret: z.string().min(1),
1398
1534
  challengeTtlSeconds: NumberOrString.optional(),
1535
+ // Same values and default as `payments.x402.paymentFlow`
1536
+ paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
1399
1537
  facilitator: FacilitatorSchema,
1400
1538
  // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1401
1539
  allowMainnet: BooleanOrString.optional(),
@@ -1426,6 +1564,8 @@ var Ap2Schema = z.object({
1426
1564
  checkoutIssuers: z.array(Ap2IssuerSchema).optional()
1427
1565
  }).strict().optional(),
1428
1566
  clockSkewSeconds: NumberOrString.optional(),
1567
+ requireMandateAudience: BooleanOrString.optional(),
1568
+ requireMandateExpiry: BooleanOrString.optional(),
1429
1569
  replay: z.object({ path: z.string().min(1) }).strict().optional()
1430
1570
  }).strict();
1431
1571
  var AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
@@ -1573,6 +1713,7 @@ function normalize(raw) {
1573
1713
  min: 1
1574
1714
  }
1575
1715
  ),
1716
+ paymentFlow: x402Raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
1576
1717
  facilitator: normalizeFacilitator(x402Raw.facilitator),
1577
1718
  ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
1578
1719
  ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
@@ -1604,7 +1745,22 @@ function normalize(raw) {
1604
1745
  const resources = Object.entries(raw.resources).map(
1605
1746
  ([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
1606
1747
  );
1748
+ const resourcePaymentFlows = Object.fromEntries(
1749
+ Object.entries(raw.resources).flatMap(
1750
+ ([id, entry]) => entry.paymentFlow !== void 0 ? [[id, entry.paymentFlow]] : []
1751
+ )
1752
+ );
1753
+ const hasFlowOverrides = Object.keys(resourcePaymentFlows).length > 0;
1754
+ const x402WithFlows = x402 !== void 0 && hasFlowOverrides ? { ...x402, resourcePaymentFlows } : x402;
1755
+ const mppWithFlows = mpp !== void 0 && hasFlowOverrides ? { ...mpp, resourcePaymentFlows } : mpp;
1607
1756
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
1757
+ if (protocols.a2a.enabled && !resources.some((r) => r.exposedVia.includes("a2a"))) {
1758
+ throw new CommerceError(
1759
+ "CONFIG_INVALID",
1760
+ 'protocols.a2a.enabled is true but no resource lists "a2a" in its expose',
1761
+ { details: { path: "protocols.a2a.enabled" } }
1762
+ );
1763
+ }
1608
1764
  return {
1609
1765
  version: SUPPORTED_CONFIG_VERSION,
1610
1766
  merchant: {
@@ -1623,8 +1779,8 @@ function normalize(raw) {
1623
1779
  protocols,
1624
1780
  resources,
1625
1781
  payments: {
1626
- ...x402 !== void 0 ? { x402 } : {},
1627
- ...mpp !== void 0 ? { mpp } : {}
1782
+ ...x402WithFlows !== void 0 ? { x402: x402WithFlows } : {},
1783
+ ...mppWithFlows !== void 0 ? { mpp: mppWithFlows } : {}
1628
1784
  },
1629
1785
  ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
1630
1786
  };
@@ -1693,6 +1849,14 @@ function normalizeAp2(raw) {
1693
1849
  "authorization.ap2.clockSkewSeconds",
1694
1850
  { min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
1695
1851
  ),
1852
+ requireMandateAudience: toBoolean(
1853
+ raw.requireMandateAudience ?? false,
1854
+ "authorization.ap2.requireMandateAudience"
1855
+ ),
1856
+ requireMandateExpiry: toBoolean(
1857
+ raw.requireMandateExpiry ?? false,
1858
+ "authorization.ap2.requireMandateExpiry"
1859
+ ),
1696
1860
  replay: { path: raw.replay.path }
1697
1861
  };
1698
1862
  }
@@ -1868,7 +2032,8 @@ function normalizeAcp(raw) {
1868
2032
  raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
1869
2033
  "protocols.acp.idempotency.retentionHours",
1870
2034
  { min: ACP_RETENTION_HOURS }
1871
- )
2035
+ ),
2036
+ merchantIdempotent: raw.idempotency.merchantIdempotent ?? false
1872
2037
  },
1873
2038
  checkout: { operations: normalizeAcpOperations(raw.checkout) },
1874
2039
  // Built key by key so absent metadata stays absent rather than `undefined`
@@ -1951,10 +2116,11 @@ function validateAcpOperationInput(path2, operation, resource) {
1951
2116
  if (schema === void 0) return;
1952
2117
  const keys = ACP_OPERATION_INPUT_KEYS[operation];
1953
2118
  const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
1954
- const properties = schema["properties"];
1955
- if (schema["additionalProperties"] === false && isRecord(properties)) {
1956
- for (const key of [...keys, ...optionalKeys]) {
1957
- if (!Object.hasOwn(properties, key)) {
2119
+ const sent = [...keys, ...optionalKeys];
2120
+ const topLevel = closedObjectProperties(schema);
2121
+ if (topLevel !== void 0) {
2122
+ for (const key of sent) {
2123
+ if (!Object.hasOwn(topLevel, key)) {
1958
2124
  const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
1959
2125
  throw acpInvalid(
1960
2126
  path2,
@@ -1964,6 +2130,23 @@ function validateAcpOperationInput(path2, operation, resource) {
1964
2130
  }
1965
2131
  }
1966
2132
  }
2133
+ const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
2134
+ const pathKeys = closedObjectProperties(declared["path"]);
2135
+ if (sent.includes("path") && pathKeys !== void 0 && !Object.hasOwn(pathKeys, ACP_SESSION_ID_INPUT_KEY)) {
2136
+ throw acpInvalid(
2137
+ path2,
2138
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its "path" schema sets additionalProperties: false without declaring "${ACP_SESSION_ID_INPUT_KEY}", which the adapter always sends inside "path"`,
2139
+ { resourceId: resource.id }
2140
+ );
2141
+ }
2142
+ const bodyKeys = closedObjectProperties(declared["body"]);
2143
+ if (sent.includes("body") && bodyKeys !== void 0 && Object.keys(bodyKeys).length === 0) {
2144
+ throw acpInvalid(
2145
+ path2,
2146
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its "body" schema accepts no keys, so it refuses every ACP document. Config load closes an object schema that omits additionalProperties: declare body as { type: object, additionalProperties: true }`,
2147
+ { resourceId: resource.id }
2148
+ );
2149
+ }
1967
2150
  const required = schema["required"];
1968
2151
  if (Array.isArray(required)) {
1969
2152
  for (const entry of required) {
@@ -1977,6 +2160,10 @@ function validateAcpOperationInput(path2, operation, resource) {
1977
2160
  }
1978
2161
  }
1979
2162
  }
2163
+ function closedObjectProperties(schema) {
2164
+ if (!isRecord(schema) || schema["additionalProperties"] !== false) return void 0;
2165
+ return isRecord(schema["properties"]) ? schema["properties"] : {};
2166
+ }
1980
2167
  function normalizeFacilitator(raw) {
1981
2168
  return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
1982
2169
  }
@@ -2007,6 +2194,7 @@ function normalizeMpp(raw) {
2007
2194
  { min: 1 }
2008
2195
  )
2009
2196
  } : {},
2197
+ paymentFlow: raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
2010
2198
  facilitator: normalizeFacilitator(raw.facilitator),
2011
2199
  ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
2012
2200
  ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
@@ -2160,6 +2348,13 @@ function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
2160
2348
  validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
2161
2349
  }
2162
2350
  }
2351
+ if (entry.paymentFlow !== void 0 && (entry.pricing.type !== "fixed" || !paymentMethods.some((method) => method === "x402" || method === "mpp"))) {
2352
+ throw new CommerceError(
2353
+ "CONFIG_INVALID",
2354
+ `Resource "${id}" sets paymentFlow, which requires fixed pricing and "x402" or "mpp" in payments`,
2355
+ { details: { path: `resources.${id}.paymentFlow`, resourceId: id } }
2356
+ );
2357
+ }
2163
2358
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
2164
2359
  const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
2165
2360
  return {
@@ -2566,36 +2761,6 @@ function resolveConfigPath(explicitPath, env, cwd) {
2566
2761
  if (fromEnv) return path.resolve(cwd, fromEnv);
2567
2762
  return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
2568
2763
  }
2569
- var BEARER_PREFIX = /^Bearer +/i;
2570
- function createBearerCheck(expectedToken) {
2571
- const expected = sha256(expectedToken);
2572
- return (header2) => {
2573
- if (header2 === void 0 || !BEARER_PREFIX.test(header2)) return false;
2574
- const presented = header2.replace(BEARER_PREFIX, "");
2575
- return presented.length > 0 && timingSafeEqual(sha256(presented), expected);
2576
- };
2577
- }
2578
- function sha256(value) {
2579
- return createHash("sha256").update(value, "utf8").digest();
2580
- }
2581
- async function readCappedBody(req, maxBytes) {
2582
- const chunks = [];
2583
- let total = 0;
2584
- try {
2585
- for await (const chunk of req) {
2586
- const buffer = chunk;
2587
- total += buffer.length;
2588
- if (total > maxBytes) return { kind: "too-large" };
2589
- chunks.push(buffer);
2590
- }
2591
- } catch {
2592
- return { kind: "unreadable" };
2593
- }
2594
- return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
2595
- }
2596
- function joinUrl(baseUrl, mountPath) {
2597
- return `${baseUrl.replace(/\/+$/, "")}/${mountPath.replace(/^\/+|\/+$/g, "")}`;
2598
- }
2599
2764
 
2600
2765
  // src/gateway/access-control.ts
2601
2766
  var CORS_METHODS = "GET,POST,OPTIONS";
@@ -2604,9 +2769,29 @@ var CORS_HEADERS = [
2604
2769
  PAYMENT_HEADER,
2605
2770
  AUTHORIZATION_HEADER,
2606
2771
  "x-request-id",
2607
- "authorization"
2772
+ "authorization",
2773
+ "mcp-protocol-version",
2774
+ "mcp-method",
2775
+ "mcp-name",
2776
+ "a2a-version",
2777
+ "a2a-extensions",
2778
+ "api-version",
2779
+ "idempotency-key",
2780
+ "request-id",
2781
+ "signature",
2782
+ "timestamp",
2783
+ "access-control-expose-headers"
2784
+ ].join(",");
2785
+ var CORS_EXPOSED_HEADERS = [
2786
+ "payment-required",
2787
+ "payment-response",
2788
+ "www-authenticate",
2789
+ "payment-receipt",
2790
+ "idempotency-key",
2791
+ "request-id",
2792
+ "idempotent-replayed",
2793
+ "retry-after"
2608
2794
  ].join(",");
2609
- var CORS_EXPOSED_HEADERS = "payment-required,payment-response,www-authenticate,payment-receipt";
2610
2795
  function buildAccessControlHook(options) {
2611
2796
  const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
2612
2797
  const publicHostname = hostnameOf(options.publicBaseUrl);
@@ -3146,6 +3331,7 @@ function buildWellKnownDocument(options) {
3146
3331
  }
3147
3332
 
3148
3333
  // src/gateway/routes.ts
3334
+ var X402_V1_PAYMENT_HEADER = "x-payment";
3149
3335
  function registerRoutes(options) {
3150
3336
  const { server } = options;
3151
3337
  const readinessProbe = createReadinessProbe({
@@ -3172,12 +3358,20 @@ function registerRoutes(options) {
3172
3358
  adapters: await readinessProbe.adapterHealth()
3173
3359
  })
3174
3360
  );
3361
+ const backed = (method) => options.paymentProviders.some((provider) => provider.name === method);
3175
3362
  server.get("/api/resources", async () => ({
3176
- resources: options.resources.list().map(toPublicResource)
3363
+ resources: options.resources.list().map((resource) => {
3364
+ const listed = toPublicResource(resource);
3365
+ return { ...listed, paymentMethods: listed.paymentMethods.filter(backed) };
3366
+ })
3177
3367
  }));
3178
- server.post("/api/resources/:id/invoke", async (request, reply) => {
3179
- await handleInvoke(request, reply, options);
3180
- });
3368
+ server.post(
3369
+ "/api/resources/:id/invoke",
3370
+ { preParsing: async (request, _reply, payload) => payload.pipe(digestingStream(request)) },
3371
+ async (request, reply) => {
3372
+ await handleInvoke(request, reply, options);
3373
+ }
3374
+ );
3181
3375
  const tokenHook = buildOperatorTokenHook(options.config.server.adminToken);
3182
3376
  server.get(
3183
3377
  "/api/receipts",
@@ -3190,6 +3384,20 @@ function registerRoutes(options) {
3190
3384
  ledgerHandler("events", (list) => options.store.listEvents(list))
3191
3385
  );
3192
3386
  }
3387
+ var bodyDigests = /* @__PURE__ */ new WeakMap();
3388
+ function digestingStream(request) {
3389
+ const chunks = [];
3390
+ return new Transform({
3391
+ transform(chunk, _encoding, callback) {
3392
+ chunks.push(chunk);
3393
+ callback(null, chunk);
3394
+ },
3395
+ flush(callback) {
3396
+ if (chunks.length > 0) bodyDigests.set(request, contentDigest(Buffer.concat(chunks)));
3397
+ callback();
3398
+ }
3399
+ });
3400
+ }
3193
3401
  function ledgerHandler(key, list) {
3194
3402
  return async (request, reply) => {
3195
3403
  try {
@@ -3204,6 +3412,7 @@ function ledgerHandler(key, list) {
3204
3412
  }
3205
3413
  async function handleInvoke(request, reply, options) {
3206
3414
  const resourceId = request.params.id;
3415
+ let paymentMethod;
3207
3416
  try {
3208
3417
  const resource = options.resources.get(resourceId);
3209
3418
  if (!resource?.exposedVia.includes("http")) {
@@ -3212,13 +3421,14 @@ async function handleInvoke(request, reply, options) {
3212
3421
  resourceId
3213
3422
  });
3214
3423
  }
3215
- const paymentMethod = resource.paymentMethods[0];
3424
+ paymentMethod = resource.paymentMethods[0];
3216
3425
  const paymentValue = paymentProof(request, paymentMethod);
3217
3426
  const payment = paymentValue !== void 0 && paymentMethod !== void 0 ? { method: paymentMethod, payload: paymentValue } : void 0;
3218
3427
  const authorization = parseAuthorizationHeader(
3219
3428
  request.headers[AUTHORIZATION_HEADER],
3220
3429
  request.id
3221
3430
  );
3431
+ const digest = bodyDigests.get(request);
3222
3432
  const canonicalRequest = {
3223
3433
  requestId: request.id,
3224
3434
  resourceId,
@@ -3226,7 +3436,8 @@ async function handleInvoke(request, reply, options) {
3226
3436
  protocol: "http",
3227
3437
  receivedAt: options.clock.nowIso(),
3228
3438
  ...payment !== void 0 ? { payment } : {},
3229
- ...authorization !== void 0 ? { authorization } : {}
3439
+ ...authorization !== void 0 ? { authorization } : {},
3440
+ ...digest !== void 0 ? { metadata: { [CONTENT_DIGEST_METADATA_KEY]: digest } } : {}
3230
3441
  };
3231
3442
  const outcome = await options.pipeline.execute(canonicalRequest);
3232
3443
  if (outcome.kind === "payment-required") {
@@ -3235,36 +3446,88 @@ async function handleInvoke(request, reply, options) {
3235
3446
  const challenge = envelope.payment.envelope?.["wwwAuthenticate"];
3236
3447
  if (typeof challenge === "string") reply.header("www-authenticate", challenge);
3237
3448
  } else if (envelope.payment.envelope !== void 0) {
3238
- reply.header(PAYMENT_REQUIRED_HEADER, encodeHeaderDocument(envelope.payment.envelope));
3449
+ const v1Only = request.headers[X402_V1_PAYMENT_HEADER] !== void 0 && request.headers[PAYMENT_HEADER] === void 0;
3450
+ reply.header(
3451
+ PAYMENT_REQUIRED_HEADER,
3452
+ encodeHeaderDocument(
3453
+ v1Only ? { ...envelope.payment.envelope, error: "invalid_x402_version" } : envelope.payment.envelope
3454
+ )
3455
+ );
3239
3456
  }
3240
3457
  reply.header("cache-control", "no-store");
3458
+ if (envelope.payment.provider === "mpp") {
3459
+ const { status: _envelopeStatus, ...members } = envelope;
3460
+ reply.header("content-type", "application/problem+json").status(402).send({
3461
+ ...mppProblem(envelope.code, 402),
3462
+ status: 402,
3463
+ detail: envelope.message,
3464
+ ...members
3465
+ });
3466
+ return;
3467
+ }
3241
3468
  reply.status(402).send(envelope);
3242
3469
  return;
3243
3470
  }
3244
3471
  if (outcome.payment) {
3245
- reply.header(PAYMENT_RESPONSE_HEADER, encodePaymentSummary(outcome.payment));
3472
+ reply.header(
3473
+ PAYMENT_RESPONSE_HEADER,
3474
+ encodeHeaderDocument(settlementResponse(outcome.payment))
3475
+ );
3246
3476
  const receipt = outcome.payment.metadata?.["receipt"];
3247
3477
  if (outcome.payment.provider === "mpp" && typeof receipt === "string") {
3248
3478
  reply.header("payment-receipt", receipt);
3249
3479
  }
3480
+ reply.header("cache-control", "private");
3250
3481
  }
3251
3482
  reply.status(outcome.backendStatus).send(outcome.body);
3252
3483
  } catch (error) {
3253
- const commerceError = toCommerceError(error);
3254
- const settledPayment = errorPaymentSummary(commerceError);
3255
- if (settledPayment !== void 0) {
3256
- reply.header(PAYMENT_RESPONSE_HEADER, encodePaymentSummary(settledPayment));
3257
- if (settledPayment.provider === "mpp" && settledPayment.receipt !== void 0) {
3258
- reply.header("payment-receipt", settledPayment.receipt);
3259
- }
3260
- }
3261
- const challenge = commerceError.details?.["challenge"];
3262
- if (commerceError.code === "PAYMENT_INVALID" && typeof challenge?.["wwwAuthenticate"] === "string") {
3484
+ sendInvokeError(reply, toCommerceError(error), paymentMethod);
3485
+ }
3486
+ }
3487
+ var MPP_PROBLEM_CODES = /* @__PURE__ */ new Set([
3488
+ "PAYMENT_INVALID",
3489
+ "PAYMENT_REPLAYED",
3490
+ "PAYMENT_SETTLEMENT_FAILED",
3491
+ "PAYMENT_PROVIDER_UNAVAILABLE"
3492
+ ]);
3493
+ function sendInvokeError(reply, error, paymentMethod) {
3494
+ const details = error.details;
3495
+ const settled = settlementResponseFromDetails(details);
3496
+ if (settled !== void 0) {
3497
+ reply.header(PAYMENT_RESPONSE_HEADER, encodeHeaderDocument(settled));
3498
+ }
3499
+ if (error.code === "PAYMENT_SETTLEMENT_FAILED") {
3500
+ reply.header(PAYMENT_RESPONSE_HEADER, encodeHeaderDocument(settlementFailure(details)));
3501
+ }
3502
+ const challenge = details?.["challenge"];
3503
+ if (error.httpStatus === 402 && isRecord(challenge)) {
3504
+ if (paymentMethod === "mpp" && typeof challenge["wwwAuthenticate"] === "string") {
3263
3505
  reply.header("www-authenticate", challenge["wwwAuthenticate"]);
3264
- reply.header("cache-control", "no-store");
3506
+ } else if (paymentMethod === "x402" && error.code !== "PAYMENT_SETTLEMENT_FAILED") {
3507
+ reply.header(
3508
+ PAYMENT_REQUIRED_HEADER,
3509
+ encodeHeaderDocument({
3510
+ ...challenge,
3511
+ error: x402ErrorCode(error.code, details?.["reason"])
3512
+ })
3513
+ );
3265
3514
  }
3266
- reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
3515
+ reply.header("cache-control", "no-store");
3516
+ }
3517
+ const envelope = toErrorEnvelope(error);
3518
+ if (paymentMethod === "mpp" && MPP_PROBLEM_CODES.has(error.code)) {
3519
+ const { status: _envelopeStatus, ...members } = envelope;
3520
+ const reason = typeof details?.["reason"] === "string" ? details["reason"] : void 0;
3521
+ reply.header("content-type", "application/problem+json").status(error.httpStatus).send({
3522
+ ...mppProblem(error.code, error.httpStatus, reason),
3523
+ status: error.httpStatus,
3524
+ // The reason token stays in `message` and `details.reason`
3525
+ detail: mppProblemDetail(reason, error.message),
3526
+ ...members
3527
+ });
3528
+ return;
3267
3529
  }
3530
+ reply.status(error.httpStatus).send(envelope);
3268
3531
  }
3269
3532
  function paymentProof(request, method) {
3270
3533
  if (method === "mpp") {
@@ -3274,38 +3537,9 @@ function paymentProof(request, method) {
3274
3537
  const value = request.headers[PAYMENT_HEADER];
3275
3538
  return Array.isArray(value) ? value[0] : value;
3276
3539
  }
3277
- function errorPaymentSummary(error) {
3278
- const payment = error.details?.["payment"];
3279
- if (!isRecord(payment)) return void 0;
3280
- const { status, provider, amount, currency, network, externalReference, receipt } = payment;
3281
- if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
3282
- return void 0;
3283
- }
3284
- return {
3285
- status,
3286
- provider,
3287
- amount,
3288
- currency,
3289
- ...typeof network === "string" ? { network } : {},
3290
- ...typeof externalReference === "string" ? { externalReference } : {},
3291
- ...typeof receipt === "string" ? { receipt } : {}
3292
- };
3293
- }
3294
3540
  function encodeHeaderDocument(document) {
3295
3541
  return Buffer.from(JSON.stringify(document), "utf8").toString("base64");
3296
3542
  }
3297
- function encodePaymentSummary(payment) {
3298
- return encodeHeaderDocument({
3299
- success: payment.status === "settled",
3300
- transaction: payment.externalReference ?? "",
3301
- network: payment.network ?? "",
3302
- status: payment.status,
3303
- provider: payment.provider,
3304
- amount: payment.amount,
3305
- currency: payment.currency,
3306
- ...payment.externalReference !== void 0 ? { externalReference: payment.externalReference } : {}
3307
- });
3308
- }
3309
3543
  function parseLimit(value) {
3310
3544
  if (Array.isArray(value)) return parseLimit(value[0]);
3311
3545
  let n;
@@ -3437,7 +3671,7 @@ async function createGateway(options) {
3437
3671
  }
3438
3672
 
3439
3673
  // src/protocols/a2a/constants.ts
3440
- var A2A_SPEC_VERSION = "1.0.0";
3674
+ var A2A_SPEC_VERSION = "1.0.1";
3441
3675
  var A2A_PROTOCOL_VERSION = "1.0";
3442
3676
  var A2A_PROTOCOL_BINDING = "JSONRPC";
3443
3677
  var A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
@@ -3446,93 +3680,26 @@ var A2A_JSON_MEDIA_TYPE = "application/json";
3446
3680
  var A2A_DEFAULT_AGENT_NAME = "agent-commerce";
3447
3681
  var A2A_VERSION_HEADER = "a2a-version";
3448
3682
  var A2A_METHOD_SEND_MESSAGE = "SendMessage";
3683
+ var A2A_PUSH_CONFIG_METHODS = [
3684
+ "CreateTaskPushNotificationConfig",
3685
+ "GetTaskPushNotificationConfig",
3686
+ "ListTaskPushNotificationConfigs",
3687
+ "DeleteTaskPushNotificationConfig"
3688
+ ];
3449
3689
  var A2A_UNSUPPORTED_METHODS = [
3450
3690
  "SendStreamingMessage",
3451
3691
  "GetTask",
3452
3692
  "ListTasks",
3453
3693
  "CancelTask",
3454
3694
  "SubscribeToTask",
3455
- "CreateTaskPushNotificationConfig",
3456
- "GetTaskPushNotificationConfig",
3457
- "ListTaskPushNotificationConfigs",
3458
- "DeleteTaskPushNotificationConfig",
3695
+ ...A2A_PUSH_CONFIG_METHODS,
3459
3696
  "GetExtendedAgentCard"
3460
3697
  ];
3698
+ var A2A_TASK_LOOKUP_METHODS = ["GetTask", "CancelTask"];
3461
3699
  var A2A_TASK_STATE_COMPLETED = "TASK_STATE_COMPLETED";
3462
3700
  var A2A_TASK_STATE_FAILED = "TASK_STATE_FAILED";
3463
-
3464
- // src/protocols/a2a/agent-card.ts
3465
- function skillTags(resource) {
3466
- return ["agent-commerce", resource.pricing.type === "free" ? "free" : "paid"];
3467
- }
3468
- function skillDescription(resource) {
3469
- const base = resource.description ?? resource.name;
3470
- if (resource.pricing.type === "fixed") {
3471
- return `${base} Costs ${resource.pricing.amount} ${resource.pricing.currency} per call.`;
3472
- }
3473
- if (resource.pricing.type === "dynamic") {
3474
- return `${base} Requires payment (amount determined at request time).`;
3475
- }
3476
- return base;
3477
- }
3478
- function buildAgentSkill(resource) {
3479
- return {
3480
- id: resource.id,
3481
- name: resource.name,
3482
- description: skillDescription(resource),
3483
- tags: skillTags(resource),
3484
- inputModes: [A2A_JSON_MEDIA_TYPE],
3485
- outputModes: [A2A_JSON_MEDIA_TYPE]
3486
- };
3487
- }
3488
- function buildAgentCard(options) {
3489
- return {
3490
- name: options.name,
3491
- description: options.description,
3492
- version: options.version,
3493
- supportedInterfaces: [
3494
- {
3495
- url: joinUrl(options.publicBaseUrl, options.mountPath),
3496
- protocolBinding: A2A_PROTOCOL_BINDING,
3497
- protocolVersion: A2A_PROTOCOL_VERSION
3498
- }
3499
- ],
3500
- capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
3501
- defaultInputModes: [A2A_JSON_MEDIA_TYPE],
3502
- defaultOutputModes: [A2A_JSON_MEDIA_TYPE],
3503
- skills: options.resources.map(buildAgentSkill)
3504
- };
3505
- }
3506
-
3507
- // src/protocols/a2a/descriptor.ts
3508
- var A2A_CAPABILITIES = ["agent-card", "jsonrpc", "SendMessage"];
3509
- var A2A_UNSUPPORTED = [
3510
- // Methods, as the protocol names them; the transport refuses the same list
3511
- ...A2A_UNSUPPORTED_METHODS,
3512
- // Transports other than the one binding served
3513
- "HTTP+JSON/REST binding",
3514
- "gRPC binding",
3515
- // Behaviors
3516
- "SSE",
3517
- "long-running task persistence",
3518
- "task resumption",
3519
- "push notifications",
3520
- "multi-turn conversational continuation",
3521
- "authenticated extended agent cards",
3522
- "A2A authentication schemes",
3523
- "artifact types beyond Agent Commerce outcome data"
3524
- ];
3525
- function buildDescriptor(implementationVersion) {
3526
- return {
3527
- name: "a2a",
3528
- kind: "protocol",
3529
- implementationVersion,
3530
- supportedSpec: A2A_SPEC_VERSION,
3531
- capabilities: A2A_CAPABILITIES,
3532
- status: "experimental",
3533
- unsupported: A2A_UNSUPPORTED
3534
- };
3535
- }
3701
+ var A2A_TASK_STATE_INPUT_REQUIRED = "TASK_STATE_INPUT_REQUIRED";
3702
+ var A2A_EXTENSIONS_HEADER = "a2a-extensions";
3536
3703
 
3537
3704
  // src/protocols/a2a/jsonrpc.ts
3538
3705
  var JSONRPC_PARSE_ERROR = -32700;
@@ -3540,7 +3707,18 @@ var JSONRPC_INVALID_REQUEST = -32600;
3540
3707
  var JSONRPC_METHOD_NOT_FOUND = -32601;
3541
3708
  var JSONRPC_INVALID_PARAMS = -32602;
3542
3709
  var JSONRPC_INTERNAL_ERROR = -32603;
3710
+ var A2A_ERROR_TASK_NOT_FOUND = -32001;
3711
+ var A2A_ERROR_PUSH_NOTIFICATION_NOT_SUPPORTED = -32003;
3543
3712
  var A2A_ERROR_UNSUPPORTED_OPERATION = -32004;
3713
+ var A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED = -32005;
3714
+ var A2A_ERROR_VERSION_NOT_SUPPORTED = -32009;
3715
+ var A2A_ERROR_REASONS = /* @__PURE__ */ new Map([
3716
+ [A2A_ERROR_TASK_NOT_FOUND, "TASK_NOT_FOUND"],
3717
+ [A2A_ERROR_PUSH_NOTIFICATION_NOT_SUPPORTED, "PUSH_NOTIFICATION_NOT_SUPPORTED"],
3718
+ [A2A_ERROR_UNSUPPORTED_OPERATION, "UNSUPPORTED_OPERATION"],
3719
+ [A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED, "CONTENT_TYPE_NOT_SUPPORTED"],
3720
+ [A2A_ERROR_VERSION_NOT_SUPPORTED, "VERSION_NOT_SUPPORTED"]
3721
+ ]);
3544
3722
  function readId(value) {
3545
3723
  if (typeof value === "string" || typeof value === "number") return value;
3546
3724
  return null;
@@ -3549,7 +3727,21 @@ function jsonRpcResult(id, result) {
3549
3727
  return { jsonrpc: "2.0", id, result };
3550
3728
  }
3551
3729
  function jsonRpcError(id, code, message) {
3552
- return { jsonrpc: "2.0", id, error: { code, message } };
3730
+ const reason = A2A_ERROR_REASONS.get(code);
3731
+ const error = {
3732
+ code,
3733
+ message,
3734
+ ...reason !== void 0 ? {
3735
+ data: [
3736
+ {
3737
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
3738
+ reason,
3739
+ domain: "a2a-protocol.org"
3740
+ }
3741
+ ]
3742
+ } : {}
3743
+ };
3744
+ return { jsonrpc: "2.0", id, error };
3553
3745
  }
3554
3746
  function parseJsonRpcRequest(rawBody) {
3555
3747
  let payload;
@@ -3576,6 +3768,16 @@ function parseJsonRpcRequest(rawBody) {
3576
3768
  };
3577
3769
  }
3578
3770
  const id = readId(payload["id"]);
3771
+ if (payload["id"] === void 0) {
3772
+ return {
3773
+ ok: false,
3774
+ id,
3775
+ error: {
3776
+ code: JSONRPC_INVALID_REQUEST,
3777
+ message: 'Request must include an "id"; notifications are unsupported.'
3778
+ }
3779
+ };
3780
+ }
3579
3781
  if (payload["jsonrpc"] !== "2.0") {
3580
3782
  return {
3581
3783
  ok: false,
@@ -3604,7 +3806,13 @@ function parseJsonRpcRequest(rawBody) {
3604
3806
  }
3605
3807
  return { ok: true, request: { id, method, params: params ?? {} } };
3606
3808
  }
3809
+
3810
+ // src/protocols/a2a/message-mapping.ts
3607
3811
  var A2A_USER_ROLE = "ROLE_USER";
3812
+ var A2A_CALL_SHAPE = 'one data part {"resource": "<skill id>", "input": {...}}';
3813
+ function userRoleProblem(role) {
3814
+ return role === A2A_USER_ROLE ? void 0 : `Unsupported message role "${String(role)}": only ${A2A_USER_ROLE} is accepted.`;
3815
+ }
3608
3816
  var UnknownRecord = z.record(z.string(), z.unknown());
3609
3817
  var MessageSchema = z.object({
3610
3818
  role: z.string(),
@@ -3622,13 +3830,20 @@ var ParamsSchema = z.object({
3622
3830
  function invalid(message) {
3623
3831
  return new CommerceError("INPUT_INVALID", message);
3624
3832
  }
3625
- function unsupported(message) {
3626
- return new CommerceError("PROTOCOL_UNSUPPORTED", message);
3833
+ function unsupported(message, a2aErrorCode) {
3834
+ return new CommerceError(
3835
+ "PROTOCOL_UNSUPPORTED",
3836
+ message,
3837
+ a2aErrorCode !== void 0 ? { details: { a2aErrorCode } } : {}
3838
+ );
3627
3839
  }
3628
3840
  function assertNoContinuation(params) {
3629
3841
  const message = params.message;
3630
3842
  if (params.taskId !== void 0 || message.taskId !== void 0) {
3631
- throw unsupported("Task continuation is not supported: send a request with no taskId.");
3843
+ throw unsupported(
3844
+ "Task not found: tasks are not persisted, so send a request with no taskId.",
3845
+ A2A_ERROR_TASK_NOT_FOUND
3846
+ );
3632
3847
  }
3633
3848
  if (params.contextId !== void 0 || message.contextId !== void 0) {
3634
3849
  throw unsupported(
@@ -3641,10 +3856,16 @@ function assertNoContinuation(params) {
3641
3856
  }
3642
3857
  function assertSupportedPart(part) {
3643
3858
  if ("file" in part || "raw" in part || "url" in part) {
3644
- throw unsupported("File and URL parts are not supported: send a structured data part.");
3859
+ throw unsupported(
3860
+ `File and URL parts are not supported: send ${A2A_CALL_SHAPE}.`,
3861
+ A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED
3862
+ );
3645
3863
  }
3646
3864
  if ("text" in part) {
3647
- throw unsupported("Text parts are not supported: send a structured data part.");
3865
+ throw unsupported(
3866
+ `Text parts are not supported: send ${A2A_CALL_SHAPE}.`,
3867
+ A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED
3868
+ );
3648
3869
  }
3649
3870
  if (!("data" in part)) {
3650
3871
  throw invalid('Message part carries no "data": send a structured data part.');
@@ -3652,7 +3873,8 @@ function assertSupportedPart(part) {
3652
3873
  const mediaType = part["mediaType"];
3653
3874
  if (mediaType !== void 0 && mediaType !== A2A_JSON_MEDIA_TYPE) {
3654
3875
  throw unsupported(
3655
- `Media type "${String(mediaType)}" is not supported: parts must be ${A2A_JSON_MEDIA_TYPE}.`
3876
+ `Media type "${String(mediaType)}" is not supported: parts must be ${A2A_JSON_MEDIA_TYPE}.`,
3877
+ A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED
3656
3878
  );
3657
3879
  }
3658
3880
  }
@@ -3665,10 +3887,13 @@ function parseInvocation(rawParams) {
3665
3887
  }
3666
3888
  const params = parsed.data;
3667
3889
  const message = params.message;
3668
- assertNoContinuation(params);
3669
- if (message.role !== A2A_USER_ROLE) {
3670
- throw invalid(`Unsupported message role "${message.role}": only ${A2A_USER_ROLE} is accepted.`);
3890
+ if (message.messageId === void 0 || message.messageId.length === 0) {
3891
+ throw invalid('Message must carry a non-empty "messageId".');
3671
3892
  }
3893
+ const messageId = message.messageId;
3894
+ assertNoContinuation(params);
3895
+ const roleProblem = userRoleProblem(message.role);
3896
+ if (roleProblem !== void 0) throw invalid(roleProblem);
3672
3897
  if (message.parts.length === 0) {
3673
3898
  throw invalid("Message carries no parts: send exactly one structured data part.");
3674
3899
  }
@@ -3677,28 +3902,287 @@ function parseInvocation(rawParams) {
3677
3902
  `Multi-part messages are not supported: send exactly one structured data part (received ${message.parts.length}).`
3678
3903
  );
3679
3904
  }
3680
- const part = message.parts[0];
3681
- if (part === void 0) throw invalid("Message carries no parts.");
3682
- assertSupportedPart(part);
3683
- const data = part["data"];
3684
- if (!isRecord(data)) {
3685
- throw invalid('Message part "data" must be a JSON object.');
3905
+ const part = message.parts[0];
3906
+ if (part === void 0) throw invalid("Message carries no parts.");
3907
+ assertSupportedPart(part);
3908
+ const data = part["data"];
3909
+ if (!isRecord(data)) {
3910
+ throw invalid('Message part "data" must be a JSON object.');
3911
+ }
3912
+ const resourceId = data["resource"];
3913
+ if (typeof resourceId !== "string") {
3914
+ throw invalid('Message part data must carry a "resource" string naming a canonical resource.');
3915
+ }
3916
+ if (resourceId.length === 0) {
3917
+ throw invalid('Message part data "resource" must not be empty.');
3918
+ }
3919
+ const rawInput = data["input"];
3920
+ if (rawInput !== void 0 && !isRecord(rawInput)) {
3921
+ throw invalid('Message part data "input" must be a JSON object.');
3922
+ }
3923
+ return { resourceId, input: rawInput ?? {}, messageId };
3924
+ }
3925
+
3926
+ // src/protocols/a2a/x402-extension.ts
3927
+ var A2A_X402_EXTENSION_URI = "https://github.com/google-a2a/a2a-x402/v0.1";
3928
+ var A2A_X402_PROTOCOL_VERSION = 2;
3929
+ var X402_PAYMENT_STATUS_KEY = "x402.payment.status";
3930
+ var X402_PAYMENT_REQUIRED_KEY = "x402.payment.required";
3931
+ var X402_PAYMENT_PAYLOAD_KEY = "x402.payment.payload";
3932
+ var X402_PAYMENT_RECEIPTS_KEY = "x402.payment.receipts";
3933
+ var X402_PAYMENT_ERROR_KEY = "x402.payment.error";
3934
+ var DEFAULT_PENDING_TTL_MS = 10 * 60 * 1e3;
3935
+ var MAX_PENDING_TTL_MS = 60 * 60 * 1e3;
3936
+ var MAX_PENDING_PAYMENTS = 256;
3937
+ function requestsX402Extension(header2) {
3938
+ const values = typeof header2 === "string" ? [header2] : header2 ?? [];
3939
+ return values.some(
3940
+ (value) => value.split(",").some((uri) => uri.trim() === A2A_X402_EXTENSION_URI)
3941
+ );
3942
+ }
3943
+ function createPendingPayments(clock, maxEntries = MAX_PENDING_PAYMENTS) {
3944
+ const tasks = /* @__PURE__ */ new Map();
3945
+ const now = () => clock.now().getTime();
3946
+ function purgeExpired() {
3947
+ const at = now();
3948
+ for (const [taskId, pending] of tasks) {
3949
+ if (pending.expiresAtMs <= at) tasks.delete(taskId);
3950
+ }
3951
+ }
3952
+ return {
3953
+ /** Holds a purchase until `expiresAt`, evicting the oldest one when full */
3954
+ put(taskId, pending, expiresAt) {
3955
+ purgeExpired();
3956
+ if (tasks.size >= maxEntries) {
3957
+ const [oldest] = tasks.keys();
3958
+ if (oldest !== void 0) tasks.delete(oldest);
3959
+ }
3960
+ const requested = expiresAt !== void 0 ? Date.parse(expiresAt) : Number.NaN;
3961
+ const ttl = Number.isFinite(requested) ? requested - now() : DEFAULT_PENDING_TTL_MS;
3962
+ const expiresAtMs = now() + Math.min(Math.max(ttl, 0), MAX_PENDING_TTL_MS);
3963
+ tasks.set(taskId, { ...pending, expiresAtMs });
3964
+ },
3965
+ get(taskId) {
3966
+ const pending = tasks.get(taskId);
3967
+ if (pending === void 0) return void 0;
3968
+ if (pending.expiresAtMs <= now()) {
3969
+ tasks.delete(taskId);
3970
+ return void 0;
3971
+ }
3972
+ return pending;
3973
+ },
3974
+ delete(taskId) {
3975
+ tasks.delete(taskId);
3976
+ },
3977
+ size() {
3978
+ return tasks.size;
3979
+ },
3980
+ clear() {
3981
+ tasks.clear();
3982
+ }
3983
+ };
3984
+ }
3985
+ function readPaymentSubmission(message) {
3986
+ const metadata = isRecord(message) ? message["metadata"] : void 0;
3987
+ if (!isRecord(metadata)) return void 0;
3988
+ const status = metadata[X402_PAYMENT_STATUS_KEY];
3989
+ if (status === "payment-rejected") return { kind: "rejected" };
3990
+ const payload = metadata[X402_PAYMENT_PAYLOAD_KEY];
3991
+ if (status === "payment-submitted" && isRecord(payload)) return { kind: "submitted", payload };
3992
+ return void 0;
3993
+ }
3994
+ function readFollowUpAuthorization(message) {
3995
+ const parts = isRecord(message) ? message["parts"] : void 0;
3996
+ if (!Array.isArray(parts)) return void 0;
3997
+ for (const part of parts) {
3998
+ const data = isRecord(part) ? part["data"] : void 0;
3999
+ const input = isRecord(data) ? data["input"] : void 0;
4000
+ if (isRecord(input) && input[AUTHORIZATION_INPUT_FIELD] !== void 0) {
4001
+ return parseAuthorizationSubmission(input[AUTHORIZATION_INPUT_FIELD]);
4002
+ }
4003
+ }
4004
+ return void 0;
4005
+ }
4006
+ function paymentRequiredMetadata(envelope) {
4007
+ return {
4008
+ [X402_PAYMENT_STATUS_KEY]: "payment-required",
4009
+ [X402_PAYMENT_REQUIRED_KEY]: envelope
4010
+ };
4011
+ }
4012
+ function paymentCompletedMetadata(payment) {
4013
+ return {
4014
+ [X402_PAYMENT_STATUS_KEY]: "payment-completed",
4015
+ [X402_PAYMENT_RECEIPTS_KEY]: [settlementResponse(payment)]
4016
+ };
4017
+ }
4018
+ function paymentFailureMetadata(error, network) {
4019
+ const settled = settlementResponseFromDetails(error.details);
4020
+ if (settled !== void 0 && settled["success"] === true) {
4021
+ return {
4022
+ [X402_PAYMENT_STATUS_KEY]: "payment-completed",
4023
+ [X402_PAYMENT_RECEIPTS_KEY]: [settled]
4024
+ };
4025
+ }
4026
+ const code = error.code.startsWith("PAYMENT_") ? x402ErrorCode(error.code, error.details?.["reason"]) : error.code.toLowerCase();
4027
+ const failure = settlementFailure(error.details);
4028
+ const receipt = failure["network"] === "" && network !== void 0 ? { ...failure, network } : failure;
4029
+ return {
4030
+ [X402_PAYMENT_STATUS_KEY]: "payment-failed",
4031
+ [X402_PAYMENT_ERROR_KEY]: code,
4032
+ // Keep the failure reason, including `settlement_pending` when a hash is known
4033
+ [X402_PAYMENT_RECEIPTS_KEY]: [
4034
+ error.details?.["settlementUncertain"] === true ? receipt : { ...receipt, errorReason: code }
4035
+ ]
4036
+ };
4037
+ }
4038
+ function paymentRejectedMetadata() {
4039
+ return { [X402_PAYMENT_STATUS_KEY]: "payment-rejected" };
4040
+ }
4041
+
4042
+ // src/protocols/a2a/agent-card.ts
4043
+ var RESOURCES_PATH = "/api/resources";
4044
+ function skillTags(resource) {
4045
+ return ["agent-commerce", resource.pricing.type === "free" ? "free" : "paid"];
4046
+ }
4047
+ function skillDescription(resource) {
4048
+ const base = resource.description ?? resource.name;
4049
+ if (resource.pricing.type === "fixed") {
4050
+ return `${base} Costs ${resource.pricing.amount} ${resource.pricing.currency} per call.`;
3686
4051
  }
3687
- const resourceId = data["resource"];
3688
- if (typeof resourceId !== "string") {
3689
- throw invalid('Message part data must carry a "resource" string naming a canonical resource.');
4052
+ if (resource.pricing.type === "dynamic") {
4053
+ return `${base} Requires payment (amount determined at request time).`;
3690
4054
  }
3691
- if (resourceId.length === 0) {
3692
- throw invalid('Message part data "resource" must not be empty.');
4055
+ return base;
4056
+ }
4057
+ function exampleValue(schema, name) {
4058
+ if (!isRecord(schema)) return `<${name}>`;
4059
+ const examples = schema["examples"];
4060
+ if (Array.isArray(examples) && examples.length > 0) return examples[0];
4061
+ if (schema["default"] !== void 0) return schema["default"];
4062
+ const allowed = schema["enum"];
4063
+ if (Array.isArray(allowed) && allowed.length > 0) return allowed[0];
4064
+ const declared = schema["type"];
4065
+ const type = Array.isArray(declared) ? declared[0] : declared;
4066
+ switch (type) {
4067
+ case "object":
4068
+ return exampleObject(schema);
4069
+ case "array":
4070
+ return schema["items"] !== void 0 ? [exampleValue(schema["items"], name)] : [];
4071
+ case "number":
4072
+ case "integer":
4073
+ return 0;
4074
+ case "boolean":
4075
+ return false;
4076
+ case "null":
4077
+ return null;
4078
+ default:
4079
+ return `<${name}>`;
3693
4080
  }
3694
- const rawInput = data["input"];
3695
- if (rawInput !== void 0 && !isRecord(rawInput)) {
3696
- throw invalid('Message part data "input" must be a JSON object.');
4081
+ }
4082
+ function exampleObject(schema) {
4083
+ const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
4084
+ const required = Array.isArray(schema["required"]) ? schema["required"] : [];
4085
+ const example = {};
4086
+ for (const key of required) {
4087
+ if (typeof key !== "string" || key === PAYMENT_INPUT_FIELD || key === AUTHORIZATION_INPUT_FIELD)
4088
+ continue;
4089
+ example[key] = exampleValue(properties[key], key);
3697
4090
  }
4091
+ return example;
4092
+ }
4093
+ function skillExample(resource) {
4094
+ return JSON.stringify({
4095
+ resource: resource.id,
4096
+ input: exampleObject(resource.inputSchema ?? {})
4097
+ });
4098
+ }
4099
+ function buildAgentSkill(resource) {
3698
4100
  return {
3699
- resourceId,
3700
- input: rawInput ?? {},
3701
- ...message.messageId !== void 0 ? { messageId: message.messageId } : {}
4101
+ id: resource.id,
4102
+ name: resource.name,
4103
+ description: skillDescription(resource),
4104
+ tags: skillTags(resource),
4105
+ examples: [skillExample(resource)],
4106
+ inputModes: [A2A_JSON_MEDIA_TYPE],
4107
+ outputModes: [A2A_JSON_MEDIA_TYPE]
4108
+ };
4109
+ }
4110
+ function agentDescription(options) {
4111
+ const call = `Call a skill by sending ${A2A_CALL_SHAPE} with media type ${A2A_JSON_MEDIA_TYPE}. Each skill has an example; input schemas are at ${joinUrl(options.publicBaseUrl, RESOURCES_PATH)}.`;
4112
+ const pay = options.resources.some((resource) => resource.pricing.type !== "free") ? ` For paid skills, retry with a payment proof in "input.${PAYMENT_INPUT_FIELD}".` : "";
4113
+ return `${options.description} ${call}${pay}`;
4114
+ }
4115
+ function buildAgentCard(options) {
4116
+ return {
4117
+ name: options.name,
4118
+ description: agentDescription(options),
4119
+ version: options.version,
4120
+ supportedInterfaces: [
4121
+ {
4122
+ url: joinUrl(options.publicBaseUrl, options.mountPath),
4123
+ protocolBinding: A2A_PROTOCOL_BINDING,
4124
+ protocolVersion: A2A_PROTOCOL_VERSION
4125
+ }
4126
+ ],
4127
+ capabilities: {
4128
+ streaming: false,
4129
+ pushNotifications: false,
4130
+ extendedAgentCard: false,
4131
+ // Only a skill whose rail is x402 can be paid through the extension.
4132
+ // Not required: `_payment` keeps working for clients without it.
4133
+ ...options.resources.some((resource) => resource.paymentMethods[0] === "x402") ? {
4134
+ extensions: [
4135
+ {
4136
+ uri: A2A_X402_EXTENSION_URI,
4137
+ description: "x402 payments: input-required tasks carrying x402 v2 documents.",
4138
+ required: false,
4139
+ // Lets a client holding an x402 v1 library tell before it pays
4140
+ params: { x402Version: A2A_X402_PROTOCOL_VERSION }
4141
+ }
4142
+ ]
4143
+ } : {}
4144
+ },
4145
+ defaultInputModes: [A2A_JSON_MEDIA_TYPE],
4146
+ defaultOutputModes: [A2A_JSON_MEDIA_TYPE],
4147
+ skills: options.resources.map(buildAgentSkill)
4148
+ };
4149
+ }
4150
+
4151
+ // src/protocols/a2a/descriptor.ts
4152
+ var A2A_CAPABILITIES = [
4153
+ "agent-card",
4154
+ "jsonrpc",
4155
+ "SendMessage",
4156
+ // The x402 A2A transport, opt-in per request
4157
+ "x402-extension"
4158
+ ];
4159
+ var A2A_UNSUPPORTED = [
4160
+ // Methods, as the protocol names them; the transport refuses the same list
4161
+ ...A2A_UNSUPPORTED_METHODS,
4162
+ // Transports other than the one binding served
4163
+ "HTTP+JSON/REST binding",
4164
+ "gRPC binding",
4165
+ // Behaviors
4166
+ "SSE",
4167
+ "long-running task persistence",
4168
+ "task resumption other than an x402 extension payment",
4169
+ "push notifications",
4170
+ "multi-turn conversational continuation",
4171
+ "authenticated extended agent cards",
4172
+ "A2A authentication schemes",
4173
+ "artifact types beyond Agent Commerce outcome data",
4174
+ // Pending x402 payments live in process memory
4175
+ "x402 extension payments across gateway restarts or instances"
4176
+ ];
4177
+ function buildDescriptor(implementationVersion) {
4178
+ return {
4179
+ name: "a2a",
4180
+ kind: "protocol",
4181
+ implementationVersion,
4182
+ supportedSpec: A2A_SPEC_VERSION,
4183
+ capabilities: A2A_CAPABILITIES,
4184
+ status: "experimental",
4185
+ unsupported: A2A_UNSUPPORTED
3702
4186
  };
3703
4187
  }
3704
4188
 
@@ -3724,19 +4208,56 @@ function completedTask(outcome, identity) {
3724
4208
  });
3725
4209
  }
3726
4210
  function paymentRequiredTask(outcome, identity) {
3727
- return task(identity, A2A_TASK_STATE_FAILED, {
3728
- name: outcome.resourceId,
3729
- parts: [{ data: { ...toPaymentRequiredEnvelope(outcome) }, mediaType: A2A_JSON_MEDIA_TYPE }]
3730
- });
4211
+ const envelope = toPaymentRequiredEnvelope(outcome);
4212
+ return withStatusMessage(
4213
+ task(identity, A2A_TASK_STATE_FAILED, {
4214
+ name: outcome.resourceId,
4215
+ parts: [{ data: { ...envelope }, mediaType: A2A_JSON_MEDIA_TYPE }]
4216
+ }),
4217
+ envelope.message,
4218
+ identity.statusMessageId
4219
+ );
4220
+ }
4221
+ function inputRequired(base, metadata, messageId) {
4222
+ return withStatusMessage(
4223
+ { ...base, status: { ...base.status, state: A2A_TASK_STATE_INPUT_REQUIRED } },
4224
+ "Payment is required. Send the x402 payment payload on this task.",
4225
+ messageId,
4226
+ metadata
4227
+ );
4228
+ }
4229
+ function withStatusMessage(base, text, messageId, metadata) {
4230
+ return {
4231
+ ...base,
4232
+ status: {
4233
+ ...base.status,
4234
+ message: {
4235
+ role: "ROLE_AGENT",
4236
+ messageId,
4237
+ contextId: base.contextId,
4238
+ taskId: base.id,
4239
+ parts: [{ text }],
4240
+ ...metadata !== void 0 ? { metadata, extensions: [A2A_X402_EXTENSION_URI] } : {}
4241
+ }
4242
+ }
4243
+ };
3731
4244
  }
3732
4245
  function failedTask(error, identity) {
3733
- return task(identity, A2A_TASK_STATE_FAILED, {
3734
- parts: [{ data: { ...toErrorEnvelope(error) }, mediaType: A2A_JSON_MEDIA_TYPE }]
3735
- });
4246
+ const envelope = toErrorEnvelope(error);
4247
+ return withStatusMessage(
4248
+ task(identity, A2A_TASK_STATE_FAILED, {
4249
+ parts: [{ data: { ...envelope }, mediaType: A2A_JSON_MEDIA_TYPE }]
4250
+ }),
4251
+ envelope.message,
4252
+ identity.statusMessageId
4253
+ );
3736
4254
  }
3737
4255
 
3738
4256
  // src/protocols/a2a/adapter.ts
3739
4257
  var MAX_REQUEST_BODY_BYTES = 256 * 1024;
4258
+ var VERSION_PATTERN = /^(\d+\.\d+)(?:\.\d+)?$/;
4259
+ var A2A_VERSION_PARAMETER = "A2A-Version";
4260
+ var AGENT_CARD_CACHE_CONTROL = "public, max-age=300";
3740
4261
  var DEFAULT_AGENT_DESCRIPTION = "Agent Commerce Gateway: canonical commerce resources exposed as A2A skills.";
3741
4262
  var A2aProtocolAdapter = class {
3742
4263
  name = "a2a";
@@ -3754,6 +4275,10 @@ var A2aProtocolAdapter = class {
3754
4275
  // Built once at start: resources are fixed at config load, and a per-request
3755
4276
  // build would be work an unauthenticated GET could trigger at will
3756
4277
  card;
4278
+ cardBody = "";
4279
+ cardEtag = "";
4280
+ // Purchases waiting for an x402 extension payment
4281
+ pendingPayments;
3757
4282
  constructor(options = {}) {
3758
4283
  this.mountPath = options.mountPath ?? A2A_DEFAULT_MOUNT_PATH;
3759
4284
  this.agentName = options.agentName ?? A2A_DEFAULT_AGENT_NAME;
@@ -3790,6 +4315,9 @@ var A2aProtocolAdapter = class {
3790
4315
  mountPath: this.mountPath,
3791
4316
  resources
3792
4317
  });
4318
+ this.pendingPayments = createPendingPayments(context.clock);
4319
+ this.cardBody = JSON.stringify(this.card);
4320
+ this.cardEtag = `"${createHash("sha256").update(this.cardBody).digest("base64url")}"`;
3793
4321
  this.started = true;
3794
4322
  context.logger.info(
3795
4323
  { mountPath: this.mountPath, cardPath: A2A_AGENT_CARD_PATH, skillCount: resources.length },
@@ -3807,7 +4335,14 @@ var A2aProtocolAdapter = class {
3807
4335
  this.writeJson(res, 405, { error: "Method not allowed. The Agent Card is read-only." });
3808
4336
  return;
3809
4337
  }
3810
- this.writeJson(res, 200, this.card);
4338
+ const cacheHeaders = { "cache-control": AGENT_CARD_CACHE_CONTROL, etag: this.cardEtag };
4339
+ if (matchesEtag(req.headers["if-none-match"], this.cardEtag)) {
4340
+ res.writeHead(304, cacheHeaders);
4341
+ res.end();
4342
+ return;
4343
+ }
4344
+ res.writeHead(200, { "content-type": A2A_JSON_MEDIA_TYPE, ...cacheHeaders });
4345
+ res.end(this.cardBody);
3811
4346
  } catch (err) {
3812
4347
  this.context?.logger.error({ err: toLogInfo(err) }, "a2a adapter: agent card request failed");
3813
4348
  this.writeJson(res, 500, { error: "Internal server error." });
@@ -3836,21 +4371,19 @@ var A2aProtocolAdapter = class {
3836
4371
  );
3837
4372
  return;
3838
4373
  }
3839
- const version = req.headers[A2A_VERSION_HEADER];
3840
- const declared = Array.isArray(version) ? version[0] : version;
3841
- if (declared !== A2A_PROTOCOL_VERSION) {
4374
+ const read = await readCappedBody(req, MAX_REQUEST_BODY_BYTES);
4375
+ if (read.kind === "too-large") {
3842
4376
  this.writeJson(
3843
4377
  res,
3844
- 200,
4378
+ 413,
3845
4379
  jsonRpcError(
3846
4380
  null,
3847
- A2A_ERROR_UNSUPPORTED_OPERATION,
3848
- `Unsupported A2A protocol version. Send the ${A2A_VERSION_HEADER} header with "${A2A_PROTOCOL_VERSION}".`
4381
+ JSONRPC_INVALID_REQUEST,
4382
+ `Request body exceeds the ${MAX_REQUEST_BODY_BYTES}-byte limit.`
3849
4383
  )
3850
4384
  );
3851
4385
  return;
3852
4386
  }
3853
- const read = await readCappedBody(req, MAX_REQUEST_BODY_BYTES);
3854
4387
  if (read.kind !== "ok") {
3855
4388
  this.writeJson(
3856
4389
  res,
@@ -3864,10 +4397,31 @@ var A2aProtocolAdapter = class {
3864
4397
  this.writeJson(res, 200, jsonRpcError(parsed.id, parsed.error.code, parsed.error.message));
3865
4398
  return;
3866
4399
  }
4400
+ const version = req.headers[A2A_VERSION_HEADER];
4401
+ const declared = (Array.isArray(version) ? version[0] : version) ?? new URL(req.url ?? "/", "http://localhost").searchParams.get(A2A_VERSION_PARAMETER);
4402
+ if (declared?.match(VERSION_PATTERN)?.[1] !== A2A_PROTOCOL_VERSION) {
4403
+ this.writeJson(
4404
+ res,
4405
+ 200,
4406
+ jsonRpcError(
4407
+ parsed.request.id,
4408
+ A2A_ERROR_VERSION_NOT_SUPPORTED,
4409
+ `Unsupported A2A protocol version. Send the ${A2A_VERSION_PARAMETER} header with "${A2A_PROTOCOL_VERSION}".`
4410
+ )
4411
+ );
4412
+ return;
4413
+ }
4414
+ const response = await this.dispatch(
4415
+ parsed.request.id,
4416
+ parsed.request.method,
4417
+ parsed.request.params,
4418
+ requestsX402Extension(req.headers[A2A_EXTENSIONS_HEADER])
4419
+ );
3867
4420
  this.writeJson(
3868
4421
  res,
3869
4422
  200,
3870
- await this.dispatch(parsed.request.id, parsed.request.method, parsed.request.params)
4423
+ response,
4424
+ usesX402Extension(response) ? { [A2A_EXTENSIONS_HEADER]: A2A_X402_EXTENSION_URI } : {}
3871
4425
  );
3872
4426
  } catch (err) {
3873
4427
  this.context?.logger.error({ err: toLogInfo(err) }, "a2a adapter: request handling failed");
@@ -3878,7 +4432,21 @@ var A2aProtocolAdapter = class {
3878
4432
  );
3879
4433
  }
3880
4434
  }
3881
- async dispatch(id, method, params) {
4435
+ async dispatch(id, method, params, x402Extension) {
4436
+ if (A2A_PUSH_CONFIG_METHODS.includes(method)) {
4437
+ return jsonRpcError(
4438
+ id,
4439
+ A2A_ERROR_PUSH_NOTIFICATION_NOT_SUPPORTED,
4440
+ `A2A method "${method}" is not supported: this agent does not support push notifications.`
4441
+ );
4442
+ }
4443
+ if (A2A_TASK_LOOKUP_METHODS.includes(method)) {
4444
+ return jsonRpcError(
4445
+ id,
4446
+ A2A_ERROR_TASK_NOT_FOUND,
4447
+ "Task not found. Finished tasks are not retained; continue a pending payment with SendMessage."
4448
+ );
4449
+ }
3882
4450
  if (A2A_UNSUPPORTED_METHODS.includes(method)) {
3883
4451
  return jsonRpcError(
3884
4452
  id,
@@ -3889,18 +4457,21 @@ var A2aProtocolAdapter = class {
3889
4457
  if (method !== A2A_METHOD_SEND_MESSAGE) {
3890
4458
  return jsonRpcError(id, JSONRPC_METHOD_NOT_FOUND, `Unknown method "${method}".`);
3891
4459
  }
4460
+ const resumed = await this.resumePayment(id, params);
4461
+ if (resumed !== void 0) return resumed;
3892
4462
  let invocation;
3893
4463
  try {
3894
4464
  invocation = parseInvocation(params);
3895
4465
  } catch (err) {
3896
4466
  const error = toCommerceError(err);
3897
- const code = error.code === "PROTOCOL_UNSUPPORTED" ? A2A_ERROR_UNSUPPORTED_OPERATION : JSONRPC_INVALID_PARAMS;
4467
+ const a2aErrorCode = error.details?.["a2aErrorCode"];
4468
+ const code = typeof a2aErrorCode === "number" ? a2aErrorCode : error.code === "PROTOCOL_UNSUPPORTED" ? A2A_ERROR_UNSUPPORTED_OPERATION : JSONRPC_INVALID_PARAMS;
3898
4469
  return jsonRpcError(id, code, error.message);
3899
4470
  }
3900
- return this.execute(id, invocation);
4471
+ return this.execute(id, invocation, x402Extension);
3901
4472
  }
3902
4473
  // One accepted invocation, one `pipeline.execute()`
3903
- async execute(id, invocation) {
4474
+ async execute(id, invocation, x402Extension) {
3904
4475
  const context = this.context;
3905
4476
  if (context === void 0) {
3906
4477
  return jsonRpcError(id, JSONRPC_INTERNAL_ERROR, "A2A adapter is not running.");
@@ -3936,9 +4507,29 @@ var A2aProtocolAdapter = class {
3936
4507
  ...authorization !== void 0 ? { authorization } : {}
3937
4508
  };
3938
4509
  const outcome = await context.pipeline.execute(request);
4510
+ if (outcome.kind !== "payment-required") {
4511
+ return this.taskResult(id, completedTask(outcome, identity));
4512
+ }
4513
+ const task2 = paymentRequiredTask(outcome, identity);
4514
+ const envelope = outcome.requirement.challenge.envelope;
4515
+ if (!x402Extension || outcome.requirement.provider !== "x402" || !isRecord(envelope)) {
4516
+ return this.taskResult(id, task2);
4517
+ }
4518
+ const network = outcome.requirement.network;
4519
+ this.pendingPayments?.put(
4520
+ identity.taskId,
4521
+ {
4522
+ contextId: identity.contextId,
4523
+ resourceId: invocation.resourceId,
4524
+ input,
4525
+ ...authorization !== void 0 ? { authorization } : {},
4526
+ ...network !== void 0 ? { network } : {}
4527
+ },
4528
+ outcome.requirement.expiresAt
4529
+ );
3939
4530
  return this.taskResult(
3940
4531
  id,
3941
- outcome.kind === "payment-required" ? paymentRequiredTask(outcome, identity) : completedTask(outcome, identity)
4532
+ inputRequired(task2, paymentRequiredMetadata(envelope), identity.statusMessageId)
3942
4533
  );
3943
4534
  } catch (err) {
3944
4535
  const error = toCommerceError(err);
@@ -3949,11 +4540,139 @@ var A2aProtocolAdapter = class {
3949
4540
  return this.taskResult(id, failedTask(error, identity));
3950
4541
  }
3951
4542
  }
4543
+ /**
4544
+ * A message on a task waiting for an x402 extension payment, recognized by
4545
+ * its `taskId` whether or not the header activated the extension again. Any
4546
+ * other task id falls through to `parseInvocation`, which answers
4547
+ * TaskNotFoundError.
4548
+ */
4549
+ async resumePayment(id, params) {
4550
+ const context = this.context;
4551
+ const message = isRecord(params) ? params["message"] : void 0;
4552
+ const taskId = isRecord(message) ? message["taskId"] : void 0;
4553
+ if (context === void 0 || typeof taskId !== "string") return void 0;
4554
+ const pending = this.pendingPayments?.get(taskId);
4555
+ if (pending === void 0) return void 0;
4556
+ const messageId = isRecord(message) ? message["messageId"] : void 0;
4557
+ if (typeof messageId !== "string" || messageId.length === 0) {
4558
+ return jsonRpcError(
4559
+ id,
4560
+ JSONRPC_INVALID_PARAMS,
4561
+ 'Message must carry a non-empty "messageId".'
4562
+ );
4563
+ }
4564
+ const roleProblem = userRoleProblem(isRecord(message) ? message["role"] : void 0);
4565
+ if (roleProblem !== void 0) return jsonRpcError(id, JSONRPC_INVALID_PARAMS, roleProblem);
4566
+ const contextId = isRecord(message) ? message["contextId"] : void 0;
4567
+ if (contextId !== void 0 && contextId !== pending.contextId) {
4568
+ return jsonRpcError(
4569
+ id,
4570
+ JSONRPC_INVALID_PARAMS,
4571
+ 'Message "contextId" does not match the task.'
4572
+ );
4573
+ }
4574
+ const submission = readPaymentSubmission(message);
4575
+ if (submission === void 0) {
4576
+ return jsonRpcError(
4577
+ id,
4578
+ JSONRPC_INVALID_PARAMS,
4579
+ `For a pending task, set "${X402_PAYMENT_STATUS_KEY}" to "payment-submitted" and provide a "${X402_PAYMENT_PAYLOAD_KEY}" object, or set it to "payment-rejected".`
4580
+ );
4581
+ }
4582
+ let authorization = pending.authorization;
4583
+ if (authorization === void 0 && submission.kind === "submitted") {
4584
+ try {
4585
+ authorization = readFollowUpAuthorization(message);
4586
+ } catch (err) {
4587
+ return jsonRpcError(id, JSONRPC_INVALID_PARAMS, toCommerceError(err).message);
4588
+ }
4589
+ }
4590
+ this.pendingPayments?.delete(taskId);
4591
+ const statusMessageId = context.ids.next("a2a-msg");
4592
+ const identity = {
4593
+ taskId,
4594
+ contextId: pending.contextId,
4595
+ artifactId: context.ids.next("a2a-artifact"),
4596
+ statusMessageId,
4597
+ timestamp: context.clock.nowIso()
4598
+ };
4599
+ if (submission.kind === "rejected") {
4600
+ const declined = {
4601
+ id: taskId,
4602
+ contextId: pending.contextId,
4603
+ status: { state: A2A_TASK_STATE_FAILED, timestamp: identity.timestamp },
4604
+ artifacts: []
4605
+ };
4606
+ return this.taskResult(
4607
+ id,
4608
+ withStatusMessage(
4609
+ declined,
4610
+ "Payment was declined.",
4611
+ statusMessageId,
4612
+ paymentRejectedMetadata()
4613
+ )
4614
+ );
4615
+ }
4616
+ return this.taskResult(
4617
+ id,
4618
+ await this.payPending(
4619
+ context,
4620
+ { ...pending, ...authorization !== void 0 ? { authorization } : {} },
4621
+ submission.payload,
4622
+ identity
4623
+ )
4624
+ );
4625
+ }
4626
+ // The pending purchase, now with the x402 `PaymentPayload` as its proof
4627
+ async payPending(context, pending, payload, identity) {
4628
+ const requestId = context.ids.next("a2a");
4629
+ const request = {
4630
+ requestId,
4631
+ resourceId: pending.resourceId,
4632
+ input: pending.input,
4633
+ protocol: "a2a",
4634
+ receivedAt: context.clock.nowIso(),
4635
+ // The same encoding as the HTTP header's base64 PaymentPayload JSON
4636
+ payment: {
4637
+ method: "x402",
4638
+ payload: Buffer.from(JSON.stringify(payload), "utf8").toString("base64")
4639
+ },
4640
+ ...pending.authorization !== void 0 ? { authorization: pending.authorization } : {}
4641
+ };
4642
+ let error;
4643
+ try {
4644
+ const outcome = await context.pipeline.execute(request);
4645
+ if (outcome.kind === "delivered" && outcome.payment !== void 0) {
4646
+ return withStatusMessage(
4647
+ completedTask(outcome, identity),
4648
+ "Payment settled.",
4649
+ identity.statusMessageId,
4650
+ paymentCompletedMetadata(outcome.payment)
4651
+ );
4652
+ }
4653
+ error = new CommerceError("PAYMENT_INVALID", "The payment was not accepted.", { requestId });
4654
+ } catch (err) {
4655
+ error = toCommerceError(err);
4656
+ context.logger.warn(
4657
+ { resourceId: pending.resourceId, requestId, err: toLogInfo(error) },
4658
+ "a2a adapter: x402 extension payment failed"
4659
+ );
4660
+ }
4661
+ const metadata = paymentFailureMetadata(error, pending.network);
4662
+ let text = "Payment failed.";
4663
+ if (metadata[X402_PAYMENT_STATUS_KEY] === "payment-completed") {
4664
+ text = "Payment settled, but the resource could not be delivered.";
4665
+ } else if (!error.code.startsWith("PAYMENT_")) {
4666
+ text = "The resource could not be delivered.";
4667
+ }
4668
+ return withStatusMessage(failedTask(error, identity), text, identity.statusMessageId, metadata);
4669
+ }
3952
4670
  taskIdentity(context, requestId) {
3953
4671
  return {
3954
4672
  taskId: requestId,
3955
4673
  contextId: context.ids.next("a2a-ctx"),
3956
4674
  artifactId: context.ids.next("a2a-artifact"),
4675
+ statusMessageId: context.ids.next("a2a-msg"),
3957
4676
  timestamp: context.clock.nowIso()
3958
4677
  };
3959
4678
  }
@@ -3971,15 +4690,27 @@ var A2aProtocolAdapter = class {
3971
4690
  async stop() {
3972
4691
  this.started = false;
3973
4692
  this.card = void 0;
4693
+ this.cardBody = "";
4694
+ this.cardEtag = "";
4695
+ this.pendingPayments?.clear();
4696
+ this.pendingPayments = void 0;
3974
4697
  this.skillsById = /* @__PURE__ */ new Map();
3975
4698
  this.context = void 0;
3976
4699
  }
3977
- writeJson(res, status, body) {
4700
+ writeJson(res, status, body, headers = {}) {
3978
4701
  if (res.headersSent) return;
3979
- res.writeHead(status, { "content-type": A2A_JSON_MEDIA_TYPE });
4702
+ res.writeHead(status, { "content-type": A2A_JSON_MEDIA_TYPE, ...headers });
3980
4703
  res.end(JSON.stringify(body));
3981
4704
  }
3982
4705
  };
4706
+ function usesX402Extension(response) {
4707
+ const result = response["result"];
4708
+ return result?.task?.status.message?.extensions?.includes(A2A_X402_EXTENSION_URI) === true;
4709
+ }
4710
+ function matchesEtag(header2, etag) {
4711
+ if (header2 === void 0) return false;
4712
+ return header2.split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === "*" || tag === etag);
4713
+ }
3983
4714
  function createA2aAdapter(options = {}) {
3984
4715
  return new A2aProtocolAdapter(options);
3985
4716
  }
@@ -3993,7 +4724,8 @@ function toCanonicalRequest(options) {
3993
4724
  input: canonicalInput(request),
3994
4725
  protocol: "acp",
3995
4726
  receivedAt,
3996
- ...idempotencyKey !== void 0 ? { idempotencyKey } : {}
4727
+ ...idempotencyKey !== void 0 ? { idempotencyKey } : {},
4728
+ ...request.forwardedHeaders !== void 0 && Object.keys(request.forwardedHeaders).length > 0 ? { backendHeaders: request.forwardedHeaders } : {}
3997
4729
  };
3998
4730
  }
3999
4731
  function canonicalInput(request) {
@@ -4001,7 +4733,7 @@ function canonicalInput(request) {
4001
4733
  const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[request.route.operation] ?? [];
4002
4734
  const input = {};
4003
4735
  if (keys.includes("path") && request.route.sessionId !== void 0) {
4004
- input["path"] = { checkout_session_id: request.route.sessionId };
4736
+ input["path"] = { [ACP_SESSION_ID_INPUT_KEY]: request.route.sessionId };
4005
4737
  }
4006
4738
  if (keys.includes("body")) {
4007
4739
  input["body"] = request.body;
@@ -4016,7 +4748,9 @@ var ACP_CAPABILITIES = [
4016
4748
  "rest transport",
4017
4749
  "well-known discovery",
4018
4750
  "bearer authentication",
4019
- "API-Version negotiation",
4751
+ "API-Version enforcement",
4752
+ // Forwarded as received: the gateway verifies neither signature header
4753
+ "request header forwarding to the merchant",
4020
4754
  "checkout service",
4021
4755
  ...ACP_CHECKOUT_OPERATIONS
4022
4756
  ];
@@ -4099,10 +4833,10 @@ function writeAcpFailure(res, failure, headers = {}) {
4099
4833
  writeAcpJson(res, failure.status, failure.error, headers);
4100
4834
  }
4101
4835
  function operationKey(scope) {
4102
- return sha2562(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
4836
+ return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
4103
4837
  }
4104
4838
  function requestFingerprint(body) {
4105
- return sha2562(canonicalize(body));
4839
+ return sha256(canonicalize(body));
4106
4840
  }
4107
4841
  function canonicalize(value) {
4108
4842
  if (value === null) return "null";
@@ -4113,7 +4847,7 @@ function canonicalize(value) {
4113
4847
  }
4114
4848
  return JSON.stringify(value) ?? "null";
4115
4849
  }
4116
- function sha2562(value) {
4850
+ function sha256(value) {
4117
4851
  return createHash("sha256").update(value, "utf8").digest("hex");
4118
4852
  }
4119
4853
 
@@ -4130,7 +4864,7 @@ function createAcpIdempotencyStore(options) {
4130
4864
  logger
4131
4865
  });
4132
4866
  migrate(db, logger);
4133
- resolveOrphanedClaims(db, logger);
4867
+ resolveOrphanedClaims(db, logger, options.releaseOrphans === true);
4134
4868
  const selectStmt = db.prepare(
4135
4869
  `SELECT state, fingerprint, status, body_json FROM acp_idempotency
4136
4870
  WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
@@ -4212,7 +4946,17 @@ function createAcpIdempotencyStore(options) {
4212
4946
  }
4213
4947
  };
4214
4948
  }
4215
- function resolveOrphanedClaims(db, logger) {
4949
+ function resolveOrphanedClaims(db, logger, release) {
4950
+ if (release) {
4951
+ const { changes: changes2 } = db.prepare("DELETE FROM acp_idempotency WHERE state = 'in_flight'").run();
4952
+ if (changes2 > 0) {
4953
+ logger.warn(
4954
+ { released: changes2 },
4955
+ "acp idempotency: freed claims left in flight by an earlier process; merchant deduplication is enabled"
4956
+ );
4957
+ }
4958
+ return;
4959
+ }
4216
4960
  const { changes } = db.prepare("UPDATE acp_idempotency SET state = 'unresolved' WHERE state = 'in_flight'").run();
4217
4961
  if (changes > 0) {
4218
4962
  logger.warn(
@@ -4257,8 +5001,7 @@ function migrate(db, logger) {
4257
5001
  }
4258
5002
 
4259
5003
  // src/protocols/acp/router.ts
4260
- var MAX_SESSION_ID_LENGTH = 128;
4261
- var SESSION_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
5004
+ var MAX_SEGMENT_LENGTH = 512;
4262
5005
  var COLLECTION = "checkout_sessions";
4263
5006
  function matchAcpRoute(method, url, mountPath) {
4264
5007
  const segments = routeSegments(url, mountPath);
@@ -4276,7 +5019,7 @@ function matchAcpRoute(method, url, mountPath) {
4276
5019
  };
4277
5020
  }
4278
5021
  const sessionId = segments[1];
4279
- if (sessionId === void 0 || !SESSION_ID_PATTERN.test(sessionId)) return { kind: "not-found" };
5022
+ if (sessionId === void 0 || !isSessionId(sessionId)) return { kind: "not-found" };
4280
5023
  if (segments.length === 2) {
4281
5024
  if (verb === "GET") {
4282
5025
  return {
@@ -4313,8 +5056,16 @@ function matchAcpRoute(method, url, mountPath) {
4313
5056
  }
4314
5057
  return { kind: "not-found" };
4315
5058
  }
5059
+ function isSessionId(value) {
5060
+ if (value.length === 0) return false;
5061
+ for (const char of value) {
5062
+ const code = char.charCodeAt(0);
5063
+ if (code < 32 || code === 127) return false;
5064
+ }
5065
+ return true;
5066
+ }
4316
5067
  function endpointPath(mountPath, segments) {
4317
- return `${mountPath.replace(/\/+$/, "")}/${segments.join("/")}`;
5068
+ return `${mountPath.replace(/\/+$/, "")}/${segments.map(encodeURIComponent).join("/")}`;
4318
5069
  }
4319
5070
  function routeSegments(url, mountPath) {
4320
5071
  if (url === void 0) return void 0;
@@ -4325,7 +5076,7 @@ function routeSegments(url, mountPath) {
4325
5076
  const segments = [];
4326
5077
  for (const raw of rest.split("/")) {
4327
5078
  if (raw.length === 0) continue;
4328
- if (raw.length > MAX_SESSION_ID_LENGTH) return void 0;
5079
+ if (raw.length > MAX_SEGMENT_LENGTH) return void 0;
4329
5080
  try {
4330
5081
  segments.push(decodeURIComponent(raw));
4331
5082
  } catch {
@@ -8269,6 +9020,7 @@ function acpValidators() {
8269
9020
  });
8270
9021
  addFormats(ajv);
8271
9022
  const document = relaxExtensibleEnums(structuredClone(schema_agentic_checkout_default));
9023
+ openCapabilityValues(document);
8272
9024
  const schemaId = typeof document["$id"] === "string" ? document["$id"] : "";
8273
9025
  ajv.addSchema(document);
8274
9026
  const compiled = /* @__PURE__ */ new Map();
@@ -8300,6 +9052,14 @@ function relaxExtensibleEnums(node) {
8300
9052
  for (const value of Object.values(record)) relaxExtensibleEnums(value);
8301
9053
  return node;
8302
9054
  }
9055
+ function openCapabilityValues(document) {
9056
+ const defs = document["$defs"];
9057
+ const properties = defs["InterventionCapabilities"]?.properties ?? {};
9058
+ for (const list of ["supported", "required"]) {
9059
+ const items = properties[list]?.items;
9060
+ if (items !== void 0) delete items["enum"];
9061
+ }
9062
+ }
8303
9063
  function validateAcpDocument(definition, value) {
8304
9064
  const validate = acpValidators().get(definition);
8305
9065
  if (validate === void 0) return { code: "unknown_definition" };
@@ -8330,7 +9090,23 @@ var REQUEST_DEFINITIONS = {
8330
9090
  cancelCheckoutSession: "cancelRequest",
8331
9091
  getCheckoutSession: void 0
8332
9092
  };
9093
+ function acpEchoHeaders(req) {
9094
+ const echo = {};
9095
+ const presentedRequestId = normalizeRequestId(header(req, ACP_REQUEST_ID_HEADER));
9096
+ if (presentedRequestId !== void 0) echo[ACP_REQUEST_ID_HEADER] = presentedRequestId;
9097
+ const presentedKey = header(req, ACP_IDEMPOTENCY_KEY_HEADER)?.trim();
9098
+ if (req.method === "POST" && presentedKey !== void 0 && isUsableIdempotencyKey(presentedKey)) {
9099
+ echo[ACP_IDEMPOTENCY_KEY_HEADER] = presentedKey;
9100
+ }
9101
+ return echo;
9102
+ }
8333
9103
  async function guardAcpRequest(req, options) {
9104
+ const echo = acpEchoHeaders(req);
9105
+ const failed = (failure2, extra = {}) => ({
9106
+ ok: false,
9107
+ ...failure2,
9108
+ headers: { ...echo, ...extra }
9109
+ });
8334
9110
  const matched = matchAcpRoute(req.method, req.url, options.mountPath);
8335
9111
  if (matched.kind === "not-found") {
8336
9112
  return failed(acpFailure(404, "invalid_request", "not_found", "Unknown ACP route."));
@@ -8342,7 +9118,8 @@ async function guardAcpRequest(req, options) {
8342
9118
  "invalid_request",
8343
9119
  "method_not_allowed",
8344
9120
  `This ACP route accepts ${matched.allow.join(", ")}.`
8345
- )
9121
+ ),
9122
+ { allow: matched.allow.join(", ") }
8346
9123
  );
8347
9124
  }
8348
9125
  const route = matched.route;
@@ -8353,7 +9130,9 @@ async function guardAcpRequest(req, options) {
8353
9130
  "invalid_request",
8354
9131
  "unauthorized",
8355
9132
  'A valid "Authorization: Bearer <token>" header is required.'
8356
- )
9133
+ ),
9134
+ // RFC 9110 requires a challenge on every 401
9135
+ { "www-authenticate": "Bearer" }
8357
9136
  );
8358
9137
  }
8359
9138
  const version = header(req, ACP_API_VERSION_HEADER);
@@ -8381,8 +9160,8 @@ async function guardAcpRequest(req, options) {
8381
9160
  }
8382
9161
  let idempotencyKey;
8383
9162
  if (route.acceptsBody) {
8384
- const presented = header(req, ACP_IDEMPOTENCY_KEY_HEADER)?.trim();
8385
- if (presented === void 0 || presented.length === 0) {
9163
+ const presentedKey = header(req, ACP_IDEMPOTENCY_KEY_HEADER)?.trim();
9164
+ if (presentedKey === void 0 || presentedKey.length === 0) {
8386
9165
  return failed(
8387
9166
  acpFailure(
8388
9167
  400,
@@ -8392,7 +9171,7 @@ async function guardAcpRequest(req, options) {
8392
9171
  )
8393
9172
  );
8394
9173
  }
8395
- if (presented.length > ACP_MAX_IDEMPOTENCY_KEY_LENGTH || !isVisibleAscii(presented)) {
9174
+ if (!isUsableIdempotencyKey(presentedKey)) {
8396
9175
  return failed(
8397
9176
  acpFailure(
8398
9177
  400,
@@ -8402,7 +9181,7 @@ async function guardAcpRequest(req, options) {
8402
9181
  )
8403
9182
  );
8404
9183
  }
8405
- idempotencyKey = presented;
9184
+ idempotencyKey = presentedKey;
8406
9185
  }
8407
9186
  const read = await readCappedBody(req, options.maxBodyBytes ?? ACP_MAX_REQUEST_BODY_BYTES);
8408
9187
  if (read.kind === "too-large") {
@@ -8481,12 +9260,23 @@ function ok(route, body, req, idempotencyKey) {
8481
9260
  route,
8482
9261
  body,
8483
9262
  ...requestId !== void 0 ? { requestId } : {},
8484
- ...idempotencyKey !== void 0 ? { idempotencyKey } : {}
9263
+ ...idempotencyKey !== void 0 ? { idempotencyKey } : {},
9264
+ forwardedHeaders: forwardedHeaders(req, requestId)
8485
9265
  }
8486
9266
  };
8487
9267
  }
8488
- function failed(failure) {
8489
- return { ok: false, ...failure };
9268
+ function forwardedHeaders(req, requestId) {
9269
+ const forwarded = {};
9270
+ for (const name of ACP_FORWARDED_HEADERS) {
9271
+ const value = name === ACP_REQUEST_ID_HEADER ? requestId : header(req, name)?.trim();
9272
+ if (value !== void 0 && value.length > 0 && value.length <= ACP_MAX_FORWARDED_HEADER_LENGTH && isVisibleAscii(value)) {
9273
+ forwarded[name] = value;
9274
+ }
9275
+ }
9276
+ return forwarded;
9277
+ }
9278
+ function isUsableIdempotencyKey(value) {
9279
+ return value.length > 0 && value.length <= ACP_MAX_IDEMPOTENCY_KEY_LENGTH && isVisibleAscii(value);
8490
9280
  }
8491
9281
  function normalizeRequestId(value) {
8492
9282
  if (value === void 0) return void 0;
@@ -8544,8 +9334,36 @@ function toAcpResponse(operation, outcome) {
8544
9334
  }
8545
9335
  };
8546
9336
  }
9337
+ const htmlPath = rawHtmlPath(outcome.body, "$");
9338
+ if (htmlPath !== void 0) {
9339
+ return {
9340
+ response: asResponse(PROCESSING_ERROR),
9341
+ logDetail: { reason: "backend-markdown-has-raw-html", operation, path: htmlPath }
9342
+ };
9343
+ }
8547
9344
  return { response: { status: expected, body: outcome.body } };
8548
9345
  }
9346
+ var RAW_HTML = /<\/?[A-Za-z][A-Za-z0-9-]*(?=[\s/>]|$)|<!--|<\?|<![A-Za-z]|<!\[CDATA\[/;
9347
+ var CODE_SPAN = /(`+)[\s\S]*?\1/g;
9348
+ function rawHtmlPath(node, path2) {
9349
+ if (Array.isArray(node)) {
9350
+ for (const [index, item] of node.entries()) {
9351
+ const found = rawHtmlPath(item, `${path2}[${index}]`);
9352
+ if (found !== void 0) return found;
9353
+ }
9354
+ return void 0;
9355
+ }
9356
+ if (!isRecord(node)) return void 0;
9357
+ const content = node["content"];
9358
+ if (node["content_type"] === "markdown" && typeof content === "string" && RAW_HTML.test(content.replace(CODE_SPAN, ""))) {
9359
+ return `${path2}.content`;
9360
+ }
9361
+ for (const [key, value] of Object.entries(node)) {
9362
+ const found = rawHtmlPath(value, `${path2}.${key}`);
9363
+ if (found !== void 0) return found;
9364
+ }
9365
+ return void 0;
9366
+ }
8549
9367
  function responseDefinition(operation, body) {
8550
9368
  if (operation !== "completeCheckoutSession") return "checkoutSession";
8551
9369
  const status = body?.status;
@@ -8581,8 +9399,39 @@ function mapCommerceErrorToAcp(error, operation) {
8581
9399
  }
8582
9400
  }
8583
9401
  function fromBackendStatus(error, operation) {
8584
- const status = error.details?.["status"];
8585
- switch (typeof status === "number" ? status : 0) {
9402
+ const failure = statusFailure(error, operation);
9403
+ return failure.status < 500 ? withMerchantError(failure, error) : failure;
9404
+ }
9405
+ function isMerchantRateLimit(error) {
9406
+ return error.code === "BACKEND_ERROR" && error.details?.["status"] === 429;
9407
+ }
9408
+ function merchantRetryAfterSeconds(error) {
9409
+ const value = error.cause instanceof BackendErrorResponse ? error.cause.headers["retry-after"] : void 0;
9410
+ if (value === void 0 || !/^\d{1,9}$/.test(value.trim())) {
9411
+ return ACP_MERCHANT_RETRY_AFTER_DEFAULT_SECONDS;
9412
+ }
9413
+ return Math.min(Math.max(Number(value), 1), ACP_MERCHANT_RETRY_AFTER_MAX_SECONDS);
9414
+ }
9415
+ var MERCHANT_CODE = /^(?!idempotency_)[a-z][a-z0-9_]{0,63}$/;
9416
+ var MERCHANT_PARAM = /^\$[\x20-\x7e]{0,255}$/;
9417
+ function withMerchantError(failure, error) {
9418
+ const body = error.cause instanceof BackendErrorResponse ? error.cause.body : void 0;
9419
+ if (!isRecord(body) || validateAcpDocument("error", body) !== void 0) return failure;
9420
+ const { type, code, param } = body;
9421
+ if (typeof code !== "string" || !MERCHANT_CODE.test(code)) return failure;
9422
+ const relayParam = typeof param === "string" && MERCHANT_PARAM.test(param) ? param : void 0;
9423
+ return acpFailure(
9424
+ failure.status,
9425
+ type,
9426
+ code,
9427
+ failure.error.message,
9428
+ relayParam !== void 0 ? { param: relayParam } : {}
9429
+ );
9430
+ }
9431
+ function statusFailure(error, operation) {
9432
+ const merchantStatus = error.details?.["status"];
9433
+ const status = typeof merchantStatus === "number" ? merchantStatus : 0;
9434
+ switch (status) {
8586
9435
  case 404:
8587
9436
  return acpFailure(
8588
9437
  404,
@@ -8605,7 +9454,7 @@ function fromBackendStatus(error, operation) {
8605
9454
  case 400:
8606
9455
  case 422:
8607
9456
  return acpFailure(
8608
- 422,
9457
+ status,
8609
9458
  "invalid_request",
8610
9459
  "invalid_request_body",
8611
9460
  "The merchant rejected this checkout request."
@@ -8617,6 +9466,17 @@ function fromBackendStatus(error, operation) {
8617
9466
  "checkout_session_conflict",
8618
9467
  "This checkout session is in a state that does not allow this operation."
8619
9468
  );
9469
+ case 429:
9470
+ case 503:
9471
+ return {
9472
+ ...acpFailure(
9473
+ 503,
9474
+ "service_unavailable",
9475
+ "service_unavailable",
9476
+ "The merchant is temporarily unavailable."
9477
+ ),
9478
+ retryAfterSeconds: merchantRetryAfterSeconds(error)
9479
+ };
8620
9480
  default:
8621
9481
  return acpFailure(
8622
9482
  502,
@@ -8627,7 +9487,11 @@ function fromBackendStatus(error, operation) {
8627
9487
  }
8628
9488
  }
8629
9489
  function asResponse(failure) {
8630
- return { status: failure.status, body: failure.error };
9490
+ return {
9491
+ status: failure.status,
9492
+ body: failure.error,
9493
+ ...failure.retryAfterSeconds !== void 0 ? { retryAfterSeconds: failure.retryAfterSeconds } : {}
9494
+ };
8631
9495
  }
8632
9496
 
8633
9497
  // src/protocols/acp/adapter.ts
@@ -8648,8 +9512,8 @@ var PRE_BACKEND_ERROR_CODES = /* @__PURE__ */ new Set([
8648
9512
  "AUTHORIZATION_REPLAYED",
8649
9513
  "AUTHORIZATION_PROVIDER_UNAVAILABLE"
8650
9514
  ]);
8651
- function reachedFor(code) {
8652
- return PRE_BACKEND_ERROR_CODES.has(code) ? "no" : "unknown";
9515
+ function reachedFor(error) {
9516
+ return PRE_BACKEND_ERROR_CODES.has(error.code) || isMerchantRateLimit(error) ? "no" : "unknown";
8653
9517
  }
8654
9518
  function notReached(failure) {
8655
9519
  return { response: asResponse(failure), reached: "no" };
@@ -8727,6 +9591,7 @@ var AcpProtocolAdapter = class {
8727
9591
  this.idempotency = createAcpIdempotencyStore({
8728
9592
  path: this.idempotencyOptions.path,
8729
9593
  retentionHours: this.idempotencyOptions.retentionHours,
9594
+ releaseOrphans: this.idempotencyOptions.merchantIdempotent === true,
8730
9595
  logger: context.logger
8731
9596
  });
8732
9597
  this.discovery = document;
@@ -8763,7 +9628,8 @@ var AcpProtocolAdapter = class {
8763
9628
  "invalid_request",
8764
9629
  "method_not_allowed",
8765
9630
  "The ACP discovery document is read-only."
8766
- )
9631
+ ),
9632
+ { allow: "GET" }
8767
9633
  );
8768
9634
  return;
8769
9635
  }
@@ -8783,7 +9649,8 @@ var AcpProtocolAdapter = class {
8783
9649
  "service_unavailable",
8784
9650
  "service_unavailable",
8785
9651
  "The ACP adapter is not running."
8786
- )
9652
+ ),
9653
+ acpEchoHeaders(req)
8787
9654
  );
8788
9655
  return;
8789
9656
  }
@@ -8792,14 +9659,14 @@ var AcpProtocolAdapter = class {
8792
9659
  isAuthorized: this.isAuthorized
8793
9660
  });
8794
9661
  if (!guarded.ok) {
8795
- writeAcpFailure(res, guarded);
9662
+ writeAcpFailure(res, guarded, guarded.headers);
8796
9663
  return;
8797
9664
  }
8798
9665
  const request = guarded.value;
8799
9666
  const response = await this.dispatch(request);
8800
9667
  writeAcpJson(res, response.status, response.body, this.responseHeaders(request, response));
8801
9668
  } catch (err) {
8802
- this.fail(res, err, "acp adapter: request handling failed");
9669
+ this.fail(res, err, "acp adapter: request handling failed", acpEchoHeaders(req));
8803
9670
  }
8804
9671
  }
8805
9672
  /**
@@ -8833,7 +9700,7 @@ var AcpProtocolAdapter = class {
8833
9700
  return asResponse(
8834
9701
  acpFailure(
8835
9702
  409,
8836
- "processing_error",
9703
+ "invalid_request",
8837
9704
  "idempotency_unresolved",
8838
9705
  "An earlier request with this Idempotency-Key reached the merchant and its outcome is unknown. Check the operation with the merchant before retrying."
8839
9706
  )
@@ -8859,7 +9726,7 @@ var AcpProtocolAdapter = class {
8859
9726
  }
8860
9727
  if (outcome.response.status < 500) {
8861
9728
  store.complete(scope, outcome.response);
8862
- } else if (outcome.reached === "no") {
9729
+ } else if (outcome.reached === "no" || this.idempotencyOptions.merchantIdempotent === true) {
8863
9730
  store.release(scope);
8864
9731
  } else {
8865
9732
  store.markUnresolved(scope);
@@ -8917,18 +9784,19 @@ var AcpProtocolAdapter = class {
8917
9784
  {
8918
9785
  resourceId: resource.id,
8919
9786
  requestId: canonical.requestId,
8920
- reached: reachedFor(error.code),
9787
+ reached: reachedFor(error),
8921
9788
  err: toLogInfo(error)
8922
9789
  },
8923
9790
  "acp adapter: checkout execution failed"
8924
9791
  );
8925
9792
  return {
8926
9793
  response: asResponse(mapCommerceErrorToAcp(error, request.route.operation)),
8927
- reached: reachedFor(error.code)
9794
+ reached: reachedFor(error)
8928
9795
  };
8929
9796
  }
8930
9797
  }
8931
- // Protocol-owned headers only; a merchant backend's headers are never proxied
9798
+ // Protocol-owned headers only. No merchant header is proxied: `Retry-After`
9799
+ // is written from seconds the mapping parsed and capped.
8932
9800
  responseHeaders(request, response) {
8933
9801
  const replayed = response?.replayed === true;
8934
9802
  return {
@@ -8938,11 +9806,12 @@ var AcpProtocolAdapter = class {
8938
9806
  ...response?.retryAfterSeconds !== void 0 ? { "retry-after": String(response.retryAfterSeconds) } : {}
8939
9807
  };
8940
9808
  }
8941
- fail(res, err, message) {
9809
+ fail(res, err, message, headers = {}) {
8942
9810
  this.context?.logger.error({ err: toLogInfo(err) }, message);
8943
9811
  writeAcpFailure(
8944
9812
  res,
8945
- acpFailure(500, "processing_error", "internal_error", "Internal server error.")
9813
+ acpFailure(500, "processing_error", "internal_error", "Internal server error."),
9814
+ headers
8946
9815
  );
8947
9816
  }
8948
9817
  async health() {
@@ -9218,6 +10087,9 @@ function createSqliteReceiptStore(options) {
9218
10087
  const listPaymentAttemptsByRequestStmt = db.prepare(
9219
10088
  "SELECT * FROM payment_attempts WHERE request_id = ? ORDER BY created_at DESC, seq DESC LIMIT ?"
9220
10089
  );
10090
+ const paymentAttemptStatusStmt = db.prepare(
10091
+ "SELECT status FROM payment_attempts WHERE replay_key = ?"
10092
+ );
9221
10093
  let closed = false;
9222
10094
  const descriptor = {
9223
10095
  name: "sqlite-receipt-store",
@@ -9274,10 +10146,15 @@ function createSqliteReceiptStore(options) {
9274
10146
  });
9275
10147
  } catch (err) {
9276
10148
  if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
10149
+ const existing = paymentAttemptStatusStmt.get(reservation.replayKey);
9277
10150
  throw new CommerceError(
9278
10151
  "PAYMENT_REPLAYED",
9279
10152
  `Payment authorization has already been used (replayKey=${reservation.replayKey})`,
9280
- { requestId: reservation.requestId, resourceId: reservation.resourceId }
10153
+ {
10154
+ requestId: reservation.requestId,
10155
+ resourceId: reservation.resourceId,
10156
+ ...existing !== void 0 ? { details: { attemptStatus: existing.status } } : {}
10157
+ }
9281
10158
  );
9282
10159
  }
9283
10160
  throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");