@astrasyncai/verification-gateway 5.18.1 → 5.19.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 (113) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +168 -4
  3. package/dist/adapters/express.d.mts +9 -1
  4. package/dist/adapters/express.d.ts +9 -1
  5. package/dist/adapters/express.js +96 -37
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +96 -37
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/http-pdlss.d.mts +7 -1
  10. package/dist/adapters/http-pdlss.d.ts +7 -1
  11. package/dist/adapters/http-pdlss.js +18 -2
  12. package/dist/adapters/http-pdlss.js.map +1 -1
  13. package/dist/adapters/http-pdlss.mjs +16 -1
  14. package/dist/adapters/http-pdlss.mjs.map +1 -1
  15. package/dist/adapters/kit-express.d.mts +93 -1
  16. package/dist/adapters/kit-express.d.ts +93 -1
  17. package/dist/adapters/kit-express.js +181 -2
  18. package/dist/adapters/kit-express.js.map +1 -1
  19. package/dist/adapters/kit-express.mjs +180 -2
  20. package/dist/adapters/kit-express.mjs.map +1 -1
  21. package/dist/adapters/mcp.d.mts +80 -9
  22. package/dist/adapters/mcp.d.ts +80 -9
  23. package/dist/adapters/mcp.js +153 -5
  24. package/dist/adapters/mcp.js.map +1 -1
  25. package/dist/adapters/mcp.mjs +151 -5
  26. package/dist/adapters/mcp.mjs.map +1 -1
  27. package/dist/adapters/nextjs.d.mts +1 -1
  28. package/dist/adapters/nextjs.d.ts +1 -1
  29. package/dist/adapters/nextjs.js +41 -4
  30. package/dist/adapters/nextjs.js.map +1 -1
  31. package/dist/adapters/nextjs.mjs +41 -4
  32. package/dist/adapters/nextjs.mjs.map +1 -1
  33. package/dist/adapters/sdk.d.mts +44 -1
  34. package/dist/adapters/sdk.d.ts +44 -1
  35. package/dist/adapters/sdk.js +76 -2
  36. package/dist/adapters/sdk.js.map +1 -1
  37. package/dist/adapters/sdk.mjs +76 -2
  38. package/dist/adapters/sdk.mjs.map +1 -1
  39. package/dist/agent/index.js +1 -1
  40. package/dist/agent/index.js.map +1 -1
  41. package/dist/agent/index.mjs +1 -1
  42. package/dist/agent/index.mjs.map +1 -1
  43. package/dist/bin/astrasync-claude-hook.js +46 -6
  44. package/dist/bin/astrasync-codex-hook.js +46 -6
  45. package/dist/bin/astrasync-guard.js +46 -6
  46. package/dist/bin/astrasync.js +77 -5
  47. package/dist/browser/background.js +46 -6
  48. package/dist/browser/background.js.map +1 -1
  49. package/dist/browser/background.mjs +46 -6
  50. package/dist/browser/background.mjs.map +1 -1
  51. package/dist/cli/index.js +1 -1
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/index.mjs +1 -1
  54. package/dist/cli/index.mjs.map +1 -1
  55. package/dist/codex/index.js +46 -6
  56. package/dist/codex/index.js.map +1 -1
  57. package/dist/codex/index.mjs +46 -6
  58. package/dist/codex/index.mjs.map +1 -1
  59. package/dist/cursor/extension.js +46 -6
  60. package/dist/cursor/extension.js.map +1 -1
  61. package/dist/cursor/extension.mjs +46 -6
  62. package/dist/cursor/extension.mjs.map +1 -1
  63. package/dist/edge-config.d.mts +1 -1
  64. package/dist/edge-config.d.ts +1 -1
  65. package/dist/edge-config.js +1 -1
  66. package/dist/edge-config.js.map +1 -1
  67. package/dist/edge-config.mjs +1 -1
  68. package/dist/edge-config.mjs.map +1 -1
  69. package/dist/edge-core/index.d.mts +1 -1
  70. package/dist/edge-core/index.d.ts +1 -1
  71. package/dist/edge-core/index.js +21 -2
  72. package/dist/edge-core/index.js.map +1 -1
  73. package/dist/edge-core/index.mjs +21 -2
  74. package/dist/edge-core/index.mjs.map +1 -1
  75. package/dist/gateway/gateway.js +46 -6
  76. package/dist/gateway/gateway.js.map +1 -1
  77. package/dist/gateway/gateway.mjs +46 -6
  78. package/dist/gateway/gateway.mjs.map +1 -1
  79. package/dist/git-trigger/git-hooks.d.mts +1 -1
  80. package/dist/git-trigger/git-hooks.d.ts +1 -1
  81. package/dist/index.d.mts +541 -29
  82. package/dist/index.d.ts +541 -29
  83. package/dist/index.js +1171 -183
  84. package/dist/index.js.map +1 -1
  85. package/dist/index.mjs +1157 -173
  86. package/dist/index.mjs.map +1 -1
  87. package/dist/registration/index.js +1 -1
  88. package/dist/registration/index.js.map +1 -1
  89. package/dist/registration/index.mjs +1 -1
  90. package/dist/registration/index.mjs.map +1 -1
  91. package/dist/transport/index.js +3 -2
  92. package/dist/transport/index.js.map +1 -1
  93. package/dist/transport/index.mjs +3 -2
  94. package/dist/transport/index.mjs.map +1 -1
  95. package/dist/{types-3ObBpsnf.d.mts → types-BoGjyon0.d.mts} +81 -2
  96. package/dist/{types-Cns4l8gX.d.mts → types-DqWPkiYl.d.mts} +90 -12
  97. package/dist/{types-Cns4l8gX.d.ts → types-DqWPkiYl.d.ts} +90 -12
  98. package/dist/{types-BoxZEYPS.d.ts → types-eWyZuz-3.d.ts} +81 -2
  99. package/dist/ui/index.d.mts +1 -1
  100. package/dist/ui/index.d.ts +1 -1
  101. package/dist/verify.d.mts +23 -2
  102. package/dist/verify.d.ts +23 -2
  103. package/dist/verify.js +23 -4
  104. package/dist/verify.js.map +1 -1
  105. package/dist/verify.mjs +23 -4
  106. package/dist/verify.mjs.map +1 -1
  107. package/dist/webhooks.d.mts +16 -3
  108. package/dist/webhooks.d.ts +16 -3
  109. package/dist/webhooks.js +24 -13
  110. package/dist/webhooks.js.map +1 -1
  111. package/dist/webhooks.mjs +24 -13
  112. package/dist/webhooks.mjs.map +1 -1
  113. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ __export(src_exports, {
53
53
  MemoryCheckoutStore: () => MemoryCheckoutStore,
54
54
  MemoryIdempotencyStore: () => MemoryIdempotencyStore,
55
55
  MemoryMppChallengeStore: () => MemoryMppChallengeStore,
56
+ MemorySeenEvents: () => MemorySeenEvents,
56
57
  PLATFORM_AGENT_SIGNATURES: () => PLATFORM_AGENT_SIGNATURES,
57
58
  RUNTIME_SIGNATURES: () => RUNTIME_SIGNATURES,
58
59
  RedisIdempotencyStore: () => RedisIdempotencyStore,
@@ -67,6 +68,7 @@ __export(src_exports, {
67
68
  VERIFICATION_STATUS_HEADER: () => VERIFICATION_STATUS_HEADER,
68
69
  VERSION: () => SDK_VERSION,
69
70
  _resetEdgeConfigCache: () => _resetEdgeConfigCache,
71
+ abandonIdempotent: () => abandonIdempotent,
70
72
  acpPaymentHandlers: () => acpPaymentHandlers,
71
73
  aepCanonicalJson: () => aepCanonicalJson,
72
74
  aepHash: () => aepHash,
@@ -78,6 +80,7 @@ __export(src_exports, {
78
80
  aepTimestamp: () => aepTimestamp,
79
81
  aepVerifySignature: () => aepVerifySignature,
80
82
  agent: () => agent_exports,
83
+ approvalEventsHandler: () => approvalEventsHandler,
81
84
  astrasyncPaymentHandlers: () => astrasyncPaymentHandlers,
82
85
  authorizeSettlement: () => authorizeSettlement,
83
86
  awaitStepUpApproval: () => awaitStepUpApproval,
@@ -124,6 +127,7 @@ __export(src_exports, {
124
127
  getEdgeConfig: () => getEdgeConfig,
125
128
  getTrustLevel: () => getTrustLevel,
126
129
  getWellKnownUrls: () => getWellKnownUrls,
130
+ handleApprovalEvents: () => handleApprovalEvents,
127
131
  hasCredentials: () => hasCredentials,
128
132
  idempotencyMiddleware: () => idempotencyMiddleware,
129
133
  isEdgeConfig: () => isEdgeConfig,
@@ -214,7 +218,7 @@ function getTrustLevel(score) {
214
218
  }
215
219
 
216
220
  // src/version.ts
217
- var SDK_VERSION = "5.18.1";
221
+ var SDK_VERSION = "5.19.0";
218
222
 
219
223
  // src/http.ts
220
224
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -466,7 +470,12 @@ function getCacheKey(request, counterpartyId) {
466
470
  // its start_checkout quote was served the cached quote-leg grant (no
467
471
  // settlementOutcome) and never reached the backend. Same F-A1-07 bug class.
468
472
  request.commercePhase || "",
469
- request.checkoutSessionId || ""
473
+ request.checkoutSessionId || "",
474
+ // 5.19.0: a route_config purpose claim is checked server-side against
475
+ // the stored rule for this method + path, so it changes the verdict.
476
+ request.purposeSource || "",
477
+ request.requestMethod || "",
478
+ request.requestPath || ""
470
479
  ].join("|");
471
480
  }
472
481
  function getCachedResult(request, counterpartyId) {
@@ -680,6 +689,11 @@ async function callVerifyAccessAPI(config, request) {
680
689
  if (requestData.buyerEmail) body.buyerEmail = requestData.buyerEmail;
681
690
  if (requestData.commerceArtifacts) body.commerceArtifacts = requestData.commerceArtifacts;
682
691
  if (requestData.attemptId) body.attemptId = requestData.attemptId;
692
+ if (requestData.purposeSource) body.purposeSource = requestData.purposeSource;
693
+ if (requestData.agentArtifacts) body.agentArtifacts = requestData.agentArtifacts;
694
+ if (requestData.metadata) body.metadata = requestData.metadata;
695
+ if (requestData.requestMethod) body.requestMethod = requestData.requestMethod;
696
+ if (requestData.requestPath) body.requestPath = requestData.requestPath;
683
697
  if (requestData.considerationSet) body.considerationSet = requestData.considerationSet;
684
698
  body.sdkVersion = SDK_VERSION;
685
699
  if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
@@ -869,9 +883,18 @@ async function verify(config, request, options) {
869
883
  if (!apiResponse.access?.allowed) {
870
884
  const aggregatedFailures = apiResponse.access?.failures;
871
885
  const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
886
+ const denyAgent = idVerifiedFromBackend && apiResponse.agent ? {
887
+ astraId: apiResponse.agent.astraId,
888
+ name: apiResponse.agent.name,
889
+ trustScore: apiResponse.agent.trustScore,
890
+ trustLevel: getTrustLevel(apiResponse.agent.trustScore),
891
+ blockchainAnchored: apiResponse.agent.blockchainStatus === "anchored",
892
+ status: apiResponse.agent.agentStatus
893
+ } : void 0;
872
894
  const result2 = {
873
895
  identityVerified: idVerifiedFromBackend,
874
896
  policyAllowed: false,
897
+ ...denyAgent && { agent: denyAgent },
875
898
  // 3.6.0: the server now emits top-level denialReasons on every deny
876
899
  // path — prefer it verbatim; the failure-message reconstruction remains
877
900
  // as the fallback for older backends.
@@ -1063,7 +1086,7 @@ async function reportAttempt(config, report, options = {}) {
1063
1086
  }
1064
1087
  var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
1065
1088
  var refusedPolicyReads = /* @__PURE__ */ new Set();
1066
- async function fetchRoutes(config, counterpartyId) {
1089
+ async function fetchRoutes(config, counterpartyId, options = {}) {
1067
1090
  if (!counterpartyId) return null;
1068
1091
  const headers = { "Content-Type": "application/json" };
1069
1092
  if (config.apiKey) {
@@ -1072,7 +1095,7 @@ async function fetchRoutes(config, counterpartyId) {
1072
1095
  }
1073
1096
  try {
1074
1097
  const response = await sdkFetch(
1075
- `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
1098
+ `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes` + (options.codeGates?.length ? `?codeGates=${options.codeGates.map(encodeURIComponent).join(",")}` : ""),
1076
1099
  { method: "GET", headers }
1077
1100
  );
1078
1101
  if (!response.ok) {
@@ -1755,6 +1778,42 @@ async function detectFetchCommerce(request) {
1755
1778
  );
1756
1779
  }
1757
1780
 
1781
+ // src/adapters/routes-cache.ts
1782
+ var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
1783
+ function createRoutesCache(config, opts = {}) {
1784
+ const refreshMs = opts.refreshMs ?? DEFAULT_ROUTES_REFRESH_MS;
1785
+ let cached = [];
1786
+ let lastFetchAt = 0;
1787
+ let refreshing = null;
1788
+ async function refresh() {
1789
+ if (!config.counterpartyId) {
1790
+ opts.onNoCounterparty?.();
1791
+ return;
1792
+ }
1793
+ const fetched = await fetchRoutes(config, config.counterpartyId, opts.fetchOptions);
1794
+ if (fetched) {
1795
+ cached = fetched;
1796
+ lastFetchAt = Date.now();
1797
+ opts.onFetched?.(fetched);
1798
+ }
1799
+ }
1800
+ function start() {
1801
+ refreshing = refresh().catch(() => void 0).finally(() => {
1802
+ refreshing = null;
1803
+ });
1804
+ }
1805
+ start();
1806
+ return {
1807
+ async current() {
1808
+ if (refreshing) await refreshing.catch(() => {
1809
+ });
1810
+ if (config.counterpartyId && Date.now() - lastFetchAt > refreshMs) start();
1811
+ return cached;
1812
+ },
1813
+ peek: () => cached
1814
+ };
1815
+ }
1816
+
1758
1817
  // src/transport/http.ts
1759
1818
  var HEADER_PREFIX = "X-Astra-";
1760
1819
  function setHttpHeaders(headers, credentials) {
@@ -1931,6 +1990,20 @@ function resolveHttpPdlss(input) {
1931
1990
  }
1932
1991
  return { purpose, action, purposeSource, actionSource };
1933
1992
  }
1993
+ function wirePurposeSource(source) {
1994
+ switch (source) {
1995
+ case "route_config":
1996
+ return "route_config";
1997
+ case "header":
1998
+ case "legacy_header":
1999
+ case "query":
2000
+ return "header";
2001
+ case "custom_extractor":
2002
+ return "body";
2003
+ default:
2004
+ return "default";
2005
+ }
2006
+ }
1934
2007
 
1935
2008
  // src/pdlss-pre-check.ts
1936
2009
  function performCounterpartyPreCheck(routeConfig, astraCreds, purpose) {
@@ -2430,12 +2503,12 @@ var BeaconBuffer = class {
2430
2503
  }
2431
2504
  /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
2432
2505
  takeBatch(now) {
2433
- const first = this.queue.find((q) => q.eligibleAt <= now);
2434
- if (!first) return [];
2506
+ const first2 = this.queue.find((q) => q.eligibleAt <= now);
2507
+ if (!first2) return [];
2435
2508
  const batch = [];
2436
2509
  const rest = [];
2437
2510
  for (const q of this.queue) {
2438
- if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
2511
+ if (batch.length < this.opts.maxBatch && q.destKey === first2.destKey && q.eligibleAt <= now) {
2439
2512
  q.inFlight = true;
2440
2513
  batch.push(q);
2441
2514
  } else {
@@ -2712,7 +2785,6 @@ function defaultOnDenied(result, _req, res) {
2712
2785
  }
2713
2786
  });
2714
2787
  }
2715
- var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
2716
2788
  function createMiddleware(options) {
2717
2789
  if ("routes" in options) {
2718
2790
  console.warn(
@@ -2728,7 +2800,7 @@ function createMiddleware(options) {
2728
2800
  recordDecisions,
2729
2801
  enableRuntimeChallenge = true,
2730
2802
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
2731
- failOnError = "open",
2803
+ failOnError = "closed",
2732
2804
  caseInsensitiveRouteMatch = false,
2733
2805
  anonymousBeacon = true,
2734
2806
  trustVerifiedHop = false,
@@ -2752,26 +2824,19 @@ function createMiddleware(options) {
2752
2824
  rawHeaders: req.headers
2753
2825
  });
2754
2826
  } : void 0;
2755
- let cachedRoutes = [];
2756
- let lastFetchAt = 0;
2757
- let refreshing = null;
2758
2827
  let warnedNoCounterparty = false;
2759
2828
  let warnedEmptyRoutes = false;
2760
- async function refreshRoutes() {
2761
- if (!config.counterpartyId) {
2762
- if (!warnedNoCounterparty) {
2763
- console.warn(
2764
- "[VerificationGateway] No counterpartyId configured \u2014 falling through (allow all). Per-route policy lives in the AstraSync dashboard now; register the endpoint and set counterpartyId in your middleware config to enforce policy."
2765
- );
2766
- warnedNoCounterparty = true;
2767
- }
2768
- return;
2769
- }
2770
- const fetched = await fetchRoutes(config, config.counterpartyId);
2771
- if (fetched) {
2772
- cachedRoutes = fetched;
2773
- lastFetchAt = Date.now();
2774
- if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
2829
+ const routesCache = createRoutesCache(config, {
2830
+ refreshMs: routesRefreshMs,
2831
+ onNoCounterparty: () => {
2832
+ if (warnedNoCounterparty) return;
2833
+ console.warn(
2834
+ "[VerificationGateway] No counterpartyId configured \u2014 falling through (allow all). Per-route policy lives in the AstraSync dashboard now; register the endpoint and set counterpartyId in your middleware config to enforce policy."
2835
+ );
2836
+ warnedNoCounterparty = true;
2837
+ },
2838
+ onFetched: (routes) => {
2839
+ if (routes.length === 0 && !warnedEmptyRoutes) {
2775
2840
  const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
2776
2841
  console.warn(
2777
2842
  `[VerificationGateway] No route policy configured for ${config.counterpartyId}. Gateway is in pass-through mode for ALL traffic until you add at least one route. Configure at ${dashboard}/dashboard/endpoints/${config.counterpartyId}/routes`
@@ -2779,9 +2844,6 @@ function createMiddleware(options) {
2779
2844
  warnedEmptyRoutes = true;
2780
2845
  }
2781
2846
  }
2782
- }
2783
- refreshing = refreshRoutes().finally(() => {
2784
- refreshing = null;
2785
2847
  });
2786
2848
  return async (req, res, next) => {
2787
2849
  try {
@@ -2789,15 +2851,9 @@ function createMiddleware(options) {
2789
2851
  if (shouldSkip) {
2790
2852
  return next();
2791
2853
  }
2792
- if (refreshing) {
2793
- await refreshing.catch(() => {
2794
- });
2795
- }
2796
- if (config.counterpartyId && Date.now() - lastFetchAt > routesRefreshMs) {
2797
- refreshing = refreshRoutes().finally(() => {
2798
- refreshing = null;
2799
- });
2800
- }
2854
+ const cachedRoutes = (await routesCache.current()).filter(
2855
+ (r) => typeof r.pattern === "string" && !r.tool
2856
+ );
2801
2857
  const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
2802
2858
  const matchedRoute = findRouteConfig(cachedRoutes, req.path, req.method, {
2803
2859
  caseInsensitive: caseInsensitiveRouteMatch,
@@ -2921,6 +2977,12 @@ function createMiddleware(options) {
2921
2977
  ...commerceArtifacts && { commerceArtifacts },
2922
2978
  purpose,
2923
2979
  action: pdlssPair.action,
2980
+ // 5.19.0: the source + the request line let verify-access check a
2981
+ // route purpose against the stored rule (item 20: an MPP paid leg
2982
+ // keeps `shopping.purchase` instead of `commerce.payment.execute`).
2983
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
2984
+ requestMethod: req.method,
2985
+ requestPath: req.path,
2924
2986
  resource: req.path,
2925
2987
  createSession: shouldRecordDecisions,
2926
2988
  counterpartyUrl,
@@ -2998,7 +3060,8 @@ function createMiddleware(options) {
2998
3060
  return;
2999
3061
  }
3000
3062
  if (routeConfig.minTrustScore && result.agent) {
3001
- if (result.agent.trustScore < routeConfig.minTrustScore) {
3063
+ const score = result.agent.trustScore;
3064
+ if (typeof score !== "number" || score < routeConfig.minTrustScore) {
3002
3065
  const trustFailure = {
3003
3066
  dimension: "endpoint.trust",
3004
3067
  message: "Trust below the route requirement for this endpoint.",
@@ -3442,6 +3505,10 @@ function createMiddleware2(options) {
3442
3505
  ...commerceArtifacts && { commerceArtifacts },
3443
3506
  purpose,
3444
3507
  action: pdlssPair.action,
3508
+ // 5.19.0: route purpose claim + request line (checked server-side).
3509
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
3510
+ requestMethod: request.method,
3511
+ requestPath: pathname,
3445
3512
  resource: pathname,
3446
3513
  counterpartyUrl,
3447
3514
  counterpartyType: config.counterpartyType || "website",
@@ -3642,6 +3709,7 @@ function isRetryableSettlementError(err) {
3642
3709
  }
3643
3710
 
3644
3711
  // src/adapters/sdk.ts
3712
+ var warnedPhaselessValue = false;
3645
3713
  var VerificationGatewayClient = class {
3646
3714
  constructor(options) {
3647
3715
  this.config = {
@@ -3673,9 +3741,16 @@ var VerificationGatewayClient = class {
3673
3741
  apiKey: options.apiKey,
3674
3742
  jwt: options.jwt
3675
3743
  };
3744
+ if (options.transactionValue && !options.commercePhase && !warnedPhaselessValue) {
3745
+ warnedPhaselessValue = true;
3746
+ console.warn(
3747
+ "[astrasync] verify() with a transactionValue and no commercePhase is a purchase leg: it records a pending order and redeems a matching owner approval. Use quoteCheckout() for a price / limit check (commercePhase 'quote') and confirmCheckout() for the purchase. The default becomes 'quote' in 6.0."
3748
+ );
3749
+ }
3676
3750
  return this.executeWithRetry(
3677
3751
  () => verify(this.config, {
3678
3752
  credentials,
3753
+ ...options.commercePhase && { commercePhase: options.commercePhase },
3679
3754
  purpose: options.purpose,
3680
3755
  action: options.action,
3681
3756
  resourceType: options.resourceType,
@@ -3691,6 +3766,43 @@ var VerificationGatewayClient = class {
3691
3766
  })
3692
3767
  );
3693
3768
  }
3769
+ /**
3770
+ * 5.19.0: the QUOTE leg of a checkout — a price / limit check before the
3771
+ * purchase. Sends `commercePhase: 'quote'`: verify-access evaluates the
3772
+ * agent's policy at this value and tells you whether a purchase would be
3773
+ * granted, held for the owner's approval, or denied, but records no order,
3774
+ * moves no money and never redeems an approval. Always reaches the backend
3775
+ * (no verdict cache).
3776
+ *
3777
+ * Quotes count toward limit-probing detection: a run of rising quotes is
3778
+ * how an agent (or a merchant) maps a spending limit. Quote what the buyer
3779
+ * asked for, not a ladder of values.
3780
+ */
3781
+ async quoteCheckout(options) {
3782
+ const credentials = {
3783
+ astraId: options.astraId,
3784
+ apiKey: options.apiKey,
3785
+ jwt: options.jwt
3786
+ };
3787
+ return this.executeWithRetry(
3788
+ () => verify(
3789
+ this.config,
3790
+ {
3791
+ credentials,
3792
+ purpose: options.purpose ?? "shopping",
3793
+ action: options.action ?? "shopping.purchase",
3794
+ transactionValue: options.transactionValue,
3795
+ currency: options.currency,
3796
+ commercePhase: "quote",
3797
+ ...options.checkoutSessionId && { checkoutSessionId: options.checkoutSessionId },
3798
+ ...options.attemptId && { attemptId: options.attemptId },
3799
+ counterpartyUrl: options.counterpartyUrl,
3800
+ counterpartyType: options.counterpartyType
3801
+ },
3802
+ { bypassCache: true }
3803
+ )
3804
+ );
3805
+ }
3694
3806
  /**
3695
3807
  * 5.4.0 (astra-pay): confirm-leg of a checkout — the ONLY call that may fire
3696
3808
  * first-party charge-at-redeem (server-side settlement on the owner's saved
@@ -3729,6 +3841,14 @@ var VerificationGatewayClient = class {
3729
3841
  * is matched to — and only to — this confirm. A held cart's approval is
3730
3842
  * bound to its checkout session and exact value; a confirm for any other
3731
3843
  * cart is held for its own approval.
3844
+ *
3845
+ * 5.19.0 checkout kits: pass the kit's `agentArtifacts` (the inbound agent
3846
+ * request — signed RFC 9421 headers with the public URL and raw-body
3847
+ * digest, or `{ unsigned: true }`) and `metadata` (`{ kitSessionId,
3848
+ * protocol }`) from the `verifyPurchase` context. The platform attributes
3849
+ * the confirm to the agent from the artifacts, and stores the metadata on
3850
+ * any approval this confirm creates so the approval event
3851
+ * (`handleApprovalEvents`) can find the kit session.
3732
3852
  */
3733
3853
  async confirmCheckout(options) {
3734
3854
  const credentials = {
@@ -3751,6 +3871,8 @@ var VerificationGatewayClient = class {
3751
3871
  ...options.checkoutJwt && { checkoutJwt: options.checkoutJwt },
3752
3872
  ...options.attemptId && { attemptId: options.attemptId },
3753
3873
  buyerEmail: options.buyerEmail,
3874
+ ...options.agentArtifacts && { agentArtifacts: options.agentArtifacts },
3875
+ ...options.metadata && { metadata: options.metadata },
3754
3876
  counterpartyUrl: options.counterpartyUrl,
3755
3877
  counterpartyType: options.counterpartyType
3756
3878
  },
@@ -4678,9 +4800,9 @@ function extractFulfillmentOption(body) {
4678
4800
  if (direct) return direct;
4679
4801
  const options = body.fulfillment_options;
4680
4802
  if (Array.isArray(options) && options.length > 0) {
4681
- const first = options[0];
4682
- if (first && typeof first === "object") {
4683
- const id = coerceString3(first.id);
4803
+ const first2 = options[0];
4804
+ if (first2 && typeof first2 === "object") {
4805
+ const id = coerceString3(first2.id);
4684
4806
  if (id) return id;
4685
4807
  }
4686
4808
  }
@@ -5687,7 +5809,8 @@ function verifyMPP(input) {
5687
5809
  }
5688
5810
  let replayOk = true;
5689
5811
  if (challenge?.digest && expiryOk) {
5690
- const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
5812
+ const c = challenge;
5813
+ const replayKey = `mpp:${challenge.digest}:${c.id ?? ""}:${c.nonce ?? ""}`;
5691
5814
  const expiresAt = (nowSec + tolerance) * 1e3;
5692
5815
  if (nonceStore.seen(replayKey, expiresAt)) {
5693
5816
  replayOk = false;
@@ -6244,11 +6367,11 @@ function resolveTransactionValue(input) {
6244
6367
  if (v) return v;
6245
6368
  }
6246
6369
  if (input.x402?.paymentRequired) {
6247
- const first = input.x402.paymentRequired.accepts[0];
6248
- if (first) {
6370
+ const first2 = input.x402.paymentRequired.accepts[0];
6371
+ if (first2) {
6249
6372
  const v = extractX402TransactionValue({
6250
- maxAmountRequired: Number(first.amount),
6251
- asset: first.asset
6373
+ maxAmountRequired: Number(first2.amount),
6374
+ asset: first2.asset
6252
6375
  });
6253
6376
  if (v) return v;
6254
6377
  }
@@ -6706,6 +6829,21 @@ function defaultMcpDenied(result, req, res) {
6706
6829
  }
6707
6830
  });
6708
6831
  }
6832
+ function dashboardToolRules(routes) {
6833
+ const out = /* @__PURE__ */ new Map();
6834
+ for (const r of routes) {
6835
+ if (typeof r.tool !== "string" || !r.tool || r.legacy === true) continue;
6836
+ if (out.has(r.tool)) continue;
6837
+ out.set(r.tool, {
6838
+ tool: r.tool,
6839
+ ...r.purpose && { purpose: r.purpose },
6840
+ ...r.action && { action: r.action },
6841
+ ...r.resource && { resource: r.resource },
6842
+ ...r.observe === true && { observe: true }
6843
+ });
6844
+ }
6845
+ return out;
6846
+ }
6709
6847
  function resolveGate(parsed, opts) {
6710
6848
  if (!parsed.toolName) {
6711
6849
  const methodGate = opts.methodGates?.[parsed.method];
@@ -6715,8 +6853,30 @@ function resolveGate(parsed, opts) {
6715
6853
  return { enforce: false, source: "discovery_default" };
6716
6854
  }
6717
6855
  const toolGate = opts.toolGates?.[parsed.toolName];
6856
+ const rule = opts.dashboardRules?.get(parsed.toolName);
6857
+ const fromRule = (r) => r.observe ? { enforce: false, source: "dashboard" } : {
6858
+ enforce: true,
6859
+ config: {
6860
+ ...r.purpose && { purpose: r.purpose },
6861
+ ...r.action && { action: r.action },
6862
+ ...r.resource && { resource: r.resource }
6863
+ },
6864
+ source: "dashboard"
6865
+ };
6866
+ const codeGated = toolGate !== void 0 && toolGate !== "observe";
6867
+ if (rule && (opts.toolGatePrecedence === "dashboard" || !codeGated)) return fromRule(rule);
6718
6868
  if (toolGate !== void 0) {
6719
- return toolGate === "observe" ? { enforce: false, source: "toolGate" } : { enforce: true, config: toolGate, source: "toolGate" };
6869
+ if (toolGate === "observe") return { enforce: false, source: "toolGate" };
6870
+ if (rule && !rule.observe && rule.purpose && (rule.purpose !== toolGate.purpose || rule.action && rule.action !== toolGate.action)) {
6871
+ const extra = fromRule(rule).config;
6872
+ return {
6873
+ enforce: true,
6874
+ config: toolGate,
6875
+ additional: { ...extra, ...!extra.resource && toolGate.resource && { resource: toolGate.resource } },
6876
+ source: "toolGate"
6877
+ };
6878
+ }
6879
+ return { enforce: true, config: toolGate, source: "toolGate" };
6720
6880
  }
6721
6881
  return { enforce: mcpDefaultEnforced(parsed), source: "tier" };
6722
6882
  }
@@ -6731,13 +6891,20 @@ function createMcpMiddleware(options) {
6731
6891
  verifiedHopMaxAgeMs,
6732
6892
  recordDecisions,
6733
6893
  enableRuntimeChallenge = true,
6734
- failOnError = "open",
6894
+ failOnError = "closed",
6895
+ toolGatePrecedence = "code",
6896
+ routesRefreshMs,
6735
6897
  ...config
6736
6898
  } = options;
6737
6899
  if (config.apiBaseUrl) {
6738
6900
  prefetchWellKnown(config.apiBaseUrl).catch(() => {
6739
6901
  });
6740
6902
  }
6903
+ const codeGatedTools = Object.entries(toolGates ?? {}).filter(([, g]) => g !== "observe").map(([name]) => name);
6904
+ const rulesCache = config.apiKey && config.counterpartyId ? createRoutesCache(config, {
6905
+ ...routesRefreshMs !== void 0 && { refreshMs: routesRefreshMs },
6906
+ ...codeGatedTools.length && { fetchOptions: { codeGates: codeGatedTools } }
6907
+ }) : void 0;
6741
6908
  return async (req, res, next) => {
6742
6909
  try {
6743
6910
  if (skip) return next();
@@ -6786,9 +6953,12 @@ function createMcpMiddleware(options) {
6786
6953
  return next();
6787
6954
  }
6788
6955
  }
6956
+ const dashboardRules = rulesCache ? dashboardToolRules(await rulesCache.current()) : void 0;
6789
6957
  const gateDecision = resolveGate(parsed, {
6790
6958
  toolGates,
6791
- methodGates
6959
+ methodGates,
6960
+ dashboardRules,
6961
+ toolGatePrecedence
6792
6962
  });
6793
6963
  const credentials = extractCredentials(
6794
6964
  req.headers,
@@ -6886,6 +7056,20 @@ function createMcpMiddleware(options) {
6886
7056
  result.stepUpOutcome = outcome;
6887
7057
  }
6888
7058
  }
7059
+ if (shouldEnforce && gateDecision.additional?.purpose && result.policyAllowed && !requiresHumanApproval(result)) {
7060
+ const extra = mcpToPdlss(parsed, req.path, void 0, void 0, gateDecision.additional);
7061
+ const extraResult = await verify(config, {
7062
+ ...verificationRequest,
7063
+ purpose: extra.purpose,
7064
+ action: extra.action,
7065
+ resource: extra.resource
7066
+ });
7067
+ if (!extraResult.policyAllowed || requiresHumanApproval(extraResult)) {
7068
+ result = extraResult;
7069
+ req.agentVerification = result;
7070
+ sessionId = result.sessionId;
7071
+ }
7072
+ }
6889
7073
  if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
6890
7074
  res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6891
7075
  return next();
@@ -10192,15 +10376,15 @@ function requireDumper() {
10192
10376
  return !isWhitespace(c) && c !== CHAR_COLON;
10193
10377
  }
10194
10378
  function codePointAt(string, pos) {
10195
- const first = string.charCodeAt(pos);
10379
+ const first2 = string.charCodeAt(pos);
10196
10380
  let second;
10197
- if (first >= 55296 && first <= 56319 && pos + 1 < string.length) {
10381
+ if (first2 >= 55296 && first2 <= 56319 && pos + 1 < string.length) {
10198
10382
  second = string.charCodeAt(pos + 1);
10199
10383
  if (second >= 56320 && second <= 57343) {
10200
- return (first - 55296) * 1024 + second - 56320 + 65536;
10384
+ return (first2 - 55296) * 1024 + second - 56320 + 65536;
10201
10385
  }
10202
10386
  }
10203
- return first;
10387
+ return first2;
10204
10388
  }
10205
10389
  function needIndentIndicator(string) {
10206
10390
  const leadingSpaceRe = /^\n* /;
@@ -11854,6 +12038,12 @@ async function finishIdempotent(opts, decision, response) {
11854
12038
  } catch {
11855
12039
  }
11856
12040
  }
12041
+ async function abandonIdempotent(opts, decision) {
12042
+ try {
12043
+ await opts.store.delete(decision.storeKey);
12044
+ } catch {
12045
+ }
12046
+ }
11857
12047
  var KEPT_HEADERS = ["content-type", "location"];
11858
12048
  function withHeader(res, name, value) {
11859
12049
  const headers = new Headers(res.headers);
@@ -12009,6 +12199,87 @@ function warnInMemoryStores(kit, missing) {
12009
12199
  `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(" and ")} (sessions are lost on restart and not shared across instances). Pass ${missing.map((m) => m.startsWith("idempotency") ? "idempotencyStore" : "store").join(" and ")} in production.`
12010
12200
  );
12011
12201
  }
12202
+ var SECRET_KEY = /token|secret|credential|cvc|cvv|number|pan|password|key|cryptogram|signature/i;
12203
+ function scrubPaymentInput(input) {
12204
+ if (!input || typeof input !== "object") return void 0;
12205
+ const out = {};
12206
+ for (const [k, v] of Object.entries(input)) {
12207
+ if (SECRET_KEY.test(k)) continue;
12208
+ if (v === null || ["string", "number", "boolean"].includes(typeof v)) out[k] = v;
12209
+ }
12210
+ return out;
12211
+ }
12212
+ var NEVER_FORWARD = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "proxy-authorization"]);
12213
+ function coveredHeaders(signatureInput) {
12214
+ const names = /* @__PURE__ */ new Set();
12215
+ for (const m of signatureInput.matchAll(/\(([^)]*)\)/g)) {
12216
+ for (const q of m[1].matchAll(/"([^"]+)"/g)) {
12217
+ const name = q[1].toLowerCase();
12218
+ if (!name.startsWith("@")) names.add(name);
12219
+ }
12220
+ }
12221
+ return [...names];
12222
+ }
12223
+ function buildAgentArtifacts(req, rawBody, opts) {
12224
+ const signature = req.headers.get("signature");
12225
+ const signatureInput = req.headers.get("signature-input");
12226
+ if (opts.protocol === "acp" || !signature || !signatureInput) {
12227
+ return { unsigned: true, protocol: opts.protocol };
12228
+ }
12229
+ const headers = {};
12230
+ const names = [
12231
+ "signature",
12232
+ "signature-input",
12233
+ "signature-agent",
12234
+ "ucp-agent",
12235
+ "content-digest",
12236
+ ...coveredHeaders(signatureInput)
12237
+ ];
12238
+ for (const name of names) {
12239
+ if (NEVER_FORWARD.has(name)) continue;
12240
+ const v = req.headers.get(name);
12241
+ if (v !== null) headers[name] = v;
12242
+ }
12243
+ const u = new URL(req.url);
12244
+ const origin = opts.publicOrigin ? opts.publicOrigin.replace(/\/+$/, "") : u.origin;
12245
+ return {
12246
+ protocol: opts.protocol,
12247
+ method: req.method,
12248
+ url: `${origin}${u.pathname}${u.search}`,
12249
+ headers,
12250
+ ...rawBody && { contentDigest: contentDigest(rawBody) }
12251
+ };
12252
+ }
12253
+ async function withReportRetries(fn, opts = {}, isRetryable) {
12254
+ const attempts = Math.max(1, opts.attempts ?? 3);
12255
+ const backoff = opts.backoffMs ?? 250;
12256
+ let last;
12257
+ for (let i = 0; i < attempts; i++) {
12258
+ try {
12259
+ return await fn();
12260
+ } catch (err) {
12261
+ last = err;
12262
+ if (i === attempts - 1 || !isRetryable(err)) break;
12263
+ await new Promise((r) => setTimeout(r, backoff * 2 ** i));
12264
+ }
12265
+ }
12266
+ throw last;
12267
+ }
12268
+ function defaultOnReportError(err, ctx) {
12269
+ console.error(
12270
+ `[astrasync] checkout kit: ${ctx.kind === "order_webhook" ? "order webhook" : "settlement report"} failed for session ${ctx.sessionId}${ctx.orderId ? ` (order ${ctx.orderId})` : ""}:`,
12271
+ err
12272
+ );
12273
+ }
12274
+ var NON_TERMINAL = /* @__PURE__ */ new Set(["busy", "still_held"]);
12275
+ function isTerminalOutcome(outcome) {
12276
+ return !NON_TERMINAL.has(outcome);
12277
+ }
12278
+ var resumeResult = (outcome, session) => ({
12279
+ outcome,
12280
+ terminal: isTerminalOutcome(outcome),
12281
+ session
12282
+ });
12012
12283
 
12013
12284
  // src/ucp/checkout-kit.ts
12014
12285
  var MemoryCheckoutStore = class {
@@ -12033,6 +12304,15 @@ var MemoryCheckoutStore = class {
12033
12304
  const id = this.orders.get(orderId);
12034
12305
  return id ? this.m.get(id) ?? null : null;
12035
12306
  }
12307
+ async findByCheckoutSessionId(checkoutSessionId) {
12308
+ for (const s of this.m.values()) {
12309
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
12310
+ }
12311
+ return this.m.get(checkoutSessionId) ?? null;
12312
+ }
12313
+ async listHeld() {
12314
+ return [...this.m.values()].filter((s) => s.status === "requires_escalation");
12315
+ }
12036
12316
  };
12037
12317
  function buildUcpOrder(session, opts = {}) {
12038
12318
  if (!session.order) return null;
@@ -12055,11 +12335,20 @@ function buildUcpOrder(session, opts = {}) {
12055
12335
  totals: session.totals
12056
12336
  };
12057
12337
  }
12338
+ function heldBody(text) {
12339
+ try {
12340
+ return JSON.parse(text).status === "requires_escalation";
12341
+ } catch {
12342
+ return false;
12343
+ }
12344
+ }
12058
12345
  var json2 = (status, body, headers = {}) => new Response(JSON.stringify(body), {
12059
12346
  status,
12060
12347
  headers: { "content-type": "application/json", ...headers }
12061
12348
  });
12062
12349
  var total = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
12350
+ var clone = (v) => JSON.parse(JSON.stringify(v));
12351
+ var inFlight = (st) => st === "complete_in_progress" || st === "finishing";
12063
12352
  function createUcpCheckoutKit(opts) {
12064
12353
  const profile = buildUcpProfile(opts.profile);
12065
12354
  const store = opts.store ?? new MemoryCheckoutStore();
@@ -12085,6 +12374,11 @@ function createUcpCheckoutKit(opts) {
12085
12374
  ...entries.map((e) => e.id)
12086
12375
  ])
12087
12376
  );
12377
+ const publicOrigin = (opts.publicOrigin ?? new URL(opts.profile.endpoint).origin).replace(
12378
+ /\/+$/,
12379
+ ""
12380
+ );
12381
+ const onReportError = opts.onReportError ?? defaultOnReportError;
12088
12382
  const fetchProfile = opts.fetchProfile ?? (async (url, fresh) => {
12089
12383
  const r = await fetchUcpProfile(url, { fresh });
12090
12384
  return r.ok ? r.profile : r;
@@ -12106,7 +12400,7 @@ function createUcpCheckoutKit(opts) {
12106
12400
  payment_handlers: profile.ucp.payment_handlers
12107
12401
  },
12108
12402
  id: s.id,
12109
- status: s.status,
12403
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
12110
12404
  currency: s.currency,
12111
12405
  line_items: s.line_items,
12112
12406
  totals: s.totals,
@@ -12152,6 +12446,23 @@ function createUcpCheckoutKit(opts) {
12152
12446
  "ephemeral"
12153
12447
  );
12154
12448
  }
12449
+ function heldNoEdit(s) {
12450
+ return render(
12451
+ {
12452
+ ...s,
12453
+ messages: [
12454
+ ...s.messages ?? [],
12455
+ ucpError(
12456
+ "checkout_awaiting_approval",
12457
+ "This checkout is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over.",
12458
+ "requires_buyer_review"
12459
+ )
12460
+ ]
12461
+ },
12462
+ 409,
12463
+ "ephemeral"
12464
+ );
12465
+ }
12155
12466
  async function save(s) {
12156
12467
  if (s.order) orderIndex.set(s.order.id, s.id);
12157
12468
  await store.put(s);
@@ -12182,31 +12493,44 @@ function createUcpCheckoutKit(opts) {
12182
12493
  return r;
12183
12494
  }
12184
12495
  function emitOrder(s) {
12185
- void deliverOrder(s).catch(() => void 0);
12186
- }
12187
- async function reportOutcome(p, settled) {
12188
- if (!opts.reportSettlement || !p.orderId) return;
12189
- await opts.reportSettlement({
12190
- orderId: p.orderId,
12191
- status: settled.status,
12192
- ...p.report,
12193
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12194
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12195
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12196
- }).catch(() => void 0);
12496
+ const ctx = { sessionId: s.id, orderId: s.order?.id, kind: "order_webhook" };
12497
+ void deliverOrder(s).then((r) => {
12498
+ if (r && !r.ok) onReportError(new Error(`order webhook failed (HTTP ${r.status})`), ctx);
12499
+ }).catch((err) => onReportError(err, ctx));
12500
+ }
12501
+ async function reportOutcome(s, p, settled) {
12502
+ const report = opts.reportSettlement;
12503
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
12504
+ if (!report || !ref) return;
12505
+ await withReportRetries(
12506
+ () => report({
12507
+ ...ref,
12508
+ status: settled.status,
12509
+ ...p.report,
12510
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12511
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12512
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12513
+ }),
12514
+ opts.reportRetry,
12515
+ isRetryableSettlementError
12516
+ ).catch(
12517
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
12518
+ );
12197
12519
  }
12198
12520
  async function finish(s, result, p, settled) {
12199
- await reportOutcome(p, settled);
12521
+ await reportOutcome(s, p, settled);
12200
12522
  delete s._pending;
12523
+ delete s._held;
12524
+ delete s._approvalId;
12201
12525
  if (settled.status === "failed") {
12202
12526
  s.status = "ready_for_complete";
12203
12527
  s.messages = [
12204
- ucpError("payment_failed", "The payment did not go through.", "requires_buyer_input")
12528
+ ucpError("payment_failed", "The payment did not go through.", "recoverable")
12205
12529
  ];
12206
12530
  await save(s);
12207
12531
  return;
12208
12532
  }
12209
- const order = await opts.fulfil({ session: s, result });
12533
+ const order = await opts.fulfil({ session: s, result, settled });
12210
12534
  s.order = {
12211
12535
  ...order,
12212
12536
  ...!order.permalink_url && opts.permalinkBase && {
@@ -12240,7 +12564,8 @@ function createUcpCheckoutKit(opts) {
12240
12564
  const out = [];
12241
12565
  const messages = [];
12242
12566
  for (const [i, l] of lines.entries()) {
12243
- const item = l.item?.id ? await opts.catalog(l.item.id) : null;
12567
+ const q = Math.max(1, Math.floor(l.quantity ?? 1));
12568
+ const item = l.item?.id ? await opts.catalog(l.item.id, q) : null;
12244
12569
  if (!item) {
12245
12570
  messages.push(
12246
12571
  ucpError(
@@ -12252,7 +12577,6 @@ function createUcpCheckoutKit(opts) {
12252
12577
  );
12253
12578
  continue;
12254
12579
  }
12255
- const q = Math.max(1, Math.floor(l.quantity ?? 1));
12256
12580
  const amount = item.price * q;
12257
12581
  out.push({
12258
12582
  id: `li_${i + 1}`,
@@ -12274,6 +12598,19 @@ function createUcpCheckoutKit(opts) {
12274
12598
  ]
12275
12599
  };
12276
12600
  }
12601
+ async function validate(s) {
12602
+ if (!opts.validateCart) return { messages: [], blocked: false };
12603
+ const found = await opts.validateCart(s);
12604
+ const messages = found.map(
12605
+ (m) => (m.type ?? "error") === "error" ? ucpError(m.code, m.content, "recoverable", m.path) : {
12606
+ type: m.type,
12607
+ code: m.code,
12608
+ content: m.content,
12609
+ ...m.path && { path: m.path }
12610
+ }
12611
+ );
12612
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
12613
+ }
12277
12614
  async function platform(req, body) {
12278
12615
  const profileUrl = parseUcpAgentHeader(req.headers.get("ucp-agent"));
12279
12616
  if (!profileUrl) {
@@ -12345,17 +12682,20 @@ function createUcpCheckoutKit(opts) {
12345
12682
  });
12346
12683
  const res = await run();
12347
12684
  if (d.kind === "proceed") {
12348
- await finishIdempotent(idem, d, {
12349
- status: res.status,
12350
- headers: { "content-type": "application/json" },
12351
- body: await res.clone().text()
12352
- });
12685
+ const text = await res.clone().text();
12686
+ if (heldBody(text)) await abandonIdempotent(idem, d);
12687
+ else
12688
+ await finishIdempotent(idem, d, {
12689
+ status: res.status,
12690
+ headers: { "content-type": "application/json" },
12691
+ body: text
12692
+ });
12353
12693
  }
12354
12694
  return res;
12355
12695
  }
12356
- async function complete(req, s, body) {
12696
+ async function complete(req, s, body, agentArtifacts) {
12357
12697
  if (s.status === "completed" || s.status === "canceled") return render(s);
12358
- if (s.status === "complete_in_progress") return busy(s);
12698
+ if (inFlight(s.status)) return busy(s);
12359
12699
  const ap2 = body.ap2;
12360
12700
  const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
12361
12701
  if (s._ap2) {
@@ -12371,13 +12711,13 @@ function createUcpCheckoutKit(opts) {
12371
12711
  return render(s);
12372
12712
  }
12373
12713
  const kidResolver = async (kid) => opts.mandateKey ? opts.mandateKey(kid, s) : (s._platformKeys ?? []).find((k) => k.kid === kid) ?? null;
12374
- const v = await verifyAp2Mandate(ap2.checkout_mandate, {
12714
+ const v2 = await verifyAp2Mandate(ap2.checkout_mandate, {
12375
12715
  resolveRootKey: kidResolver,
12376
12716
  expectedVct: AP2_VCT.checkout,
12377
12717
  ...checkoutJwt && { checkoutJwt }
12378
12718
  });
12379
- if (!v.ok) {
12380
- s.messages = [ucpError(v.code, v.error, "unrecoverable")];
12719
+ if (!v2.ok) {
12720
+ s.messages = [ucpError(v2.code, v2.error, "unrecoverable")];
12381
12721
  await store.put(s);
12382
12722
  return render(s);
12383
12723
  }
@@ -12396,19 +12736,32 @@ function createUcpCheckoutKit(opts) {
12396
12736
  await store.put(s);
12397
12737
  return render(s);
12398
12738
  }
12739
+ const v = await validate(s);
12740
+ if (v.blocked) {
12741
+ s.status = "incomplete";
12742
+ s.messages = v.messages;
12743
+ delete s._held;
12744
+ delete s._approvalId;
12745
+ await store.put(s);
12746
+ return render(s);
12747
+ }
12399
12748
  const from = s.status;
12400
12749
  if (!await claim2(s, from)) {
12401
12750
  const current = await store.get(s.id);
12402
12751
  return busy(current ?? s);
12403
12752
  }
12753
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
12754
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
12404
12755
  try {
12405
12756
  const result = await opts.verifyPurchase({
12406
12757
  session: s,
12407
12758
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
12408
- astraId: req.headers.get("x-astra-id") ?? void 0,
12409
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
12759
+ astraId,
12760
+ attemptId,
12410
12761
  ...checkoutJwt && { checkoutJwt },
12411
- valueMajor: total(s) / 100
12762
+ valueMajor: total(s) / 100,
12763
+ agentArtifacts,
12764
+ metadata: { kitSessionId: s.id, protocol: "ucp" }
12412
12765
  });
12413
12766
  const binding2 = await bindAgent(req, result);
12414
12767
  if (binding2) {
@@ -12420,6 +12773,16 @@ function createUcpCheckoutKit(opts) {
12420
12773
  if (isHeld(result)) {
12421
12774
  s.status = "requires_escalation";
12422
12775
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
12776
+ s._approvalId = result.stepUpApproval?.approvalId;
12777
+ s._astraId = astraId ?? result.agent?.astraId;
12778
+ s._attemptId = attemptId ?? result.attemptId;
12779
+ s._held = {
12780
+ line_items: clone(s.line_items),
12781
+ totals: clone(s.totals),
12782
+ ...scrubPaymentInput(instrument) && { instrument: scrubPaymentInput(instrument) },
12783
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
12784
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
12785
+ };
12423
12786
  s.messages = [
12424
12787
  ucpError(
12425
12788
  "buyer_approval_required",
@@ -12442,21 +12805,7 @@ function createUcpCheckoutKit(opts) {
12442
12805
  await store.put(s);
12443
12806
  return render(s);
12444
12807
  }
12445
- const settled = await opts.settle({ session: s, result, instrument });
12446
- const pending = {
12447
- ...result.settlementOutcome?.orderId && {
12448
- orderId: result.settlementOutcome.orderId
12449
- },
12450
- report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
12451
- result: pendingResult(result)
12452
- };
12453
- if (settled.status === "pending") {
12454
- s._pending = pending;
12455
- s.messages = [];
12456
- await store.put(s);
12457
- return render(s);
12458
- }
12459
- await finish(s, result, pending, settled);
12808
+ await settleAndFinish(s, result, instrument);
12460
12809
  return render(s);
12461
12810
  } catch (e) {
12462
12811
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -12466,13 +12815,184 @@ function createUcpCheckoutKit(opts) {
12466
12815
  throw e;
12467
12816
  }
12468
12817
  }
12818
+ async function settleAndFinish(s, result, instrument) {
12819
+ const settled = await opts.settle({ session: s, result, instrument });
12820
+ const enhanced = result;
12821
+ const pending = {
12822
+ ...result.settlementOutcome?.orderId && {
12823
+ orderId: result.settlementOutcome.orderId
12824
+ },
12825
+ ...enhanced.sessionId && { sessionId: enhanced.sessionId },
12826
+ report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
12827
+ result: pendingResult(result)
12828
+ };
12829
+ if (settled.status === "pending") {
12830
+ s._pending = pending;
12831
+ s.messages = [];
12832
+ delete s._held;
12833
+ delete s._approvalId;
12834
+ await store.put(s);
12835
+ return "pending";
12836
+ }
12837
+ await finish(s, result, pending, settled);
12838
+ return settled.status === "failed" ? "payment_failed" : "completed";
12839
+ }
12469
12840
  async function resolvePending(sessionId, outcome) {
12470
12841
  const s = await store.get(sessionId);
12471
12842
  if (!s) return null;
12472
12843
  if (s.status !== "complete_in_progress" || !s._pending) return s;
12473
- await finish(s, s._pending.result, s._pending, outcome);
12844
+ if (store.claim) {
12845
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
12846
+ return await store.get(sessionId) ?? s;
12847
+ }
12848
+ s.status = "finishing";
12849
+ } else {
12850
+ s.status = "finishing";
12851
+ await store.put(s);
12852
+ }
12853
+ const pending = s._pending;
12854
+ try {
12855
+ await finish(s, pending.result, pending, outcome);
12856
+ } catch (e) {
12857
+ s.status = "complete_in_progress";
12858
+ s._pending = pending;
12859
+ await store.put(s).catch(() => void 0);
12860
+ throw e;
12861
+ }
12474
12862
  return s;
12475
12863
  }
12864
+ async function resumeApproved(sessionId, event) {
12865
+ const s = await store.get(sessionId);
12866
+ if (!s) return resumeResult("not_found", null);
12867
+ if (s.status === "completed") return resumeResult("already_completed", s);
12868
+ if (s.status === "canceled") return resumeResult("canceled", s);
12869
+ if (inFlight(s.status)) return resumeResult("busy", s);
12870
+ if (s.status !== "requires_escalation" || !s._held) return resumeResult("not_held", s);
12871
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
12872
+ return resumeResult("stale", s);
12873
+ if (!await claim2(s, "requires_escalation")) {
12874
+ return resumeResult("busy", await store.get(sessionId) ?? s);
12875
+ }
12876
+ const held = s._held;
12877
+ const backToHeld = async () => {
12878
+ s.status = "requires_escalation";
12879
+ s._held = held;
12880
+ await store.put(s).catch(() => void 0);
12881
+ };
12882
+ try {
12883
+ s.line_items = clone(held.line_items);
12884
+ s.totals = clone(held.totals);
12885
+ const v = await validate(s);
12886
+ if (v.blocked) {
12887
+ s.status = "incomplete";
12888
+ s.messages = v.messages;
12889
+ delete s._held;
12890
+ delete s._approvalId;
12891
+ delete s.continue_url;
12892
+ await store.put(s);
12893
+ return resumeResult("cart_invalid", s);
12894
+ }
12895
+ const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
12896
+ const result = await opts.verifyPurchase({
12897
+ session: s,
12898
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
12899
+ astraId: s._astraId,
12900
+ attemptId: s._attemptId,
12901
+ ...checkoutJwt && { checkoutJwt },
12902
+ valueMajor: total(s) / 100,
12903
+ metadata: { kitSessionId: s.id, protocol: "ucp" },
12904
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
12905
+ });
12906
+ if (isHeld(result)) {
12907
+ await backToHeld();
12908
+ return resumeResult("still_held", s);
12909
+ }
12910
+ if (!result.identityVerified || !result.policyAllowed) {
12911
+ s.status = "ready_for_complete";
12912
+ s.messages = [
12913
+ ucpError(
12914
+ "agent_not_authorized",
12915
+ "The agent is not authorized for this purchase.",
12916
+ "unrecoverable"
12917
+ )
12918
+ ];
12919
+ delete s._held;
12920
+ delete s._approvalId;
12921
+ delete s.continue_url;
12922
+ await store.put(s);
12923
+ return resumeResult("not_authorized", s);
12924
+ }
12925
+ delete s.continue_url;
12926
+ const outcome = await settleAndFinish(s, result, held.instrument);
12927
+ return resumeResult(outcome, s);
12928
+ } catch (e) {
12929
+ if (!s._pending && inFlight(s.status)) await backToHeld();
12930
+ throw e;
12931
+ }
12932
+ }
12933
+ async function resumeDeclined(sessionId, reason = "denied", event) {
12934
+ const s = await store.get(sessionId);
12935
+ if (!s) return resumeResult("not_found", null);
12936
+ if (s.status === "completed") return resumeResult("already_completed", s);
12937
+ if (s.status === "canceled") return resumeResult("canceled", s);
12938
+ if (inFlight(s.status)) return resumeResult("busy", s);
12939
+ if (s.status !== "requires_escalation") return resumeResult("not_held", s);
12940
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
12941
+ return resumeResult("stale", s);
12942
+ if (store.claim && !await store.claim(s.id, "requires_escalation", "ready_for_complete")) {
12943
+ return resumeResult("busy", await store.get(sessionId) ?? s);
12944
+ }
12945
+ s.status = "ready_for_complete";
12946
+ delete s._held;
12947
+ delete s._approvalId;
12948
+ delete s.continue_url;
12949
+ s.messages = [
12950
+ ucpError(
12951
+ `approval_${reason}`,
12952
+ reason === "denied" ? "The buyer declined this purchase." : "The approval for this purchase is no longer valid. Complete the checkout again to request a new one.",
12953
+ "requires_buyer_review"
12954
+ )
12955
+ ];
12956
+ await store.put(s);
12957
+ return resumeResult("declined", s);
12958
+ }
12959
+ async function findForEvent(event) {
12960
+ if (event.kitSessionId) {
12961
+ const s = await store.get(event.kitSessionId);
12962
+ if (s) return s;
12963
+ }
12964
+ if (store.findByCheckoutSessionId)
12965
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
12966
+ return store.get(event.checkoutSessionId);
12967
+ }
12968
+ async function applyApprovalEvent(event) {
12969
+ const s = await findForEvent(event);
12970
+ if (!s) return resumeResult("not_found", null);
12971
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
12972
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
12973
+ }
12974
+ async function pollHeldSessions(o = {}) {
12975
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
12976
+ (x) => Boolean(x)
12977
+ ) : store.listHeld ? await store.listHeld() : [];
12978
+ const out = [];
12979
+ for (const s of sessions) {
12980
+ if (s.status !== "requires_escalation" || !s._held?.pollUrl || !s._approvalId) continue;
12981
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
12982
+ approvalId: s._approvalId,
12983
+ pollUrl: s._held.pollUrl,
12984
+ expiresAt: s._held.expiresAt ?? ""
12985
+ });
12986
+ if (status === "approved" || status === "consumed") {
12987
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
12988
+ } else if (status === "denied" || status === "expired") {
12989
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
12990
+ } else {
12991
+ out.push({ sessionId: s.id, outcome: "pending" });
12992
+ }
12993
+ }
12994
+ return out;
12995
+ }
12476
12996
  async function sendOrderWebhook(sessionId) {
12477
12997
  const s = await store.get(sessionId);
12478
12998
  return s ? deliverOrder(s) : null;
@@ -12491,6 +13011,7 @@ function createUcpCheckoutKit(opts) {
12491
13011
  const p = await platform(req, body);
12492
13012
  if ("error" in p && p.error) return p.error;
12493
13013
  const ctx = p;
13014
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "ucp", publicOrigin });
12494
13015
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId) ?? fallbackScope("ucp-profile", ctx.profileUrl);
12495
13016
  if (req.method === "POST" && path === "/checkout-sessions") {
12496
13017
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12523,6 +13044,11 @@ function createUcpCheckoutKit(opts) {
12523
13044
  )
12524
13045
  );
12525
13046
  }
13047
+ if (priced.lines.length && !priced.messages.length) {
13048
+ const v = await validate(s2);
13049
+ s2.messages = [...s2.messages ?? [], ...v.messages];
13050
+ if (v.blocked) s2.status = "incomplete";
13051
+ }
12526
13052
  await store.put(s2);
12527
13053
  return render(s2, 201);
12528
13054
  });
@@ -12548,13 +13074,19 @@ function createUcpCheckoutKit(opts) {
12548
13074
  if (req.method === "PUT" && !m[2]) {
12549
13075
  return withIdempotency2(req, body, path, caller, async () => {
12550
13076
  if (s.status === "completed" || s.status === "canceled") return render(s);
12551
- if (s.status === "complete_in_progress") return busy(s);
13077
+ if (inFlight(s.status)) return busy(s);
13078
+ if (s.status === "requires_escalation") return heldNoEdit(s);
12552
13079
  if (parsed.line_items) {
12553
13080
  const priced = await priceLines(parsed.line_items);
12554
13081
  s.line_items = priced.lines;
12555
13082
  s.totals = priced.totals;
12556
13083
  s.messages = priced.messages;
12557
13084
  s.status = priced.lines.length && !priced.messages.length ? "ready_for_complete" : "incomplete";
13085
+ if (s.status === "ready_for_complete") {
13086
+ const v = await validate(s);
13087
+ s.messages = [...s.messages, ...v.messages];
13088
+ if (v.blocked) s.status = "incomplete";
13089
+ }
12558
13090
  }
12559
13091
  if (parsed.buyer) s.buyer = parsed.buyer;
12560
13092
  await store.put(s);
@@ -12562,7 +13094,13 @@ function createUcpCheckoutKit(opts) {
12562
13094
  });
12563
13095
  }
12564
13096
  if (req.method === "POST" && m[2] === "/complete") {
12565
- return withIdempotency2(req, body, path, caller, () => complete(req, s, parsed));
13097
+ return withIdempotency2(
13098
+ req,
13099
+ body,
13100
+ path,
13101
+ caller,
13102
+ () => complete(req, s, parsed, agentArtifacts)
13103
+ );
12566
13104
  }
12567
13105
  if (req.method === "POST" && m[2] === "/cancel") {
12568
13106
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12571,7 +13109,7 @@ function createUcpCheckoutKit(opts) {
12571
13109
  code: "method_not_allowed",
12572
13110
  content: `A ${s.status} checkout can't be canceled.`
12573
13111
  });
12574
- if (s.status === "complete_in_progress") return busy(s);
13112
+ if (inFlight(s.status)) return busy(s);
12575
13113
  s.status = "canceled";
12576
13114
  await store.put(s);
12577
13115
  return render(s);
@@ -12584,10 +13122,156 @@ function createUcpCheckoutKit(opts) {
12584
13122
  profile,
12585
13123
  profileHandler: () => json2(200, profile, { "cache-control": "public, max-age=300" }),
12586
13124
  resolvePending,
12587
- sendOrderWebhook
13125
+ sendOrderWebhook,
13126
+ /** 5.19.0 */
13127
+ protocol: "ucp",
13128
+ publicOrigin,
13129
+ resumeApproved,
13130
+ resumeDeclined,
13131
+ applyApprovalEvent,
13132
+ pollHeldSessions
12588
13133
  };
12589
13134
  }
12590
13135
 
13136
+ // src/webhooks.ts
13137
+ var import_crypto9 = require("crypto");
13138
+ var DEFAULT_TOLERANCE_SECONDS = 300;
13139
+ function parseSignatureHeader(header) {
13140
+ let t;
13141
+ const signatures = [];
13142
+ for (const segment of header.split(",")) {
13143
+ const eq = segment.indexOf("=");
13144
+ if (eq < 0) continue;
13145
+ const k = segment.slice(0, eq).trim();
13146
+ const v = segment.slice(eq + 1).trim();
13147
+ if (k === "t") t = v;
13148
+ else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
13149
+ }
13150
+ return { t, signatures };
13151
+ }
13152
+ function computeHmac(secret, signedPayload) {
13153
+ return (0, import_crypto9.createHmac)("sha256", secret).update(signedPayload, "utf8").digest("hex");
13154
+ }
13155
+ function constantTimeEquals(a, b) {
13156
+ const aBuf = Buffer.from(a, "utf8");
13157
+ const bBuf = Buffer.from(b, "utf8");
13158
+ if (aBuf.length !== bBuf.length) return false;
13159
+ return (0, import_crypto9.timingSafeEqual)(aBuf, bBuf);
13160
+ }
13161
+ function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
13162
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
13163
+ (x) => typeof x === "string" && x.length > 0
13164
+ );
13165
+ if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
13166
+ const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
13167
+ if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
13168
+ const headerValue3 = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
13169
+ const { t: timestamp2, signatures } = parseSignatureHeader(headerValue3);
13170
+ if (!timestamp2 || !signatures.length) {
13171
+ return { ok: false, reason: "malformed_signature_header" };
13172
+ }
13173
+ const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
13174
+ const now = options.nowMs ?? Date.now();
13175
+ const tsSeconds = Number(timestamp2);
13176
+ if (!Number.isFinite(tsSeconds)) {
13177
+ return { ok: false, reason: "invalid_timestamp" };
13178
+ }
13179
+ const ageSeconds = Math.abs(now / 1e3 - tsSeconds);
13180
+ if (ageSeconds > tolerance) {
13181
+ return { ok: false, reason: "timestamp_outside_tolerance" };
13182
+ }
13183
+ for (const key of secrets) {
13184
+ const expectedSignature = computeHmac(key, `${timestamp2}.${rawBody}`);
13185
+ if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
13186
+ return { ok: true };
13187
+ }
13188
+ }
13189
+ return { ok: false, reason: "signature_mismatch" };
13190
+ }
13191
+
13192
+ // src/ucp/approval-events.ts
13193
+ var MemorySeenEvents = class {
13194
+ constructor(max = 1e4) {
13195
+ this.max = max;
13196
+ this.ids = /* @__PURE__ */ new Set();
13197
+ }
13198
+ async has(eventId) {
13199
+ return this.ids.has(eventId);
13200
+ }
13201
+ async add(eventId) {
13202
+ if (this.ids.size >= this.max) {
13203
+ const first2 = this.ids.values().next().value;
13204
+ if (first2 !== void 0) this.ids.delete(first2);
13205
+ }
13206
+ this.ids.add(eventId);
13207
+ }
13208
+ };
13209
+ var defaultSeen = new MemorySeenEvents();
13210
+ var TYPES = /* @__PURE__ */ new Set(["step_up.approved", "step_up.denied", "step_up.expired", "step_up.lapsed"]);
13211
+ function toHeaderRecord(headers) {
13212
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
13213
+ const out2 = {};
13214
+ headers.forEach((v, k) => out2[k.toLowerCase()] = v);
13215
+ return out2;
13216
+ }
13217
+ const out = {};
13218
+ for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
13219
+ return out;
13220
+ }
13221
+ async function handleApprovalEvents(rawBody, headers, secret, opts) {
13222
+ const raw = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
13223
+ const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {
13224
+ ...opts.toleranceSeconds !== void 0 && { toleranceSeconds: opts.toleranceSeconds },
13225
+ ...opts.nowMs !== void 0 && { nowMs: opts.nowMs }
13226
+ });
13227
+ if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };
13228
+ let event;
13229
+ try {
13230
+ event = JSON.parse(raw);
13231
+ } catch {
13232
+ return { status: 400, body: { ok: false, error: "invalid_json" } };
13233
+ }
13234
+ if (!event || typeof event.eventId !== "string" || typeof event.type !== "string") {
13235
+ return { status: 400, body: { ok: false, error: "invalid_event" } };
13236
+ }
13237
+ if (!TYPES.has(event.type)) {
13238
+ opts.onEvent?.(event, "ignored");
13239
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "ignored" } };
13240
+ }
13241
+ const seen = opts.seen ?? defaultSeen;
13242
+ const has = typeof seen === "function" ? seen : (id) => seen.has(id);
13243
+ try {
13244
+ if (await has(event.eventId)) {
13245
+ opts.onEvent?.(event, "duplicate");
13246
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "duplicate" } };
13247
+ }
13248
+ const candidates = event.protocol ? opts.kits.filter((k) => k.protocol === event.protocol) : opts.kits;
13249
+ let last = "not_found";
13250
+ let terminal = true;
13251
+ for (const kit of candidates) {
13252
+ const r = await kit.applyApprovalEvent(event);
13253
+ last = r.outcome;
13254
+ terminal = r.terminal;
13255
+ if (r.outcome !== "not_found") break;
13256
+ }
13257
+ opts.onEvent?.(event, last);
13258
+ if (!terminal) {
13259
+ return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };
13260
+ }
13261
+ if (typeof seen !== "function") await seen.add(event.eventId);
13262
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };
13263
+ } catch (err) {
13264
+ return {
13265
+ status: 503,
13266
+ body: {
13267
+ ok: false,
13268
+ eventId: event.eventId,
13269
+ error: err instanceof Error ? err.message : "transient_error"
13270
+ }
13271
+ };
13272
+ }
13273
+ }
13274
+
12591
13275
  // src/payments/x402.ts
12592
13276
  var CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
12593
13277
  var b64 = (v) => Buffer.from(JSON.stringify(v), "utf8").toString("base64");
@@ -12637,7 +13321,7 @@ function x402PaymentMatchesOffer(payment, offered) {
12637
13321
  }
12638
13322
 
12639
13323
  // src/payments/mpp.ts
12640
- var import_crypto9 = require("crypto");
13324
+ var import_crypto10 = require("crypto");
12641
13325
  var b64url = (s) => Buffer.from(s, "utf8").toString("base64url");
12642
13326
  function canonical(value) {
12643
13327
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
@@ -12658,7 +13342,7 @@ function binding(c, secret) {
12658
13342
  ];
12659
13343
  if (c.header && c.header.toLowerCase() !== "authorization") slots.push(c.header);
12660
13344
  slots.push(c.opaque ?? "");
12661
- return (0, import_crypto9.createHmac)("sha256", secret).update(slots.join("|")).digest("base64url");
13345
+ return (0, import_crypto10.createHmac)("sha256", secret).update(slots.join("|")).digest("base64url");
12662
13346
  }
12663
13347
  var quote = (v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
12664
13348
  function buildMppChallenge(input, hmacSecret) {
@@ -12700,13 +13384,13 @@ var MemoryMppChallengeStore = class {
12700
13384
  }
12701
13385
  };
12702
13386
  function mppBodyDigest(body) {
12703
- return `sha-256=${(0, import_crypto9.createHash)("sha256").update(body).digest("base64")}`;
13387
+ return `sha-256=${(0, import_crypto10.createHash)("sha256").update(body).digest("base64")}`;
12704
13388
  }
12705
13389
  function checkChallenge(challenge, hmacSecret, now) {
12706
13390
  const { id, ...rest } = challenge;
12707
13391
  const expected = Buffer.from(binding(rest, hmacSecret));
12708
13392
  const got = Buffer.from(String(id ?? ""));
12709
- if (expected.length !== got.length || !(0, import_crypto9.timingSafeEqual)(expected, got))
13393
+ if (expected.length !== got.length || !(0, import_crypto10.timingSafeEqual)(expected, got))
12710
13394
  return { ok: false, reason: "invalid_id" };
12711
13395
  if (challenge.expires && Date.parse(challenge.expires) < now.getTime())
12712
13396
  return { ok: false, reason: "expired" };
@@ -12722,7 +13406,7 @@ function verifyMppChallenge(challenge, hmacSecret, nowOrOptions = /* @__PURE__ *
12722
13406
  if (opts.body === void 0) return { ok: false, reason: "body_required" };
12723
13407
  const want = Buffer.from(challenge.digest);
12724
13408
  const got = Buffer.from(mppBodyDigest(opts.body));
12725
- if (want.length !== got.length || !(0, import_crypto9.timingSafeEqual)(want, got))
13409
+ if (want.length !== got.length || !(0, import_crypto10.timingSafeEqual)(want, got))
12726
13410
  return { ok: false, reason: "digest_mismatch" };
12727
13411
  }
12728
13412
  if (opts.store) {
@@ -12826,7 +13510,7 @@ async function verifyKyaPayTokens(header, opts) {
12826
13510
  }
12827
13511
 
12828
13512
  // src/aep/core.ts
12829
- var import_crypto10 = require("crypto");
13513
+ var import_crypto11 = require("crypto");
12830
13514
  var AEP_SPEC_VERSION = "aep-1.0.3-rc.2";
12831
13515
  function aepCanonicalJson(value) {
12832
13516
  if (value === null) return "null";
@@ -12847,7 +13531,7 @@ function aepCanonicalJson(value) {
12847
13531
  throw new Error(`AEP canonical JSON: unsupported value of type ${typeof value}`);
12848
13532
  }
12849
13533
  function digest(input) {
12850
- return (0, import_crypto10.createHash)("sha256").update(aepCanonicalJson(input), "utf8").digest();
13534
+ return (0, import_crypto11.createHash)("sha256").update(aepCanonicalJson(input), "utf8").digest();
12851
13535
  }
12852
13536
  function aepHash(input) {
12853
13537
  return `sha256:${digest(input).toString("hex")}`;
@@ -12860,7 +13544,7 @@ var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
12860
13544
  function aepPrivateKeyFromSeed(seedBase64) {
12861
13545
  const seed = Buffer.from(seedBase64, "base64");
12862
13546
  if (seed.length !== 32) throw new Error("Ed25519 seed must be 32 bytes");
12863
- return (0, import_crypto10.createPrivateKey)({
13547
+ return (0, import_crypto11.createPrivateKey)({
12864
13548
  key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
12865
13549
  format: "der",
12866
13550
  type: "pkcs8"
@@ -12869,14 +13553,14 @@ function aepPrivateKeyFromSeed(seedBase64) {
12869
13553
  function aepPublicKeyFromBase64(raw) {
12870
13554
  const bytes2 = Buffer.from(raw, "base64");
12871
13555
  if (bytes2.length !== 32) throw new Error("Ed25519 public key must be 32 bytes");
12872
- return (0, import_crypto10.createPublicKey)({
13556
+ return (0, import_crypto11.createPublicKey)({
12873
13557
  key: Buffer.concat([ED25519_SPKI_PREFIX, bytes2]),
12874
13558
  format: "der",
12875
13559
  type: "spki"
12876
13560
  });
12877
13561
  }
12878
13562
  function aepRawPublicKey(key) {
12879
- const pub = key.type === "private" ? (0, import_crypto10.createPublicKey)(key) : key;
13563
+ const pub = key.type === "private" ? (0, import_crypto11.createPublicKey)(key) : key;
12880
13564
  return pub.export({ format: "der", type: "spki" }).subarray(12).toString("base64");
12881
13565
  }
12882
13566
  function hashBytes(currentHash) {
@@ -12885,11 +13569,11 @@ function hashBytes(currentHash) {
12885
13569
  return Buffer.from(m[1], "hex");
12886
13570
  }
12887
13571
  function aepSign(currentHash, privateKey) {
12888
- return (0, import_crypto10.sign)(null, hashBytes(currentHash), privateKey).toString("base64");
13572
+ return (0, import_crypto11.sign)(null, hashBytes(currentHash), privateKey).toString("base64");
12889
13573
  }
12890
13574
  function aepVerifySignature(currentHash, signature, publicKey) {
12891
13575
  try {
12892
- return (0, import_crypto10.verify)(null, hashBytes(currentHash), publicKey, Buffer.from(signature, "base64"));
13576
+ return (0, import_crypto11.verify)(null, hashBytes(currentHash), publicKey, Buffer.from(signature, "base64"));
12893
13577
  } catch {
12894
13578
  return false;
12895
13579
  }
@@ -12987,7 +13671,7 @@ function aepPublicKeyDocument(keys, revocations = []) {
12987
13671
  }
12988
13672
 
12989
13673
  // src/acp/checkout-kit.ts
12990
- var import_crypto11 = require("crypto");
13674
+ var import_crypto12 = require("crypto");
12991
13675
  var ACP_API_VERSION = "2026-04-17";
12992
13676
  var ACP_SUPPORTED_API_VERSIONS = [ACP_API_VERSION];
12993
13677
  function acpPaymentHandlers(names) {
@@ -13017,6 +13701,23 @@ function buildAcpWellKnown(opts) {
13017
13701
  var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
13018
13702
  var apiError = (status, code, message, param) => json3(status, { type: "invalid_request", code, message, ...param && { param } });
13019
13703
  var total2 = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
13704
+ var clone2 = (v) => JSON.parse(JSON.stringify(v));
13705
+ var inFlight2 = (st) => st === "complete_in_progress" || st === "finishing";
13706
+ var ACP_CODES = /* @__PURE__ */ new Set([
13707
+ "missing",
13708
+ "invalid",
13709
+ "out_of_stock",
13710
+ "payment_declined",
13711
+ "requires_sign_in",
13712
+ "requires_3ds"
13713
+ ]);
13714
+ function heldBody2(text) {
13715
+ try {
13716
+ return JSON.parse(text).status === "pending_approval";
13717
+ } catch {
13718
+ return false;
13719
+ }
13720
+ }
13020
13721
  function createAcpCheckoutKit(opts) {
13021
13722
  const mem = /* @__PURE__ */ new Map();
13022
13723
  const store = opts.store ?? {
@@ -13027,7 +13728,14 @@ function createAcpCheckoutKit(opts) {
13027
13728
  if (!s || s.status !== from) return false;
13028
13729
  s.status = to;
13029
13730
  return true;
13030
- }
13731
+ },
13732
+ findByCheckoutSessionId: async (checkoutSessionId) => {
13733
+ for (const s of mem.values()) {
13734
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
13735
+ }
13736
+ return mem.get(checkoutSessionId) ?? null;
13737
+ },
13738
+ listHeld: async () => [...mem.values()].filter((s) => s.status === "pending_approval")
13031
13739
  };
13032
13740
  const idem = {
13033
13741
  store: opts.idempotencyStore ?? new MemoryIdempotencyStore(),
@@ -13045,16 +13753,19 @@ function createAcpCheckoutKit(opts) {
13045
13753
  );
13046
13754
  }
13047
13755
  const handlers = acpPaymentHandlers(opts.paymentHandlers);
13756
+ const handlerIds = new Set(handlers.flatMap((h) => [h.id, h.name]));
13757
+ const publicOrigin = opts.publicOrigin?.replace(/\/+$/, "");
13758
+ const onReportError = opts.onReportError ?? defaultOnReportError;
13048
13759
  function view(s) {
13049
13760
  return {
13050
13761
  id: s.id,
13051
- status: s.status,
13762
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
13052
13763
  currency: s.currency.toLowerCase(),
13053
13764
  line_items: s.line_items,
13054
13765
  totals: s.totals,
13055
13766
  fulfillment_options: [],
13056
13767
  messages: s.messages,
13057
- links: [],
13768
+ links: opts.links ?? [],
13058
13769
  capabilities: { payment: { handlers } },
13059
13770
  ...s.continue_url && s.status === "pending_approval" && { continue_url: s.continue_url },
13060
13771
  ...s.order && { order: s.order }
@@ -13064,7 +13775,8 @@ function createAcpCheckoutKit(opts) {
13064
13775
  const lines = [];
13065
13776
  const messages = [];
13066
13777
  for (const [i, it] of items.entries()) {
13067
- const c = it.id ? await opts.catalog(it.id) : null;
13778
+ const q = Math.max(1, Math.floor(it.quantity ?? 1));
13779
+ const c = it.id ? await opts.catalog(it.id, q) : null;
13068
13780
  if (!c) {
13069
13781
  messages.push({
13070
13782
  type: "error",
@@ -13075,7 +13787,6 @@ function createAcpCheckoutKit(opts) {
13075
13787
  });
13076
13788
  continue;
13077
13789
  }
13078
- const q = Math.max(1, Math.floor(it.quantity ?? 1));
13079
13790
  const amount = c.price * q;
13080
13791
  lines.push({
13081
13792
  id: `li_${i + 1}`,
@@ -13099,6 +13810,21 @@ function createAcpCheckoutKit(opts) {
13099
13810
  ]
13100
13811
  };
13101
13812
  }
13813
+ async function validate(s) {
13814
+ if (!opts.validateCart) return { messages: [], blocked: false };
13815
+ const found = await opts.validateCart(s);
13816
+ const messages = found.map((m) => {
13817
+ const error = (m.type ?? "error") === "error";
13818
+ return {
13819
+ type: m.type ?? "error",
13820
+ code: error ? ACP_CODES.has(m.code) ? m.code : "invalid" : m.code,
13821
+ content: m.content,
13822
+ ...error && { resolution: "recoverable" },
13823
+ ...m.path && { param: m.path }
13824
+ };
13825
+ });
13826
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
13827
+ }
13102
13828
  async function idempotent(req, body, path, caller, run) {
13103
13829
  const d = await beginIdempotent(idem, {
13104
13830
  method: req.method,
@@ -13114,29 +13840,43 @@ function createAcpCheckoutKit(opts) {
13114
13840
  headers: { ...d.headers, "idempotent-replayed": "true" }
13115
13841
  });
13116
13842
  const res = await run();
13117
- if (d.kind === "proceed")
13118
- await finishIdempotent(idem, d, {
13119
- status: res.status,
13120
- headers: { "content-type": "application/json" },
13121
- body: await res.clone().text()
13122
- });
13843
+ if (d.kind === "proceed") {
13844
+ const text = await res.clone().text();
13845
+ if (heldBody2(text)) await abandonIdempotent(idem, d);
13846
+ else
13847
+ await finishIdempotent(idem, d, {
13848
+ status: res.status,
13849
+ headers: { "content-type": "application/json" },
13850
+ body: text
13851
+ });
13852
+ }
13123
13853
  return res;
13124
13854
  }
13125
13855
  const inProgress = () => apiError(409, "checkout_in_progress", "This checkout session is already being completed.");
13126
- async function reportOutcome(p, settled) {
13127
- if (!opts.reportSettlement || !p.orderId) return;
13128
- await opts.reportSettlement({
13129
- orderId: p.orderId,
13130
- status: settled.status,
13131
- ...p.report,
13132
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
13133
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
13134
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
13135
- }).catch(() => void 0);
13856
+ async function reportOutcome(s, p, settled) {
13857
+ const report = opts.reportSettlement;
13858
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
13859
+ if (!report || !ref) return;
13860
+ await withReportRetries(
13861
+ () => report({
13862
+ ...ref,
13863
+ status: settled.status,
13864
+ ...p.report,
13865
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
13866
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
13867
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
13868
+ }),
13869
+ opts.reportRetry,
13870
+ isRetryableSettlementError
13871
+ ).catch(
13872
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
13873
+ );
13136
13874
  }
13137
13875
  async function finish(s, result, p, settled) {
13138
- await reportOutcome(p, settled);
13876
+ await reportOutcome(s, p, settled);
13139
13877
  delete s._pending;
13878
+ delete s._held;
13879
+ delete s._approvalId;
13140
13880
  if (settled.status === "failed") {
13141
13881
  s.status = "ready_for_payment";
13142
13882
  s.messages = [
@@ -13145,13 +13885,13 @@ function createAcpCheckoutKit(opts) {
13145
13885
  code: "payment_declined",
13146
13886
  content: "The payment was declined.",
13147
13887
  severity: "high",
13148
- resolution: "requires_buyer_input"
13888
+ resolution: "recoverable"
13149
13889
  }
13150
13890
  ];
13151
13891
  await store.put(s);
13152
13892
  return;
13153
13893
  }
13154
- const order = await opts.fulfil({ session: s, result });
13894
+ const order = await opts.fulfil({ session: s, result, settled });
13155
13895
  const permalink = order.permalink_url ?? (opts.permalinkBase && `${opts.permalinkBase.replace(/\/+$/, "")}/${encodeURIComponent(order.id)}`);
13156
13896
  s.order = {
13157
13897
  id: order.id,
@@ -13174,11 +13914,29 @@ function createAcpCheckoutKit(opts) {
13174
13914
  s.messages = [];
13175
13915
  await store.put(s);
13176
13916
  }
13177
- async function complete(req, s, body) {
13917
+ async function complete(req, s, body, agentArtifacts) {
13178
13918
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13179
- if (s.status === "complete_in_progress") return inProgress();
13919
+ if (inFlight2(s.status)) return inProgress();
13180
13920
  if (!body.payment_data)
13181
13921
  return apiError(400, "missing", "payment_data is required.", "$.payment_data");
13922
+ const paymentData = body.payment_data;
13923
+ if (Array.isArray(paymentData) || typeof paymentData !== "object" || typeof paymentData.handler_id !== "string" || !handlerIds.has(paymentData.handler_id)) {
13924
+ return apiError(
13925
+ 400,
13926
+ "invalid",
13927
+ "payment_data must be one object whose handler_id is the id or name of a payment handler this merchant declares.",
13928
+ "$.payment_data.handler_id"
13929
+ );
13930
+ }
13931
+ const v = await validate(s);
13932
+ if (v.blocked) {
13933
+ s.status = "not_ready_for_payment";
13934
+ s.messages = v.messages;
13935
+ delete s._held;
13936
+ delete s._approvalId;
13937
+ await store.put(s);
13938
+ return json3(200, view(s));
13939
+ }
13182
13940
  const from = s.status;
13183
13941
  if (store.claim) {
13184
13942
  if (!await store.claim(s.id, from, "complete_in_progress")) return inProgress();
@@ -13187,17 +13945,31 @@ function createAcpCheckoutKit(opts) {
13187
13945
  s.status = "complete_in_progress";
13188
13946
  await store.put(s);
13189
13947
  }
13948
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
13949
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
13190
13950
  try {
13191
13951
  const result = await opts.verifyPurchase({
13192
13952
  session: s,
13193
13953
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
13194
- astraId: req.headers.get("x-astra-id") ?? void 0,
13195
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
13196
- valueMajor: total2(s) / 100
13954
+ astraId,
13955
+ attemptId,
13956
+ valueMajor: total2(s) / 100,
13957
+ agentArtifacts,
13958
+ metadata: { kitSessionId: s.id, protocol: "acp" }
13197
13959
  });
13198
13960
  if (isHeld(result)) {
13199
13961
  s.status = "pending_approval";
13200
13962
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
13963
+ s._approvalId = result.stepUpApproval?.approvalId;
13964
+ s._astraId = astraId ?? result.agent?.astraId;
13965
+ s._attemptId = attemptId ?? result.attemptId;
13966
+ s._held = {
13967
+ line_items: clone2(s.line_items),
13968
+ totals: clone2(s.totals),
13969
+ ...scrubPaymentInput(paymentData) && { paymentData: scrubPaymentInput(paymentData) },
13970
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
13971
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
13972
+ };
13201
13973
  s.messages = [
13202
13974
  {
13203
13975
  type: "error",
@@ -13212,36 +13984,11 @@ function createAcpCheckoutKit(opts) {
13212
13984
  }
13213
13985
  if (!result.identityVerified || !result.policyAllowed) {
13214
13986
  s.status = from;
13215
- s.messages = [
13216
- {
13217
- type: "error",
13218
- code: "intervention_required",
13219
- content: "The agent is not authorized for this purchase.",
13220
- severity: "high",
13221
- resolution: "requires_buyer_review"
13222
- }
13223
- ];
13224
- await store.put(s);
13225
- return json3(200, view(s));
13226
- }
13227
- const settled = await opts.settle({
13228
- session: s,
13229
- result,
13230
- paymentData: body.payment_data
13231
- });
13232
- const orderId = result.settlementOutcome?.orderId;
13233
- const pending = {
13234
- ...orderId && { orderId },
13235
- report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
13236
- result: pendingResult(result)
13237
- };
13238
- if (settled.status === "pending") {
13239
- s._pending = pending;
13240
- s.messages = [];
13987
+ s.messages = [notAuthorized()];
13241
13988
  await store.put(s);
13242
13989
  return json3(200, view(s));
13243
13990
  }
13244
- await finish(s, result, pending, settled);
13991
+ await settleAndFinish(s, result, paymentData);
13245
13992
  return json3(200, view(s));
13246
13993
  } catch (e) {
13247
13994
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -13251,13 +13998,188 @@ function createAcpCheckoutKit(opts) {
13251
13998
  throw e;
13252
13999
  }
13253
14000
  }
14001
+ const notAuthorized = () => ({
14002
+ type: "error",
14003
+ code: "intervention_required",
14004
+ content: "The agent is not authorized for this purchase.",
14005
+ severity: "high",
14006
+ resolution: "requires_buyer_review"
14007
+ });
14008
+ async function settleAndFinish(s, result, paymentData) {
14009
+ const settled = await opts.settle({ session: s, result, paymentData });
14010
+ const orderId = result.settlementOutcome?.orderId;
14011
+ const sessionId = result.sessionId;
14012
+ const pending = {
14013
+ ...orderId && { orderId },
14014
+ ...sessionId && { sessionId },
14015
+ report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
14016
+ result: pendingResult(result)
14017
+ };
14018
+ if (settled.status === "pending") {
14019
+ s._pending = pending;
14020
+ s.messages = [];
14021
+ delete s._held;
14022
+ delete s._approvalId;
14023
+ await store.put(s);
14024
+ return "pending";
14025
+ }
14026
+ await finish(s, result, pending, settled);
14027
+ return settled.status === "failed" ? "payment_failed" : "completed";
14028
+ }
13254
14029
  async function resolvePending(sessionId, outcome) {
13255
14030
  const s = await store.get(sessionId);
13256
14031
  if (!s) return null;
13257
14032
  if (s.status !== "complete_in_progress" || !s._pending) return s;
13258
- await finish(s, s._pending.result, s._pending, outcome);
14033
+ if (store.claim) {
14034
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
14035
+ return await store.get(sessionId) ?? s;
14036
+ }
14037
+ s.status = "finishing";
14038
+ } else {
14039
+ s.status = "finishing";
14040
+ await store.put(s);
14041
+ }
14042
+ const pending = s._pending;
14043
+ try {
14044
+ await finish(s, pending.result, pending, outcome);
14045
+ } catch (e) {
14046
+ s.status = "complete_in_progress";
14047
+ s._pending = pending;
14048
+ await store.put(s).catch(() => void 0);
14049
+ throw e;
14050
+ }
13259
14051
  return s;
13260
14052
  }
14053
+ async function resumeApproved(sessionId, event) {
14054
+ const s = await store.get(sessionId);
14055
+ if (!s) return resumeResult("not_found", null);
14056
+ if (s.status === "completed") return resumeResult("already_completed", s);
14057
+ if (s.status === "canceled") return resumeResult("canceled", s);
14058
+ if (inFlight2(s.status)) return resumeResult("busy", s);
14059
+ if (s.status !== "pending_approval" || !s._held) return resumeResult("not_held", s);
14060
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
14061
+ return resumeResult("stale", s);
14062
+ if (store.claim) {
14063
+ if (!await store.claim(s.id, "pending_approval", "complete_in_progress"))
14064
+ return resumeResult("busy", await store.get(sessionId) ?? s);
14065
+ s.status = "complete_in_progress";
14066
+ } else {
14067
+ s.status = "complete_in_progress";
14068
+ await store.put(s);
14069
+ }
14070
+ const held = s._held;
14071
+ const backToHeld = async () => {
14072
+ s.status = "pending_approval";
14073
+ s._held = held;
14074
+ await store.put(s).catch(() => void 0);
14075
+ };
14076
+ try {
14077
+ s.line_items = clone2(held.line_items);
14078
+ s.totals = clone2(held.totals);
14079
+ const v = await validate(s);
14080
+ if (v.blocked) {
14081
+ s.status = "not_ready_for_payment";
14082
+ s.messages = v.messages;
14083
+ delete s._held;
14084
+ delete s._approvalId;
14085
+ delete s.continue_url;
14086
+ await store.put(s);
14087
+ return resumeResult("cart_invalid", s);
14088
+ }
14089
+ const result = await opts.verifyPurchase({
14090
+ session: s,
14091
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
14092
+ astraId: s._astraId,
14093
+ attemptId: s._attemptId,
14094
+ valueMajor: total2(s) / 100,
14095
+ metadata: { kitSessionId: s.id, protocol: "acp" },
14096
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
14097
+ });
14098
+ if (isHeld(result)) {
14099
+ await backToHeld();
14100
+ return resumeResult("still_held", s);
14101
+ }
14102
+ if (!result.identityVerified || !result.policyAllowed) {
14103
+ s.status = "ready_for_payment";
14104
+ s.messages = [notAuthorized()];
14105
+ delete s._held;
14106
+ delete s._approvalId;
14107
+ delete s.continue_url;
14108
+ await store.put(s);
14109
+ return resumeResult("not_authorized", s);
14110
+ }
14111
+ delete s.continue_url;
14112
+ return resumeResult(await settleAndFinish(s, result, held.paymentData), s);
14113
+ } catch (e) {
14114
+ if (!s._pending && inFlight2(s.status)) await backToHeld();
14115
+ throw e;
14116
+ }
14117
+ }
14118
+ async function resumeDeclined(sessionId, reason = "denied", event) {
14119
+ const s = await store.get(sessionId);
14120
+ if (!s) return resumeResult("not_found", null);
14121
+ if (s.status === "completed") return resumeResult("already_completed", s);
14122
+ if (s.status === "canceled") return resumeResult("canceled", s);
14123
+ if (inFlight2(s.status)) return resumeResult("busy", s);
14124
+ if (s.status !== "pending_approval") return resumeResult("not_held", s);
14125
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
14126
+ return resumeResult("stale", s);
14127
+ if (store.claim && !await store.claim(s.id, "pending_approval", "ready_for_payment")) {
14128
+ return resumeResult("busy", await store.get(sessionId) ?? s);
14129
+ }
14130
+ s.status = "ready_for_payment";
14131
+ delete s._held;
14132
+ delete s._approvalId;
14133
+ delete s.continue_url;
14134
+ s.messages = [
14135
+ {
14136
+ type: "error",
14137
+ code: `approval_${reason}`,
14138
+ content: reason === "denied" ? "The buyer declined this purchase." : "The approval for this purchase is no longer valid. Complete the session again to request a new one.",
14139
+ severity: "medium",
14140
+ resolution: "requires_buyer_review"
14141
+ }
14142
+ ];
14143
+ await store.put(s);
14144
+ return resumeResult("declined", s);
14145
+ }
14146
+ async function findForEvent(event) {
14147
+ if (event.kitSessionId) {
14148
+ const s = await store.get(event.kitSessionId);
14149
+ if (s) return s;
14150
+ }
14151
+ if (store.findByCheckoutSessionId)
14152
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
14153
+ return store.get(event.checkoutSessionId);
14154
+ }
14155
+ async function applyApprovalEvent(event) {
14156
+ const s = await findForEvent(event);
14157
+ if (!s) return resumeResult("not_found", null);
14158
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
14159
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
14160
+ }
14161
+ async function pollHeldSessions(o = {}) {
14162
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
14163
+ (x) => Boolean(x)
14164
+ ) : store.listHeld ? await store.listHeld() : [];
14165
+ const out = [];
14166
+ for (const s of sessions) {
14167
+ if (s.status !== "pending_approval" || !s._held?.pollUrl || !s._approvalId) continue;
14168
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
14169
+ approvalId: s._approvalId,
14170
+ pollUrl: s._held.pollUrl,
14171
+ expiresAt: s._held.expiresAt ?? ""
14172
+ });
14173
+ if (status === "approved" || status === "consumed") {
14174
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
14175
+ } else if (status === "denied" || status === "expired") {
14176
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
14177
+ } else {
14178
+ out.push({ sessionId: s.id, outcome: "pending" });
14179
+ }
14180
+ }
14181
+ return out;
14182
+ }
13261
14183
  async function route(req, reqCtx) {
13262
14184
  const version = req.headers.get("api-version");
13263
14185
  if (!version) return apiError(400, "missing", "The API-Version header is required.");
@@ -13282,6 +14204,7 @@ function createAcpCheckoutKit(opts) {
13282
14204
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId);
13283
14205
  if (!caller)
13284
14206
  return apiError(401, "unauthorized", "Send an Authorization header (or X-Astra-Id).");
14207
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "acp", publicOrigin });
13285
14208
  if (req.method === "POST" && path === "/checkout_sessions") {
13286
14209
  return idempotent(req, body, path, caller, async () => {
13287
14210
  const items = parsed.line_items ?? parsed.items;
@@ -13289,7 +14212,7 @@ function createAcpCheckoutKit(opts) {
13289
14212
  return apiError(400, "missing", "line_items is required.", "$.line_items");
13290
14213
  const p = await price(items);
13291
14214
  const s2 = {
13292
- id: `cs_${(0, import_crypto11.randomUUID)().replace(/-/g, "")}`,
14215
+ id: `cs_${(0, import_crypto12.randomUUID)().replace(/-/g, "")}`,
13293
14216
  status: p.lines.length && !p.messages.length ? "ready_for_payment" : "not_ready_for_payment",
13294
14217
  currency: opts.currency,
13295
14218
  line_items: p.lines,
@@ -13300,6 +14223,11 @@ function createAcpCheckoutKit(opts) {
13300
14223
  _astraCheckoutSessionId: req.headers.get("x-astra-checkout-session").slice(0, 64)
13301
14224
  }
13302
14225
  };
14226
+ if (s2.status === "ready_for_payment") {
14227
+ const v = await validate(s2);
14228
+ s2.messages = [...s2.messages, ...v.messages];
14229
+ if (v.blocked) s2.status = "not_ready_for_payment";
14230
+ }
13303
14231
  await store.put(s2);
13304
14232
  return json3(201, view(s2));
13305
14233
  });
@@ -13313,7 +14241,13 @@ function createAcpCheckoutKit(opts) {
13313
14241
  if (!m[2]) {
13314
14242
  return idempotent(req, body, path, caller, async () => {
13315
14243
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13316
- if (s.status === "complete_in_progress") return inProgress();
14244
+ if (inFlight2(s.status)) return inProgress();
14245
+ if (s.status === "pending_approval")
14246
+ return apiError(
14247
+ 409,
14248
+ "checkout_awaiting_approval",
14249
+ "This session is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over."
14250
+ );
13317
14251
  const items = parsed.line_items ?? parsed.items;
13318
14252
  if (items) {
13319
14253
  const p = await price(items);
@@ -13323,17 +14257,22 @@ function createAcpCheckoutKit(opts) {
13323
14257
  messages: p.messages,
13324
14258
  status: p.lines.length && !p.messages.length ? "ready_for_payment" : "not_ready_for_payment"
13325
14259
  });
14260
+ if (s.status === "ready_for_payment") {
14261
+ const v = await validate(s);
14262
+ s.messages = [...s.messages, ...v.messages];
14263
+ if (v.blocked) s.status = "not_ready_for_payment";
14264
+ }
13326
14265
  }
13327
14266
  await store.put(s);
13328
14267
  return json3(200, view(s));
13329
14268
  });
13330
14269
  }
13331
14270
  if (m[2] === "/complete")
13332
- return idempotent(req, body, path, caller, () => complete(req, s, parsed));
14271
+ return idempotent(req, body, path, caller, () => complete(req, s, parsed, agentArtifacts));
13333
14272
  return idempotent(req, body, path, caller, async () => {
13334
14273
  if (s.status === "completed" || s.status === "canceled")
13335
14274
  return apiError(405, "method_not_allowed", `A ${s.status} session can't be canceled.`);
13336
- if (s.status === "complete_in_progress") return inProgress();
14275
+ if (inFlight2(s.status)) return inProgress();
13337
14276
  s.status = "canceled";
13338
14277
  await store.put(s);
13339
14278
  return json3(200, view(s));
@@ -13349,7 +14288,17 @@ function createAcpCheckoutKit(opts) {
13349
14288
  if (requestId) headers.set("request-id", requestId);
13350
14289
  return new Response(res.body, { status: res.status, headers });
13351
14290
  }
13352
- return { handle, resolvePending };
14291
+ return {
14292
+ handle,
14293
+ resolvePending,
14294
+ /** 5.19.0 */
14295
+ protocol: "acp",
14296
+ publicOrigin,
14297
+ resumeApproved,
14298
+ resumeDeclined,
14299
+ applyApprovalEvent,
14300
+ pollHeldSessions
14301
+ };
13353
14302
  }
13354
14303
 
13355
14304
  // src/adapters/kit-express.ts
@@ -13371,11 +14320,46 @@ function rawBodyOf(req) {
13371
14320
  }
13372
14321
  return void 0;
13373
14322
  }
14323
+ var first = (v) => (Array.isArray(v) ? v[0] : v)?.split(",")[0]?.trim() || void 0;
14324
+ function originOf(req, publicOrigin) {
14325
+ if (publicOrigin) return publicOrigin.replace(/\/+$/, "");
14326
+ const trustProxy = Boolean(req.app?.get("trust proxy"));
14327
+ const host = trustProxy && first(req.headers["x-forwarded-host"]) || req.get?.("host") || req.headers.host || "localhost";
14328
+ const proto = trustProxy && first(req.headers["x-forwarded-proto"]) || req.protocol || "http";
14329
+ return `${proto}://${host}`;
14330
+ }
14331
+ var isKit = (k) => typeof k.applyApprovalEvent === "function" && (k.protocol === "ucp" || k.protocol === "acp");
14332
+ async function writeEvents(req, res, secret, kits, seen) {
14333
+ const raw = rawBodyOf(req);
14334
+ const text = raw === void 0 ? "" : typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
14335
+ const out = secret ? await handleApprovalEvents(text, req.headers, secret, { kits, ...seen && { seen } }) : { status: 500, body: { ok: false, error: "webhook_secret_not_configured" } };
14336
+ res.status(out.status);
14337
+ res.setHeader("content-type", "application/json");
14338
+ res.end(JSON.stringify(out.body));
14339
+ }
14340
+ function approvalEventsHandler(options) {
14341
+ return async (req, res, next) => {
14342
+ try {
14343
+ await writeEvents(req, res, options.secret, options.kits, options.seen);
14344
+ } catch (err) {
14345
+ next(err);
14346
+ }
14347
+ };
14348
+ }
13374
14349
  function expressHandler(kit, options = {}) {
13375
14350
  return async (req, res, next) => {
13376
14351
  try {
13377
- const host = req.get?.("host") ?? req.headers.host ?? "localhost";
13378
- const url = `${req.protocol ?? "http"}://${host}${req.originalUrl ?? req.url}`;
14352
+ if (options.approvalEventsPath && req.method === "POST") {
14353
+ const p = options.approvalEventsPath.replace(/\/+$/, "");
14354
+ const full = (req.originalUrl ?? req.url).split("?")[0].replace(/\/+$/, "");
14355
+ const local = req.url.split("?")[0].replace(/\/+$/, "");
14356
+ if (full === p || local === p) {
14357
+ const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);
14358
+ await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);
14359
+ return;
14360
+ }
14361
+ }
14362
+ const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;
13379
14363
  const headers = new Headers();
13380
14364
  for (const [k, v] of Object.entries(req.headers)) {
13381
14365
  if (v === void 0) continue;
@@ -13426,6 +14410,7 @@ function expressHandler(kit, options = {}) {
13426
14410
  MemoryCheckoutStore,
13427
14411
  MemoryIdempotencyStore,
13428
14412
  MemoryMppChallengeStore,
14413
+ MemorySeenEvents,
13429
14414
  PLATFORM_AGENT_SIGNATURES,
13430
14415
  RUNTIME_SIGNATURES,
13431
14416
  RedisIdempotencyStore,
@@ -13440,6 +14425,7 @@ function expressHandler(kit, options = {}) {
13440
14425
  VERIFICATION_STATUS_HEADER,
13441
14426
  VERSION,
13442
14427
  _resetEdgeConfigCache,
14428
+ abandonIdempotent,
13443
14429
  acpPaymentHandlers,
13444
14430
  aepCanonicalJson,
13445
14431
  aepHash,
@@ -13451,6 +14437,7 @@ function expressHandler(kit, options = {}) {
13451
14437
  aepTimestamp,
13452
14438
  aepVerifySignature,
13453
14439
  agent,
14440
+ approvalEventsHandler,
13454
14441
  astrasyncPaymentHandlers,
13455
14442
  authorizeSettlement,
13456
14443
  awaitStepUpApproval,
@@ -13497,6 +14484,7 @@ function expressHandler(kit, options = {}) {
13497
14484
  getEdgeConfig,
13498
14485
  getTrustLevel,
13499
14486
  getWellKnownUrls,
14487
+ handleApprovalEvents,
13500
14488
  hasCredentials,
13501
14489
  idempotencyMiddleware,
13502
14490
  isEdgeConfig,