@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.js CHANGED
@@ -53,6 +53,7 @@ __export(src_exports, {
53
53
  MemoryCheckoutStore: () => MemoryCheckoutStore,
54
54
  MemoryIdempotencyStore: () => MemoryIdempotencyStore,
55
55
  MemoryMppChallengeStore: () => MemoryMppChallengeStore,
56
+ MemorySeenEvents: () => MemorySeenEvents,
56
57
  PLATFORM_AGENT_SIGNATURES: () => PLATFORM_AGENT_SIGNATURES,
57
58
  RUNTIME_SIGNATURES: () => RUNTIME_SIGNATURES,
58
59
  RedisIdempotencyStore: () => RedisIdempotencyStore,
@@ -67,6 +68,7 @@ __export(src_exports, {
67
68
  VERIFICATION_STATUS_HEADER: () => VERIFICATION_STATUS_HEADER,
68
69
  VERSION: () => SDK_VERSION,
69
70
  _resetEdgeConfigCache: () => _resetEdgeConfigCache,
71
+ abandonIdempotent: () => abandonIdempotent,
70
72
  acpPaymentHandlers: () => acpPaymentHandlers,
71
73
  aepCanonicalJson: () => aepCanonicalJson,
72
74
  aepHash: () => aepHash,
@@ -78,6 +80,7 @@ __export(src_exports, {
78
80
  aepTimestamp: () => aepTimestamp,
79
81
  aepVerifySignature: () => aepVerifySignature,
80
82
  agent: () => agent_exports,
83
+ approvalEventsHandler: () => approvalEventsHandler,
81
84
  astrasyncPaymentHandlers: () => astrasyncPaymentHandlers,
82
85
  authorizeSettlement: () => authorizeSettlement,
83
86
  awaitStepUpApproval: () => awaitStepUpApproval,
@@ -124,11 +127,13 @@ __export(src_exports, {
124
127
  getEdgeConfig: () => getEdgeConfig,
125
128
  getTrustLevel: () => getTrustLevel,
126
129
  getWellKnownUrls: () => getWellKnownUrls,
130
+ handleApprovalEvents: () => handleApprovalEvents,
127
131
  hasCredentials: () => hasCredentials,
128
132
  idempotencyMiddleware: () => idempotencyMiddleware,
129
133
  isEdgeConfig: () => isEdgeConfig,
130
134
  isRejectedIdentityClaim: () => isRejectedIdentityClaim,
131
135
  isRetryableSettlementError: () => isRetryableSettlementError,
136
+ isSharedVerdict: () => isSharedVerdict,
132
137
  isSpecialUseIp: () => isSpecialUseIp,
133
138
  isVerificationUnavailable: () => isVerificationUnavailable,
134
139
  isVerifiedHopValidFor: () => isVerifiedHopValidFor,
@@ -140,6 +145,7 @@ __export(src_exports, {
140
145
  negotiateUcp: () => negotiateUcp,
141
146
  nextjs: () => nextjs_exports,
142
147
  normalizeDeclaredSource: () => normalizeDeclaredSource,
148
+ onVerificationResult: () => onVerificationResult,
143
149
  openCheckoutMandate: () => openCheckoutMandate,
144
150
  openPaymentMandate: () => openPaymentMandate,
145
151
  parseLocalPolicyYaml: () => parseYaml,
@@ -173,6 +179,7 @@ __export(src_exports, {
173
179
  ucpEscalation: () => ucpEscalation,
174
180
  ucpFailure: () => ucpFailure,
175
181
  ucpProfileHandler: () => ucpProfileHandler,
182
+ ucpProfileKeys: () => ucpProfileKeys,
176
183
  ucpRequiredComponents: () => ucpRequiredComponents,
177
184
  unavailableRetryAfterSeconds: () => unavailableRetryAfterSeconds,
178
185
  validateLocalPolicy: () => validatePolicy,
@@ -214,7 +221,7 @@ function getTrustLevel(score) {
214
221
  }
215
222
 
216
223
  // src/version.ts
217
- var SDK_VERSION = "5.18.1";
224
+ var SDK_VERSION = "5.20.0";
218
225
 
219
226
  // src/http.ts
220
227
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -466,7 +473,12 @@ function getCacheKey(request, counterpartyId) {
466
473
  // its start_checkout quote was served the cached quote-leg grant (no
467
474
  // settlementOutcome) and never reached the backend. Same F-A1-07 bug class.
468
475
  request.commercePhase || "",
469
- request.checkoutSessionId || ""
476
+ request.checkoutSessionId || "",
477
+ // 5.19.0: a route_config purpose claim is checked server-side against
478
+ // the stored rule for this method + path, so it changes the verdict.
479
+ request.purposeSource || "",
480
+ request.requestMethod || "",
481
+ request.requestPath || ""
470
482
  ].join("|");
471
483
  }
472
484
  function getCachedResult(request, counterpartyId) {
@@ -491,6 +503,7 @@ function cacheResult(request, result, configuredTtl, counterpartyId) {
491
503
  }
492
504
  function clearCache() {
493
505
  verificationCache.clear();
506
+ inflightVerifies.clear();
494
507
  }
495
508
  function extractCredentials(headers, query) {
496
509
  const credentials = {};
@@ -664,6 +677,9 @@ async function callVerifyAccessAPI(config, request) {
664
677
  if (requestData.enableRuntimeChallenge)
665
678
  body.enableRuntimeChallenge = requestData.enableRuntimeChallenge;
666
679
  if (requestData.routePolicy) body.routePolicy = requestData.routePolicy;
680
+ if (config.actingProof?.connectorToken) {
681
+ body.actingProof = { connectorToken: config.actingProof.connectorToken };
682
+ }
667
683
  if (requestData.approvalHeldByRoute) body.approvalHeldByRoute = true;
668
684
  if (requestData.createSession) body.createSession = requestData.createSession;
669
685
  if (requestData.durationRequired) body.durationRequired = requestData.durationRequired;
@@ -680,6 +696,11 @@ async function callVerifyAccessAPI(config, request) {
680
696
  if (requestData.buyerEmail) body.buyerEmail = requestData.buyerEmail;
681
697
  if (requestData.commerceArtifacts) body.commerceArtifacts = requestData.commerceArtifacts;
682
698
  if (requestData.attemptId) body.attemptId = requestData.attemptId;
699
+ if (requestData.purposeSource) body.purposeSource = requestData.purposeSource;
700
+ if (requestData.agentArtifacts) body.agentArtifacts = requestData.agentArtifacts;
701
+ if (requestData.metadata) body.metadata = requestData.metadata;
702
+ if (requestData.requestMethod) body.requestMethod = requestData.requestMethod;
703
+ if (requestData.requestPath) body.requestPath = requestData.requestPath;
683
704
  if (requestData.considerationSet) body.considerationSet = requestData.considerationSet;
684
705
  body.sdkVersion = SDK_VERSION;
685
706
  if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
@@ -826,22 +847,46 @@ async function verify(config, request, options) {
826
847
  "[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."
827
848
  );
828
849
  }
829
- const uncacheable = Boolean(request.commerceArtifacts);
850
+ const uncacheable = Boolean(request.commerceArtifacts) || Boolean(mergedConfig.actingProof);
830
851
  if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
831
852
  const cached = getCachedResult(request, mergedConfig.counterpartyId);
832
853
  if (cached) {
833
854
  if (mergedConfig.debug) {
834
855
  console.log("[VerificationGateway] Returning cached result");
835
856
  }
836
- const hit = { ...cached };
837
- if (request.attemptId) {
838
- hit.attemptId = request.attemptId;
839
- } else {
840
- delete hit.attemptId;
841
- }
842
- return hit;
857
+ return sharedVerdict(cached, request);
858
+ }
859
+ }
860
+ if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
861
+ const key = getCacheKey(request, mergedConfig.counterpartyId);
862
+ const pending = inflightVerifies.get(key);
863
+ if (pending) return sharedVerdict(await pending, request);
864
+ const flight = verifyViaApi(mergedConfig, request, urls, uncacheable);
865
+ inflightVerifies.set(key, flight);
866
+ try {
867
+ return await flight;
868
+ } finally {
869
+ inflightVerifies.delete(key);
843
870
  }
844
871
  }
872
+ return verifyViaApi(mergedConfig, request, urls, uncacheable);
873
+ }
874
+ var inflightVerifies = /* @__PURE__ */ new Map();
875
+ var sharedVerdicts = /* @__PURE__ */ new WeakSet();
876
+ function isSharedVerdict(result) {
877
+ return sharedVerdicts.has(result);
878
+ }
879
+ function sharedVerdict(source, request) {
880
+ const hit = { ...source };
881
+ if (request.attemptId) {
882
+ hit.attemptId = request.attemptId;
883
+ } else {
884
+ delete hit.attemptId;
885
+ }
886
+ sharedVerdicts.add(hit);
887
+ return hit;
888
+ }
889
+ async function verifyViaApi(mergedConfig, request, urls, uncacheable) {
845
890
  const enrichedRequest = { ...request };
846
891
  if (!enrichedRequest.counterpartyUrl && mergedConfig.counterpartyUrl) {
847
892
  enrichedRequest.counterpartyUrl = mergedConfig.counterpartyUrl;
@@ -869,9 +914,18 @@ async function verify(config, request, options) {
869
914
  if (!apiResponse.access?.allowed) {
870
915
  const aggregatedFailures = apiResponse.access?.failures;
871
916
  const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
917
+ const denyAgent = idVerifiedFromBackend && apiResponse.agent ? {
918
+ astraId: apiResponse.agent.astraId,
919
+ name: apiResponse.agent.name,
920
+ trustScore: apiResponse.agent.trustScore,
921
+ trustLevel: getTrustLevel(apiResponse.agent.trustScore),
922
+ blockchainAnchored: apiResponse.agent.blockchainStatus === "anchored",
923
+ status: apiResponse.agent.agentStatus
924
+ } : void 0;
872
925
  const result2 = {
873
926
  identityVerified: idVerifiedFromBackend,
874
927
  policyAllowed: false,
928
+ ...denyAgent && { agent: denyAgent },
875
929
  // 3.6.0: the server now emits top-level denialReasons on every deny
876
930
  // path — prefer it verbatim; the failure-message reconstruction remains
877
931
  // as the fallback for older backends.
@@ -911,7 +965,9 @@ async function verify(config, request, options) {
911
965
  fulfilment: apiResponse.fulfilment,
912
966
  mandates: apiResponse.mandates,
913
967
  kyaToken: apiResponse.kyaToken,
914
- aep: apiResponse.aep
968
+ aep: apiResponse.aep,
969
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
970
+ warnings: apiResponse.warnings
915
971
  };
916
972
  return result2;
917
973
  }
@@ -979,7 +1035,9 @@ async function verify(config, request, options) {
979
1035
  // 5.15.0: merchant-lane AP2 mandates + KYAPay identity token.
980
1036
  mandates: apiResponse.mandates,
981
1037
  kyaToken: apiResponse.kyaToken,
982
- aep: apiResponse.aep
1038
+ aep: apiResponse.aep,
1039
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
1040
+ warnings: apiResponse.warnings
983
1041
  };
984
1042
  if (result.recommendation === "deny") {
985
1043
  result.policyAllowed = false;
@@ -1005,8 +1063,24 @@ async function verify(config, request, options) {
1005
1063
  cacheResult(request, result, mergedConfig.cacheTtl, mergedConfig.counterpartyId);
1006
1064
  }
1007
1065
  }
1066
+ notifyVerificationListeners(request, result);
1008
1067
  return result;
1009
1068
  }
1069
+ var verificationListeners = /* @__PURE__ */ new Set();
1070
+ function onVerificationResult(listener) {
1071
+ verificationListeners.add(listener);
1072
+ return () => {
1073
+ verificationListeners.delete(listener);
1074
+ };
1075
+ }
1076
+ function notifyVerificationListeners(request, result) {
1077
+ for (const listener of verificationListeners) {
1078
+ try {
1079
+ listener(result, request);
1080
+ } catch {
1081
+ }
1082
+ }
1083
+ }
1010
1084
  async function recordDecision(config, sessionId, decision, reason) {
1011
1085
  const headers = { "Content-Type": "application/json" };
1012
1086
  if (config.apiKey) {
@@ -1063,7 +1137,7 @@ async function reportAttempt(config, report, options = {}) {
1063
1137
  }
1064
1138
  var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
1065
1139
  var refusedPolicyReads = /* @__PURE__ */ new Set();
1066
- async function fetchRoutes(config, counterpartyId) {
1140
+ async function fetchRoutes(config, counterpartyId, options = {}) {
1067
1141
  if (!counterpartyId) return null;
1068
1142
  const headers = { "Content-Type": "application/json" };
1069
1143
  if (config.apiKey) {
@@ -1071,8 +1145,9 @@ async function fetchRoutes(config, counterpartyId) {
1071
1145
  headers["X-API-Key"] = config.apiKey;
1072
1146
  }
1073
1147
  try {
1148
+ headers["X-AstraSync-SDK-Version"] = SDK_VERSION;
1074
1149
  const response = await sdkFetch(
1075
- `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
1150
+ `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes` + (options.codeGates?.length ? `?codeGates=${options.codeGates.map(encodeURIComponent).join(",")}` : ""),
1076
1151
  { method: "GET", headers }
1077
1152
  );
1078
1153
  if (!response.ok) {
@@ -1085,6 +1160,13 @@ async function fetchRoutes(config, counterpartyId) {
1085
1160
  return null;
1086
1161
  }
1087
1162
  const body = await response.json();
1163
+ const type2 = body.data?.counterpartyType;
1164
+ if (typeof type2 === "string" && options.onEndpointType) {
1165
+ try {
1166
+ options.onEndpointType(type2);
1167
+ } catch {
1168
+ }
1169
+ }
1088
1170
  return body.data?.routes ?? [];
1089
1171
  } catch {
1090
1172
  return null;
@@ -1755,6 +1837,42 @@ async function detectFetchCommerce(request) {
1755
1837
  );
1756
1838
  }
1757
1839
 
1840
+ // src/adapters/routes-cache.ts
1841
+ var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
1842
+ function createRoutesCache(config, opts = {}) {
1843
+ const refreshMs = opts.refreshMs ?? DEFAULT_ROUTES_REFRESH_MS;
1844
+ let cached = [];
1845
+ let lastFetchAt = 0;
1846
+ let refreshing = null;
1847
+ async function refresh() {
1848
+ if (!config.counterpartyId) {
1849
+ opts.onNoCounterparty?.();
1850
+ return;
1851
+ }
1852
+ const fetched = await fetchRoutes(config, config.counterpartyId, opts.fetchOptions);
1853
+ if (fetched) {
1854
+ cached = fetched;
1855
+ lastFetchAt = Date.now();
1856
+ opts.onFetched?.(fetched);
1857
+ }
1858
+ }
1859
+ function start() {
1860
+ refreshing = refresh().catch(() => void 0).finally(() => {
1861
+ refreshing = null;
1862
+ });
1863
+ }
1864
+ start();
1865
+ return {
1866
+ async current() {
1867
+ if (refreshing) await refreshing.catch(() => {
1868
+ });
1869
+ if (config.counterpartyId && Date.now() - lastFetchAt > refreshMs) start();
1870
+ return cached;
1871
+ },
1872
+ peek: () => cached
1873
+ };
1874
+ }
1875
+
1758
1876
  // src/transport/http.ts
1759
1877
  var HEADER_PREFIX = "X-Astra-";
1760
1878
  function setHttpHeaders(headers, credentials) {
@@ -1931,6 +2049,20 @@ function resolveHttpPdlss(input) {
1931
2049
  }
1932
2050
  return { purpose, action, purposeSource, actionSource };
1933
2051
  }
2052
+ function wirePurposeSource(source) {
2053
+ switch (source) {
2054
+ case "route_config":
2055
+ return "route_config";
2056
+ case "header":
2057
+ case "legacy_header":
2058
+ case "query":
2059
+ return "header";
2060
+ case "custom_extractor":
2061
+ return "body";
2062
+ default:
2063
+ return "default";
2064
+ }
2065
+ }
1934
2066
 
1935
2067
  // src/pdlss-pre-check.ts
1936
2068
  function performCounterpartyPreCheck(routeConfig, astraCreds, purpose) {
@@ -2430,12 +2562,12 @@ var BeaconBuffer = class {
2430
2562
  }
2431
2563
  /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
2432
2564
  takeBatch(now) {
2433
- const first = this.queue.find((q) => q.eligibleAt <= now);
2434
- if (!first) return [];
2565
+ const first2 = this.queue.find((q) => q.eligibleAt <= now);
2566
+ if (!first2) return [];
2435
2567
  const batch = [];
2436
2568
  const rest = [];
2437
2569
  for (const q of this.queue) {
2438
- if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
2570
+ if (batch.length < this.opts.maxBatch && q.destKey === first2.destKey && q.eligibleAt <= now) {
2439
2571
  q.inFlight = true;
2440
2572
  batch.push(q);
2441
2573
  } else {
@@ -2712,7 +2844,6 @@ function defaultOnDenied(result, _req, res) {
2712
2844
  }
2713
2845
  });
2714
2846
  }
2715
- var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
2716
2847
  function createMiddleware(options) {
2717
2848
  if ("routes" in options) {
2718
2849
  console.warn(
@@ -2728,7 +2859,7 @@ function createMiddleware(options) {
2728
2859
  recordDecisions,
2729
2860
  enableRuntimeChallenge = true,
2730
2861
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
2731
- failOnError = "open",
2862
+ failOnError = "closed",
2732
2863
  caseInsensitiveRouteMatch = false,
2733
2864
  anonymousBeacon = true,
2734
2865
  trustVerifiedHop = false,
@@ -2752,26 +2883,19 @@ function createMiddleware(options) {
2752
2883
  rawHeaders: req.headers
2753
2884
  });
2754
2885
  } : void 0;
2755
- let cachedRoutes = [];
2756
- let lastFetchAt = 0;
2757
- let refreshing = null;
2758
2886
  let warnedNoCounterparty = false;
2759
2887
  let warnedEmptyRoutes = false;
2760
- async function refreshRoutes() {
2761
- if (!config.counterpartyId) {
2762
- if (!warnedNoCounterparty) {
2763
- console.warn(
2764
- "[VerificationGateway] No counterpartyId configured \u2014 falling through (allow all). Per-route policy lives in the AstraSync dashboard now; register the endpoint and set counterpartyId in your middleware config to enforce policy."
2765
- );
2766
- warnedNoCounterparty = true;
2767
- }
2768
- return;
2769
- }
2770
- const fetched = await fetchRoutes(config, config.counterpartyId);
2771
- if (fetched) {
2772
- cachedRoutes = fetched;
2773
- lastFetchAt = Date.now();
2774
- if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
2888
+ const routesCache = createRoutesCache(config, {
2889
+ refreshMs: routesRefreshMs,
2890
+ onNoCounterparty: () => {
2891
+ if (warnedNoCounterparty) return;
2892
+ console.warn(
2893
+ "[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."
2894
+ );
2895
+ warnedNoCounterparty = true;
2896
+ },
2897
+ onFetched: (routes) => {
2898
+ if (routes.length === 0 && !warnedEmptyRoutes) {
2775
2899
  const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
2776
2900
  console.warn(
2777
2901
  `[VerificationGateway] No route policy configured for ${config.counterpartyId}. Gateway is in pass-through mode for ALL traffic until you add at least one route. Configure at ${dashboard}/dashboard/endpoints/${config.counterpartyId}/routes`
@@ -2779,9 +2903,6 @@ function createMiddleware(options) {
2779
2903
  warnedEmptyRoutes = true;
2780
2904
  }
2781
2905
  }
2782
- }
2783
- refreshing = refreshRoutes().finally(() => {
2784
- refreshing = null;
2785
2906
  });
2786
2907
  return async (req, res, next) => {
2787
2908
  try {
@@ -2789,15 +2910,9 @@ function createMiddleware(options) {
2789
2910
  if (shouldSkip) {
2790
2911
  return next();
2791
2912
  }
2792
- if (refreshing) {
2793
- await refreshing.catch(() => {
2794
- });
2795
- }
2796
- if (config.counterpartyId && Date.now() - lastFetchAt > routesRefreshMs) {
2797
- refreshing = refreshRoutes().finally(() => {
2798
- refreshing = null;
2799
- });
2800
- }
2913
+ const cachedRoutes = (await routesCache.current()).filter(
2914
+ (r) => typeof r.pattern === "string" && !r.tool
2915
+ );
2801
2916
  const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
2802
2917
  const matchedRoute = findRouteConfig(cachedRoutes, req.path, req.method, {
2803
2918
  caseInsensitive: caseInsensitiveRouteMatch,
@@ -2921,6 +3036,12 @@ function createMiddleware(options) {
2921
3036
  ...commerceArtifacts && { commerceArtifacts },
2922
3037
  purpose,
2923
3038
  action: pdlssPair.action,
3039
+ // 5.19.0: the source + the request line let verify-access check a
3040
+ // route purpose against the stored rule (item 20: an MPP paid leg
3041
+ // keeps `shopping.purchase` instead of `commerce.payment.execute`).
3042
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
3043
+ requestMethod: req.method,
3044
+ requestPath: req.path,
2924
3045
  resource: req.path,
2925
3046
  createSession: shouldRecordDecisions,
2926
3047
  counterpartyUrl,
@@ -2969,14 +3090,14 @@ function createMiddleware(options) {
2969
3090
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
2970
3091
  res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2971
3092
  }
2972
- if (shouldRecordDecisions && sessionId) {
3093
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2973
3094
  recordDecision(config, sessionId, "granted").catch(() => {
2974
3095
  });
2975
3096
  }
2976
3097
  return next();
2977
3098
  }
2978
3099
  if (!result.policyAllowed) {
2979
- if (shouldRecordDecisions && sessionId) {
3100
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2980
3101
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
2981
3102
  });
2982
3103
  }
@@ -2989,7 +3110,7 @@ function createMiddleware(options) {
2989
3110
  }
2990
3111
  if (requiresHumanApproval(result)) {
2991
3112
  annotateApprovalRequired(result);
2992
- if (shouldRecordDecisions && sessionId) {
3113
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
2993
3114
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
2994
3115
  });
2995
3116
  }
@@ -2998,7 +3119,8 @@ function createMiddleware(options) {
2998
3119
  return;
2999
3120
  }
3000
3121
  if (routeConfig.minTrustScore && result.agent) {
3001
- if (result.agent.trustScore < routeConfig.minTrustScore) {
3122
+ const score = result.agent.trustScore;
3123
+ if (typeof score !== "number" || score < routeConfig.minTrustScore) {
3002
3124
  const trustFailure = {
3003
3125
  dimension: "endpoint.trust",
3004
3126
  message: "Trust below the route requirement for this endpoint.",
@@ -3013,7 +3135,7 @@ function createMiddleware(options) {
3013
3135
  documentationUrl: wellKnownUrls.documentationUrl
3014
3136
  };
3015
3137
  }
3016
- if (shouldRecordDecisions && sessionId) {
3138
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
3017
3139
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
3018
3140
  });
3019
3141
  }
@@ -3022,7 +3144,7 @@ function createMiddleware(options) {
3022
3144
  return;
3023
3145
  }
3024
3146
  }
3025
- if (shouldRecordDecisions && sessionId) {
3147
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
3026
3148
  recordDecision(config, sessionId, "granted").catch(() => {
3027
3149
  });
3028
3150
  }
@@ -3442,6 +3564,10 @@ function createMiddleware2(options) {
3442
3564
  ...commerceArtifacts && { commerceArtifacts },
3443
3565
  purpose,
3444
3566
  action: pdlssPair.action,
3567
+ // 5.19.0: route purpose claim + request line (checked server-side).
3568
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
3569
+ requestMethod: request.method,
3570
+ requestPath: pathname,
3445
3571
  resource: pathname,
3446
3572
  counterpartyUrl,
3447
3573
  counterpartyType: config.counterpartyType || "website",
@@ -3642,6 +3768,7 @@ function isRetryableSettlementError(err) {
3642
3768
  }
3643
3769
 
3644
3770
  // src/adapters/sdk.ts
3771
+ var warnedPhaselessValue = false;
3645
3772
  var VerificationGatewayClient = class {
3646
3773
  constructor(options) {
3647
3774
  this.config = {
@@ -3673,9 +3800,16 @@ var VerificationGatewayClient = class {
3673
3800
  apiKey: options.apiKey,
3674
3801
  jwt: options.jwt
3675
3802
  };
3803
+ if (options.transactionValue && !options.commercePhase && !warnedPhaselessValue) {
3804
+ warnedPhaselessValue = true;
3805
+ console.warn(
3806
+ "[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."
3807
+ );
3808
+ }
3676
3809
  return this.executeWithRetry(
3677
3810
  () => verify(this.config, {
3678
3811
  credentials,
3812
+ ...options.commercePhase && { commercePhase: options.commercePhase },
3679
3813
  purpose: options.purpose,
3680
3814
  action: options.action,
3681
3815
  resourceType: options.resourceType,
@@ -3691,6 +3825,43 @@ var VerificationGatewayClient = class {
3691
3825
  })
3692
3826
  );
3693
3827
  }
3828
+ /**
3829
+ * 5.19.0: the QUOTE leg of a checkout — a price / limit check before the
3830
+ * purchase. Sends `commercePhase: 'quote'`: verify-access evaluates the
3831
+ * agent's policy at this value and tells you whether a purchase would be
3832
+ * granted, held for the owner's approval, or denied, but records no order,
3833
+ * moves no money and never redeems an approval. Always reaches the backend
3834
+ * (no verdict cache).
3835
+ *
3836
+ * Quotes count toward limit-probing detection: a run of rising quotes is
3837
+ * how an agent (or a merchant) maps a spending limit. Quote what the buyer
3838
+ * asked for, not a ladder of values.
3839
+ */
3840
+ async quoteCheckout(options) {
3841
+ const credentials = {
3842
+ astraId: options.astraId,
3843
+ apiKey: options.apiKey,
3844
+ jwt: options.jwt
3845
+ };
3846
+ return this.executeWithRetry(
3847
+ () => verify(
3848
+ this.config,
3849
+ {
3850
+ credentials,
3851
+ purpose: options.purpose ?? "shopping",
3852
+ action: options.action ?? "shopping.purchase",
3853
+ transactionValue: options.transactionValue,
3854
+ currency: options.currency,
3855
+ commercePhase: "quote",
3856
+ ...options.checkoutSessionId && { checkoutSessionId: options.checkoutSessionId },
3857
+ ...options.attemptId && { attemptId: options.attemptId },
3858
+ counterpartyUrl: options.counterpartyUrl,
3859
+ counterpartyType: options.counterpartyType
3860
+ },
3861
+ { bypassCache: true }
3862
+ )
3863
+ );
3864
+ }
3694
3865
  /**
3695
3866
  * 5.4.0 (astra-pay): confirm-leg of a checkout — the ONLY call that may fire
3696
3867
  * first-party charge-at-redeem (server-side settlement on the owner's saved
@@ -3729,6 +3900,14 @@ var VerificationGatewayClient = class {
3729
3900
  * is matched to — and only to — this confirm. A held cart's approval is
3730
3901
  * bound to its checkout session and exact value; a confirm for any other
3731
3902
  * cart is held for its own approval.
3903
+ *
3904
+ * 5.19.0 checkout kits: pass the kit's `agentArtifacts` (the inbound agent
3905
+ * request — signed RFC 9421 headers with the public URL and raw-body
3906
+ * digest, or `{ unsigned: true }`) and `metadata` (`{ kitSessionId,
3907
+ * protocol }`) from the `verifyPurchase` context. The platform attributes
3908
+ * the confirm to the agent from the artifacts, and stores the metadata on
3909
+ * any approval this confirm creates so the approval event
3910
+ * (`handleApprovalEvents`) can find the kit session.
3732
3911
  */
3733
3912
  async confirmCheckout(options) {
3734
3913
  const credentials = {
@@ -3751,6 +3930,8 @@ var VerificationGatewayClient = class {
3751
3930
  ...options.checkoutJwt && { checkoutJwt: options.checkoutJwt },
3752
3931
  ...options.attemptId && { attemptId: options.attemptId },
3753
3932
  buyerEmail: options.buyerEmail,
3933
+ ...options.agentArtifacts && { agentArtifacts: options.agentArtifacts },
3934
+ ...options.metadata && { metadata: options.metadata },
3754
3935
  counterpartyUrl: options.counterpartyUrl,
3755
3936
  counterpartyType: options.counterpartyType
3756
3937
  },
@@ -3811,6 +3992,80 @@ var VerificationGatewayClient = class {
3811
3992
  }
3812
3993
  return json4.data;
3813
3994
  }
3995
+ /**
3996
+ * 5.20.0 (round 29.1): change an endpoint's OPERATIONAL URLs with this
3997
+ * client's API key — `PATCH /api/endpoints/:id`. Accepts the key of the
3998
+ * endpoint's owner, or a personal key of an individual with an active seat
3999
+ * in the owning organisation. Policy (rules, trust floor, challenge, edge
4000
+ * config) stays dashboard-only. `null` clears a field. The owner is alerted
4001
+ * when `eventsUrl` changes.
4002
+ *
4003
+ * @param endpointId the ASTRAE id (defaults to the client's `counterpartyId`)
4004
+ * @returns the endpoint as stored, plus `changed` (the fields that changed)
4005
+ * @throws AstraSyncError — 400 `UNKNOWN_FIELDS` / validation, 401/403, 404
4006
+ */
4007
+ async updateEndpoint(endpointId, patch) {
4008
+ return this.endpointCall("PATCH", endpointId, "", patch);
4009
+ }
4010
+ /**
4011
+ * 5.20.0 (round 29.1): ask AstraSync to send ONE signed step-up event to the
4012
+ * endpoint's Events URL now — `POST /api/endpoints/:id/events/test`.
4013
+ * Delivered synchronously, outside the event outbox (random `eventId`, no
4014
+ * retries), signed with the endpoint's webhook secret like a real event.
4015
+ * Without `approvalId` the payload is synthetic (a kit acknowledges and
4016
+ * ignores it). With the id of a held kit approval at this endpoint the
4017
+ * payload is the real one, so your resume path runs: `approved` makes the
4018
+ * kit re-confirm (held again — nothing is charged); `denied` / `expired` /
4019
+ * `lapsed` release the session.
4020
+ *
4021
+ * Resolves even when your endpoint refused the event (`delivered: false`,
4022
+ * with `responseCode` / `error`).
4023
+ */
4024
+ async sendTestEvent(endpointId, options) {
4025
+ return this.endpointCall("POST", endpointId, "/events/test", options);
4026
+ }
4027
+ async endpointCall(method, endpointId, suffix, body) {
4028
+ const id = endpointId ?? this.config.counterpartyId;
4029
+ if (!id) {
4030
+ throw new AstraSyncError(
4031
+ "An endpoint id (ASTRAE-\u2026) is required: pass it, or set counterpartyId on the client",
4032
+ 400,
4033
+ "VALIDATION"
4034
+ );
4035
+ }
4036
+ if (!this.config.apiKey) {
4037
+ throw new AstraSyncError("This call needs the client apiKey", 401, "AUTH_REQUIRED");
4038
+ }
4039
+ const base = this.config.apiBaseUrl.replace(/\/+$/, "");
4040
+ const url = `${base}/endpoints/${encodeURIComponent(id)}${suffix}`;
4041
+ let res;
4042
+ try {
4043
+ res = await sdkFetch(url, {
4044
+ method,
4045
+ headers: {
4046
+ "Content-Type": "application/json",
4047
+ Authorization: `Bearer ${this.config.apiKey}`,
4048
+ "X-API-Key": this.config.apiKey
4049
+ },
4050
+ body: JSON.stringify(body),
4051
+ signal: AbortSignal.timeout(Math.max(this.timeout, 15e3))
4052
+ });
4053
+ } catch (err) {
4054
+ throw new AstraSyncError(
4055
+ `${method} ${suffix || "/endpoints/:id"} failed: ${err instanceof Error ? err.message : String(err)}`,
4056
+ 0,
4057
+ "network_error"
4058
+ );
4059
+ }
4060
+ const json4 = await res.json().catch(() => ({}));
4061
+ if (!res.ok || json4.success === false || json4.data === void 0) {
4062
+ const errObj = typeof json4.error === "object" ? json4.error : void 0;
4063
+ const code = json4.code ?? errObj?.code ?? (typeof json4.error === "string" ? json4.error : "unknown_error");
4064
+ const message = json4.message ?? errObj?.message ?? `HTTP ${res.status}`;
4065
+ throw new AstraSyncError(`${message} (${code})`, res.status, code);
4066
+ }
4067
+ return json4.data;
4068
+ }
3814
4069
  /**
3815
4070
  * Quick verification — checks credentials and policy in one call.
3816
4071
  *
@@ -4678,9 +4933,9 @@ function extractFulfillmentOption(body) {
4678
4933
  if (direct) return direct;
4679
4934
  const options = body.fulfillment_options;
4680
4935
  if (Array.isArray(options) && options.length > 0) {
4681
- const first = options[0];
4682
- if (first && typeof first === "object") {
4683
- const id = coerceString3(first.id);
4936
+ const first2 = options[0];
4937
+ if (first2 && typeof first2 === "object") {
4938
+ const id = coerceString3(first2.id);
4684
4939
  if (id) return id;
4685
4940
  }
4686
4941
  }
@@ -5687,7 +5942,8 @@ function verifyMPP(input) {
5687
5942
  }
5688
5943
  let replayOk = true;
5689
5944
  if (challenge?.digest && expiryOk) {
5690
- const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
5945
+ const c = challenge;
5946
+ const replayKey = `mpp:${challenge.digest}:${c.id ?? ""}:${c.nonce ?? ""}`;
5691
5947
  const expiresAt = (nowSec + tolerance) * 1e3;
5692
5948
  if (nonceStore.seen(replayKey, expiresAt)) {
5693
5949
  replayOk = false;
@@ -6244,11 +6500,11 @@ function resolveTransactionValue(input) {
6244
6500
  if (v) return v;
6245
6501
  }
6246
6502
  if (input.x402?.paymentRequired) {
6247
- const first = input.x402.paymentRequired.accepts[0];
6248
- if (first) {
6503
+ const first2 = input.x402.paymentRequired.accepts[0];
6504
+ if (first2) {
6249
6505
  const v = extractX402TransactionValue({
6250
- maxAmountRequired: Number(first.amount),
6251
- asset: first.asset
6506
+ maxAmountRequired: Number(first2.amount),
6507
+ asset: first2.asset
6252
6508
  });
6253
6509
  if (v) return v;
6254
6510
  }
@@ -6706,6 +6962,21 @@ function defaultMcpDenied(result, req, res) {
6706
6962
  }
6707
6963
  });
6708
6964
  }
6965
+ function dashboardToolRules(routes) {
6966
+ const out = /* @__PURE__ */ new Map();
6967
+ for (const r of routes) {
6968
+ if (typeof r.tool !== "string" || !r.tool || r.legacy === true) continue;
6969
+ if (out.has(r.tool)) continue;
6970
+ out.set(r.tool, {
6971
+ tool: r.tool,
6972
+ ...r.purpose && { purpose: r.purpose },
6973
+ ...r.action && { action: r.action },
6974
+ ...r.resource && { resource: r.resource },
6975
+ ...r.observe === true && { observe: true }
6976
+ });
6977
+ }
6978
+ return out;
6979
+ }
6709
6980
  function resolveGate(parsed, opts) {
6710
6981
  if (!parsed.toolName) {
6711
6982
  const methodGate = opts.methodGates?.[parsed.method];
@@ -6715,8 +6986,30 @@ function resolveGate(parsed, opts) {
6715
6986
  return { enforce: false, source: "discovery_default" };
6716
6987
  }
6717
6988
  const toolGate = opts.toolGates?.[parsed.toolName];
6989
+ const rule = opts.dashboardRules?.get(parsed.toolName);
6990
+ const fromRule = (r) => r.observe ? { enforce: false, source: "dashboard" } : {
6991
+ enforce: true,
6992
+ config: {
6993
+ ...r.purpose && { purpose: r.purpose },
6994
+ ...r.action && { action: r.action },
6995
+ ...r.resource && { resource: r.resource }
6996
+ },
6997
+ source: "dashboard"
6998
+ };
6999
+ const codeGated = toolGate !== void 0 && toolGate !== "observe";
7000
+ if (rule && (opts.toolGatePrecedence === "dashboard" || !codeGated)) return fromRule(rule);
6718
7001
  if (toolGate !== void 0) {
6719
- return toolGate === "observe" ? { enforce: false, source: "toolGate" } : { enforce: true, config: toolGate, source: "toolGate" };
7002
+ if (toolGate === "observe") return { enforce: false, source: "toolGate" };
7003
+ if (rule && !rule.observe && rule.purpose && (rule.purpose !== toolGate.purpose || rule.action && rule.action !== toolGate.action)) {
7004
+ const extra = fromRule(rule).config;
7005
+ return {
7006
+ enforce: true,
7007
+ config: toolGate,
7008
+ additional: { ...extra, ...!extra.resource && toolGate.resource && { resource: toolGate.resource } },
7009
+ source: "toolGate"
7010
+ };
7011
+ }
7012
+ return { enforce: true, config: toolGate, source: "toolGate" };
6720
7013
  }
6721
7014
  return { enforce: mcpDefaultEnforced(parsed), source: "tier" };
6722
7015
  }
@@ -6731,13 +7024,34 @@ function createMcpMiddleware(options) {
6731
7024
  verifiedHopMaxAgeMs,
6732
7025
  recordDecisions,
6733
7026
  enableRuntimeChallenge = true,
6734
- failOnError = "open",
7027
+ failOnError = "closed",
7028
+ toolGatePrecedence = "code",
7029
+ routesRefreshMs,
7030
+ skipEvaluation,
6735
7031
  ...config
6736
7032
  } = options;
7033
+ const skipTools = new Set(skipEvaluation?.tools ?? []);
7034
+ const skipMethods = new Set(skipEvaluation?.methods ?? []);
6737
7035
  if (config.apiBaseUrl) {
6738
7036
  prefetchWellKnown(config.apiBaseUrl).catch(() => {
6739
7037
  });
6740
7038
  }
7039
+ const codeGatedTools = Object.entries(toolGates ?? {}).filter(([, g]) => g !== "observe").map(([name]) => name);
7040
+ let warnedEndpointType = false;
7041
+ const onEndpointType = (counterpartyType) => {
7042
+ if (counterpartyType === "mcp_server" || warnedEndpointType) return;
7043
+ warnedEndpointType = true;
7044
+ console.warn(
7045
+ `[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.`
7046
+ );
7047
+ };
7048
+ const rulesCache = config.apiKey && config.counterpartyId ? createRoutesCache(config, {
7049
+ ...routesRefreshMs !== void 0 && { refreshMs: routesRefreshMs },
7050
+ fetchOptions: {
7051
+ ...codeGatedTools.length && { codeGates: codeGatedTools },
7052
+ onEndpointType
7053
+ }
7054
+ }) : void 0;
6741
7055
  return async (req, res, next) => {
6742
7056
  try {
6743
7057
  if (skip) return next();
@@ -6786,10 +7100,19 @@ function createMcpMiddleware(options) {
6786
7100
  return next();
6787
7101
  }
6788
7102
  }
7103
+ if (!parsed.toolName && skipMethods.has(parsed.method) && methodGates?.[parsed.method] === void 0) {
7104
+ return next();
7105
+ }
7106
+ const dashboardRules = rulesCache ? dashboardToolRules(await rulesCache.current()) : void 0;
6789
7107
  const gateDecision = resolveGate(parsed, {
6790
7108
  toolGates,
6791
- methodGates
7109
+ methodGates,
7110
+ dashboardRules,
7111
+ toolGatePrecedence
6792
7112
  });
7113
+ if (!gateDecision.enforce && (parsed.toolName && skipTools.has(parsed.toolName) || !parsed.toolName && skipMethods.has(parsed.method))) {
7114
+ return next();
7115
+ }
6793
7116
  const credentials = extractCredentials(
6794
7117
  req.headers,
6795
7118
  req.query
@@ -6846,8 +7169,14 @@ function createMcpMiddleware(options) {
6846
7169
  const forwardedFor = req.headers["x-forwarded-for"];
6847
7170
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
6848
7171
  const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
7172
+ const attemptRaw = readSingleHeader(req.headers["x-astra-attempt-id"]);
7173
+ const metaAttempt = req.body?.params?._meta?.astrasync?.attemptId;
7174
+ const attemptId = [attemptRaw, metaAttempt].find(
7175
+ (v) => typeof v === "string" && /^att_[0-9a-f]{32}$/.test(v)
7176
+ );
6849
7177
  const verificationRequest = {
6850
7178
  credentials,
7179
+ ...attemptId && { attemptId },
6851
7180
  purpose: pdlss.purpose,
6852
7181
  action: pdlss.action,
6853
7182
  resource: pdlss.resource,
@@ -6886,6 +7215,20 @@ function createMcpMiddleware(options) {
6886
7215
  result.stepUpOutcome = outcome;
6887
7216
  }
6888
7217
  }
7218
+ if (shouldEnforce && gateDecision.additional?.purpose && result.policyAllowed && !requiresHumanApproval(result)) {
7219
+ const extra = mcpToPdlss(parsed, req.path, void 0, void 0, gateDecision.additional);
7220
+ const extraResult = await verify(config, {
7221
+ ...verificationRequest,
7222
+ purpose: extra.purpose,
7223
+ action: extra.action,
7224
+ resource: extra.resource
7225
+ });
7226
+ if (!extraResult.policyAllowed || requiresHumanApproval(extraResult)) {
7227
+ result = extraResult;
7228
+ req.agentVerification = result;
7229
+ sessionId = result.sessionId;
7230
+ }
7231
+ }
6889
7232
  if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
6890
7233
  res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6891
7234
  return next();
@@ -6895,14 +7238,14 @@ function createMcpMiddleware(options) {
6895
7238
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
6896
7239
  res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
6897
7240
  }
6898
- if (shouldRecordDecisions && sessionId) {
7241
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6899
7242
  recordDecision(config, sessionId, "granted").catch(() => {
6900
7243
  });
6901
7244
  }
6902
7245
  return next();
6903
7246
  }
6904
7247
  if (!result.policyAllowed) {
6905
- if (shouldRecordDecisions && sessionId) {
7248
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6906
7249
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
6907
7250
  });
6908
7251
  }
@@ -6912,7 +7255,7 @@ function createMcpMiddleware(options) {
6912
7255
  }
6913
7256
  if (requiresHumanApproval(result)) {
6914
7257
  annotateApprovalRequired(result);
6915
- if (shouldRecordDecisions && sessionId) {
7258
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6916
7259
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
6917
7260
  });
6918
7261
  }
@@ -6930,7 +7273,7 @@ function createMcpMiddleware(options) {
6930
7273
  })
6931
7274
  );
6932
7275
  }
6933
- if (shouldRecordDecisions && sessionId) {
7276
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
6934
7277
  recordDecision(config, sessionId, "granted").catch(() => {
6935
7278
  });
6936
7279
  }
@@ -10192,15 +10535,15 @@ function requireDumper() {
10192
10535
  return !isWhitespace(c) && c !== CHAR_COLON;
10193
10536
  }
10194
10537
  function codePointAt(string, pos) {
10195
- const first = string.charCodeAt(pos);
10538
+ const first2 = string.charCodeAt(pos);
10196
10539
  let second;
10197
- if (first >= 55296 && first <= 56319 && pos + 1 < string.length) {
10540
+ if (first2 >= 55296 && first2 <= 56319 && pos + 1 < string.length) {
10198
10541
  second = string.charCodeAt(pos + 1);
10199
10542
  if (second >= 56320 && second <= 57343) {
10200
- return (first - 55296) * 1024 + second - 56320 + 65536;
10543
+ return (first2 - 55296) * 1024 + second - 56320 + 65536;
10201
10544
  }
10202
10545
  }
10203
- return first;
10546
+ return first2;
10204
10547
  }
10205
10548
  function needIndentIndicator(string) {
10206
10549
  const leadingSpaceRe = /^\n* /;
@@ -10939,6 +11282,16 @@ function checkUcpAgentBinding(header, identities) {
10939
11282
 
10940
11283
  // src/ucp/profile.ts
10941
11284
  var UCP_VERSION = "2026-08-25";
11285
+ function ucpProfileKeys(profile) {
11286
+ const out = [];
11287
+ for (const list of [profile?.keys, profile?.signing_keys]) {
11288
+ if (!Array.isArray(list)) continue;
11289
+ for (const k of list) {
11290
+ if (k && typeof k === "object" && k.use !== "enc") out.push(k);
11291
+ }
11292
+ }
11293
+ return out;
11294
+ }
10942
11295
  var SPEC = `https://ucp.dev/${UCP_VERSION}/specification`;
10943
11296
  var SCHEMAS = `https://ucp.dev/${UCP_VERSION}/schemas`;
10944
11297
  function astrasyncPaymentHandlers(handlers, specBase = "https://astrasync.ai/ucp/handlers") {
@@ -11854,6 +12207,12 @@ async function finishIdempotent(opts, decision, response) {
11854
12207
  } catch {
11855
12208
  }
11856
12209
  }
12210
+ async function abandonIdempotent(opts, decision) {
12211
+ try {
12212
+ await opts.store.delete(decision.storeKey);
12213
+ } catch {
12214
+ }
12215
+ }
11857
12216
  var KEPT_HEADERS = ["content-type", "location"];
11858
12217
  function withHeader(res, name, value) {
11859
12218
  const headers = new Headers(res.headers);
@@ -12009,8 +12368,102 @@ function warnInMemoryStores(kit, missing) {
12009
12368
  `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(" and ")} (sessions are lost on restart and not shared across instances). Pass ${missing.map((m) => m.startsWith("idempotency") ? "idempotencyStore" : "store").join(" and ")} in production.`
12010
12369
  );
12011
12370
  }
12371
+ var SECRET_KEY = /token|secret|credential|cvc|cvv|number|pan|password|key|cryptogram|signature/i;
12372
+ function scrubPaymentInput(input) {
12373
+ if (!input || typeof input !== "object") return void 0;
12374
+ const out = {};
12375
+ for (const [k, v] of Object.entries(input)) {
12376
+ if (SECRET_KEY.test(k)) continue;
12377
+ if (v === null || ["string", "number", "boolean"].includes(typeof v)) out[k] = v;
12378
+ }
12379
+ return out;
12380
+ }
12381
+ var NEVER_FORWARD = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "proxy-authorization"]);
12382
+ function coveredHeaders(signatureInput) {
12383
+ const names = /* @__PURE__ */ new Set();
12384
+ for (const m of signatureInput.matchAll(/\(([^)]*)\)/g)) {
12385
+ for (const q of m[1].matchAll(/"([^"]+)"/g)) {
12386
+ const name = q[1].toLowerCase();
12387
+ if (!name.startsWith("@")) names.add(name);
12388
+ }
12389
+ }
12390
+ return [...names];
12391
+ }
12392
+ function buildAgentArtifacts(req, rawBody, opts) {
12393
+ const signature = req.headers.get("signature");
12394
+ const signatureInput = req.headers.get("signature-input");
12395
+ if (opts.protocol === "acp" || !signature || !signatureInput) {
12396
+ return { unsigned: true, protocol: opts.protocol };
12397
+ }
12398
+ const headers = {};
12399
+ const names = [
12400
+ "signature",
12401
+ "signature-input",
12402
+ "signature-agent",
12403
+ "ucp-agent",
12404
+ "content-digest",
12405
+ ...coveredHeaders(signatureInput)
12406
+ ];
12407
+ for (const name of names) {
12408
+ if (NEVER_FORWARD.has(name)) continue;
12409
+ const v = req.headers.get(name);
12410
+ if (v !== null) headers[name] = v;
12411
+ }
12412
+ const u = new URL(req.url);
12413
+ const origin = opts.publicOrigin ? opts.publicOrigin.replace(/\/+$/, "") : u.origin;
12414
+ return {
12415
+ protocol: opts.protocol,
12416
+ method: req.method,
12417
+ url: `${origin}${u.pathname}${u.search}`,
12418
+ headers,
12419
+ ...rawBody && { contentDigest: contentDigest(rawBody) }
12420
+ };
12421
+ }
12422
+ async function withReportRetries(fn, opts = {}, isRetryable) {
12423
+ const attempts = Math.max(1, opts.attempts ?? 3);
12424
+ const backoff = opts.backoffMs ?? 250;
12425
+ let last;
12426
+ for (let i = 0; i < attempts; i++) {
12427
+ try {
12428
+ return await fn();
12429
+ } catch (err) {
12430
+ last = err;
12431
+ if (i === attempts - 1 || !isRetryable(err)) break;
12432
+ await new Promise((r) => setTimeout(r, backoff * 2 ** i));
12433
+ }
12434
+ }
12435
+ throw last;
12436
+ }
12437
+ function defaultOnReportError(err, ctx) {
12438
+ console.error(
12439
+ `[astrasync] checkout kit: ${ctx.kind === "order_webhook" ? "order webhook" : "settlement report"} failed for session ${ctx.sessionId}${ctx.orderId ? ` (order ${ctx.orderId})` : ""}:`,
12440
+ err
12441
+ );
12442
+ }
12443
+ var NON_TERMINAL = /* @__PURE__ */ new Set(["busy", "still_held"]);
12444
+ function isTerminalOutcome(outcome) {
12445
+ return !NON_TERMINAL.has(outcome);
12446
+ }
12447
+ var resumeResult = (outcome, session) => ({
12448
+ outcome,
12449
+ terminal: isTerminalOutcome(outcome),
12450
+ session
12451
+ });
12012
12452
 
12013
12453
  // src/ucp/checkout-kit.ts
12454
+ function withValidImageUrl(item) {
12455
+ if (item.image_url === void 0) return item;
12456
+ let ok = false;
12457
+ try {
12458
+ const u = new URL(item.image_url);
12459
+ ok = u.protocol === "https:" || u.protocol === "http:";
12460
+ } catch {
12461
+ ok = false;
12462
+ }
12463
+ if (ok) return item;
12464
+ const { image_url: _drop, ...rest } = item;
12465
+ return rest;
12466
+ }
12014
12467
  var MemoryCheckoutStore = class {
12015
12468
  constructor() {
12016
12469
  this.m = /* @__PURE__ */ new Map();
@@ -12033,6 +12486,15 @@ var MemoryCheckoutStore = class {
12033
12486
  const id = this.orders.get(orderId);
12034
12487
  return id ? this.m.get(id) ?? null : null;
12035
12488
  }
12489
+ async findByCheckoutSessionId(checkoutSessionId) {
12490
+ for (const s of this.m.values()) {
12491
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
12492
+ }
12493
+ return this.m.get(checkoutSessionId) ?? null;
12494
+ }
12495
+ async listHeld() {
12496
+ return [...this.m.values()].filter((s) => s.status === "requires_escalation");
12497
+ }
12036
12498
  };
12037
12499
  function buildUcpOrder(session, opts = {}) {
12038
12500
  if (!session.order) return null;
@@ -12055,11 +12517,20 @@ function buildUcpOrder(session, opts = {}) {
12055
12517
  totals: session.totals
12056
12518
  };
12057
12519
  }
12520
+ function heldBody(text) {
12521
+ try {
12522
+ return JSON.parse(text).status === "requires_escalation";
12523
+ } catch {
12524
+ return false;
12525
+ }
12526
+ }
12058
12527
  var json2 = (status, body, headers = {}) => new Response(JSON.stringify(body), {
12059
12528
  status,
12060
12529
  headers: { "content-type": "application/json", ...headers }
12061
12530
  });
12062
12531
  var total = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
12532
+ var clone = (v) => JSON.parse(JSON.stringify(v));
12533
+ var inFlight = (st) => st === "complete_in_progress" || st === "finishing";
12063
12534
  function createUcpCheckoutKit(opts) {
12064
12535
  const profile = buildUcpProfile(opts.profile);
12065
12536
  const store = opts.store ?? new MemoryCheckoutStore();
@@ -12085,6 +12556,11 @@ function createUcpCheckoutKit(opts) {
12085
12556
  ...entries.map((e) => e.id)
12086
12557
  ])
12087
12558
  );
12559
+ const publicOrigin = (opts.publicOrigin ?? new URL(opts.profile.endpoint).origin).replace(
12560
+ /\/+$/,
12561
+ ""
12562
+ );
12563
+ const onReportError = opts.onReportError ?? defaultOnReportError;
12088
12564
  const fetchProfile = opts.fetchProfile ?? (async (url, fresh) => {
12089
12565
  const r = await fetchUcpProfile(url, { fresh });
12090
12566
  return r.ok ? r.profile : r;
@@ -12106,7 +12582,7 @@ function createUcpCheckoutKit(opts) {
12106
12582
  payment_handlers: profile.ucp.payment_handlers
12107
12583
  },
12108
12584
  id: s.id,
12109
- status: s.status,
12585
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
12110
12586
  currency: s.currency,
12111
12587
  line_items: s.line_items,
12112
12588
  totals: s.totals,
@@ -12152,6 +12628,23 @@ function createUcpCheckoutKit(opts) {
12152
12628
  "ephemeral"
12153
12629
  );
12154
12630
  }
12631
+ function heldNoEdit(s) {
12632
+ return render(
12633
+ {
12634
+ ...s,
12635
+ messages: [
12636
+ ...s.messages ?? [],
12637
+ ucpError(
12638
+ "checkout_awaiting_approval",
12639
+ "This checkout is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over.",
12640
+ "requires_buyer_review"
12641
+ )
12642
+ ]
12643
+ },
12644
+ 409,
12645
+ "ephemeral"
12646
+ );
12647
+ }
12155
12648
  async function save(s) {
12156
12649
  if (s.order) orderIndex.set(s.order.id, s.id);
12157
12650
  await store.put(s);
@@ -12182,31 +12675,44 @@ function createUcpCheckoutKit(opts) {
12182
12675
  return r;
12183
12676
  }
12184
12677
  function emitOrder(s) {
12185
- void deliverOrder(s).catch(() => void 0);
12186
- }
12187
- async function reportOutcome(p, settled) {
12188
- if (!opts.reportSettlement || !p.orderId) return;
12189
- await opts.reportSettlement({
12190
- orderId: p.orderId,
12191
- status: settled.status,
12192
- ...p.report,
12193
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12194
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12195
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12196
- }).catch(() => void 0);
12678
+ const ctx = { sessionId: s.id, orderId: s.order?.id, kind: "order_webhook" };
12679
+ void deliverOrder(s).then((r) => {
12680
+ if (r && !r.ok) onReportError(new Error(`order webhook failed (HTTP ${r.status})`), ctx);
12681
+ }).catch((err) => onReportError(err, ctx));
12682
+ }
12683
+ async function reportOutcome(s, p, settled) {
12684
+ const report = opts.reportSettlement;
12685
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
12686
+ if (!report || !ref) return;
12687
+ await withReportRetries(
12688
+ () => report({
12689
+ ...ref,
12690
+ status: settled.status,
12691
+ ...p.report,
12692
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12693
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12694
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12695
+ }),
12696
+ opts.reportRetry,
12697
+ isRetryableSettlementError
12698
+ ).catch(
12699
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
12700
+ );
12197
12701
  }
12198
12702
  async function finish(s, result, p, settled) {
12199
- await reportOutcome(p, settled);
12703
+ await reportOutcome(s, p, settled);
12200
12704
  delete s._pending;
12705
+ delete s._held;
12706
+ delete s._approvalId;
12201
12707
  if (settled.status === "failed") {
12202
12708
  s.status = "ready_for_complete";
12203
12709
  s.messages = [
12204
- ucpError("payment_failed", "The payment did not go through.", "requires_buyer_input")
12710
+ ucpError("payment_failed", "The payment did not go through.", "recoverable")
12205
12711
  ];
12206
12712
  await save(s);
12207
12713
  return;
12208
12714
  }
12209
- const order = await opts.fulfil({ session: s, result });
12715
+ const order = await opts.fulfil({ session: s, result, settled });
12210
12716
  s.order = {
12211
12717
  ...order,
12212
12718
  ...!order.permalink_url && opts.permalinkBase && {
@@ -12240,7 +12746,9 @@ function createUcpCheckoutKit(opts) {
12240
12746
  const out = [];
12241
12747
  const messages = [];
12242
12748
  for (const [i, l] of lines.entries()) {
12243
- const item = l.item?.id ? await opts.catalog(l.item.id) : null;
12749
+ const q = Math.max(1, Math.floor(l.quantity ?? 1));
12750
+ const found = l.item?.id ? await opts.catalog(l.item.id, q) : null;
12751
+ const item = found && withValidImageUrl(found);
12244
12752
  if (!item) {
12245
12753
  messages.push(
12246
12754
  ucpError(
@@ -12252,7 +12760,6 @@ function createUcpCheckoutKit(opts) {
12252
12760
  );
12253
12761
  continue;
12254
12762
  }
12255
- const q = Math.max(1, Math.floor(l.quantity ?? 1));
12256
12763
  const amount = item.price * q;
12257
12764
  out.push({
12258
12765
  id: `li_${i + 1}`,
@@ -12274,6 +12781,19 @@ function createUcpCheckoutKit(opts) {
12274
12781
  ]
12275
12782
  };
12276
12783
  }
12784
+ async function validate(s) {
12785
+ if (!opts.validateCart) return { messages: [], blocked: false };
12786
+ const found = await opts.validateCart(s);
12787
+ const messages = found.map(
12788
+ (m) => (m.type ?? "error") === "error" ? ucpError(m.code, m.content, "recoverable", m.path) : {
12789
+ type: m.type,
12790
+ code: m.code,
12791
+ content: m.content,
12792
+ ...m.path && { path: m.path }
12793
+ }
12794
+ );
12795
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
12796
+ }
12277
12797
  async function platform(req, body) {
12278
12798
  const profileUrl = parseUcpAgentHeader(req.headers.get("ucp-agent"));
12279
12799
  if (!profileUrl) {
@@ -12317,7 +12837,7 @@ function createUcpCheckoutKit(opts) {
12317
12837
  { method: req.method, url: req.url, headers, body: body || void 0 },
12318
12838
  async (fresh) => {
12319
12839
  const pp = fresh ? await fetchProfile(profileUrl, true) : plat;
12320
- return "keys" in pp && pp.keys ? pp.keys : [];
12840
+ return "ucp" in pp ? ucpProfileKeys(pp) : [];
12321
12841
  }
12322
12842
  );
12323
12843
  if (!sig.ok) return { error: json2(sig.status, { code: sig.code, content: sig.content }) };
@@ -12345,17 +12865,20 @@ function createUcpCheckoutKit(opts) {
12345
12865
  });
12346
12866
  const res = await run();
12347
12867
  if (d.kind === "proceed") {
12348
- await finishIdempotent(idem, d, {
12349
- status: res.status,
12350
- headers: { "content-type": "application/json" },
12351
- body: await res.clone().text()
12352
- });
12868
+ const text = await res.clone().text();
12869
+ if (heldBody(text)) await abandonIdempotent(idem, d);
12870
+ else
12871
+ await finishIdempotent(idem, d, {
12872
+ status: res.status,
12873
+ headers: { "content-type": "application/json" },
12874
+ body: text
12875
+ });
12353
12876
  }
12354
12877
  return res;
12355
12878
  }
12356
- async function complete(req, s, body) {
12879
+ async function complete(req, s, body, agentArtifacts) {
12357
12880
  if (s.status === "completed" || s.status === "canceled") return render(s);
12358
- if (s.status === "complete_in_progress") return busy(s);
12881
+ if (inFlight(s.status)) return busy(s);
12359
12882
  const ap2 = body.ap2;
12360
12883
  const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
12361
12884
  if (s._ap2) {
@@ -12371,13 +12894,13 @@ function createUcpCheckoutKit(opts) {
12371
12894
  return render(s);
12372
12895
  }
12373
12896
  const kidResolver = async (kid) => opts.mandateKey ? opts.mandateKey(kid, s) : (s._platformKeys ?? []).find((k) => k.kid === kid) ?? null;
12374
- const v = await verifyAp2Mandate(ap2.checkout_mandate, {
12897
+ const v2 = await verifyAp2Mandate(ap2.checkout_mandate, {
12375
12898
  resolveRootKey: kidResolver,
12376
12899
  expectedVct: AP2_VCT.checkout,
12377
12900
  ...checkoutJwt && { checkoutJwt }
12378
12901
  });
12379
- if (!v.ok) {
12380
- s.messages = [ucpError(v.code, v.error, "unrecoverable")];
12902
+ if (!v2.ok) {
12903
+ s.messages = [ucpError(v2.code, v2.error, "unrecoverable")];
12381
12904
  await store.put(s);
12382
12905
  return render(s);
12383
12906
  }
@@ -12396,19 +12919,32 @@ function createUcpCheckoutKit(opts) {
12396
12919
  await store.put(s);
12397
12920
  return render(s);
12398
12921
  }
12922
+ const v = await validate(s);
12923
+ if (v.blocked) {
12924
+ s.status = "incomplete";
12925
+ s.messages = v.messages;
12926
+ delete s._held;
12927
+ delete s._approvalId;
12928
+ await store.put(s);
12929
+ return render(s);
12930
+ }
12399
12931
  const from = s.status;
12400
12932
  if (!await claim2(s, from)) {
12401
12933
  const current = await store.get(s.id);
12402
12934
  return busy(current ?? s);
12403
12935
  }
12936
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
12937
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
12404
12938
  try {
12405
12939
  const result = await opts.verifyPurchase({
12406
12940
  session: s,
12407
12941
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
12408
- astraId: req.headers.get("x-astra-id") ?? void 0,
12409
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
12942
+ astraId,
12943
+ attemptId,
12410
12944
  ...checkoutJwt && { checkoutJwt },
12411
- valueMajor: total(s) / 100
12945
+ valueMajor: total(s) / 100,
12946
+ agentArtifacts,
12947
+ metadata: { kitSessionId: s.id, protocol: "ucp" }
12412
12948
  });
12413
12949
  const binding2 = await bindAgent(req, result);
12414
12950
  if (binding2) {
@@ -12420,6 +12956,16 @@ function createUcpCheckoutKit(opts) {
12420
12956
  if (isHeld(result)) {
12421
12957
  s.status = "requires_escalation";
12422
12958
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
12959
+ s._approvalId = result.stepUpApproval?.approvalId;
12960
+ s._astraId = astraId ?? result.agent?.astraId;
12961
+ s._attemptId = attemptId ?? result.attemptId;
12962
+ s._held = {
12963
+ line_items: clone(s.line_items),
12964
+ totals: clone(s.totals),
12965
+ ...scrubPaymentInput(instrument) && { instrument: scrubPaymentInput(instrument) },
12966
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
12967
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
12968
+ };
12423
12969
  s.messages = [
12424
12970
  ucpError(
12425
12971
  "buyer_approval_required",
@@ -12442,21 +12988,7 @@ function createUcpCheckoutKit(opts) {
12442
12988
  await store.put(s);
12443
12989
  return render(s);
12444
12990
  }
12445
- const settled = await opts.settle({ session: s, result, instrument });
12446
- const pending = {
12447
- ...result.settlementOutcome?.orderId && {
12448
- orderId: result.settlementOutcome.orderId
12449
- },
12450
- report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
12451
- result: pendingResult(result)
12452
- };
12453
- if (settled.status === "pending") {
12454
- s._pending = pending;
12455
- s.messages = [];
12456
- await store.put(s);
12457
- return render(s);
12458
- }
12459
- await finish(s, result, pending, settled);
12991
+ await settleAndFinish(s, result, instrument);
12460
12992
  return render(s);
12461
12993
  } catch (e) {
12462
12994
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -12466,13 +12998,184 @@ function createUcpCheckoutKit(opts) {
12466
12998
  throw e;
12467
12999
  }
12468
13000
  }
13001
+ async function settleAndFinish(s, result, instrument) {
13002
+ const settled = await opts.settle({ session: s, result, instrument });
13003
+ const enhanced = result;
13004
+ const pending = {
13005
+ ...result.settlementOutcome?.orderId && {
13006
+ orderId: result.settlementOutcome.orderId
13007
+ },
13008
+ ...enhanced.sessionId && { sessionId: enhanced.sessionId },
13009
+ report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
13010
+ result: pendingResult(result)
13011
+ };
13012
+ if (settled.status === "pending") {
13013
+ s._pending = pending;
13014
+ s.messages = [];
13015
+ delete s._held;
13016
+ delete s._approvalId;
13017
+ await store.put(s);
13018
+ return "pending";
13019
+ }
13020
+ await finish(s, result, pending, settled);
13021
+ return settled.status === "failed" ? "payment_failed" : "completed";
13022
+ }
12469
13023
  async function resolvePending(sessionId, outcome) {
12470
13024
  const s = await store.get(sessionId);
12471
13025
  if (!s) return null;
12472
13026
  if (s.status !== "complete_in_progress" || !s._pending) return s;
12473
- await finish(s, s._pending.result, s._pending, outcome);
13027
+ if (store.claim) {
13028
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
13029
+ return await store.get(sessionId) ?? s;
13030
+ }
13031
+ s.status = "finishing";
13032
+ } else {
13033
+ s.status = "finishing";
13034
+ await store.put(s);
13035
+ }
13036
+ const pending = s._pending;
13037
+ try {
13038
+ await finish(s, pending.result, pending, outcome);
13039
+ } catch (e) {
13040
+ s.status = "complete_in_progress";
13041
+ s._pending = pending;
13042
+ await store.put(s).catch(() => void 0);
13043
+ throw e;
13044
+ }
12474
13045
  return s;
12475
13046
  }
13047
+ async function resumeApproved(sessionId, event) {
13048
+ const s = await store.get(sessionId);
13049
+ if (!s) return resumeResult("not_found", null);
13050
+ if (s.status === "completed") return resumeResult("already_completed", s);
13051
+ if (s.status === "canceled") return resumeResult("canceled", s);
13052
+ if (inFlight(s.status)) return resumeResult("busy", s);
13053
+ if (s.status !== "requires_escalation" || !s._held) return resumeResult("not_held", s);
13054
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
13055
+ return resumeResult("stale", s);
13056
+ if (!await claim2(s, "requires_escalation")) {
13057
+ return resumeResult("busy", await store.get(sessionId) ?? s);
13058
+ }
13059
+ const held = s._held;
13060
+ const backToHeld = async () => {
13061
+ s.status = "requires_escalation";
13062
+ s._held = held;
13063
+ await store.put(s).catch(() => void 0);
13064
+ };
13065
+ try {
13066
+ s.line_items = clone(held.line_items);
13067
+ s.totals = clone(held.totals);
13068
+ const v = await validate(s);
13069
+ if (v.blocked) {
13070
+ s.status = "incomplete";
13071
+ s.messages = v.messages;
13072
+ delete s._held;
13073
+ delete s._approvalId;
13074
+ delete s.continue_url;
13075
+ await store.put(s);
13076
+ return resumeResult("cart_invalid", s);
13077
+ }
13078
+ const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
13079
+ const result = await opts.verifyPurchase({
13080
+ session: s,
13081
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
13082
+ astraId: s._astraId,
13083
+ attemptId: s._attemptId,
13084
+ ...checkoutJwt && { checkoutJwt },
13085
+ valueMajor: total(s) / 100,
13086
+ metadata: { kitSessionId: s.id, protocol: "ucp" },
13087
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
13088
+ });
13089
+ if (isHeld(result)) {
13090
+ await backToHeld();
13091
+ return resumeResult("still_held", s);
13092
+ }
13093
+ if (!result.identityVerified || !result.policyAllowed) {
13094
+ s.status = "ready_for_complete";
13095
+ s.messages = [
13096
+ ucpError(
13097
+ "agent_not_authorized",
13098
+ "The agent is not authorized for this purchase.",
13099
+ "unrecoverable"
13100
+ )
13101
+ ];
13102
+ delete s._held;
13103
+ delete s._approvalId;
13104
+ delete s.continue_url;
13105
+ await store.put(s);
13106
+ return resumeResult("not_authorized", s);
13107
+ }
13108
+ delete s.continue_url;
13109
+ const outcome = await settleAndFinish(s, result, held.instrument);
13110
+ return resumeResult(outcome, s);
13111
+ } catch (e) {
13112
+ if (!s._pending && inFlight(s.status)) await backToHeld();
13113
+ throw e;
13114
+ }
13115
+ }
13116
+ async function resumeDeclined(sessionId, reason = "denied", event) {
13117
+ const s = await store.get(sessionId);
13118
+ if (!s) return resumeResult("not_found", null);
13119
+ if (s.status === "completed") return resumeResult("already_completed", s);
13120
+ if (s.status === "canceled") return resumeResult("canceled", s);
13121
+ if (inFlight(s.status)) return resumeResult("busy", s);
13122
+ if (s.status !== "requires_escalation") return resumeResult("not_held", s);
13123
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
13124
+ return resumeResult("stale", s);
13125
+ if (store.claim && !await store.claim(s.id, "requires_escalation", "ready_for_complete")) {
13126
+ return resumeResult("busy", await store.get(sessionId) ?? s);
13127
+ }
13128
+ s.status = "ready_for_complete";
13129
+ delete s._held;
13130
+ delete s._approvalId;
13131
+ delete s.continue_url;
13132
+ s.messages = [
13133
+ ucpError(
13134
+ `approval_${reason}`,
13135
+ 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.",
13136
+ "recoverable"
13137
+ )
13138
+ ];
13139
+ await store.put(s);
13140
+ return resumeResult("declined", s);
13141
+ }
13142
+ async function findForEvent(event) {
13143
+ if (event.kitSessionId) {
13144
+ const s = await store.get(event.kitSessionId);
13145
+ if (s) return s;
13146
+ }
13147
+ if (store.findByCheckoutSessionId)
13148
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
13149
+ return store.get(event.checkoutSessionId);
13150
+ }
13151
+ async function applyApprovalEvent(event) {
13152
+ const s = await findForEvent(event);
13153
+ if (!s) return resumeResult("not_found", null);
13154
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
13155
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
13156
+ }
13157
+ async function pollHeldSessions(o = {}) {
13158
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
13159
+ (x) => Boolean(x)
13160
+ ) : store.listHeld ? await store.listHeld() : [];
13161
+ const out = [];
13162
+ for (const s of sessions) {
13163
+ if (s.status !== "requires_escalation" || !s._held?.pollUrl || !s._approvalId) continue;
13164
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
13165
+ approvalId: s._approvalId,
13166
+ pollUrl: s._held.pollUrl,
13167
+ expiresAt: s._held.expiresAt ?? ""
13168
+ });
13169
+ if (status === "approved" || status === "consumed") {
13170
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
13171
+ } else if (status === "denied" || status === "expired" || status === "lapsed") {
13172
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
13173
+ } else {
13174
+ out.push({ sessionId: s.id, outcome: "pending" });
13175
+ }
13176
+ }
13177
+ return out;
13178
+ }
12476
13179
  async function sendOrderWebhook(sessionId) {
12477
13180
  const s = await store.get(sessionId);
12478
13181
  return s ? deliverOrder(s) : null;
@@ -12491,6 +13194,7 @@ function createUcpCheckoutKit(opts) {
12491
13194
  const p = await platform(req, body);
12492
13195
  if ("error" in p && p.error) return p.error;
12493
13196
  const ctx = p;
13197
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "ucp", publicOrigin });
12494
13198
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId) ?? fallbackScope("ucp-profile", ctx.profileUrl);
12495
13199
  if (req.method === "POST" && path === "/checkout-sessions") {
12496
13200
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12507,7 +13211,7 @@ function createUcpCheckoutKit(opts) {
12507
13211
  _caller: caller,
12508
13212
  _platformProfile: ctx.profileUrl,
12509
13213
  _ap2: ctx.ap2,
12510
- _platformKeys: ctx.plat.keys ?? [],
13214
+ _platformKeys: ucpProfileKeys(ctx.plat),
12511
13215
  ...typeof webhookUrl === "string" && /^https:\/\//.test(webhookUrl) && { _webhookUrl: webhookUrl },
12512
13216
  ...req.headers.get("x-astra-checkout-session") && {
12513
13217
  _astraCheckoutSessionId: req.headers.get("x-astra-checkout-session").slice(0, 64)
@@ -12523,6 +13227,11 @@ function createUcpCheckoutKit(opts) {
12523
13227
  )
12524
13228
  );
12525
13229
  }
13230
+ if (priced.lines.length && !priced.messages.length) {
13231
+ const v = await validate(s2);
13232
+ s2.messages = [...s2.messages ?? [], ...v.messages];
13233
+ if (v.blocked) s2.status = "incomplete";
13234
+ }
12526
13235
  await store.put(s2);
12527
13236
  return render(s2, 201);
12528
13237
  });
@@ -12548,13 +13257,19 @@ function createUcpCheckoutKit(opts) {
12548
13257
  if (req.method === "PUT" && !m[2]) {
12549
13258
  return withIdempotency2(req, body, path, caller, async () => {
12550
13259
  if (s.status === "completed" || s.status === "canceled") return render(s);
12551
- if (s.status === "complete_in_progress") return busy(s);
13260
+ if (inFlight(s.status)) return busy(s);
13261
+ if (s.status === "requires_escalation") return heldNoEdit(s);
12552
13262
  if (parsed.line_items) {
12553
13263
  const priced = await priceLines(parsed.line_items);
12554
13264
  s.line_items = priced.lines;
12555
13265
  s.totals = priced.totals;
12556
13266
  s.messages = priced.messages;
12557
13267
  s.status = priced.lines.length && !priced.messages.length ? "ready_for_complete" : "incomplete";
13268
+ if (s.status === "ready_for_complete") {
13269
+ const v = await validate(s);
13270
+ s.messages = [...s.messages, ...v.messages];
13271
+ if (v.blocked) s.status = "incomplete";
13272
+ }
12558
13273
  }
12559
13274
  if (parsed.buyer) s.buyer = parsed.buyer;
12560
13275
  await store.put(s);
@@ -12562,7 +13277,13 @@ function createUcpCheckoutKit(opts) {
12562
13277
  });
12563
13278
  }
12564
13279
  if (req.method === "POST" && m[2] === "/complete") {
12565
- return withIdempotency2(req, body, path, caller, () => complete(req, s, parsed));
13280
+ return withIdempotency2(
13281
+ req,
13282
+ body,
13283
+ path,
13284
+ caller,
13285
+ () => complete(req, s, parsed, agentArtifacts)
13286
+ );
12566
13287
  }
12567
13288
  if (req.method === "POST" && m[2] === "/cancel") {
12568
13289
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12571,7 +13292,7 @@ function createUcpCheckoutKit(opts) {
12571
13292
  code: "method_not_allowed",
12572
13293
  content: `A ${s.status} checkout can't be canceled.`
12573
13294
  });
12574
- if (s.status === "complete_in_progress") return busy(s);
13295
+ if (inFlight(s.status)) return busy(s);
12575
13296
  s.status = "canceled";
12576
13297
  await store.put(s);
12577
13298
  return render(s);
@@ -12584,10 +13305,156 @@ function createUcpCheckoutKit(opts) {
12584
13305
  profile,
12585
13306
  profileHandler: () => json2(200, profile, { "cache-control": "public, max-age=300" }),
12586
13307
  resolvePending,
12587
- sendOrderWebhook
13308
+ sendOrderWebhook,
13309
+ /** 5.19.0 */
13310
+ protocol: "ucp",
13311
+ publicOrigin,
13312
+ resumeApproved,
13313
+ resumeDeclined,
13314
+ applyApprovalEvent,
13315
+ pollHeldSessions
12588
13316
  };
12589
13317
  }
12590
13318
 
13319
+ // src/webhooks.ts
13320
+ var import_crypto9 = require("crypto");
13321
+ var DEFAULT_TOLERANCE_SECONDS = 300;
13322
+ function parseSignatureHeader(header) {
13323
+ let t;
13324
+ const signatures = [];
13325
+ for (const segment of header.split(",")) {
13326
+ const eq = segment.indexOf("=");
13327
+ if (eq < 0) continue;
13328
+ const k = segment.slice(0, eq).trim();
13329
+ const v = segment.slice(eq + 1).trim();
13330
+ if (k === "t") t = v;
13331
+ else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
13332
+ }
13333
+ return { t, signatures };
13334
+ }
13335
+ function computeHmac(secret, signedPayload) {
13336
+ return (0, import_crypto9.createHmac)("sha256", secret).update(signedPayload, "utf8").digest("hex");
13337
+ }
13338
+ function constantTimeEquals(a, b) {
13339
+ const aBuf = Buffer.from(a, "utf8");
13340
+ const bBuf = Buffer.from(b, "utf8");
13341
+ if (aBuf.length !== bBuf.length) return false;
13342
+ return (0, import_crypto9.timingSafeEqual)(aBuf, bBuf);
13343
+ }
13344
+ function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
13345
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
13346
+ (x) => typeof x === "string" && x.length > 0
13347
+ );
13348
+ if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
13349
+ const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
13350
+ if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
13351
+ const headerValue3 = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
13352
+ const { t: timestamp2, signatures } = parseSignatureHeader(headerValue3);
13353
+ if (!timestamp2 || !signatures.length) {
13354
+ return { ok: false, reason: "malformed_signature_header" };
13355
+ }
13356
+ const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
13357
+ const now = options.nowMs ?? Date.now();
13358
+ const tsSeconds = Number(timestamp2);
13359
+ if (!Number.isFinite(tsSeconds)) {
13360
+ return { ok: false, reason: "invalid_timestamp" };
13361
+ }
13362
+ const ageSeconds = Math.abs(now / 1e3 - tsSeconds);
13363
+ if (ageSeconds > tolerance) {
13364
+ return { ok: false, reason: "timestamp_outside_tolerance" };
13365
+ }
13366
+ for (const key of secrets) {
13367
+ const expectedSignature = computeHmac(key, `${timestamp2}.${rawBody}`);
13368
+ if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
13369
+ return { ok: true };
13370
+ }
13371
+ }
13372
+ return { ok: false, reason: "signature_mismatch" };
13373
+ }
13374
+
13375
+ // src/ucp/approval-events.ts
13376
+ var MemorySeenEvents = class {
13377
+ constructor(max = 1e4) {
13378
+ this.max = max;
13379
+ this.ids = /* @__PURE__ */ new Set();
13380
+ }
13381
+ async has(eventId) {
13382
+ return this.ids.has(eventId);
13383
+ }
13384
+ async add(eventId) {
13385
+ if (this.ids.size >= this.max) {
13386
+ const first2 = this.ids.values().next().value;
13387
+ if (first2 !== void 0) this.ids.delete(first2);
13388
+ }
13389
+ this.ids.add(eventId);
13390
+ }
13391
+ };
13392
+ var defaultSeen = new MemorySeenEvents();
13393
+ var TYPES = /* @__PURE__ */ new Set(["step_up.approved", "step_up.denied", "step_up.expired", "step_up.lapsed"]);
13394
+ function toHeaderRecord(headers) {
13395
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
13396
+ const out2 = {};
13397
+ headers.forEach((v, k) => out2[k.toLowerCase()] = v);
13398
+ return out2;
13399
+ }
13400
+ const out = {};
13401
+ for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
13402
+ return out;
13403
+ }
13404
+ async function handleApprovalEvents(rawBody, headers, secret, opts) {
13405
+ const raw = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
13406
+ const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {
13407
+ ...opts.toleranceSeconds !== void 0 && { toleranceSeconds: opts.toleranceSeconds },
13408
+ ...opts.nowMs !== void 0 && { nowMs: opts.nowMs }
13409
+ });
13410
+ if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };
13411
+ let event;
13412
+ try {
13413
+ event = JSON.parse(raw);
13414
+ } catch {
13415
+ return { status: 400, body: { ok: false, error: "invalid_json" } };
13416
+ }
13417
+ if (!event || typeof event.eventId !== "string" || typeof event.type !== "string") {
13418
+ return { status: 400, body: { ok: false, error: "invalid_event" } };
13419
+ }
13420
+ if (!TYPES.has(event.type)) {
13421
+ opts.onEvent?.(event, "ignored");
13422
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "ignored" } };
13423
+ }
13424
+ const seen = opts.seen ?? defaultSeen;
13425
+ const has = typeof seen === "function" ? seen : (id) => seen.has(id);
13426
+ try {
13427
+ if (await has(event.eventId)) {
13428
+ opts.onEvent?.(event, "duplicate");
13429
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "duplicate" } };
13430
+ }
13431
+ const candidates = event.protocol ? opts.kits.filter((k) => k.protocol === event.protocol) : opts.kits;
13432
+ let last = "not_found";
13433
+ let terminal = true;
13434
+ for (const kit of candidates) {
13435
+ const r = await kit.applyApprovalEvent(event);
13436
+ last = r.outcome;
13437
+ terminal = r.terminal;
13438
+ if (r.outcome !== "not_found") break;
13439
+ }
13440
+ opts.onEvent?.(event, last);
13441
+ if (!terminal) {
13442
+ return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };
13443
+ }
13444
+ if (typeof seen !== "function") await seen.add(event.eventId);
13445
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };
13446
+ } catch (err) {
13447
+ return {
13448
+ status: 503,
13449
+ body: {
13450
+ ok: false,
13451
+ eventId: event.eventId,
13452
+ error: err instanceof Error ? err.message : "transient_error"
13453
+ }
13454
+ };
13455
+ }
13456
+ }
13457
+
12591
13458
  // src/payments/x402.ts
12592
13459
  var CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
12593
13460
  var b64 = (v) => Buffer.from(JSON.stringify(v), "utf8").toString("base64");
@@ -12637,7 +13504,7 @@ function x402PaymentMatchesOffer(payment, offered) {
12637
13504
  }
12638
13505
 
12639
13506
  // src/payments/mpp.ts
12640
- var import_crypto9 = require("crypto");
13507
+ var import_crypto10 = require("crypto");
12641
13508
  var b64url = (s) => Buffer.from(s, "utf8").toString("base64url");
12642
13509
  function canonical(value) {
12643
13510
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
@@ -12658,7 +13525,7 @@ function binding(c, secret) {
12658
13525
  ];
12659
13526
  if (c.header && c.header.toLowerCase() !== "authorization") slots.push(c.header);
12660
13527
  slots.push(c.opaque ?? "");
12661
- return (0, import_crypto9.createHmac)("sha256", secret).update(slots.join("|")).digest("base64url");
13528
+ return (0, import_crypto10.createHmac)("sha256", secret).update(slots.join("|")).digest("base64url");
12662
13529
  }
12663
13530
  var quote = (v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
12664
13531
  function buildMppChallenge(input, hmacSecret) {
@@ -12700,13 +13567,13 @@ var MemoryMppChallengeStore = class {
12700
13567
  }
12701
13568
  };
12702
13569
  function mppBodyDigest(body) {
12703
- return `sha-256=${(0, import_crypto9.createHash)("sha256").update(body).digest("base64")}`;
13570
+ return `sha-256=${(0, import_crypto10.createHash)("sha256").update(body).digest("base64")}`;
12704
13571
  }
12705
13572
  function checkChallenge(challenge, hmacSecret, now) {
12706
13573
  const { id, ...rest } = challenge;
12707
13574
  const expected = Buffer.from(binding(rest, hmacSecret));
12708
13575
  const got = Buffer.from(String(id ?? ""));
12709
- if (expected.length !== got.length || !(0, import_crypto9.timingSafeEqual)(expected, got))
13576
+ if (expected.length !== got.length || !(0, import_crypto10.timingSafeEqual)(expected, got))
12710
13577
  return { ok: false, reason: "invalid_id" };
12711
13578
  if (challenge.expires && Date.parse(challenge.expires) < now.getTime())
12712
13579
  return { ok: false, reason: "expired" };
@@ -12722,7 +13589,7 @@ function verifyMppChallenge(challenge, hmacSecret, nowOrOptions = /* @__PURE__ *
12722
13589
  if (opts.body === void 0) return { ok: false, reason: "body_required" };
12723
13590
  const want = Buffer.from(challenge.digest);
12724
13591
  const got = Buffer.from(mppBodyDigest(opts.body));
12725
- if (want.length !== got.length || !(0, import_crypto9.timingSafeEqual)(want, got))
13592
+ if (want.length !== got.length || !(0, import_crypto10.timingSafeEqual)(want, got))
12726
13593
  return { ok: false, reason: "digest_mismatch" };
12727
13594
  }
12728
13595
  if (opts.store) {
@@ -12826,7 +13693,7 @@ async function verifyKyaPayTokens(header, opts) {
12826
13693
  }
12827
13694
 
12828
13695
  // src/aep/core.ts
12829
- var import_crypto10 = require("crypto");
13696
+ var import_crypto11 = require("crypto");
12830
13697
  var AEP_SPEC_VERSION = "aep-1.0.3-rc.2";
12831
13698
  function aepCanonicalJson(value) {
12832
13699
  if (value === null) return "null";
@@ -12847,7 +13714,7 @@ function aepCanonicalJson(value) {
12847
13714
  throw new Error(`AEP canonical JSON: unsupported value of type ${typeof value}`);
12848
13715
  }
12849
13716
  function digest(input) {
12850
- return (0, import_crypto10.createHash)("sha256").update(aepCanonicalJson(input), "utf8").digest();
13717
+ return (0, import_crypto11.createHash)("sha256").update(aepCanonicalJson(input), "utf8").digest();
12851
13718
  }
12852
13719
  function aepHash(input) {
12853
13720
  return `sha256:${digest(input).toString("hex")}`;
@@ -12860,7 +13727,7 @@ var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
12860
13727
  function aepPrivateKeyFromSeed(seedBase64) {
12861
13728
  const seed = Buffer.from(seedBase64, "base64");
12862
13729
  if (seed.length !== 32) throw new Error("Ed25519 seed must be 32 bytes");
12863
- return (0, import_crypto10.createPrivateKey)({
13730
+ return (0, import_crypto11.createPrivateKey)({
12864
13731
  key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
12865
13732
  format: "der",
12866
13733
  type: "pkcs8"
@@ -12869,14 +13736,14 @@ function aepPrivateKeyFromSeed(seedBase64) {
12869
13736
  function aepPublicKeyFromBase64(raw) {
12870
13737
  const bytes2 = Buffer.from(raw, "base64");
12871
13738
  if (bytes2.length !== 32) throw new Error("Ed25519 public key must be 32 bytes");
12872
- return (0, import_crypto10.createPublicKey)({
13739
+ return (0, import_crypto11.createPublicKey)({
12873
13740
  key: Buffer.concat([ED25519_SPKI_PREFIX, bytes2]),
12874
13741
  format: "der",
12875
13742
  type: "spki"
12876
13743
  });
12877
13744
  }
12878
13745
  function aepRawPublicKey(key) {
12879
- const pub = key.type === "private" ? (0, import_crypto10.createPublicKey)(key) : key;
13746
+ const pub = key.type === "private" ? (0, import_crypto11.createPublicKey)(key) : key;
12880
13747
  return pub.export({ format: "der", type: "spki" }).subarray(12).toString("base64");
12881
13748
  }
12882
13749
  function hashBytes(currentHash) {
@@ -12885,11 +13752,11 @@ function hashBytes(currentHash) {
12885
13752
  return Buffer.from(m[1], "hex");
12886
13753
  }
12887
13754
  function aepSign(currentHash, privateKey) {
12888
- return (0, import_crypto10.sign)(null, hashBytes(currentHash), privateKey).toString("base64");
13755
+ return (0, import_crypto11.sign)(null, hashBytes(currentHash), privateKey).toString("base64");
12889
13756
  }
12890
13757
  function aepVerifySignature(currentHash, signature, publicKey) {
12891
13758
  try {
12892
- return (0, import_crypto10.verify)(null, hashBytes(currentHash), publicKey, Buffer.from(signature, "base64"));
13759
+ return (0, import_crypto11.verify)(null, hashBytes(currentHash), publicKey, Buffer.from(signature, "base64"));
12893
13760
  } catch {
12894
13761
  return false;
12895
13762
  }
@@ -12987,7 +13854,7 @@ function aepPublicKeyDocument(keys, revocations = []) {
12987
13854
  }
12988
13855
 
12989
13856
  // src/acp/checkout-kit.ts
12990
- var import_crypto11 = require("crypto");
13857
+ var import_crypto12 = require("crypto");
12991
13858
  var ACP_API_VERSION = "2026-04-17";
12992
13859
  var ACP_SUPPORTED_API_VERSIONS = [ACP_API_VERSION];
12993
13860
  function acpPaymentHandlers(names) {
@@ -13017,6 +13884,28 @@ function buildAcpWellKnown(opts) {
13017
13884
  var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
13018
13885
  var apiError = (status, code, message, param) => json3(status, { type: "invalid_request", code, message, ...param && { param } });
13019
13886
  var total2 = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
13887
+ var clone2 = (v) => JSON.parse(JSON.stringify(v));
13888
+ var inFlight2 = (st) => st === "complete_in_progress" || st === "finishing";
13889
+ var ACP_CODES = /* @__PURE__ */ new Set([
13890
+ "missing",
13891
+ "invalid",
13892
+ "out_of_stock",
13893
+ "payment_declined",
13894
+ "requires_sign_in",
13895
+ "requires_3ds",
13896
+ // 5.20.0: the owner-approval codes the kit itself emits.
13897
+ "approval_required",
13898
+ "approval_denied",
13899
+ "approval_expired",
13900
+ "approval_lapsed"
13901
+ ]);
13902
+ function heldBody2(text) {
13903
+ try {
13904
+ return JSON.parse(text).status === "pending_approval";
13905
+ } catch {
13906
+ return false;
13907
+ }
13908
+ }
13020
13909
  function createAcpCheckoutKit(opts) {
13021
13910
  const mem = /* @__PURE__ */ new Map();
13022
13911
  const store = opts.store ?? {
@@ -13027,7 +13916,14 @@ function createAcpCheckoutKit(opts) {
13027
13916
  if (!s || s.status !== from) return false;
13028
13917
  s.status = to;
13029
13918
  return true;
13030
- }
13919
+ },
13920
+ findByCheckoutSessionId: async (checkoutSessionId) => {
13921
+ for (const s of mem.values()) {
13922
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
13923
+ }
13924
+ return mem.get(checkoutSessionId) ?? null;
13925
+ },
13926
+ listHeld: async () => [...mem.values()].filter((s) => s.status === "pending_approval")
13031
13927
  };
13032
13928
  const idem = {
13033
13929
  store: opts.idempotencyStore ?? new MemoryIdempotencyStore(),
@@ -13045,16 +13941,19 @@ function createAcpCheckoutKit(opts) {
13045
13941
  );
13046
13942
  }
13047
13943
  const handlers = acpPaymentHandlers(opts.paymentHandlers);
13944
+ const handlerIds = new Set(handlers.flatMap((h) => [h.id, h.name]));
13945
+ const publicOrigin = opts.publicOrigin?.replace(/\/+$/, "");
13946
+ const onReportError = opts.onReportError ?? defaultOnReportError;
13048
13947
  function view(s) {
13049
13948
  return {
13050
13949
  id: s.id,
13051
- status: s.status,
13950
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
13052
13951
  currency: s.currency.toLowerCase(),
13053
13952
  line_items: s.line_items,
13054
13953
  totals: s.totals,
13055
13954
  fulfillment_options: [],
13056
13955
  messages: s.messages,
13057
- links: [],
13956
+ links: opts.links ?? [],
13058
13957
  capabilities: { payment: { handlers } },
13059
13958
  ...s.continue_url && s.status === "pending_approval" && { continue_url: s.continue_url },
13060
13959
  ...s.order && { order: s.order }
@@ -13064,7 +13963,9 @@ function createAcpCheckoutKit(opts) {
13064
13963
  const lines = [];
13065
13964
  const messages = [];
13066
13965
  for (const [i, it] of items.entries()) {
13067
- const c = it.id ? await opts.catalog(it.id) : null;
13966
+ const q = Math.max(1, Math.floor(it.quantity ?? 1));
13967
+ const found = it.id ? await opts.catalog(it.id, q) : null;
13968
+ const c = found && withValidImageUrl(found);
13068
13969
  if (!c) {
13069
13970
  messages.push({
13070
13971
  type: "error",
@@ -13075,11 +13976,16 @@ function createAcpCheckoutKit(opts) {
13075
13976
  });
13076
13977
  continue;
13077
13978
  }
13078
- const q = Math.max(1, Math.floor(it.quantity ?? 1));
13079
13979
  const amount = c.price * q;
13080
13980
  lines.push({
13081
13981
  id: `li_${i + 1}`,
13082
- item: { id: c.id, name: c.title, unit_amount: c.price },
13982
+ item: {
13983
+ id: c.id,
13984
+ name: c.title,
13985
+ unit_amount: c.price,
13986
+ ...c.image_url && { image_url: c.image_url },
13987
+ ...typeof c.description === "string" && { description: c.description }
13988
+ },
13083
13989
  quantity: q,
13084
13990
  name: c.title,
13085
13991
  unit_amount: c.price,
@@ -13099,6 +14005,21 @@ function createAcpCheckoutKit(opts) {
13099
14005
  ]
13100
14006
  };
13101
14007
  }
14008
+ async function validate(s) {
14009
+ if (!opts.validateCart) return { messages: [], blocked: false };
14010
+ const found = await opts.validateCart(s);
14011
+ const messages = found.map((m) => {
14012
+ const error = (m.type ?? "error") === "error";
14013
+ return {
14014
+ type: m.type ?? "error",
14015
+ code: error ? ACP_CODES.has(m.code) ? m.code : "invalid" : m.code,
14016
+ content: m.content,
14017
+ ...error && { resolution: "recoverable" },
14018
+ ...m.path && { param: m.path }
14019
+ };
14020
+ });
14021
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
14022
+ }
13102
14023
  async function idempotent(req, body, path, caller, run) {
13103
14024
  const d = await beginIdempotent(idem, {
13104
14025
  method: req.method,
@@ -13114,29 +14035,43 @@ function createAcpCheckoutKit(opts) {
13114
14035
  headers: { ...d.headers, "idempotent-replayed": "true" }
13115
14036
  });
13116
14037
  const res = await run();
13117
- if (d.kind === "proceed")
13118
- await finishIdempotent(idem, d, {
13119
- status: res.status,
13120
- headers: { "content-type": "application/json" },
13121
- body: await res.clone().text()
13122
- });
14038
+ if (d.kind === "proceed") {
14039
+ const text = await res.clone().text();
14040
+ if (heldBody2(text)) await abandonIdempotent(idem, d);
14041
+ else
14042
+ await finishIdempotent(idem, d, {
14043
+ status: res.status,
14044
+ headers: { "content-type": "application/json" },
14045
+ body: text
14046
+ });
14047
+ }
13123
14048
  return res;
13124
14049
  }
13125
14050
  const inProgress = () => apiError(409, "checkout_in_progress", "This checkout session is already being completed.");
13126
- async function reportOutcome(p, settled) {
13127
- if (!opts.reportSettlement || !p.orderId) return;
13128
- await opts.reportSettlement({
13129
- orderId: p.orderId,
13130
- status: settled.status,
13131
- ...p.report,
13132
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
13133
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
13134
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
13135
- }).catch(() => void 0);
14051
+ async function reportOutcome(s, p, settled) {
14052
+ const report = opts.reportSettlement;
14053
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
14054
+ if (!report || !ref) return;
14055
+ await withReportRetries(
14056
+ () => report({
14057
+ ...ref,
14058
+ status: settled.status,
14059
+ ...p.report,
14060
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
14061
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
14062
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
14063
+ }),
14064
+ opts.reportRetry,
14065
+ isRetryableSettlementError
14066
+ ).catch(
14067
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
14068
+ );
13136
14069
  }
13137
14070
  async function finish(s, result, p, settled) {
13138
- await reportOutcome(p, settled);
14071
+ await reportOutcome(s, p, settled);
13139
14072
  delete s._pending;
14073
+ delete s._held;
14074
+ delete s._approvalId;
13140
14075
  if (settled.status === "failed") {
13141
14076
  s.status = "ready_for_payment";
13142
14077
  s.messages = [
@@ -13145,13 +14080,13 @@ function createAcpCheckoutKit(opts) {
13145
14080
  code: "payment_declined",
13146
14081
  content: "The payment was declined.",
13147
14082
  severity: "high",
13148
- resolution: "requires_buyer_input"
14083
+ resolution: "recoverable"
13149
14084
  }
13150
14085
  ];
13151
14086
  await store.put(s);
13152
14087
  return;
13153
14088
  }
13154
- const order = await opts.fulfil({ session: s, result });
14089
+ const order = await opts.fulfil({ session: s, result, settled });
13155
14090
  const permalink = order.permalink_url ?? (opts.permalinkBase && `${opts.permalinkBase.replace(/\/+$/, "")}/${encodeURIComponent(order.id)}`);
13156
14091
  s.order = {
13157
14092
  id: order.id,
@@ -13174,11 +14109,29 @@ function createAcpCheckoutKit(opts) {
13174
14109
  s.messages = [];
13175
14110
  await store.put(s);
13176
14111
  }
13177
- async function complete(req, s, body) {
14112
+ async function complete(req, s, body, agentArtifacts) {
13178
14113
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13179
- if (s.status === "complete_in_progress") return inProgress();
14114
+ if (inFlight2(s.status)) return inProgress();
13180
14115
  if (!body.payment_data)
13181
14116
  return apiError(400, "missing", "payment_data is required.", "$.payment_data");
14117
+ const paymentData = body.payment_data;
14118
+ if (Array.isArray(paymentData) || typeof paymentData !== "object" || typeof paymentData.handler_id !== "string" || !handlerIds.has(paymentData.handler_id)) {
14119
+ return apiError(
14120
+ 400,
14121
+ "invalid",
14122
+ "payment_data must be one object whose handler_id is the id or name of a payment handler this merchant declares.",
14123
+ "$.payment_data.handler_id"
14124
+ );
14125
+ }
14126
+ const v = await validate(s);
14127
+ if (v.blocked) {
14128
+ s.status = "not_ready_for_payment";
14129
+ s.messages = v.messages;
14130
+ delete s._held;
14131
+ delete s._approvalId;
14132
+ await store.put(s);
14133
+ return json3(200, view(s));
14134
+ }
13182
14135
  const from = s.status;
13183
14136
  if (store.claim) {
13184
14137
  if (!await store.claim(s.id, from, "complete_in_progress")) return inProgress();
@@ -13187,17 +14140,31 @@ function createAcpCheckoutKit(opts) {
13187
14140
  s.status = "complete_in_progress";
13188
14141
  await store.put(s);
13189
14142
  }
14143
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
14144
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
13190
14145
  try {
13191
14146
  const result = await opts.verifyPurchase({
13192
14147
  session: s,
13193
14148
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
13194
- astraId: req.headers.get("x-astra-id") ?? void 0,
13195
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
13196
- valueMajor: total2(s) / 100
14149
+ astraId,
14150
+ attemptId,
14151
+ valueMajor: total2(s) / 100,
14152
+ agentArtifacts,
14153
+ metadata: { kitSessionId: s.id, protocol: "acp" }
13197
14154
  });
13198
14155
  if (isHeld(result)) {
13199
14156
  s.status = "pending_approval";
13200
14157
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
14158
+ s._approvalId = result.stepUpApproval?.approvalId;
14159
+ s._astraId = astraId ?? result.agent?.astraId;
14160
+ s._attemptId = attemptId ?? result.attemptId;
14161
+ s._held = {
14162
+ line_items: clone2(s.line_items),
14163
+ totals: clone2(s.totals),
14164
+ ...scrubPaymentInput(paymentData) && { paymentData: scrubPaymentInput(paymentData) },
14165
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
14166
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
14167
+ };
13201
14168
  s.messages = [
13202
14169
  {
13203
14170
  type: "error",
@@ -13212,36 +14179,11 @@ function createAcpCheckoutKit(opts) {
13212
14179
  }
13213
14180
  if (!result.identityVerified || !result.policyAllowed) {
13214
14181
  s.status = from;
13215
- s.messages = [
13216
- {
13217
- type: "error",
13218
- code: "intervention_required",
13219
- content: "The agent is not authorized for this purchase.",
13220
- severity: "high",
13221
- resolution: "requires_buyer_review"
13222
- }
13223
- ];
13224
- await store.put(s);
13225
- return json3(200, view(s));
13226
- }
13227
- const settled = await opts.settle({
13228
- session: s,
13229
- result,
13230
- paymentData: body.payment_data
13231
- });
13232
- const orderId = result.settlementOutcome?.orderId;
13233
- const pending = {
13234
- ...orderId && { orderId },
13235
- report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
13236
- result: pendingResult(result)
13237
- };
13238
- if (settled.status === "pending") {
13239
- s._pending = pending;
13240
- s.messages = [];
14182
+ s.messages = [notAuthorized()];
13241
14183
  await store.put(s);
13242
14184
  return json3(200, view(s));
13243
14185
  }
13244
- await finish(s, result, pending, settled);
14186
+ await settleAndFinish(s, result, paymentData);
13245
14187
  return json3(200, view(s));
13246
14188
  } catch (e) {
13247
14189
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -13251,13 +14193,188 @@ function createAcpCheckoutKit(opts) {
13251
14193
  throw e;
13252
14194
  }
13253
14195
  }
14196
+ const notAuthorized = () => ({
14197
+ type: "error",
14198
+ code: "intervention_required",
14199
+ content: "The agent is not authorized for this purchase.",
14200
+ severity: "high",
14201
+ resolution: "requires_buyer_review"
14202
+ });
14203
+ async function settleAndFinish(s, result, paymentData) {
14204
+ const settled = await opts.settle({ session: s, result, paymentData });
14205
+ const orderId = result.settlementOutcome?.orderId;
14206
+ const sessionId = result.sessionId;
14207
+ const pending = {
14208
+ ...orderId && { orderId },
14209
+ ...sessionId && { sessionId },
14210
+ report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
14211
+ result: pendingResult(result)
14212
+ };
14213
+ if (settled.status === "pending") {
14214
+ s._pending = pending;
14215
+ s.messages = [];
14216
+ delete s._held;
14217
+ delete s._approvalId;
14218
+ await store.put(s);
14219
+ return "pending";
14220
+ }
14221
+ await finish(s, result, pending, settled);
14222
+ return settled.status === "failed" ? "payment_failed" : "completed";
14223
+ }
13254
14224
  async function resolvePending(sessionId, outcome) {
13255
14225
  const s = await store.get(sessionId);
13256
14226
  if (!s) return null;
13257
14227
  if (s.status !== "complete_in_progress" || !s._pending) return s;
13258
- await finish(s, s._pending.result, s._pending, outcome);
14228
+ if (store.claim) {
14229
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
14230
+ return await store.get(sessionId) ?? s;
14231
+ }
14232
+ s.status = "finishing";
14233
+ } else {
14234
+ s.status = "finishing";
14235
+ await store.put(s);
14236
+ }
14237
+ const pending = s._pending;
14238
+ try {
14239
+ await finish(s, pending.result, pending, outcome);
14240
+ } catch (e) {
14241
+ s.status = "complete_in_progress";
14242
+ s._pending = pending;
14243
+ await store.put(s).catch(() => void 0);
14244
+ throw e;
14245
+ }
13259
14246
  return s;
13260
14247
  }
14248
+ async function resumeApproved(sessionId, event) {
14249
+ const s = await store.get(sessionId);
14250
+ if (!s) return resumeResult("not_found", null);
14251
+ if (s.status === "completed") return resumeResult("already_completed", s);
14252
+ if (s.status === "canceled") return resumeResult("canceled", s);
14253
+ if (inFlight2(s.status)) return resumeResult("busy", s);
14254
+ if (s.status !== "pending_approval" || !s._held) return resumeResult("not_held", s);
14255
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
14256
+ return resumeResult("stale", s);
14257
+ if (store.claim) {
14258
+ if (!await store.claim(s.id, "pending_approval", "complete_in_progress"))
14259
+ return resumeResult("busy", await store.get(sessionId) ?? s);
14260
+ s.status = "complete_in_progress";
14261
+ } else {
14262
+ s.status = "complete_in_progress";
14263
+ await store.put(s);
14264
+ }
14265
+ const held = s._held;
14266
+ const backToHeld = async () => {
14267
+ s.status = "pending_approval";
14268
+ s._held = held;
14269
+ await store.put(s).catch(() => void 0);
14270
+ };
14271
+ try {
14272
+ s.line_items = clone2(held.line_items);
14273
+ s.totals = clone2(held.totals);
14274
+ const v = await validate(s);
14275
+ if (v.blocked) {
14276
+ s.status = "not_ready_for_payment";
14277
+ s.messages = v.messages;
14278
+ delete s._held;
14279
+ delete s._approvalId;
14280
+ delete s.continue_url;
14281
+ await store.put(s);
14282
+ return resumeResult("cart_invalid", s);
14283
+ }
14284
+ const result = await opts.verifyPurchase({
14285
+ session: s,
14286
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
14287
+ astraId: s._astraId,
14288
+ attemptId: s._attemptId,
14289
+ valueMajor: total2(s) / 100,
14290
+ metadata: { kitSessionId: s.id, protocol: "acp" },
14291
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
14292
+ });
14293
+ if (isHeld(result)) {
14294
+ await backToHeld();
14295
+ return resumeResult("still_held", s);
14296
+ }
14297
+ if (!result.identityVerified || !result.policyAllowed) {
14298
+ s.status = "ready_for_payment";
14299
+ s.messages = [notAuthorized()];
14300
+ delete s._held;
14301
+ delete s._approvalId;
14302
+ delete s.continue_url;
14303
+ await store.put(s);
14304
+ return resumeResult("not_authorized", s);
14305
+ }
14306
+ delete s.continue_url;
14307
+ return resumeResult(await settleAndFinish(s, result, held.paymentData), s);
14308
+ } catch (e) {
14309
+ if (!s._pending && inFlight2(s.status)) await backToHeld();
14310
+ throw e;
14311
+ }
14312
+ }
14313
+ async function resumeDeclined(sessionId, reason = "denied", event) {
14314
+ const s = await store.get(sessionId);
14315
+ if (!s) return resumeResult("not_found", null);
14316
+ if (s.status === "completed") return resumeResult("already_completed", s);
14317
+ if (s.status === "canceled") return resumeResult("canceled", s);
14318
+ if (inFlight2(s.status)) return resumeResult("busy", s);
14319
+ if (s.status !== "pending_approval") return resumeResult("not_held", s);
14320
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
14321
+ return resumeResult("stale", s);
14322
+ if (store.claim && !await store.claim(s.id, "pending_approval", "ready_for_payment")) {
14323
+ return resumeResult("busy", await store.get(sessionId) ?? s);
14324
+ }
14325
+ s.status = "ready_for_payment";
14326
+ delete s._held;
14327
+ delete s._approvalId;
14328
+ delete s.continue_url;
14329
+ s.messages = [
14330
+ {
14331
+ type: "error",
14332
+ code: `approval_${reason}`,
14333
+ 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.",
14334
+ severity: "medium",
14335
+ resolution: "recoverable"
14336
+ }
14337
+ ];
14338
+ await store.put(s);
14339
+ return resumeResult("declined", s);
14340
+ }
14341
+ async function findForEvent(event) {
14342
+ if (event.kitSessionId) {
14343
+ const s = await store.get(event.kitSessionId);
14344
+ if (s) return s;
14345
+ }
14346
+ if (store.findByCheckoutSessionId)
14347
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
14348
+ return store.get(event.checkoutSessionId);
14349
+ }
14350
+ async function applyApprovalEvent(event) {
14351
+ const s = await findForEvent(event);
14352
+ if (!s) return resumeResult("not_found", null);
14353
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
14354
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
14355
+ }
14356
+ async function pollHeldSessions(o = {}) {
14357
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
14358
+ (x) => Boolean(x)
14359
+ ) : store.listHeld ? await store.listHeld() : [];
14360
+ const out = [];
14361
+ for (const s of sessions) {
14362
+ if (s.status !== "pending_approval" || !s._held?.pollUrl || !s._approvalId) continue;
14363
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
14364
+ approvalId: s._approvalId,
14365
+ pollUrl: s._held.pollUrl,
14366
+ expiresAt: s._held.expiresAt ?? ""
14367
+ });
14368
+ if (status === "approved" || status === "consumed") {
14369
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
14370
+ } else if (status === "denied" || status === "expired" || status === "lapsed") {
14371
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
14372
+ } else {
14373
+ out.push({ sessionId: s.id, outcome: "pending" });
14374
+ }
14375
+ }
14376
+ return out;
14377
+ }
13261
14378
  async function route(req, reqCtx) {
13262
14379
  const version = req.headers.get("api-version");
13263
14380
  if (!version) return apiError(400, "missing", "The API-Version header is required.");
@@ -13282,6 +14399,7 @@ function createAcpCheckoutKit(opts) {
13282
14399
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId);
13283
14400
  if (!caller)
13284
14401
  return apiError(401, "unauthorized", "Send an Authorization header (or X-Astra-Id).");
14402
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "acp", publicOrigin });
13285
14403
  if (req.method === "POST" && path === "/checkout_sessions") {
13286
14404
  return idempotent(req, body, path, caller, async () => {
13287
14405
  const items = parsed.line_items ?? parsed.items;
@@ -13289,7 +14407,7 @@ function createAcpCheckoutKit(opts) {
13289
14407
  return apiError(400, "missing", "line_items is required.", "$.line_items");
13290
14408
  const p = await price(items);
13291
14409
  const s2 = {
13292
- id: `cs_${(0, import_crypto11.randomUUID)().replace(/-/g, "")}`,
14410
+ id: `cs_${(0, import_crypto12.randomUUID)().replace(/-/g, "")}`,
13293
14411
  status: p.lines.length && !p.messages.length ? "ready_for_payment" : "not_ready_for_payment",
13294
14412
  currency: opts.currency,
13295
14413
  line_items: p.lines,
@@ -13300,6 +14418,11 @@ function createAcpCheckoutKit(opts) {
13300
14418
  _astraCheckoutSessionId: req.headers.get("x-astra-checkout-session").slice(0, 64)
13301
14419
  }
13302
14420
  };
14421
+ if (s2.status === "ready_for_payment") {
14422
+ const v = await validate(s2);
14423
+ s2.messages = [...s2.messages, ...v.messages];
14424
+ if (v.blocked) s2.status = "not_ready_for_payment";
14425
+ }
13303
14426
  await store.put(s2);
13304
14427
  return json3(201, view(s2));
13305
14428
  });
@@ -13313,7 +14436,13 @@ function createAcpCheckoutKit(opts) {
13313
14436
  if (!m[2]) {
13314
14437
  return idempotent(req, body, path, caller, async () => {
13315
14438
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13316
- if (s.status === "complete_in_progress") return inProgress();
14439
+ if (inFlight2(s.status)) return inProgress();
14440
+ if (s.status === "pending_approval")
14441
+ return apiError(
14442
+ 409,
14443
+ "checkout_awaiting_approval",
14444
+ "This session is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over."
14445
+ );
13317
14446
  const items = parsed.line_items ?? parsed.items;
13318
14447
  if (items) {
13319
14448
  const p = await price(items);
@@ -13323,17 +14452,22 @@ function createAcpCheckoutKit(opts) {
13323
14452
  messages: p.messages,
13324
14453
  status: p.lines.length && !p.messages.length ? "ready_for_payment" : "not_ready_for_payment"
13325
14454
  });
14455
+ if (s.status === "ready_for_payment") {
14456
+ const v = await validate(s);
14457
+ s.messages = [...s.messages, ...v.messages];
14458
+ if (v.blocked) s.status = "not_ready_for_payment";
14459
+ }
13326
14460
  }
13327
14461
  await store.put(s);
13328
14462
  return json3(200, view(s));
13329
14463
  });
13330
14464
  }
13331
14465
  if (m[2] === "/complete")
13332
- return idempotent(req, body, path, caller, () => complete(req, s, parsed));
14466
+ return idempotent(req, body, path, caller, () => complete(req, s, parsed, agentArtifacts));
13333
14467
  return idempotent(req, body, path, caller, async () => {
13334
14468
  if (s.status === "completed" || s.status === "canceled")
13335
14469
  return apiError(405, "method_not_allowed", `A ${s.status} session can't be canceled.`);
13336
- if (s.status === "complete_in_progress") return inProgress();
14470
+ if (inFlight2(s.status)) return inProgress();
13337
14471
  s.status = "canceled";
13338
14472
  await store.put(s);
13339
14473
  return json3(200, view(s));
@@ -13349,7 +14483,17 @@ function createAcpCheckoutKit(opts) {
13349
14483
  if (requestId) headers.set("request-id", requestId);
13350
14484
  return new Response(res.body, { status: res.status, headers });
13351
14485
  }
13352
- return { handle, resolvePending };
14486
+ return {
14487
+ handle,
14488
+ resolvePending,
14489
+ /** 5.19.0 */
14490
+ protocol: "acp",
14491
+ publicOrigin,
14492
+ resumeApproved,
14493
+ resumeDeclined,
14494
+ applyApprovalEvent,
14495
+ pollHeldSessions
14496
+ };
13353
14497
  }
13354
14498
 
13355
14499
  // src/adapters/kit-express.ts
@@ -13371,11 +14515,46 @@ function rawBodyOf(req) {
13371
14515
  }
13372
14516
  return void 0;
13373
14517
  }
14518
+ var first = (v) => (Array.isArray(v) ? v[0] : v)?.split(",")[0]?.trim() || void 0;
14519
+ function originOf(req, publicOrigin) {
14520
+ if (publicOrigin) return publicOrigin.replace(/\/+$/, "");
14521
+ const trustProxy = Boolean(req.app?.get("trust proxy"));
14522
+ const host = trustProxy && first(req.headers["x-forwarded-host"]) || req.get?.("host") || req.headers.host || "localhost";
14523
+ const proto = trustProxy && first(req.headers["x-forwarded-proto"]) || req.protocol || "http";
14524
+ return `${proto}://${host}`;
14525
+ }
14526
+ var isKit = (k) => typeof k.applyApprovalEvent === "function" && (k.protocol === "ucp" || k.protocol === "acp");
14527
+ async function writeEvents(req, res, secret, kits, seen) {
14528
+ const raw = rawBodyOf(req);
14529
+ const text = raw === void 0 ? "" : typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
14530
+ const out = secret ? await handleApprovalEvents(text, req.headers, secret, { kits, ...seen && { seen } }) : { status: 500, body: { ok: false, error: "webhook_secret_not_configured" } };
14531
+ res.status(out.status);
14532
+ res.setHeader("content-type", "application/json");
14533
+ res.end(JSON.stringify(out.body));
14534
+ }
14535
+ function approvalEventsHandler(options) {
14536
+ return async (req, res, next) => {
14537
+ try {
14538
+ await writeEvents(req, res, options.secret, options.kits, options.seen);
14539
+ } catch (err) {
14540
+ next(err);
14541
+ }
14542
+ };
14543
+ }
13374
14544
  function expressHandler(kit, options = {}) {
13375
14545
  return async (req, res, next) => {
13376
14546
  try {
13377
- const host = req.get?.("host") ?? req.headers.host ?? "localhost";
13378
- const url = `${req.protocol ?? "http"}://${host}${req.originalUrl ?? req.url}`;
14547
+ if (options.approvalEventsPath && req.method === "POST") {
14548
+ const p = options.approvalEventsPath.replace(/\/+$/, "");
14549
+ const full = (req.originalUrl ?? req.url).split("?")[0].replace(/\/+$/, "");
14550
+ const local = req.url.split("?")[0].replace(/\/+$/, "");
14551
+ if (full === p || local === p) {
14552
+ const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);
14553
+ await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);
14554
+ return;
14555
+ }
14556
+ }
14557
+ const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;
13379
14558
  const headers = new Headers();
13380
14559
  for (const [k, v] of Object.entries(req.headers)) {
13381
14560
  if (v === void 0) continue;
@@ -13426,6 +14605,7 @@ function expressHandler(kit, options = {}) {
13426
14605
  MemoryCheckoutStore,
13427
14606
  MemoryIdempotencyStore,
13428
14607
  MemoryMppChallengeStore,
14608
+ MemorySeenEvents,
13429
14609
  PLATFORM_AGENT_SIGNATURES,
13430
14610
  RUNTIME_SIGNATURES,
13431
14611
  RedisIdempotencyStore,
@@ -13440,6 +14620,7 @@ function expressHandler(kit, options = {}) {
13440
14620
  VERIFICATION_STATUS_HEADER,
13441
14621
  VERSION,
13442
14622
  _resetEdgeConfigCache,
14623
+ abandonIdempotent,
13443
14624
  acpPaymentHandlers,
13444
14625
  aepCanonicalJson,
13445
14626
  aepHash,
@@ -13451,6 +14632,7 @@ function expressHandler(kit, options = {}) {
13451
14632
  aepTimestamp,
13452
14633
  aepVerifySignature,
13453
14634
  agent,
14635
+ approvalEventsHandler,
13454
14636
  astrasyncPaymentHandlers,
13455
14637
  authorizeSettlement,
13456
14638
  awaitStepUpApproval,
@@ -13497,11 +14679,13 @@ function expressHandler(kit, options = {}) {
13497
14679
  getEdgeConfig,
13498
14680
  getTrustLevel,
13499
14681
  getWellKnownUrls,
14682
+ handleApprovalEvents,
13500
14683
  hasCredentials,
13501
14684
  idempotencyMiddleware,
13502
14685
  isEdgeConfig,
13503
14686
  isRejectedIdentityClaim,
13504
14687
  isRetryableSettlementError,
14688
+ isSharedVerdict,
13505
14689
  isSpecialUseIp,
13506
14690
  isVerificationUnavailable,
13507
14691
  isVerifiedHopValidFor,
@@ -13513,6 +14697,7 @@ function expressHandler(kit, options = {}) {
13513
14697
  negotiateUcp,
13514
14698
  nextjs,
13515
14699
  normalizeDeclaredSource,
14700
+ onVerificationResult,
13516
14701
  openCheckoutMandate,
13517
14702
  openPaymentMandate,
13518
14703
  parseLocalPolicyYaml,
@@ -13546,6 +14731,7 @@ function expressHandler(kit, options = {}) {
13546
14731
  ucpEscalation,
13547
14732
  ucpFailure,
13548
14733
  ucpProfileHandler,
14734
+ ucpProfileKeys,
13549
14735
  ucpRequiredComponents,
13550
14736
  unavailableRetryAfterSeconds,
13551
14737
  validateLocalPolicy,