@astrasyncai/verification-gateway 5.18.1 → 5.20.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 (129) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +217 -4
  3. package/dist/adapter-interface/interface.d.mts +2 -2
  4. package/dist/adapter-interface/interface.d.ts +2 -2
  5. package/dist/adapters/express.d.mts +9 -1
  6. package/dist/adapters/express.d.ts +9 -1
  7. package/dist/adapters/express.js +160 -52
  8. package/dist/adapters/express.js.map +1 -1
  9. package/dist/adapters/express.mjs +160 -52
  10. package/dist/adapters/express.mjs.map +1 -1
  11. package/dist/adapters/http-pdlss.d.mts +7 -1
  12. package/dist/adapters/http-pdlss.d.ts +7 -1
  13. package/dist/adapters/http-pdlss.js +18 -2
  14. package/dist/adapters/http-pdlss.js.map +1 -1
  15. package/dist/adapters/http-pdlss.mjs +16 -1
  16. package/dist/adapters/http-pdlss.mjs.map +1 -1
  17. package/dist/adapters/kit-express.d.mts +93 -1
  18. package/dist/adapters/kit-express.d.ts +93 -1
  19. package/dist/adapters/kit-express.js +181 -2
  20. package/dist/adapters/kit-express.js.map +1 -1
  21. package/dist/adapters/kit-express.mjs +180 -2
  22. package/dist/adapters/kit-express.mjs.map +1 -1
  23. package/dist/adapters/mcp.d.mts +102 -9
  24. package/dist/adapters/mcp.d.ts +102 -9
  25. package/dist/adapters/mcp.js +242 -19
  26. package/dist/adapters/mcp.js.map +1 -1
  27. package/dist/adapters/mcp.mjs +240 -19
  28. package/dist/adapters/mcp.mjs.map +1 -1
  29. package/dist/adapters/nextjs.d.mts +1 -1
  30. package/dist/adapters/nextjs.d.ts +1 -1
  31. package/dist/adapters/nextjs.js +97 -14
  32. package/dist/adapters/nextjs.js.map +1 -1
  33. package/dist/adapters/nextjs.mjs +97 -14
  34. package/dist/adapters/nextjs.mjs.map +1 -1
  35. package/dist/adapters/sdk.d.mts +101 -2
  36. package/dist/adapters/sdk.d.ts +101 -2
  37. package/dist/adapters/sdk.js +199 -12
  38. package/dist/adapters/sdk.js.map +1 -1
  39. package/dist/adapters/sdk.mjs +199 -12
  40. package/dist/adapters/sdk.mjs.map +1 -1
  41. package/dist/agent/index.d.mts +1 -1
  42. package/dist/agent/index.d.ts +1 -1
  43. package/dist/agent/index.js +1 -1
  44. package/dist/agent/index.js.map +1 -1
  45. package/dist/agent/index.mjs +1 -1
  46. package/dist/agent/index.mjs.map +1 -1
  47. package/dist/bin/astrasync-claude-hook.js +94 -16
  48. package/dist/bin/astrasync-codex-hook.js +94 -16
  49. package/dist/bin/astrasync-guard.js +94 -16
  50. package/dist/bin/astrasync.js +127 -16
  51. package/dist/browser/background.js +94 -16
  52. package/dist/browser/background.js.map +1 -1
  53. package/dist/browser/background.mjs +94 -16
  54. package/dist/browser/background.mjs.map +1 -1
  55. package/dist/cli/index.js +1 -1
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/index.mjs +1 -1
  58. package/dist/cli/index.mjs.map +1 -1
  59. package/dist/codex/index.js +94 -16
  60. package/dist/codex/index.js.map +1 -1
  61. package/dist/codex/index.mjs +94 -16
  62. package/dist/codex/index.mjs.map +1 -1
  63. package/dist/cursor/extension.js +94 -16
  64. package/dist/cursor/extension.js.map +1 -1
  65. package/dist/cursor/extension.mjs +94 -16
  66. package/dist/cursor/extension.mjs.map +1 -1
  67. package/dist/edge-config.d.mts +1 -1
  68. package/dist/edge-config.d.ts +1 -1
  69. package/dist/edge-config.js +1 -1
  70. package/dist/edge-config.js.map +1 -1
  71. package/dist/edge-config.mjs +1 -1
  72. package/dist/edge-config.mjs.map +1 -1
  73. package/dist/edge-core/index.d.mts +1 -1
  74. package/dist/edge-core/index.d.ts +1 -1
  75. package/dist/edge-core/index.js +69 -12
  76. package/dist/edge-core/index.js.map +1 -1
  77. package/dist/edge-core/index.mjs +69 -12
  78. package/dist/edge-core/index.mjs.map +1 -1
  79. package/dist/gateway/gateway.d.mts +2 -2
  80. package/dist/gateway/gateway.d.ts +2 -2
  81. package/dist/gateway/gateway.js +94 -16
  82. package/dist/gateway/gateway.js.map +1 -1
  83. package/dist/gateway/gateway.mjs +94 -16
  84. package/dist/gateway/gateway.mjs.map +1 -1
  85. package/dist/git-trigger/git-hooks.d.mts +1 -1
  86. package/dist/git-trigger/git-hooks.d.ts +1 -1
  87. package/dist/index.d.mts +706 -34
  88. package/dist/index.d.ts +706 -34
  89. package/dist/index.js +1391 -205
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +1374 -195
  92. package/dist/index.mjs.map +1 -1
  93. package/dist/local-evaluator/evaluator.d.mts +2 -2
  94. package/dist/local-evaluator/evaluator.d.ts +2 -2
  95. package/dist/registration/index.d.mts +11 -2
  96. package/dist/registration/index.d.ts +11 -2
  97. package/dist/registration/index.js +1 -1
  98. package/dist/registration/index.js.map +1 -1
  99. package/dist/registration/index.mjs +1 -1
  100. package/dist/registration/index.mjs.map +1 -1
  101. package/dist/transport/index.d.mts +1 -1
  102. package/dist/transport/index.d.ts +1 -1
  103. package/dist/transport/index.js +3 -2
  104. package/dist/transport/index.js.map +1 -1
  105. package/dist/transport/index.mjs +3 -2
  106. package/dist/transport/index.mjs.map +1 -1
  107. package/dist/{types-BRz2U0Pn.d.ts → types-B_HLhZ9n.d.ts} +1 -1
  108. package/dist/{types-BfILnheI.d.mts → types-CU17R4Nr.d.mts} +11 -0
  109. package/dist/{types-BfILnheI.d.ts → types-CU17R4Nr.d.ts} +11 -0
  110. package/dist/{types-Cns4l8gX.d.mts → types-CxjqmPl-.d.mts} +115 -13
  111. package/dist/{types-Cns4l8gX.d.ts → types-CxjqmPl-.d.ts} +115 -13
  112. package/dist/{types-3ObBpsnf.d.mts → types-D37aSQ9c.d.mts} +106 -3
  113. package/dist/{types-CfBpm3w5.d.mts → types-Djpz-L0w.d.mts} +1 -1
  114. package/dist/{types-BoxZEYPS.d.ts → types-Dn8tXnei.d.ts} +106 -3
  115. package/dist/ui/index.d.mts +1 -1
  116. package/dist/ui/index.d.ts +1 -1
  117. package/dist/verify.d.mts +50 -4
  118. package/dist/verify.d.ts +50 -4
  119. package/dist/verify.js +93 -14
  120. package/dist/verify.js.map +1 -1
  121. package/dist/verify.mjs +91 -14
  122. package/dist/verify.mjs.map +1 -1
  123. package/dist/webhooks.d.mts +16 -3
  124. package/dist/webhooks.d.ts +16 -3
  125. package/dist/webhooks.js +24 -13
  126. package/dist/webhooks.js.map +1 -1
  127. package/dist/webhooks.mjs +24 -13
  128. package/dist/webhooks.mjs.map +1 -1
  129. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -30,7 +30,7 @@ function getTrustLevel(score) {
30
30
  }
31
31
 
32
32
  // src/version.ts
33
- var SDK_VERSION = "5.18.1";
33
+ var SDK_VERSION = "5.20.0";
34
34
 
35
35
  // src/http.ts
36
36
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -282,7 +282,12 @@ function getCacheKey(request, counterpartyId) {
282
282
  // its start_checkout quote was served the cached quote-leg grant (no
283
283
  // settlementOutcome) and never reached the backend. Same F-A1-07 bug class.
284
284
  request.commercePhase || "",
285
- request.checkoutSessionId || ""
285
+ request.checkoutSessionId || "",
286
+ // 5.19.0: a route_config purpose claim is checked server-side against
287
+ // the stored rule for this method + path, so it changes the verdict.
288
+ request.purposeSource || "",
289
+ request.requestMethod || "",
290
+ request.requestPath || ""
286
291
  ].join("|");
287
292
  }
288
293
  function getCachedResult(request, counterpartyId) {
@@ -307,6 +312,7 @@ function cacheResult(request, result, configuredTtl, counterpartyId) {
307
312
  }
308
313
  function clearCache() {
309
314
  verificationCache.clear();
315
+ inflightVerifies.clear();
310
316
  }
311
317
  function extractCredentials(headers, query) {
312
318
  const credentials = {};
@@ -480,6 +486,9 @@ async function callVerifyAccessAPI(config, request) {
480
486
  if (requestData.enableRuntimeChallenge)
481
487
  body.enableRuntimeChallenge = requestData.enableRuntimeChallenge;
482
488
  if (requestData.routePolicy) body.routePolicy = requestData.routePolicy;
489
+ if (config.actingProof?.connectorToken) {
490
+ body.actingProof = { connectorToken: config.actingProof.connectorToken };
491
+ }
483
492
  if (requestData.approvalHeldByRoute) body.approvalHeldByRoute = true;
484
493
  if (requestData.createSession) body.createSession = requestData.createSession;
485
494
  if (requestData.durationRequired) body.durationRequired = requestData.durationRequired;
@@ -496,6 +505,11 @@ async function callVerifyAccessAPI(config, request) {
496
505
  if (requestData.buyerEmail) body.buyerEmail = requestData.buyerEmail;
497
506
  if (requestData.commerceArtifacts) body.commerceArtifacts = requestData.commerceArtifacts;
498
507
  if (requestData.attemptId) body.attemptId = requestData.attemptId;
508
+ if (requestData.purposeSource) body.purposeSource = requestData.purposeSource;
509
+ if (requestData.agentArtifacts) body.agentArtifacts = requestData.agentArtifacts;
510
+ if (requestData.metadata) body.metadata = requestData.metadata;
511
+ if (requestData.requestMethod) body.requestMethod = requestData.requestMethod;
512
+ if (requestData.requestPath) body.requestPath = requestData.requestPath;
499
513
  if (requestData.considerationSet) body.considerationSet = requestData.considerationSet;
500
514
  body.sdkVersion = SDK_VERSION;
501
515
  if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
@@ -642,22 +656,46 @@ async function verify(config, request, options) {
642
656
  "[VerificationGateway] minTrustScore / minTrustScoreForFull are deprecated in v2.3.0 and have no effect. Server is now the single source of truth for access decisions (verified ID + runtime challenge + PDLSS + trust score). To gate access to an endpoint, configure the endpoint's trust_score_requirement server-side."
643
657
  );
644
658
  }
645
- const uncacheable = Boolean(request.commerceArtifacts);
659
+ const uncacheable = Boolean(request.commerceArtifacts) || Boolean(mergedConfig.actingProof);
646
660
  if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
647
661
  const cached = getCachedResult(request, mergedConfig.counterpartyId);
648
662
  if (cached) {
649
663
  if (mergedConfig.debug) {
650
664
  console.log("[VerificationGateway] Returning cached result");
651
665
  }
652
- const hit = { ...cached };
653
- if (request.attemptId) {
654
- hit.attemptId = request.attemptId;
655
- } else {
656
- delete hit.attemptId;
657
- }
658
- return hit;
666
+ return sharedVerdict(cached, request);
667
+ }
668
+ }
669
+ if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
670
+ const key = getCacheKey(request, mergedConfig.counterpartyId);
671
+ const pending = inflightVerifies.get(key);
672
+ if (pending) return sharedVerdict(await pending, request);
673
+ const flight = verifyViaApi(mergedConfig, request, urls, uncacheable);
674
+ inflightVerifies.set(key, flight);
675
+ try {
676
+ return await flight;
677
+ } finally {
678
+ inflightVerifies.delete(key);
659
679
  }
660
680
  }
681
+ return verifyViaApi(mergedConfig, request, urls, uncacheable);
682
+ }
683
+ var inflightVerifies = /* @__PURE__ */ new Map();
684
+ var sharedVerdicts = /* @__PURE__ */ new WeakSet();
685
+ function isSharedVerdict(result) {
686
+ return sharedVerdicts.has(result);
687
+ }
688
+ function sharedVerdict(source, request) {
689
+ const hit = { ...source };
690
+ if (request.attemptId) {
691
+ hit.attemptId = request.attemptId;
692
+ } else {
693
+ delete hit.attemptId;
694
+ }
695
+ sharedVerdicts.add(hit);
696
+ return hit;
697
+ }
698
+ async function verifyViaApi(mergedConfig, request, urls, uncacheable) {
661
699
  const enrichedRequest = { ...request };
662
700
  if (!enrichedRequest.counterpartyUrl && mergedConfig.counterpartyUrl) {
663
701
  enrichedRequest.counterpartyUrl = mergedConfig.counterpartyUrl;
@@ -685,9 +723,18 @@ async function verify(config, request, options) {
685
723
  if (!apiResponse.access?.allowed) {
686
724
  const aggregatedFailures = apiResponse.access?.failures;
687
725
  const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
726
+ const denyAgent = idVerifiedFromBackend && apiResponse.agent ? {
727
+ astraId: apiResponse.agent.astraId,
728
+ name: apiResponse.agent.name,
729
+ trustScore: apiResponse.agent.trustScore,
730
+ trustLevel: getTrustLevel(apiResponse.agent.trustScore),
731
+ blockchainAnchored: apiResponse.agent.blockchainStatus === "anchored",
732
+ status: apiResponse.agent.agentStatus
733
+ } : void 0;
688
734
  const result2 = {
689
735
  identityVerified: idVerifiedFromBackend,
690
736
  policyAllowed: false,
737
+ ...denyAgent && { agent: denyAgent },
691
738
  // 3.6.0: the server now emits top-level denialReasons on every deny
692
739
  // path — prefer it verbatim; the failure-message reconstruction remains
693
740
  // as the fallback for older backends.
@@ -727,7 +774,9 @@ async function verify(config, request, options) {
727
774
  fulfilment: apiResponse.fulfilment,
728
775
  mandates: apiResponse.mandates,
729
776
  kyaToken: apiResponse.kyaToken,
730
- aep: apiResponse.aep
777
+ aep: apiResponse.aep,
778
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
779
+ warnings: apiResponse.warnings
731
780
  };
732
781
  return result2;
733
782
  }
@@ -795,7 +844,9 @@ async function verify(config, request, options) {
795
844
  // 5.15.0: merchant-lane AP2 mandates + KYAPay identity token.
796
845
  mandates: apiResponse.mandates,
797
846
  kyaToken: apiResponse.kyaToken,
798
- aep: apiResponse.aep
847
+ aep: apiResponse.aep,
848
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
849
+ warnings: apiResponse.warnings
799
850
  };
800
851
  if (result.recommendation === "deny") {
801
852
  result.policyAllowed = false;
@@ -821,8 +872,24 @@ async function verify(config, request, options) {
821
872
  cacheResult(request, result, mergedConfig.cacheTtl, mergedConfig.counterpartyId);
822
873
  }
823
874
  }
875
+ notifyVerificationListeners(request, result);
824
876
  return result;
825
877
  }
878
+ var verificationListeners = /* @__PURE__ */ new Set();
879
+ function onVerificationResult(listener) {
880
+ verificationListeners.add(listener);
881
+ return () => {
882
+ verificationListeners.delete(listener);
883
+ };
884
+ }
885
+ function notifyVerificationListeners(request, result) {
886
+ for (const listener of verificationListeners) {
887
+ try {
888
+ listener(result, request);
889
+ } catch {
890
+ }
891
+ }
892
+ }
826
893
  async function recordDecision(config, sessionId, decision, reason) {
827
894
  const headers = { "Content-Type": "application/json" };
828
895
  if (config.apiKey) {
@@ -879,7 +946,7 @@ async function reportAttempt(config, report, options = {}) {
879
946
  }
880
947
  var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
881
948
  var refusedPolicyReads = /* @__PURE__ */ new Set();
882
- async function fetchRoutes(config, counterpartyId) {
949
+ async function fetchRoutes(config, counterpartyId, options = {}) {
883
950
  if (!counterpartyId) return null;
884
951
  const headers = { "Content-Type": "application/json" };
885
952
  if (config.apiKey) {
@@ -887,8 +954,9 @@ async function fetchRoutes(config, counterpartyId) {
887
954
  headers["X-API-Key"] = config.apiKey;
888
955
  }
889
956
  try {
957
+ headers["X-AstraSync-SDK-Version"] = SDK_VERSION;
890
958
  const response = await sdkFetch(
891
- `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
959
+ `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes` + (options.codeGates?.length ? `?codeGates=${options.codeGates.map(encodeURIComponent).join(",")}` : ""),
892
960
  { method: "GET", headers }
893
961
  );
894
962
  if (!response.ok) {
@@ -901,6 +969,13 @@ async function fetchRoutes(config, counterpartyId) {
901
969
  return null;
902
970
  }
903
971
  const body = await response.json();
972
+ const type2 = body.data?.counterpartyType;
973
+ if (typeof type2 === "string" && options.onEndpointType) {
974
+ try {
975
+ options.onEndpointType(type2);
976
+ } catch {
977
+ }
978
+ }
904
979
  return body.data?.routes ?? [];
905
980
  } catch {
906
981
  return null;
@@ -1571,6 +1646,42 @@ async function detectFetchCommerce(request) {
1571
1646
  );
1572
1647
  }
1573
1648
 
1649
+ // src/adapters/routes-cache.ts
1650
+ var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
1651
+ function createRoutesCache(config, opts = {}) {
1652
+ const refreshMs = opts.refreshMs ?? DEFAULT_ROUTES_REFRESH_MS;
1653
+ let cached = [];
1654
+ let lastFetchAt = 0;
1655
+ let refreshing = null;
1656
+ async function refresh() {
1657
+ if (!config.counterpartyId) {
1658
+ opts.onNoCounterparty?.();
1659
+ return;
1660
+ }
1661
+ const fetched = await fetchRoutes(config, config.counterpartyId, opts.fetchOptions);
1662
+ if (fetched) {
1663
+ cached = fetched;
1664
+ lastFetchAt = Date.now();
1665
+ opts.onFetched?.(fetched);
1666
+ }
1667
+ }
1668
+ function start() {
1669
+ refreshing = refresh().catch(() => void 0).finally(() => {
1670
+ refreshing = null;
1671
+ });
1672
+ }
1673
+ start();
1674
+ return {
1675
+ async current() {
1676
+ if (refreshing) await refreshing.catch(() => {
1677
+ });
1678
+ if (config.counterpartyId && Date.now() - lastFetchAt > refreshMs) start();
1679
+ return cached;
1680
+ },
1681
+ peek: () => cached
1682
+ };
1683
+ }
1684
+
1574
1685
  // src/transport/http.ts
1575
1686
  var HEADER_PREFIX = "X-Astra-";
1576
1687
  function setHttpHeaders(headers, credentials) {
@@ -1747,6 +1858,20 @@ function resolveHttpPdlss(input) {
1747
1858
  }
1748
1859
  return { purpose, action, purposeSource, actionSource };
1749
1860
  }
1861
+ function wirePurposeSource(source) {
1862
+ switch (source) {
1863
+ case "route_config":
1864
+ return "route_config";
1865
+ case "header":
1866
+ case "legacy_header":
1867
+ case "query":
1868
+ return "header";
1869
+ case "custom_extractor":
1870
+ return "body";
1871
+ default:
1872
+ return "default";
1873
+ }
1874
+ }
1750
1875
 
1751
1876
  // src/pdlss-pre-check.ts
1752
1877
  function performCounterpartyPreCheck(routeConfig, astraCreds, purpose) {
@@ -2246,12 +2371,12 @@ var BeaconBuffer = class {
2246
2371
  }
2247
2372
  /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
2248
2373
  takeBatch(now) {
2249
- const first = this.queue.find((q) => q.eligibleAt <= now);
2250
- if (!first) return [];
2374
+ const first2 = this.queue.find((q) => q.eligibleAt <= now);
2375
+ if (!first2) return [];
2251
2376
  const batch = [];
2252
2377
  const rest = [];
2253
2378
  for (const q of this.queue) {
2254
- if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
2379
+ if (batch.length < this.opts.maxBatch && q.destKey === first2.destKey && q.eligibleAt <= now) {
2255
2380
  q.inFlight = true;
2256
2381
  batch.push(q);
2257
2382
  } else {
@@ -2528,7 +2653,6 @@ function defaultOnDenied(result, _req, res) {
2528
2653
  }
2529
2654
  });
2530
2655
  }
2531
- var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
2532
2656
  function createMiddleware(options) {
2533
2657
  if ("routes" in options) {
2534
2658
  console.warn(
@@ -2544,7 +2668,7 @@ function createMiddleware(options) {
2544
2668
  recordDecisions,
2545
2669
  enableRuntimeChallenge = true,
2546
2670
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
2547
- failOnError = "open",
2671
+ failOnError = "closed",
2548
2672
  caseInsensitiveRouteMatch = false,
2549
2673
  anonymousBeacon = true,
2550
2674
  trustVerifiedHop = false,
@@ -2568,26 +2692,19 @@ function createMiddleware(options) {
2568
2692
  rawHeaders: req.headers
2569
2693
  });
2570
2694
  } : void 0;
2571
- let cachedRoutes = [];
2572
- let lastFetchAt = 0;
2573
- let refreshing = null;
2574
2695
  let warnedNoCounterparty = false;
2575
2696
  let warnedEmptyRoutes = false;
2576
- async function refreshRoutes() {
2577
- if (!config.counterpartyId) {
2578
- if (!warnedNoCounterparty) {
2579
- console.warn(
2580
- "[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."
2581
- );
2582
- warnedNoCounterparty = true;
2583
- }
2584
- return;
2585
- }
2586
- const fetched = await fetchRoutes(config, config.counterpartyId);
2587
- if (fetched) {
2588
- cachedRoutes = fetched;
2589
- lastFetchAt = Date.now();
2590
- if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
2697
+ const routesCache = createRoutesCache(config, {
2698
+ refreshMs: routesRefreshMs,
2699
+ onNoCounterparty: () => {
2700
+ if (warnedNoCounterparty) return;
2701
+ console.warn(
2702
+ "[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."
2703
+ );
2704
+ warnedNoCounterparty = true;
2705
+ },
2706
+ onFetched: (routes) => {
2707
+ if (routes.length === 0 && !warnedEmptyRoutes) {
2591
2708
  const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
2592
2709
  console.warn(
2593
2710
  `[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`
@@ -2595,9 +2712,6 @@ function createMiddleware(options) {
2595
2712
  warnedEmptyRoutes = true;
2596
2713
  }
2597
2714
  }
2598
- }
2599
- refreshing = refreshRoutes().finally(() => {
2600
- refreshing = null;
2601
2715
  });
2602
2716
  return async (req, res, next) => {
2603
2717
  try {
@@ -2605,15 +2719,9 @@ function createMiddleware(options) {
2605
2719
  if (shouldSkip) {
2606
2720
  return next();
2607
2721
  }
2608
- if (refreshing) {
2609
- await refreshing.catch(() => {
2610
- });
2611
- }
2612
- if (config.counterpartyId && Date.now() - lastFetchAt > routesRefreshMs) {
2613
- refreshing = refreshRoutes().finally(() => {
2614
- refreshing = null;
2615
- });
2616
- }
2722
+ const cachedRoutes = (await routesCache.current()).filter(
2723
+ (r) => typeof r.pattern === "string" && !r.tool
2724
+ );
2617
2725
  const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
2618
2726
  const matchedRoute = findRouteConfig(cachedRoutes, req.path, req.method, {
2619
2727
  caseInsensitive: caseInsensitiveRouteMatch,
@@ -2737,6 +2845,12 @@ function createMiddleware(options) {
2737
2845
  ...commerceArtifacts && { commerceArtifacts },
2738
2846
  purpose,
2739
2847
  action: pdlssPair.action,
2848
+ // 5.19.0: the source + the request line let verify-access check a
2849
+ // route purpose against the stored rule (item 20: an MPP paid leg
2850
+ // keeps `shopping.purchase` instead of `commerce.payment.execute`).
2851
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
2852
+ requestMethod: req.method,
2853
+ requestPath: req.path,
2740
2854
  resource: req.path,
2741
2855
  createSession: shouldRecordDecisions,
2742
2856
  counterpartyUrl,
@@ -2785,14 +2899,14 @@ function createMiddleware(options) {
2785
2899
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
2786
2900
  res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2787
2901
  }
2788
- if (shouldRecordDecisions && sessionId) {
2902
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2789
2903
  recordDecision(config, sessionId, "granted").catch(() => {
2790
2904
  });
2791
2905
  }
2792
2906
  return next();
2793
2907
  }
2794
2908
  if (!result.policyAllowed) {
2795
- if (shouldRecordDecisions && sessionId) {
2909
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2796
2910
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
2797
2911
  });
2798
2912
  }
@@ -2805,7 +2919,7 @@ function createMiddleware(options) {
2805
2919
  }
2806
2920
  if (requiresHumanApproval(result)) {
2807
2921
  annotateApprovalRequired(result);
2808
- if (shouldRecordDecisions && sessionId) {
2922
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2809
2923
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
2810
2924
  });
2811
2925
  }
@@ -2814,7 +2928,8 @@ function createMiddleware(options) {
2814
2928
  return;
2815
2929
  }
2816
2930
  if (routeConfig.minTrustScore && result.agent) {
2817
- if (result.agent.trustScore < routeConfig.minTrustScore) {
2931
+ const score = result.agent.trustScore;
2932
+ if (typeof score !== "number" || score < routeConfig.minTrustScore) {
2818
2933
  const trustFailure = {
2819
2934
  dimension: "endpoint.trust",
2820
2935
  message: "Trust below the route requirement for this endpoint.",
@@ -2829,7 +2944,7 @@ function createMiddleware(options) {
2829
2944
  documentationUrl: wellKnownUrls.documentationUrl
2830
2945
  };
2831
2946
  }
2832
- if (shouldRecordDecisions && sessionId) {
2947
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2833
2948
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
2834
2949
  });
2835
2950
  }
@@ -2838,7 +2953,7 @@ function createMiddleware(options) {
2838
2953
  return;
2839
2954
  }
2840
2955
  }
2841
- if (shouldRecordDecisions && sessionId) {
2956
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2842
2957
  recordDecision(config, sessionId, "granted").catch(() => {
2843
2958
  });
2844
2959
  }
@@ -3258,6 +3373,10 @@ function createMiddleware2(options) {
3258
3373
  ...commerceArtifacts && { commerceArtifacts },
3259
3374
  purpose,
3260
3375
  action: pdlssPair.action,
3376
+ // 5.19.0: route purpose claim + request line (checked server-side).
3377
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
3378
+ requestMethod: request.method,
3379
+ requestPath: pathname,
3261
3380
  resource: pathname,
3262
3381
  counterpartyUrl,
3263
3382
  counterpartyType: config.counterpartyType || "website",
@@ -3458,6 +3577,7 @@ function isRetryableSettlementError(err) {
3458
3577
  }
3459
3578
 
3460
3579
  // src/adapters/sdk.ts
3580
+ var warnedPhaselessValue = false;
3461
3581
  var VerificationGatewayClient = class {
3462
3582
  constructor(options) {
3463
3583
  this.config = {
@@ -3489,9 +3609,16 @@ var VerificationGatewayClient = class {
3489
3609
  apiKey: options.apiKey,
3490
3610
  jwt: options.jwt
3491
3611
  };
3612
+ if (options.transactionValue && !options.commercePhase && !warnedPhaselessValue) {
3613
+ warnedPhaselessValue = true;
3614
+ console.warn(
3615
+ "[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."
3616
+ );
3617
+ }
3492
3618
  return this.executeWithRetry(
3493
3619
  () => verify(this.config, {
3494
3620
  credentials,
3621
+ ...options.commercePhase && { commercePhase: options.commercePhase },
3495
3622
  purpose: options.purpose,
3496
3623
  action: options.action,
3497
3624
  resourceType: options.resourceType,
@@ -3507,6 +3634,43 @@ var VerificationGatewayClient = class {
3507
3634
  })
3508
3635
  );
3509
3636
  }
3637
+ /**
3638
+ * 5.19.0: the QUOTE leg of a checkout — a price / limit check before the
3639
+ * purchase. Sends `commercePhase: 'quote'`: verify-access evaluates the
3640
+ * agent's policy at this value and tells you whether a purchase would be
3641
+ * granted, held for the owner's approval, or denied, but records no order,
3642
+ * moves no money and never redeems an approval. Always reaches the backend
3643
+ * (no verdict cache).
3644
+ *
3645
+ * Quotes count toward limit-probing detection: a run of rising quotes is
3646
+ * how an agent (or a merchant) maps a spending limit. Quote what the buyer
3647
+ * asked for, not a ladder of values.
3648
+ */
3649
+ async quoteCheckout(options) {
3650
+ const credentials = {
3651
+ astraId: options.astraId,
3652
+ apiKey: options.apiKey,
3653
+ jwt: options.jwt
3654
+ };
3655
+ return this.executeWithRetry(
3656
+ () => verify(
3657
+ this.config,
3658
+ {
3659
+ credentials,
3660
+ purpose: options.purpose ?? "shopping",
3661
+ action: options.action ?? "shopping.purchase",
3662
+ transactionValue: options.transactionValue,
3663
+ currency: options.currency,
3664
+ commercePhase: "quote",
3665
+ ...options.checkoutSessionId && { checkoutSessionId: options.checkoutSessionId },
3666
+ ...options.attemptId && { attemptId: options.attemptId },
3667
+ counterpartyUrl: options.counterpartyUrl,
3668
+ counterpartyType: options.counterpartyType
3669
+ },
3670
+ { bypassCache: true }
3671
+ )
3672
+ );
3673
+ }
3510
3674
  /**
3511
3675
  * 5.4.0 (astra-pay): confirm-leg of a checkout — the ONLY call that may fire
3512
3676
  * first-party charge-at-redeem (server-side settlement on the owner's saved
@@ -3545,6 +3709,14 @@ var VerificationGatewayClient = class {
3545
3709
  * is matched to — and only to — this confirm. A held cart's approval is
3546
3710
  * bound to its checkout session and exact value; a confirm for any other
3547
3711
  * cart is held for its own approval.
3712
+ *
3713
+ * 5.19.0 checkout kits: pass the kit's `agentArtifacts` (the inbound agent
3714
+ * request — signed RFC 9421 headers with the public URL and raw-body
3715
+ * digest, or `{ unsigned: true }`) and `metadata` (`{ kitSessionId,
3716
+ * protocol }`) from the `verifyPurchase` context. The platform attributes
3717
+ * the confirm to the agent from the artifacts, and stores the metadata on
3718
+ * any approval this confirm creates so the approval event
3719
+ * (`handleApprovalEvents`) can find the kit session.
3548
3720
  */
3549
3721
  async confirmCheckout(options) {
3550
3722
  const credentials = {
@@ -3567,6 +3739,8 @@ var VerificationGatewayClient = class {
3567
3739
  ...options.checkoutJwt && { checkoutJwt: options.checkoutJwt },
3568
3740
  ...options.attemptId && { attemptId: options.attemptId },
3569
3741
  buyerEmail: options.buyerEmail,
3742
+ ...options.agentArtifacts && { agentArtifacts: options.agentArtifacts },
3743
+ ...options.metadata && { metadata: options.metadata },
3570
3744
  counterpartyUrl: options.counterpartyUrl,
3571
3745
  counterpartyType: options.counterpartyType
3572
3746
  },
@@ -3627,6 +3801,80 @@ var VerificationGatewayClient = class {
3627
3801
  }
3628
3802
  return json4.data;
3629
3803
  }
3804
+ /**
3805
+ * 5.20.0 (round 29.1): change an endpoint's OPERATIONAL URLs with this
3806
+ * client's API key — `PATCH /api/endpoints/:id`. Accepts the key of the
3807
+ * endpoint's owner, or a personal key of an individual with an active seat
3808
+ * in the owning organisation. Policy (rules, trust floor, challenge, edge
3809
+ * config) stays dashboard-only. `null` clears a field. The owner is alerted
3810
+ * when `eventsUrl` changes.
3811
+ *
3812
+ * @param endpointId the ASTRAE id (defaults to the client's `counterpartyId`)
3813
+ * @returns the endpoint as stored, plus `changed` (the fields that changed)
3814
+ * @throws AstraSyncError — 400 `UNKNOWN_FIELDS` / validation, 401/403, 404
3815
+ */
3816
+ async updateEndpoint(endpointId, patch) {
3817
+ return this.endpointCall("PATCH", endpointId, "", patch);
3818
+ }
3819
+ /**
3820
+ * 5.20.0 (round 29.1): ask AstraSync to send ONE signed step-up event to the
3821
+ * endpoint's Events URL now — `POST /api/endpoints/:id/events/test`.
3822
+ * Delivered synchronously, outside the event outbox (random `eventId`, no
3823
+ * retries), signed with the endpoint's webhook secret like a real event.
3824
+ * Without `approvalId` the payload is synthetic (a kit acknowledges and
3825
+ * ignores it). With the id of a held kit approval at this endpoint the
3826
+ * payload is the real one, so your resume path runs: `approved` makes the
3827
+ * kit re-confirm (held again — nothing is charged); `denied` / `expired` /
3828
+ * `lapsed` release the session.
3829
+ *
3830
+ * Resolves even when your endpoint refused the event (`delivered: false`,
3831
+ * with `responseCode` / `error`).
3832
+ */
3833
+ async sendTestEvent(endpointId, options) {
3834
+ return this.endpointCall("POST", endpointId, "/events/test", options);
3835
+ }
3836
+ async endpointCall(method, endpointId, suffix, body) {
3837
+ const id = endpointId ?? this.config.counterpartyId;
3838
+ if (!id) {
3839
+ throw new AstraSyncError(
3840
+ "An endpoint id (ASTRAE-\u2026) is required: pass it, or set counterpartyId on the client",
3841
+ 400,
3842
+ "VALIDATION"
3843
+ );
3844
+ }
3845
+ if (!this.config.apiKey) {
3846
+ throw new AstraSyncError("This call needs the client apiKey", 401, "AUTH_REQUIRED");
3847
+ }
3848
+ const base = this.config.apiBaseUrl.replace(/\/+$/, "");
3849
+ const url = `${base}/endpoints/${encodeURIComponent(id)}${suffix}`;
3850
+ let res;
3851
+ try {
3852
+ res = await sdkFetch(url, {
3853
+ method,
3854
+ headers: {
3855
+ "Content-Type": "application/json",
3856
+ Authorization: `Bearer ${this.config.apiKey}`,
3857
+ "X-API-Key": this.config.apiKey
3858
+ },
3859
+ body: JSON.stringify(body),
3860
+ signal: AbortSignal.timeout(Math.max(this.timeout, 15e3))
3861
+ });
3862
+ } catch (err) {
3863
+ throw new AstraSyncError(
3864
+ `${method} ${suffix || "/endpoints/:id"} failed: ${err instanceof Error ? err.message : String(err)}`,
3865
+ 0,
3866
+ "network_error"
3867
+ );
3868
+ }
3869
+ const json4 = await res.json().catch(() => ({}));
3870
+ if (!res.ok || json4.success === false || json4.data === void 0) {
3871
+ const errObj = typeof json4.error === "object" ? json4.error : void 0;
3872
+ const code = json4.code ?? errObj?.code ?? (typeof json4.error === "string" ? json4.error : "unknown_error");
3873
+ const message = json4.message ?? errObj?.message ?? `HTTP ${res.status}`;
3874
+ throw new AstraSyncError(`${message} (${code})`, res.status, code);
3875
+ }
3876
+ return json4.data;
3877
+ }
3630
3878
  /**
3631
3879
  * Quick verification — checks credentials and policy in one call.
3632
3880
  *
@@ -4494,9 +4742,9 @@ function extractFulfillmentOption(body) {
4494
4742
  if (direct) return direct;
4495
4743
  const options = body.fulfillment_options;
4496
4744
  if (Array.isArray(options) && options.length > 0) {
4497
- const first = options[0];
4498
- if (first && typeof first === "object") {
4499
- const id = coerceString3(first.id);
4745
+ const first2 = options[0];
4746
+ if (first2 && typeof first2 === "object") {
4747
+ const id = coerceString3(first2.id);
4500
4748
  if (id) return id;
4501
4749
  }
4502
4750
  }
@@ -5503,7 +5751,8 @@ function verifyMPP(input) {
5503
5751
  }
5504
5752
  let replayOk = true;
5505
5753
  if (challenge?.digest && expiryOk) {
5506
- const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
5754
+ const c = challenge;
5755
+ const replayKey = `mpp:${challenge.digest}:${c.id ?? ""}:${c.nonce ?? ""}`;
5507
5756
  const expiresAt = (nowSec + tolerance) * 1e3;
5508
5757
  if (nonceStore.seen(replayKey, expiresAt)) {
5509
5758
  replayOk = false;
@@ -6063,11 +6312,11 @@ function resolveTransactionValue(input) {
6063
6312
  if (v) return v;
6064
6313
  }
6065
6314
  if (input.x402?.paymentRequired) {
6066
- const first = input.x402.paymentRequired.accepts[0];
6067
- if (first) {
6315
+ const first2 = input.x402.paymentRequired.accepts[0];
6316
+ if (first2) {
6068
6317
  const v = extractX402TransactionValue({
6069
- maxAmountRequired: Number(first.amount),
6070
- asset: first.asset
6318
+ maxAmountRequired: Number(first2.amount),
6319
+ asset: first2.asset
6071
6320
  });
6072
6321
  if (v) return v;
6073
6322
  }
@@ -6525,6 +6774,21 @@ function defaultMcpDenied(result, req, res) {
6525
6774
  }
6526
6775
  });
6527
6776
  }
6777
+ function dashboardToolRules(routes) {
6778
+ const out = /* @__PURE__ */ new Map();
6779
+ for (const r of routes) {
6780
+ if (typeof r.tool !== "string" || !r.tool || r.legacy === true) continue;
6781
+ if (out.has(r.tool)) continue;
6782
+ out.set(r.tool, {
6783
+ tool: r.tool,
6784
+ ...r.purpose && { purpose: r.purpose },
6785
+ ...r.action && { action: r.action },
6786
+ ...r.resource && { resource: r.resource },
6787
+ ...r.observe === true && { observe: true }
6788
+ });
6789
+ }
6790
+ return out;
6791
+ }
6528
6792
  function resolveGate(parsed, opts) {
6529
6793
  if (!parsed.toolName) {
6530
6794
  const methodGate = opts.methodGates?.[parsed.method];
@@ -6534,8 +6798,30 @@ function resolveGate(parsed, opts) {
6534
6798
  return { enforce: false, source: "discovery_default" };
6535
6799
  }
6536
6800
  const toolGate = opts.toolGates?.[parsed.toolName];
6801
+ const rule = opts.dashboardRules?.get(parsed.toolName);
6802
+ const fromRule = (r) => r.observe ? { enforce: false, source: "dashboard" } : {
6803
+ enforce: true,
6804
+ config: {
6805
+ ...r.purpose && { purpose: r.purpose },
6806
+ ...r.action && { action: r.action },
6807
+ ...r.resource && { resource: r.resource }
6808
+ },
6809
+ source: "dashboard"
6810
+ };
6811
+ const codeGated = toolGate !== void 0 && toolGate !== "observe";
6812
+ if (rule && (opts.toolGatePrecedence === "dashboard" || !codeGated)) return fromRule(rule);
6537
6813
  if (toolGate !== void 0) {
6538
- return toolGate === "observe" ? { enforce: false, source: "toolGate" } : { enforce: true, config: toolGate, source: "toolGate" };
6814
+ if (toolGate === "observe") return { enforce: false, source: "toolGate" };
6815
+ if (rule && !rule.observe && rule.purpose && (rule.purpose !== toolGate.purpose || rule.action && rule.action !== toolGate.action)) {
6816
+ const extra = fromRule(rule).config;
6817
+ return {
6818
+ enforce: true,
6819
+ config: toolGate,
6820
+ additional: { ...extra, ...!extra.resource && toolGate.resource && { resource: toolGate.resource } },
6821
+ source: "toolGate"
6822
+ };
6823
+ }
6824
+ return { enforce: true, config: toolGate, source: "toolGate" };
6539
6825
  }
6540
6826
  return { enforce: mcpDefaultEnforced(parsed), source: "tier" };
6541
6827
  }
@@ -6550,13 +6836,34 @@ function createMcpMiddleware(options) {
6550
6836
  verifiedHopMaxAgeMs,
6551
6837
  recordDecisions,
6552
6838
  enableRuntimeChallenge = true,
6553
- failOnError = "open",
6839
+ failOnError = "closed",
6840
+ toolGatePrecedence = "code",
6841
+ routesRefreshMs,
6842
+ skipEvaluation,
6554
6843
  ...config
6555
6844
  } = options;
6845
+ const skipTools = new Set(skipEvaluation?.tools ?? []);
6846
+ const skipMethods = new Set(skipEvaluation?.methods ?? []);
6556
6847
  if (config.apiBaseUrl) {
6557
6848
  prefetchWellKnown(config.apiBaseUrl).catch(() => {
6558
6849
  });
6559
6850
  }
6851
+ const codeGatedTools = Object.entries(toolGates ?? {}).filter(([, g]) => g !== "observe").map(([name]) => name);
6852
+ let warnedEndpointType = false;
6853
+ const onEndpointType = (counterpartyType) => {
6854
+ if (counterpartyType === "mcp_server" || warnedEndpointType) return;
6855
+ warnedEndpointType = true;
6856
+ console.warn(
6857
+ `[VerificationGateway/MCP] counterpartyId ${config.counterpartyId} is a '${counterpartyType}' endpoint, not an 'mcp_server' endpoint. Dashboard tool rules are read from an mcp_server endpoint only: register a separate MCP endpoint for this server (same owner) and use its id here. See https://astrasync.ai/docs/mcp-integration#tool-rules.`
6858
+ );
6859
+ };
6860
+ const rulesCache = config.apiKey && config.counterpartyId ? createRoutesCache(config, {
6861
+ ...routesRefreshMs !== void 0 && { refreshMs: routesRefreshMs },
6862
+ fetchOptions: {
6863
+ ...codeGatedTools.length && { codeGates: codeGatedTools },
6864
+ onEndpointType
6865
+ }
6866
+ }) : void 0;
6560
6867
  return async (req, res, next) => {
6561
6868
  try {
6562
6869
  if (skip) return next();
@@ -6605,10 +6912,19 @@ function createMcpMiddleware(options) {
6605
6912
  return next();
6606
6913
  }
6607
6914
  }
6915
+ if (!parsed.toolName && skipMethods.has(parsed.method) && methodGates?.[parsed.method] === void 0) {
6916
+ return next();
6917
+ }
6918
+ const dashboardRules = rulesCache ? dashboardToolRules(await rulesCache.current()) : void 0;
6608
6919
  const gateDecision = resolveGate(parsed, {
6609
6920
  toolGates,
6610
- methodGates
6921
+ methodGates,
6922
+ dashboardRules,
6923
+ toolGatePrecedence
6611
6924
  });
6925
+ if (!gateDecision.enforce && (parsed.toolName && skipTools.has(parsed.toolName) || !parsed.toolName && skipMethods.has(parsed.method))) {
6926
+ return next();
6927
+ }
6612
6928
  const credentials = extractCredentials(
6613
6929
  req.headers,
6614
6930
  req.query
@@ -6665,8 +6981,14 @@ function createMcpMiddleware(options) {
6665
6981
  const forwardedFor = req.headers["x-forwarded-for"];
6666
6982
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
6667
6983
  const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
6984
+ const attemptRaw = readSingleHeader(req.headers["x-astra-attempt-id"]);
6985
+ const metaAttempt = req.body?.params?._meta?.astrasync?.attemptId;
6986
+ const attemptId = [attemptRaw, metaAttempt].find(
6987
+ (v) => typeof v === "string" && /^att_[0-9a-f]{32}$/.test(v)
6988
+ );
6668
6989
  const verificationRequest = {
6669
6990
  credentials,
6991
+ ...attemptId && { attemptId },
6670
6992
  purpose: pdlss.purpose,
6671
6993
  action: pdlss.action,
6672
6994
  resource: pdlss.resource,
@@ -6705,6 +7027,20 @@ function createMcpMiddleware(options) {
6705
7027
  result.stepUpOutcome = outcome;
6706
7028
  }
6707
7029
  }
7030
+ if (shouldEnforce && gateDecision.additional?.purpose && result.policyAllowed && !requiresHumanApproval(result)) {
7031
+ const extra = mcpToPdlss(parsed, req.path, void 0, void 0, gateDecision.additional);
7032
+ const extraResult = await verify(config, {
7033
+ ...verificationRequest,
7034
+ purpose: extra.purpose,
7035
+ action: extra.action,
7036
+ resource: extra.resource
7037
+ });
7038
+ if (!extraResult.policyAllowed || requiresHumanApproval(extraResult)) {
7039
+ result = extraResult;
7040
+ req.agentVerification = result;
7041
+ sessionId = result.sessionId;
7042
+ }
7043
+ }
6708
7044
  if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
6709
7045
  res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6710
7046
  return next();
@@ -6714,14 +7050,14 @@ function createMcpMiddleware(options) {
6714
7050
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
6715
7051
  res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
6716
7052
  }
6717
- if (shouldRecordDecisions && sessionId) {
7053
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6718
7054
  recordDecision(config, sessionId, "granted").catch(() => {
6719
7055
  });
6720
7056
  }
6721
7057
  return next();
6722
7058
  }
6723
7059
  if (!result.policyAllowed) {
6724
- if (shouldRecordDecisions && sessionId) {
7060
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6725
7061
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
6726
7062
  });
6727
7063
  }
@@ -6731,7 +7067,7 @@ function createMcpMiddleware(options) {
6731
7067
  }
6732
7068
  if (requiresHumanApproval(result)) {
6733
7069
  annotateApprovalRequired(result);
6734
- if (shouldRecordDecisions && sessionId) {
7070
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6735
7071
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
6736
7072
  });
6737
7073
  }
@@ -6749,7 +7085,7 @@ function createMcpMiddleware(options) {
6749
7085
  })
6750
7086
  );
6751
7087
  }
6752
- if (shouldRecordDecisions && sessionId) {
7088
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6753
7089
  recordDecision(config, sessionId, "granted").catch(() => {
6754
7090
  });
6755
7091
  }
@@ -10011,15 +10347,15 @@ function requireDumper() {
10011
10347
  return !isWhitespace(c) && c !== CHAR_COLON;
10012
10348
  }
10013
10349
  function codePointAt(string, pos) {
10014
- const first = string.charCodeAt(pos);
10350
+ const first2 = string.charCodeAt(pos);
10015
10351
  let second;
10016
- if (first >= 55296 && first <= 56319 && pos + 1 < string.length) {
10352
+ if (first2 >= 55296 && first2 <= 56319 && pos + 1 < string.length) {
10017
10353
  second = string.charCodeAt(pos + 1);
10018
10354
  if (second >= 56320 && second <= 57343) {
10019
- return (first - 55296) * 1024 + second - 56320 + 65536;
10355
+ return (first2 - 55296) * 1024 + second - 56320 + 65536;
10020
10356
  }
10021
10357
  }
10022
- return first;
10358
+ return first2;
10023
10359
  }
10024
10360
  function needIndentIndicator(string) {
10025
10361
  const leadingSpaceRe = /^\n* /;
@@ -10758,6 +11094,16 @@ function checkUcpAgentBinding(header, identities) {
10758
11094
 
10759
11095
  // src/ucp/profile.ts
10760
11096
  var UCP_VERSION = "2026-08-25";
11097
+ function ucpProfileKeys(profile) {
11098
+ const out = [];
11099
+ for (const list of [profile?.keys, profile?.signing_keys]) {
11100
+ if (!Array.isArray(list)) continue;
11101
+ for (const k of list) {
11102
+ if (k && typeof k === "object" && k.use !== "enc") out.push(k);
11103
+ }
11104
+ }
11105
+ return out;
11106
+ }
10761
11107
  var SPEC = `https://ucp.dev/${UCP_VERSION}/specification`;
10762
11108
  var SCHEMAS = `https://ucp.dev/${UCP_VERSION}/schemas`;
10763
11109
  function astrasyncPaymentHandlers(handlers, specBase = "https://astrasync.ai/ucp/handlers") {
@@ -11678,6 +12024,12 @@ async function finishIdempotent(opts, decision, response) {
11678
12024
  } catch {
11679
12025
  }
11680
12026
  }
12027
+ async function abandonIdempotent(opts, decision) {
12028
+ try {
12029
+ await opts.store.delete(decision.storeKey);
12030
+ } catch {
12031
+ }
12032
+ }
11681
12033
  var KEPT_HEADERS = ["content-type", "location"];
11682
12034
  function withHeader(res, name, value) {
11683
12035
  const headers = new Headers(res.headers);
@@ -11833,8 +12185,102 @@ function warnInMemoryStores(kit, missing) {
11833
12185
  `[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.`
11834
12186
  );
11835
12187
  }
12188
+ var SECRET_KEY = /token|secret|credential|cvc|cvv|number|pan|password|key|cryptogram|signature/i;
12189
+ function scrubPaymentInput(input) {
12190
+ if (!input || typeof input !== "object") return void 0;
12191
+ const out = {};
12192
+ for (const [k, v] of Object.entries(input)) {
12193
+ if (SECRET_KEY.test(k)) continue;
12194
+ if (v === null || ["string", "number", "boolean"].includes(typeof v)) out[k] = v;
12195
+ }
12196
+ return out;
12197
+ }
12198
+ var NEVER_FORWARD = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "proxy-authorization"]);
12199
+ function coveredHeaders(signatureInput) {
12200
+ const names = /* @__PURE__ */ new Set();
12201
+ for (const m of signatureInput.matchAll(/\(([^)]*)\)/g)) {
12202
+ for (const q of m[1].matchAll(/"([^"]+)"/g)) {
12203
+ const name = q[1].toLowerCase();
12204
+ if (!name.startsWith("@")) names.add(name);
12205
+ }
12206
+ }
12207
+ return [...names];
12208
+ }
12209
+ function buildAgentArtifacts(req, rawBody, opts) {
12210
+ const signature = req.headers.get("signature");
12211
+ const signatureInput = req.headers.get("signature-input");
12212
+ if (opts.protocol === "acp" || !signature || !signatureInput) {
12213
+ return { unsigned: true, protocol: opts.protocol };
12214
+ }
12215
+ const headers = {};
12216
+ const names = [
12217
+ "signature",
12218
+ "signature-input",
12219
+ "signature-agent",
12220
+ "ucp-agent",
12221
+ "content-digest",
12222
+ ...coveredHeaders(signatureInput)
12223
+ ];
12224
+ for (const name of names) {
12225
+ if (NEVER_FORWARD.has(name)) continue;
12226
+ const v = req.headers.get(name);
12227
+ if (v !== null) headers[name] = v;
12228
+ }
12229
+ const u = new URL(req.url);
12230
+ const origin = opts.publicOrigin ? opts.publicOrigin.replace(/\/+$/, "") : u.origin;
12231
+ return {
12232
+ protocol: opts.protocol,
12233
+ method: req.method,
12234
+ url: `${origin}${u.pathname}${u.search}`,
12235
+ headers,
12236
+ ...rawBody && { contentDigest: contentDigest(rawBody) }
12237
+ };
12238
+ }
12239
+ async function withReportRetries(fn, opts = {}, isRetryable) {
12240
+ const attempts = Math.max(1, opts.attempts ?? 3);
12241
+ const backoff = opts.backoffMs ?? 250;
12242
+ let last;
12243
+ for (let i = 0; i < attempts; i++) {
12244
+ try {
12245
+ return await fn();
12246
+ } catch (err) {
12247
+ last = err;
12248
+ if (i === attempts - 1 || !isRetryable(err)) break;
12249
+ await new Promise((r) => setTimeout(r, backoff * 2 ** i));
12250
+ }
12251
+ }
12252
+ throw last;
12253
+ }
12254
+ function defaultOnReportError(err, ctx) {
12255
+ console.error(
12256
+ `[astrasync] checkout kit: ${ctx.kind === "order_webhook" ? "order webhook" : "settlement report"} failed for session ${ctx.sessionId}${ctx.orderId ? ` (order ${ctx.orderId})` : ""}:`,
12257
+ err
12258
+ );
12259
+ }
12260
+ var NON_TERMINAL = /* @__PURE__ */ new Set(["busy", "still_held"]);
12261
+ function isTerminalOutcome(outcome) {
12262
+ return !NON_TERMINAL.has(outcome);
12263
+ }
12264
+ var resumeResult = (outcome, session) => ({
12265
+ outcome,
12266
+ terminal: isTerminalOutcome(outcome),
12267
+ session
12268
+ });
11836
12269
 
11837
12270
  // src/ucp/checkout-kit.ts
12271
+ function withValidImageUrl(item) {
12272
+ if (item.image_url === void 0) return item;
12273
+ let ok = false;
12274
+ try {
12275
+ const u = new URL(item.image_url);
12276
+ ok = u.protocol === "https:" || u.protocol === "http:";
12277
+ } catch {
12278
+ ok = false;
12279
+ }
12280
+ if (ok) return item;
12281
+ const { image_url: _drop, ...rest } = item;
12282
+ return rest;
12283
+ }
11838
12284
  var MemoryCheckoutStore = class {
11839
12285
  constructor() {
11840
12286
  this.m = /* @__PURE__ */ new Map();
@@ -11857,6 +12303,15 @@ var MemoryCheckoutStore = class {
11857
12303
  const id = this.orders.get(orderId);
11858
12304
  return id ? this.m.get(id) ?? null : null;
11859
12305
  }
12306
+ async findByCheckoutSessionId(checkoutSessionId) {
12307
+ for (const s of this.m.values()) {
12308
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
12309
+ }
12310
+ return this.m.get(checkoutSessionId) ?? null;
12311
+ }
12312
+ async listHeld() {
12313
+ return [...this.m.values()].filter((s) => s.status === "requires_escalation");
12314
+ }
11860
12315
  };
11861
12316
  function buildUcpOrder(session, opts = {}) {
11862
12317
  if (!session.order) return null;
@@ -11879,11 +12334,20 @@ function buildUcpOrder(session, opts = {}) {
11879
12334
  totals: session.totals
11880
12335
  };
11881
12336
  }
12337
+ function heldBody(text) {
12338
+ try {
12339
+ return JSON.parse(text).status === "requires_escalation";
12340
+ } catch {
12341
+ return false;
12342
+ }
12343
+ }
11882
12344
  var json2 = (status, body, headers = {}) => new Response(JSON.stringify(body), {
11883
12345
  status,
11884
12346
  headers: { "content-type": "application/json", ...headers }
11885
12347
  });
11886
12348
  var total = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
12349
+ var clone = (v) => JSON.parse(JSON.stringify(v));
12350
+ var inFlight = (st) => st === "complete_in_progress" || st === "finishing";
11887
12351
  function createUcpCheckoutKit(opts) {
11888
12352
  const profile = buildUcpProfile(opts.profile);
11889
12353
  const store = opts.store ?? new MemoryCheckoutStore();
@@ -11909,6 +12373,11 @@ function createUcpCheckoutKit(opts) {
11909
12373
  ...entries.map((e) => e.id)
11910
12374
  ])
11911
12375
  );
12376
+ const publicOrigin = (opts.publicOrigin ?? new URL(opts.profile.endpoint).origin).replace(
12377
+ /\/+$/,
12378
+ ""
12379
+ );
12380
+ const onReportError = opts.onReportError ?? defaultOnReportError;
11912
12381
  const fetchProfile = opts.fetchProfile ?? (async (url, fresh) => {
11913
12382
  const r = await fetchUcpProfile(url, { fresh });
11914
12383
  return r.ok ? r.profile : r;
@@ -11930,7 +12399,7 @@ function createUcpCheckoutKit(opts) {
11930
12399
  payment_handlers: profile.ucp.payment_handlers
11931
12400
  },
11932
12401
  id: s.id,
11933
- status: s.status,
12402
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
11934
12403
  currency: s.currency,
11935
12404
  line_items: s.line_items,
11936
12405
  totals: s.totals,
@@ -11976,6 +12445,23 @@ function createUcpCheckoutKit(opts) {
11976
12445
  "ephemeral"
11977
12446
  );
11978
12447
  }
12448
+ function heldNoEdit(s) {
12449
+ return render(
12450
+ {
12451
+ ...s,
12452
+ messages: [
12453
+ ...s.messages ?? [],
12454
+ ucpError(
12455
+ "checkout_awaiting_approval",
12456
+ "This checkout is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over.",
12457
+ "requires_buyer_review"
12458
+ )
12459
+ ]
12460
+ },
12461
+ 409,
12462
+ "ephemeral"
12463
+ );
12464
+ }
11979
12465
  async function save(s) {
11980
12466
  if (s.order) orderIndex.set(s.order.id, s.id);
11981
12467
  await store.put(s);
@@ -12006,31 +12492,44 @@ function createUcpCheckoutKit(opts) {
12006
12492
  return r;
12007
12493
  }
12008
12494
  function emitOrder(s) {
12009
- void deliverOrder(s).catch(() => void 0);
12010
- }
12011
- async function reportOutcome(p, settled) {
12012
- if (!opts.reportSettlement || !p.orderId) return;
12013
- await opts.reportSettlement({
12014
- orderId: p.orderId,
12015
- status: settled.status,
12016
- ...p.report,
12017
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12018
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12019
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12020
- }).catch(() => void 0);
12495
+ const ctx = { sessionId: s.id, orderId: s.order?.id, kind: "order_webhook" };
12496
+ void deliverOrder(s).then((r) => {
12497
+ if (r && !r.ok) onReportError(new Error(`order webhook failed (HTTP ${r.status})`), ctx);
12498
+ }).catch((err) => onReportError(err, ctx));
12499
+ }
12500
+ async function reportOutcome(s, p, settled) {
12501
+ const report = opts.reportSettlement;
12502
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
12503
+ if (!report || !ref) return;
12504
+ await withReportRetries(
12505
+ () => report({
12506
+ ...ref,
12507
+ status: settled.status,
12508
+ ...p.report,
12509
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12510
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12511
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12512
+ }),
12513
+ opts.reportRetry,
12514
+ isRetryableSettlementError
12515
+ ).catch(
12516
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
12517
+ );
12021
12518
  }
12022
12519
  async function finish(s, result, p, settled) {
12023
- await reportOutcome(p, settled);
12520
+ await reportOutcome(s, p, settled);
12024
12521
  delete s._pending;
12522
+ delete s._held;
12523
+ delete s._approvalId;
12025
12524
  if (settled.status === "failed") {
12026
12525
  s.status = "ready_for_complete";
12027
12526
  s.messages = [
12028
- ucpError("payment_failed", "The payment did not go through.", "requires_buyer_input")
12527
+ ucpError("payment_failed", "The payment did not go through.", "recoverable")
12029
12528
  ];
12030
12529
  await save(s);
12031
12530
  return;
12032
12531
  }
12033
- const order = await opts.fulfil({ session: s, result });
12532
+ const order = await opts.fulfil({ session: s, result, settled });
12034
12533
  s.order = {
12035
12534
  ...order,
12036
12535
  ...!order.permalink_url && opts.permalinkBase && {
@@ -12064,7 +12563,9 @@ function createUcpCheckoutKit(opts) {
12064
12563
  const out = [];
12065
12564
  const messages = [];
12066
12565
  for (const [i, l] of lines.entries()) {
12067
- const item = l.item?.id ? await opts.catalog(l.item.id) : null;
12566
+ const q = Math.max(1, Math.floor(l.quantity ?? 1));
12567
+ const found = l.item?.id ? await opts.catalog(l.item.id, q) : null;
12568
+ const item = found && withValidImageUrl(found);
12068
12569
  if (!item) {
12069
12570
  messages.push(
12070
12571
  ucpError(
@@ -12076,7 +12577,6 @@ function createUcpCheckoutKit(opts) {
12076
12577
  );
12077
12578
  continue;
12078
12579
  }
12079
- const q = Math.max(1, Math.floor(l.quantity ?? 1));
12080
12580
  const amount = item.price * q;
12081
12581
  out.push({
12082
12582
  id: `li_${i + 1}`,
@@ -12098,6 +12598,19 @@ function createUcpCheckoutKit(opts) {
12098
12598
  ]
12099
12599
  };
12100
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
+ }
12101
12614
  async function platform(req, body) {
12102
12615
  const profileUrl = parseUcpAgentHeader(req.headers.get("ucp-agent"));
12103
12616
  if (!profileUrl) {
@@ -12141,7 +12654,7 @@ function createUcpCheckoutKit(opts) {
12141
12654
  { method: req.method, url: req.url, headers, body: body || void 0 },
12142
12655
  async (fresh) => {
12143
12656
  const pp = fresh ? await fetchProfile(profileUrl, true) : plat;
12144
- return "keys" in pp && pp.keys ? pp.keys : [];
12657
+ return "ucp" in pp ? ucpProfileKeys(pp) : [];
12145
12658
  }
12146
12659
  );
12147
12660
  if (!sig.ok) return { error: json2(sig.status, { code: sig.code, content: sig.content }) };
@@ -12169,17 +12682,20 @@ function createUcpCheckoutKit(opts) {
12169
12682
  });
12170
12683
  const res = await run();
12171
12684
  if (d.kind === "proceed") {
12172
- await finishIdempotent(idem, d, {
12173
- status: res.status,
12174
- headers: { "content-type": "application/json" },
12175
- body: await res.clone().text()
12176
- });
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
+ });
12177
12693
  }
12178
12694
  return res;
12179
12695
  }
12180
- async function complete(req, s, body) {
12696
+ async function complete(req, s, body, agentArtifacts) {
12181
12697
  if (s.status === "completed" || s.status === "canceled") return render(s);
12182
- if (s.status === "complete_in_progress") return busy(s);
12698
+ if (inFlight(s.status)) return busy(s);
12183
12699
  const ap2 = body.ap2;
12184
12700
  const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
12185
12701
  if (s._ap2) {
@@ -12195,13 +12711,13 @@ function createUcpCheckoutKit(opts) {
12195
12711
  return render(s);
12196
12712
  }
12197
12713
  const kidResolver = async (kid) => opts.mandateKey ? opts.mandateKey(kid, s) : (s._platformKeys ?? []).find((k) => k.kid === kid) ?? null;
12198
- const v = await verifyAp2Mandate(ap2.checkout_mandate, {
12714
+ const v2 = await verifyAp2Mandate(ap2.checkout_mandate, {
12199
12715
  resolveRootKey: kidResolver,
12200
12716
  expectedVct: AP2_VCT.checkout,
12201
12717
  ...checkoutJwt && { checkoutJwt }
12202
12718
  });
12203
- if (!v.ok) {
12204
- s.messages = [ucpError(v.code, v.error, "unrecoverable")];
12719
+ if (!v2.ok) {
12720
+ s.messages = [ucpError(v2.code, v2.error, "unrecoverable")];
12205
12721
  await store.put(s);
12206
12722
  return render(s);
12207
12723
  }
@@ -12220,19 +12736,32 @@ function createUcpCheckoutKit(opts) {
12220
12736
  await store.put(s);
12221
12737
  return render(s);
12222
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
+ }
12223
12748
  const from = s.status;
12224
12749
  if (!await claim2(s, from)) {
12225
12750
  const current = await store.get(s.id);
12226
12751
  return busy(current ?? s);
12227
12752
  }
12753
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
12754
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
12228
12755
  try {
12229
12756
  const result = await opts.verifyPurchase({
12230
12757
  session: s,
12231
12758
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
12232
- astraId: req.headers.get("x-astra-id") ?? void 0,
12233
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
12759
+ astraId,
12760
+ attemptId,
12234
12761
  ...checkoutJwt && { checkoutJwt },
12235
- valueMajor: total(s) / 100
12762
+ valueMajor: total(s) / 100,
12763
+ agentArtifacts,
12764
+ metadata: { kitSessionId: s.id, protocol: "ucp" }
12236
12765
  });
12237
12766
  const binding2 = await bindAgent(req, result);
12238
12767
  if (binding2) {
@@ -12244,6 +12773,16 @@ function createUcpCheckoutKit(opts) {
12244
12773
  if (isHeld(result)) {
12245
12774
  s.status = "requires_escalation";
12246
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
+ };
12247
12786
  s.messages = [
12248
12787
  ucpError(
12249
12788
  "buyer_approval_required",
@@ -12266,21 +12805,7 @@ function createUcpCheckoutKit(opts) {
12266
12805
  await store.put(s);
12267
12806
  return render(s);
12268
12807
  }
12269
- const settled = await opts.settle({ session: s, result, instrument });
12270
- const pending = {
12271
- ...result.settlementOutcome?.orderId && {
12272
- orderId: result.settlementOutcome.orderId
12273
- },
12274
- report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
12275
- result: pendingResult(result)
12276
- };
12277
- if (settled.status === "pending") {
12278
- s._pending = pending;
12279
- s.messages = [];
12280
- await store.put(s);
12281
- return render(s);
12282
- }
12283
- await finish(s, result, pending, settled);
12808
+ await settleAndFinish(s, result, instrument);
12284
12809
  return render(s);
12285
12810
  } catch (e) {
12286
12811
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -12290,13 +12815,184 @@ function createUcpCheckoutKit(opts) {
12290
12815
  throw e;
12291
12816
  }
12292
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
+ }
12293
12840
  async function resolvePending(sessionId, outcome) {
12294
12841
  const s = await store.get(sessionId);
12295
12842
  if (!s) return null;
12296
12843
  if (s.status !== "complete_in_progress" || !s._pending) return s;
12297
- 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
+ }
12298
12862
  return s;
12299
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
+ "recoverable"
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" || status === "lapsed") {
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
+ }
12300
12996
  async function sendOrderWebhook(sessionId) {
12301
12997
  const s = await store.get(sessionId);
12302
12998
  return s ? deliverOrder(s) : null;
@@ -12315,6 +13011,7 @@ function createUcpCheckoutKit(opts) {
12315
13011
  const p = await platform(req, body);
12316
13012
  if ("error" in p && p.error) return p.error;
12317
13013
  const ctx = p;
13014
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "ucp", publicOrigin });
12318
13015
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId) ?? fallbackScope("ucp-profile", ctx.profileUrl);
12319
13016
  if (req.method === "POST" && path === "/checkout-sessions") {
12320
13017
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12331,7 +13028,7 @@ function createUcpCheckoutKit(opts) {
12331
13028
  _caller: caller,
12332
13029
  _platformProfile: ctx.profileUrl,
12333
13030
  _ap2: ctx.ap2,
12334
- _platformKeys: ctx.plat.keys ?? [],
13031
+ _platformKeys: ucpProfileKeys(ctx.plat),
12335
13032
  ...typeof webhookUrl === "string" && /^https:\/\//.test(webhookUrl) && { _webhookUrl: webhookUrl },
12336
13033
  ...req.headers.get("x-astra-checkout-session") && {
12337
13034
  _astraCheckoutSessionId: req.headers.get("x-astra-checkout-session").slice(0, 64)
@@ -12347,6 +13044,11 @@ function createUcpCheckoutKit(opts) {
12347
13044
  )
12348
13045
  );
12349
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
+ }
12350
13052
  await store.put(s2);
12351
13053
  return render(s2, 201);
12352
13054
  });
@@ -12372,13 +13074,19 @@ function createUcpCheckoutKit(opts) {
12372
13074
  if (req.method === "PUT" && !m[2]) {
12373
13075
  return withIdempotency2(req, body, path, caller, async () => {
12374
13076
  if (s.status === "completed" || s.status === "canceled") return render(s);
12375
- 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);
12376
13079
  if (parsed.line_items) {
12377
13080
  const priced = await priceLines(parsed.line_items);
12378
13081
  s.line_items = priced.lines;
12379
13082
  s.totals = priced.totals;
12380
13083
  s.messages = priced.messages;
12381
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
+ }
12382
13090
  }
12383
13091
  if (parsed.buyer) s.buyer = parsed.buyer;
12384
13092
  await store.put(s);
@@ -12386,7 +13094,13 @@ function createUcpCheckoutKit(opts) {
12386
13094
  });
12387
13095
  }
12388
13096
  if (req.method === "POST" && m[2] === "/complete") {
12389
- 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
+ );
12390
13104
  }
12391
13105
  if (req.method === "POST" && m[2] === "/cancel") {
12392
13106
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12395,7 +13109,7 @@ function createUcpCheckoutKit(opts) {
12395
13109
  code: "method_not_allowed",
12396
13110
  content: `A ${s.status} checkout can't be canceled.`
12397
13111
  });
12398
- if (s.status === "complete_in_progress") return busy(s);
13112
+ if (inFlight(s.status)) return busy(s);
12399
13113
  s.status = "canceled";
12400
13114
  await store.put(s);
12401
13115
  return render(s);
@@ -12408,10 +13122,156 @@ function createUcpCheckoutKit(opts) {
12408
13122
  profile,
12409
13123
  profileHandler: () => json2(200, profile, { "cache-control": "public, max-age=300" }),
12410
13124
  resolvePending,
12411
- sendOrderWebhook
13125
+ sendOrderWebhook,
13126
+ /** 5.19.0 */
13127
+ protocol: "ucp",
13128
+ publicOrigin,
13129
+ resumeApproved,
13130
+ resumeDeclined,
13131
+ applyApprovalEvent,
13132
+ pollHeldSessions
12412
13133
  };
12413
13134
  }
12414
13135
 
13136
+ // src/webhooks.ts
13137
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "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 createHmac2("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 timingSafeEqual2(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
+
12415
13275
  // src/payments/x402.ts
12416
13276
  var CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
12417
13277
  var b64 = (v) => Buffer.from(JSON.stringify(v), "utf8").toString("base64");
@@ -12461,7 +13321,7 @@ function x402PaymentMatchesOffer(payment, offered) {
12461
13321
  }
12462
13322
 
12463
13323
  // src/payments/mpp.ts
12464
- import { createHash as createHash11, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
13324
+ import { createHash as createHash11, createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
12465
13325
  var b64url = (s) => Buffer.from(s, "utf8").toString("base64url");
12466
13326
  function canonical(value) {
12467
13327
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
@@ -12482,7 +13342,7 @@ function binding(c, secret) {
12482
13342
  ];
12483
13343
  if (c.header && c.header.toLowerCase() !== "authorization") slots.push(c.header);
12484
13344
  slots.push(c.opaque ?? "");
12485
- return createHmac2("sha256", secret).update(slots.join("|")).digest("base64url");
13345
+ return createHmac3("sha256", secret).update(slots.join("|")).digest("base64url");
12486
13346
  }
12487
13347
  var quote = (v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
12488
13348
  function buildMppChallenge(input, hmacSecret) {
@@ -12530,7 +13390,7 @@ function checkChallenge(challenge, hmacSecret, now) {
12530
13390
  const { id, ...rest } = challenge;
12531
13391
  const expected = Buffer.from(binding(rest, hmacSecret));
12532
13392
  const got = Buffer.from(String(id ?? ""));
12533
- if (expected.length !== got.length || !timingSafeEqual2(expected, got))
13393
+ if (expected.length !== got.length || !timingSafeEqual3(expected, got))
12534
13394
  return { ok: false, reason: "invalid_id" };
12535
13395
  if (challenge.expires && Date.parse(challenge.expires) < now.getTime())
12536
13396
  return { ok: false, reason: "expired" };
@@ -12546,7 +13406,7 @@ function verifyMppChallenge(challenge, hmacSecret, nowOrOptions = /* @__PURE__ *
12546
13406
  if (opts.body === void 0) return { ok: false, reason: "body_required" };
12547
13407
  const want = Buffer.from(challenge.digest);
12548
13408
  const got = Buffer.from(mppBodyDigest(opts.body));
12549
- if (want.length !== got.length || !timingSafeEqual2(want, got))
13409
+ if (want.length !== got.length || !timingSafeEqual3(want, got))
12550
13410
  return { ok: false, reason: "digest_mismatch" };
12551
13411
  }
12552
13412
  if (opts.store) {
@@ -12852,6 +13712,28 @@ function buildAcpWellKnown(opts) {
12852
13712
  var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
12853
13713
  var apiError = (status, code, message, param) => json3(status, { type: "invalid_request", code, message, ...param && { param } });
12854
13714
  var total2 = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
13715
+ var clone2 = (v) => JSON.parse(JSON.stringify(v));
13716
+ var inFlight2 = (st) => st === "complete_in_progress" || st === "finishing";
13717
+ var ACP_CODES = /* @__PURE__ */ new Set([
13718
+ "missing",
13719
+ "invalid",
13720
+ "out_of_stock",
13721
+ "payment_declined",
13722
+ "requires_sign_in",
13723
+ "requires_3ds",
13724
+ // 5.20.0: the owner-approval codes the kit itself emits.
13725
+ "approval_required",
13726
+ "approval_denied",
13727
+ "approval_expired",
13728
+ "approval_lapsed"
13729
+ ]);
13730
+ function heldBody2(text) {
13731
+ try {
13732
+ return JSON.parse(text).status === "pending_approval";
13733
+ } catch {
13734
+ return false;
13735
+ }
13736
+ }
12855
13737
  function createAcpCheckoutKit(opts) {
12856
13738
  const mem = /* @__PURE__ */ new Map();
12857
13739
  const store = opts.store ?? {
@@ -12862,7 +13744,14 @@ function createAcpCheckoutKit(opts) {
12862
13744
  if (!s || s.status !== from) return false;
12863
13745
  s.status = to;
12864
13746
  return true;
12865
- }
13747
+ },
13748
+ findByCheckoutSessionId: async (checkoutSessionId) => {
13749
+ for (const s of mem.values()) {
13750
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
13751
+ }
13752
+ return mem.get(checkoutSessionId) ?? null;
13753
+ },
13754
+ listHeld: async () => [...mem.values()].filter((s) => s.status === "pending_approval")
12866
13755
  };
12867
13756
  const idem = {
12868
13757
  store: opts.idempotencyStore ?? new MemoryIdempotencyStore(),
@@ -12880,16 +13769,19 @@ function createAcpCheckoutKit(opts) {
12880
13769
  );
12881
13770
  }
12882
13771
  const handlers = acpPaymentHandlers(opts.paymentHandlers);
13772
+ const handlerIds = new Set(handlers.flatMap((h) => [h.id, h.name]));
13773
+ const publicOrigin = opts.publicOrigin?.replace(/\/+$/, "");
13774
+ const onReportError = opts.onReportError ?? defaultOnReportError;
12883
13775
  function view(s) {
12884
13776
  return {
12885
13777
  id: s.id,
12886
- status: s.status,
13778
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
12887
13779
  currency: s.currency.toLowerCase(),
12888
13780
  line_items: s.line_items,
12889
13781
  totals: s.totals,
12890
13782
  fulfillment_options: [],
12891
13783
  messages: s.messages,
12892
- links: [],
13784
+ links: opts.links ?? [],
12893
13785
  capabilities: { payment: { handlers } },
12894
13786
  ...s.continue_url && s.status === "pending_approval" && { continue_url: s.continue_url },
12895
13787
  ...s.order && { order: s.order }
@@ -12899,7 +13791,9 @@ function createAcpCheckoutKit(opts) {
12899
13791
  const lines = [];
12900
13792
  const messages = [];
12901
13793
  for (const [i, it] of items.entries()) {
12902
- const c = it.id ? await opts.catalog(it.id) : null;
13794
+ const q = Math.max(1, Math.floor(it.quantity ?? 1));
13795
+ const found = it.id ? await opts.catalog(it.id, q) : null;
13796
+ const c = found && withValidImageUrl(found);
12903
13797
  if (!c) {
12904
13798
  messages.push({
12905
13799
  type: "error",
@@ -12910,11 +13804,16 @@ function createAcpCheckoutKit(opts) {
12910
13804
  });
12911
13805
  continue;
12912
13806
  }
12913
- const q = Math.max(1, Math.floor(it.quantity ?? 1));
12914
13807
  const amount = c.price * q;
12915
13808
  lines.push({
12916
13809
  id: `li_${i + 1}`,
12917
- item: { id: c.id, name: c.title, unit_amount: c.price },
13810
+ item: {
13811
+ id: c.id,
13812
+ name: c.title,
13813
+ unit_amount: c.price,
13814
+ ...c.image_url && { image_url: c.image_url },
13815
+ ...typeof c.description === "string" && { description: c.description }
13816
+ },
12918
13817
  quantity: q,
12919
13818
  name: c.title,
12920
13819
  unit_amount: c.price,
@@ -12934,6 +13833,21 @@ function createAcpCheckoutKit(opts) {
12934
13833
  ]
12935
13834
  };
12936
13835
  }
13836
+ async function validate(s) {
13837
+ if (!opts.validateCart) return { messages: [], blocked: false };
13838
+ const found = await opts.validateCart(s);
13839
+ const messages = found.map((m) => {
13840
+ const error = (m.type ?? "error") === "error";
13841
+ return {
13842
+ type: m.type ?? "error",
13843
+ code: error ? ACP_CODES.has(m.code) ? m.code : "invalid" : m.code,
13844
+ content: m.content,
13845
+ ...error && { resolution: "recoverable" },
13846
+ ...m.path && { param: m.path }
13847
+ };
13848
+ });
13849
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
13850
+ }
12937
13851
  async function idempotent(req, body, path, caller, run) {
12938
13852
  const d = await beginIdempotent(idem, {
12939
13853
  method: req.method,
@@ -12949,29 +13863,43 @@ function createAcpCheckoutKit(opts) {
12949
13863
  headers: { ...d.headers, "idempotent-replayed": "true" }
12950
13864
  });
12951
13865
  const res = await run();
12952
- if (d.kind === "proceed")
12953
- await finishIdempotent(idem, d, {
12954
- status: res.status,
12955
- headers: { "content-type": "application/json" },
12956
- body: await res.clone().text()
12957
- });
13866
+ if (d.kind === "proceed") {
13867
+ const text = await res.clone().text();
13868
+ if (heldBody2(text)) await abandonIdempotent(idem, d);
13869
+ else
13870
+ await finishIdempotent(idem, d, {
13871
+ status: res.status,
13872
+ headers: { "content-type": "application/json" },
13873
+ body: text
13874
+ });
13875
+ }
12958
13876
  return res;
12959
13877
  }
12960
13878
  const inProgress = () => apiError(409, "checkout_in_progress", "This checkout session is already being completed.");
12961
- async function reportOutcome(p, settled) {
12962
- if (!opts.reportSettlement || !p.orderId) return;
12963
- await opts.reportSettlement({
12964
- orderId: p.orderId,
12965
- status: settled.status,
12966
- ...p.report,
12967
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12968
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12969
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12970
- }).catch(() => void 0);
13879
+ async function reportOutcome(s, p, settled) {
13880
+ const report = opts.reportSettlement;
13881
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
13882
+ if (!report || !ref) return;
13883
+ await withReportRetries(
13884
+ () => report({
13885
+ ...ref,
13886
+ status: settled.status,
13887
+ ...p.report,
13888
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
13889
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
13890
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
13891
+ }),
13892
+ opts.reportRetry,
13893
+ isRetryableSettlementError
13894
+ ).catch(
13895
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
13896
+ );
12971
13897
  }
12972
13898
  async function finish(s, result, p, settled) {
12973
- await reportOutcome(p, settled);
13899
+ await reportOutcome(s, p, settled);
12974
13900
  delete s._pending;
13901
+ delete s._held;
13902
+ delete s._approvalId;
12975
13903
  if (settled.status === "failed") {
12976
13904
  s.status = "ready_for_payment";
12977
13905
  s.messages = [
@@ -12980,13 +13908,13 @@ function createAcpCheckoutKit(opts) {
12980
13908
  code: "payment_declined",
12981
13909
  content: "The payment was declined.",
12982
13910
  severity: "high",
12983
- resolution: "requires_buyer_input"
13911
+ resolution: "recoverable"
12984
13912
  }
12985
13913
  ];
12986
13914
  await store.put(s);
12987
13915
  return;
12988
13916
  }
12989
- const order = await opts.fulfil({ session: s, result });
13917
+ const order = await opts.fulfil({ session: s, result, settled });
12990
13918
  const permalink = order.permalink_url ?? (opts.permalinkBase && `${opts.permalinkBase.replace(/\/+$/, "")}/${encodeURIComponent(order.id)}`);
12991
13919
  s.order = {
12992
13920
  id: order.id,
@@ -13009,11 +13937,29 @@ function createAcpCheckoutKit(opts) {
13009
13937
  s.messages = [];
13010
13938
  await store.put(s);
13011
13939
  }
13012
- async function complete(req, s, body) {
13940
+ async function complete(req, s, body, agentArtifacts) {
13013
13941
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13014
- if (s.status === "complete_in_progress") return inProgress();
13942
+ if (inFlight2(s.status)) return inProgress();
13015
13943
  if (!body.payment_data)
13016
13944
  return apiError(400, "missing", "payment_data is required.", "$.payment_data");
13945
+ const paymentData = body.payment_data;
13946
+ if (Array.isArray(paymentData) || typeof paymentData !== "object" || typeof paymentData.handler_id !== "string" || !handlerIds.has(paymentData.handler_id)) {
13947
+ return apiError(
13948
+ 400,
13949
+ "invalid",
13950
+ "payment_data must be one object whose handler_id is the id or name of a payment handler this merchant declares.",
13951
+ "$.payment_data.handler_id"
13952
+ );
13953
+ }
13954
+ const v = await validate(s);
13955
+ if (v.blocked) {
13956
+ s.status = "not_ready_for_payment";
13957
+ s.messages = v.messages;
13958
+ delete s._held;
13959
+ delete s._approvalId;
13960
+ await store.put(s);
13961
+ return json3(200, view(s));
13962
+ }
13017
13963
  const from = s.status;
13018
13964
  if (store.claim) {
13019
13965
  if (!await store.claim(s.id, from, "complete_in_progress")) return inProgress();
@@ -13022,17 +13968,31 @@ function createAcpCheckoutKit(opts) {
13022
13968
  s.status = "complete_in_progress";
13023
13969
  await store.put(s);
13024
13970
  }
13971
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
13972
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
13025
13973
  try {
13026
13974
  const result = await opts.verifyPurchase({
13027
13975
  session: s,
13028
13976
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
13029
- astraId: req.headers.get("x-astra-id") ?? void 0,
13030
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
13031
- valueMajor: total2(s) / 100
13977
+ astraId,
13978
+ attemptId,
13979
+ valueMajor: total2(s) / 100,
13980
+ agentArtifacts,
13981
+ metadata: { kitSessionId: s.id, protocol: "acp" }
13032
13982
  });
13033
13983
  if (isHeld(result)) {
13034
13984
  s.status = "pending_approval";
13035
13985
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
13986
+ s._approvalId = result.stepUpApproval?.approvalId;
13987
+ s._astraId = astraId ?? result.agent?.astraId;
13988
+ s._attemptId = attemptId ?? result.attemptId;
13989
+ s._held = {
13990
+ line_items: clone2(s.line_items),
13991
+ totals: clone2(s.totals),
13992
+ ...scrubPaymentInput(paymentData) && { paymentData: scrubPaymentInput(paymentData) },
13993
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
13994
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
13995
+ };
13036
13996
  s.messages = [
13037
13997
  {
13038
13998
  type: "error",
@@ -13047,36 +14007,11 @@ function createAcpCheckoutKit(opts) {
13047
14007
  }
13048
14008
  if (!result.identityVerified || !result.policyAllowed) {
13049
14009
  s.status = from;
13050
- s.messages = [
13051
- {
13052
- type: "error",
13053
- code: "intervention_required",
13054
- content: "The agent is not authorized for this purchase.",
13055
- severity: "high",
13056
- resolution: "requires_buyer_review"
13057
- }
13058
- ];
13059
- await store.put(s);
13060
- return json3(200, view(s));
13061
- }
13062
- const settled = await opts.settle({
13063
- session: s,
13064
- result,
13065
- paymentData: body.payment_data
13066
- });
13067
- const orderId = result.settlementOutcome?.orderId;
13068
- const pending = {
13069
- ...orderId && { orderId },
13070
- report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
13071
- result: pendingResult(result)
13072
- };
13073
- if (settled.status === "pending") {
13074
- s._pending = pending;
13075
- s.messages = [];
14010
+ s.messages = [notAuthorized()];
13076
14011
  await store.put(s);
13077
14012
  return json3(200, view(s));
13078
14013
  }
13079
- await finish(s, result, pending, settled);
14014
+ await settleAndFinish(s, result, paymentData);
13080
14015
  return json3(200, view(s));
13081
14016
  } catch (e) {
13082
14017
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -13086,13 +14021,188 @@ function createAcpCheckoutKit(opts) {
13086
14021
  throw e;
13087
14022
  }
13088
14023
  }
14024
+ const notAuthorized = () => ({
14025
+ type: "error",
14026
+ code: "intervention_required",
14027
+ content: "The agent is not authorized for this purchase.",
14028
+ severity: "high",
14029
+ resolution: "requires_buyer_review"
14030
+ });
14031
+ async function settleAndFinish(s, result, paymentData) {
14032
+ const settled = await opts.settle({ session: s, result, paymentData });
14033
+ const orderId = result.settlementOutcome?.orderId;
14034
+ const sessionId = result.sessionId;
14035
+ const pending = {
14036
+ ...orderId && { orderId },
14037
+ ...sessionId && { sessionId },
14038
+ report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
14039
+ result: pendingResult(result)
14040
+ };
14041
+ if (settled.status === "pending") {
14042
+ s._pending = pending;
14043
+ s.messages = [];
14044
+ delete s._held;
14045
+ delete s._approvalId;
14046
+ await store.put(s);
14047
+ return "pending";
14048
+ }
14049
+ await finish(s, result, pending, settled);
14050
+ return settled.status === "failed" ? "payment_failed" : "completed";
14051
+ }
13089
14052
  async function resolvePending(sessionId, outcome) {
13090
14053
  const s = await store.get(sessionId);
13091
14054
  if (!s) return null;
13092
14055
  if (s.status !== "complete_in_progress" || !s._pending) return s;
13093
- await finish(s, s._pending.result, s._pending, outcome);
14056
+ if (store.claim) {
14057
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
14058
+ return await store.get(sessionId) ?? s;
14059
+ }
14060
+ s.status = "finishing";
14061
+ } else {
14062
+ s.status = "finishing";
14063
+ await store.put(s);
14064
+ }
14065
+ const pending = s._pending;
14066
+ try {
14067
+ await finish(s, pending.result, pending, outcome);
14068
+ } catch (e) {
14069
+ s.status = "complete_in_progress";
14070
+ s._pending = pending;
14071
+ await store.put(s).catch(() => void 0);
14072
+ throw e;
14073
+ }
13094
14074
  return s;
13095
14075
  }
14076
+ async function resumeApproved(sessionId, event) {
14077
+ const s = await store.get(sessionId);
14078
+ if (!s) return resumeResult("not_found", null);
14079
+ if (s.status === "completed") return resumeResult("already_completed", s);
14080
+ if (s.status === "canceled") return resumeResult("canceled", s);
14081
+ if (inFlight2(s.status)) return resumeResult("busy", s);
14082
+ if (s.status !== "pending_approval" || !s._held) return resumeResult("not_held", s);
14083
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
14084
+ return resumeResult("stale", s);
14085
+ if (store.claim) {
14086
+ if (!await store.claim(s.id, "pending_approval", "complete_in_progress"))
14087
+ return resumeResult("busy", await store.get(sessionId) ?? s);
14088
+ s.status = "complete_in_progress";
14089
+ } else {
14090
+ s.status = "complete_in_progress";
14091
+ await store.put(s);
14092
+ }
14093
+ const held = s._held;
14094
+ const backToHeld = async () => {
14095
+ s.status = "pending_approval";
14096
+ s._held = held;
14097
+ await store.put(s).catch(() => void 0);
14098
+ };
14099
+ try {
14100
+ s.line_items = clone2(held.line_items);
14101
+ s.totals = clone2(held.totals);
14102
+ const v = await validate(s);
14103
+ if (v.blocked) {
14104
+ s.status = "not_ready_for_payment";
14105
+ s.messages = v.messages;
14106
+ delete s._held;
14107
+ delete s._approvalId;
14108
+ delete s.continue_url;
14109
+ await store.put(s);
14110
+ return resumeResult("cart_invalid", s);
14111
+ }
14112
+ const result = await opts.verifyPurchase({
14113
+ session: s,
14114
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
14115
+ astraId: s._astraId,
14116
+ attemptId: s._attemptId,
14117
+ valueMajor: total2(s) / 100,
14118
+ metadata: { kitSessionId: s.id, protocol: "acp" },
14119
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
14120
+ });
14121
+ if (isHeld(result)) {
14122
+ await backToHeld();
14123
+ return resumeResult("still_held", s);
14124
+ }
14125
+ if (!result.identityVerified || !result.policyAllowed) {
14126
+ s.status = "ready_for_payment";
14127
+ s.messages = [notAuthorized()];
14128
+ delete s._held;
14129
+ delete s._approvalId;
14130
+ delete s.continue_url;
14131
+ await store.put(s);
14132
+ return resumeResult("not_authorized", s);
14133
+ }
14134
+ delete s.continue_url;
14135
+ return resumeResult(await settleAndFinish(s, result, held.paymentData), s);
14136
+ } catch (e) {
14137
+ if (!s._pending && inFlight2(s.status)) await backToHeld();
14138
+ throw e;
14139
+ }
14140
+ }
14141
+ async function resumeDeclined(sessionId, reason = "denied", event) {
14142
+ const s = await store.get(sessionId);
14143
+ if (!s) return resumeResult("not_found", null);
14144
+ if (s.status === "completed") return resumeResult("already_completed", s);
14145
+ if (s.status === "canceled") return resumeResult("canceled", s);
14146
+ if (inFlight2(s.status)) return resumeResult("busy", s);
14147
+ if (s.status !== "pending_approval") return resumeResult("not_held", s);
14148
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
14149
+ return resumeResult("stale", s);
14150
+ if (store.claim && !await store.claim(s.id, "pending_approval", "ready_for_payment")) {
14151
+ return resumeResult("busy", await store.get(sessionId) ?? s);
14152
+ }
14153
+ s.status = "ready_for_payment";
14154
+ delete s._held;
14155
+ delete s._approvalId;
14156
+ delete s.continue_url;
14157
+ s.messages = [
14158
+ {
14159
+ type: "error",
14160
+ code: `approval_${reason}`,
14161
+ 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.",
14162
+ severity: "medium",
14163
+ resolution: "recoverable"
14164
+ }
14165
+ ];
14166
+ await store.put(s);
14167
+ return resumeResult("declined", s);
14168
+ }
14169
+ async function findForEvent(event) {
14170
+ if (event.kitSessionId) {
14171
+ const s = await store.get(event.kitSessionId);
14172
+ if (s) return s;
14173
+ }
14174
+ if (store.findByCheckoutSessionId)
14175
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
14176
+ return store.get(event.checkoutSessionId);
14177
+ }
14178
+ async function applyApprovalEvent(event) {
14179
+ const s = await findForEvent(event);
14180
+ if (!s) return resumeResult("not_found", null);
14181
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
14182
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
14183
+ }
14184
+ async function pollHeldSessions(o = {}) {
14185
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
14186
+ (x) => Boolean(x)
14187
+ ) : store.listHeld ? await store.listHeld() : [];
14188
+ const out = [];
14189
+ for (const s of sessions) {
14190
+ if (s.status !== "pending_approval" || !s._held?.pollUrl || !s._approvalId) continue;
14191
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
14192
+ approvalId: s._approvalId,
14193
+ pollUrl: s._held.pollUrl,
14194
+ expiresAt: s._held.expiresAt ?? ""
14195
+ });
14196
+ if (status === "approved" || status === "consumed") {
14197
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
14198
+ } else if (status === "denied" || status === "expired" || status === "lapsed") {
14199
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
14200
+ } else {
14201
+ out.push({ sessionId: s.id, outcome: "pending" });
14202
+ }
14203
+ }
14204
+ return out;
14205
+ }
13096
14206
  async function route(req, reqCtx) {
13097
14207
  const version = req.headers.get("api-version");
13098
14208
  if (!version) return apiError(400, "missing", "The API-Version header is required.");
@@ -13117,6 +14227,7 @@ function createAcpCheckoutKit(opts) {
13117
14227
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId);
13118
14228
  if (!caller)
13119
14229
  return apiError(401, "unauthorized", "Send an Authorization header (or X-Astra-Id).");
14230
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "acp", publicOrigin });
13120
14231
  if (req.method === "POST" && path === "/checkout_sessions") {
13121
14232
  return idempotent(req, body, path, caller, async () => {
13122
14233
  const items = parsed.line_items ?? parsed.items;
@@ -13135,6 +14246,11 @@ function createAcpCheckoutKit(opts) {
13135
14246
  _astraCheckoutSessionId: req.headers.get("x-astra-checkout-session").slice(0, 64)
13136
14247
  }
13137
14248
  };
14249
+ if (s2.status === "ready_for_payment") {
14250
+ const v = await validate(s2);
14251
+ s2.messages = [...s2.messages, ...v.messages];
14252
+ if (v.blocked) s2.status = "not_ready_for_payment";
14253
+ }
13138
14254
  await store.put(s2);
13139
14255
  return json3(201, view(s2));
13140
14256
  });
@@ -13148,7 +14264,13 @@ function createAcpCheckoutKit(opts) {
13148
14264
  if (!m[2]) {
13149
14265
  return idempotent(req, body, path, caller, async () => {
13150
14266
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13151
- if (s.status === "complete_in_progress") return inProgress();
14267
+ if (inFlight2(s.status)) return inProgress();
14268
+ if (s.status === "pending_approval")
14269
+ return apiError(
14270
+ 409,
14271
+ "checkout_awaiting_approval",
14272
+ "This session is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over."
14273
+ );
13152
14274
  const items = parsed.line_items ?? parsed.items;
13153
14275
  if (items) {
13154
14276
  const p = await price(items);
@@ -13158,17 +14280,22 @@ function createAcpCheckoutKit(opts) {
13158
14280
  messages: p.messages,
13159
14281
  status: p.lines.length && !p.messages.length ? "ready_for_payment" : "not_ready_for_payment"
13160
14282
  });
14283
+ if (s.status === "ready_for_payment") {
14284
+ const v = await validate(s);
14285
+ s.messages = [...s.messages, ...v.messages];
14286
+ if (v.blocked) s.status = "not_ready_for_payment";
14287
+ }
13161
14288
  }
13162
14289
  await store.put(s);
13163
14290
  return json3(200, view(s));
13164
14291
  });
13165
14292
  }
13166
14293
  if (m[2] === "/complete")
13167
- return idempotent(req, body, path, caller, () => complete(req, s, parsed));
14294
+ return idempotent(req, body, path, caller, () => complete(req, s, parsed, agentArtifacts));
13168
14295
  return idempotent(req, body, path, caller, async () => {
13169
14296
  if (s.status === "completed" || s.status === "canceled")
13170
14297
  return apiError(405, "method_not_allowed", `A ${s.status} session can't be canceled.`);
13171
- if (s.status === "complete_in_progress") return inProgress();
14298
+ if (inFlight2(s.status)) return inProgress();
13172
14299
  s.status = "canceled";
13173
14300
  await store.put(s);
13174
14301
  return json3(200, view(s));
@@ -13184,7 +14311,17 @@ function createAcpCheckoutKit(opts) {
13184
14311
  if (requestId) headers.set("request-id", requestId);
13185
14312
  return new Response(res.body, { status: res.status, headers });
13186
14313
  }
13187
- return { handle, resolvePending };
14314
+ return {
14315
+ handle,
14316
+ resolvePending,
14317
+ /** 5.19.0 */
14318
+ protocol: "acp",
14319
+ publicOrigin,
14320
+ resumeApproved,
14321
+ resumeDeclined,
14322
+ applyApprovalEvent,
14323
+ pollHeldSessions
14324
+ };
13188
14325
  }
13189
14326
 
13190
14327
  // src/adapters/kit-express.ts
@@ -13206,11 +14343,46 @@ function rawBodyOf(req) {
13206
14343
  }
13207
14344
  return void 0;
13208
14345
  }
14346
+ var first = (v) => (Array.isArray(v) ? v[0] : v)?.split(",")[0]?.trim() || void 0;
14347
+ function originOf(req, publicOrigin) {
14348
+ if (publicOrigin) return publicOrigin.replace(/\/+$/, "");
14349
+ const trustProxy = Boolean(req.app?.get("trust proxy"));
14350
+ const host = trustProxy && first(req.headers["x-forwarded-host"]) || req.get?.("host") || req.headers.host || "localhost";
14351
+ const proto = trustProxy && first(req.headers["x-forwarded-proto"]) || req.protocol || "http";
14352
+ return `${proto}://${host}`;
14353
+ }
14354
+ var isKit = (k) => typeof k.applyApprovalEvent === "function" && (k.protocol === "ucp" || k.protocol === "acp");
14355
+ async function writeEvents(req, res, secret, kits, seen) {
14356
+ const raw = rawBodyOf(req);
14357
+ const text = raw === void 0 ? "" : typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
14358
+ const out = secret ? await handleApprovalEvents(text, req.headers, secret, { kits, ...seen && { seen } }) : { status: 500, body: { ok: false, error: "webhook_secret_not_configured" } };
14359
+ res.status(out.status);
14360
+ res.setHeader("content-type", "application/json");
14361
+ res.end(JSON.stringify(out.body));
14362
+ }
14363
+ function approvalEventsHandler(options) {
14364
+ return async (req, res, next) => {
14365
+ try {
14366
+ await writeEvents(req, res, options.secret, options.kits, options.seen);
14367
+ } catch (err) {
14368
+ next(err);
14369
+ }
14370
+ };
14371
+ }
13209
14372
  function expressHandler(kit, options = {}) {
13210
14373
  return async (req, res, next) => {
13211
14374
  try {
13212
- const host = req.get?.("host") ?? req.headers.host ?? "localhost";
13213
- const url = `${req.protocol ?? "http"}://${host}${req.originalUrl ?? req.url}`;
14375
+ if (options.approvalEventsPath && req.method === "POST") {
14376
+ const p = options.approvalEventsPath.replace(/\/+$/, "");
14377
+ const full = (req.originalUrl ?? req.url).split("?")[0].replace(/\/+$/, "");
14378
+ const local = req.url.split("?")[0].replace(/\/+$/, "");
14379
+ if (full === p || local === p) {
14380
+ const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);
14381
+ await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);
14382
+ return;
14383
+ }
14384
+ }
14385
+ const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;
13214
14386
  const headers = new Headers();
13215
14387
  for (const [k, v] of Object.entries(req.headers)) {
13216
14388
  if (v === void 0) continue;
@@ -13260,6 +14432,7 @@ export {
13260
14432
  MemoryCheckoutStore,
13261
14433
  MemoryIdempotencyStore,
13262
14434
  MemoryMppChallengeStore,
14435
+ MemorySeenEvents,
13263
14436
  PLATFORM_AGENT_SIGNATURES,
13264
14437
  RUNTIME_SIGNATURES,
13265
14438
  RedisIdempotencyStore,
@@ -13274,6 +14447,7 @@ export {
13274
14447
  VERIFICATION_STATUS_HEADER,
13275
14448
  SDK_VERSION as VERSION,
13276
14449
  _resetEdgeConfigCache,
14450
+ abandonIdempotent,
13277
14451
  acpPaymentHandlers,
13278
14452
  aepCanonicalJson,
13279
14453
  aepHash,
@@ -13285,6 +14459,7 @@ export {
13285
14459
  aepTimestamp,
13286
14460
  aepVerifySignature,
13287
14461
  agent_exports as agent,
14462
+ approvalEventsHandler,
13288
14463
  astrasyncPaymentHandlers,
13289
14464
  authorizeSettlement,
13290
14465
  awaitStepUpApproval,
@@ -13331,11 +14506,13 @@ export {
13331
14506
  getEdgeConfig,
13332
14507
  getTrustLevel,
13333
14508
  getWellKnownUrls,
14509
+ handleApprovalEvents,
13334
14510
  hasCredentials,
13335
14511
  idempotencyMiddleware,
13336
14512
  isEdgeConfig,
13337
14513
  isRejectedIdentityClaim,
13338
14514
  isRetryableSettlementError,
14515
+ isSharedVerdict,
13339
14516
  isSpecialUseIp,
13340
14517
  isVerificationUnavailable,
13341
14518
  isVerifiedHopValidFor,
@@ -13347,6 +14524,7 @@ export {
13347
14524
  negotiateUcp,
13348
14525
  nextjs_exports as nextjs,
13349
14526
  normalizeDeclaredSource,
14527
+ onVerificationResult,
13350
14528
  openCheckoutMandate,
13351
14529
  openPaymentMandate,
13352
14530
  parseYaml as parseLocalPolicyYaml,
@@ -13380,6 +14558,7 @@ export {
13380
14558
  ucpEscalation,
13381
14559
  ucpFailure,
13382
14560
  ucpProfileHandler,
14561
+ ucpProfileKeys,
13383
14562
  ucpRequiredComponents,
13384
14563
  unavailableRetryAfterSeconds,
13385
14564
  validatePolicy as validateLocalPolicy,