@astrasyncai/verification-gateway 5.18.0 → 5.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +168 -4
  3. package/dist/adapters/express.d.mts +9 -1
  4. package/dist/adapters/express.d.ts +9 -1
  5. package/dist/adapters/express.js +96 -37
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +96 -37
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/http-pdlss.d.mts +7 -1
  10. package/dist/adapters/http-pdlss.d.ts +7 -1
  11. package/dist/adapters/http-pdlss.js +18 -2
  12. package/dist/adapters/http-pdlss.js.map +1 -1
  13. package/dist/adapters/http-pdlss.mjs +16 -1
  14. package/dist/adapters/http-pdlss.mjs.map +1 -1
  15. package/dist/adapters/kit-express.d.mts +93 -1
  16. package/dist/adapters/kit-express.d.ts +93 -1
  17. package/dist/adapters/kit-express.js +181 -2
  18. package/dist/adapters/kit-express.js.map +1 -1
  19. package/dist/adapters/kit-express.mjs +180 -2
  20. package/dist/adapters/kit-express.mjs.map +1 -1
  21. package/dist/adapters/mcp.d.mts +80 -9
  22. package/dist/adapters/mcp.d.ts +80 -9
  23. package/dist/adapters/mcp.js +153 -5
  24. package/dist/adapters/mcp.js.map +1 -1
  25. package/dist/adapters/mcp.mjs +151 -5
  26. package/dist/adapters/mcp.mjs.map +1 -1
  27. package/dist/adapters/nextjs.d.mts +1 -1
  28. package/dist/adapters/nextjs.d.ts +1 -1
  29. package/dist/adapters/nextjs.js +41 -4
  30. package/dist/adapters/nextjs.js.map +1 -1
  31. package/dist/adapters/nextjs.mjs +41 -4
  32. package/dist/adapters/nextjs.mjs.map +1 -1
  33. package/dist/adapters/sdk.d.mts +44 -1
  34. package/dist/adapters/sdk.d.ts +44 -1
  35. package/dist/adapters/sdk.js +76 -2
  36. package/dist/adapters/sdk.js.map +1 -1
  37. package/dist/adapters/sdk.mjs +76 -2
  38. package/dist/adapters/sdk.mjs.map +1 -1
  39. package/dist/agent/index.js +1 -1
  40. package/dist/agent/index.js.map +1 -1
  41. package/dist/agent/index.mjs +1 -1
  42. package/dist/agent/index.mjs.map +1 -1
  43. package/dist/bin/astrasync-claude-hook.js +46 -6
  44. package/dist/bin/astrasync-codex-hook.js +46 -6
  45. package/dist/bin/astrasync-guard.js +375 -117
  46. package/dist/bin/astrasync.js +417 -117
  47. package/dist/browser/background.js +46 -6
  48. package/dist/browser/background.js.map +1 -1
  49. package/dist/browser/background.mjs +46 -6
  50. package/dist/browser/background.mjs.map +1 -1
  51. package/dist/cli/index.js +1 -1
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/index.mjs +1 -1
  54. package/dist/cli/index.mjs.map +1 -1
  55. package/dist/codex/index.js +46 -6
  56. package/dist/codex/index.js.map +1 -1
  57. package/dist/codex/index.mjs +46 -6
  58. package/dist/codex/index.mjs.map +1 -1
  59. package/dist/cursor/extension.js +46 -6
  60. package/dist/cursor/extension.js.map +1 -1
  61. package/dist/cursor/extension.mjs +46 -6
  62. package/dist/cursor/extension.mjs.map +1 -1
  63. package/dist/edge-config.d.mts +1 -1
  64. package/dist/edge-config.d.ts +1 -1
  65. package/dist/edge-config.js +1 -1
  66. package/dist/edge-config.js.map +1 -1
  67. package/dist/edge-config.mjs +1 -1
  68. package/dist/edge-config.mjs.map +1 -1
  69. package/dist/edge-core/index.d.mts +1 -1
  70. package/dist/edge-core/index.d.ts +1 -1
  71. package/dist/edge-core/index.js +21 -2
  72. package/dist/edge-core/index.js.map +1 -1
  73. package/dist/edge-core/index.mjs +21 -2
  74. package/dist/edge-core/index.mjs.map +1 -1
  75. package/dist/gateway/gateway.js +46 -6
  76. package/dist/gateway/gateway.js.map +1 -1
  77. package/dist/gateway/gateway.mjs +46 -6
  78. package/dist/gateway/gateway.mjs.map +1 -1
  79. package/dist/git-trigger/git-hooks.d.mts +1 -1
  80. package/dist/git-trigger/git-hooks.d.ts +1 -1
  81. package/dist/index.d.mts +542 -30
  82. package/dist/index.d.ts +542 -30
  83. package/dist/index.js +1171 -183
  84. package/dist/index.js.map +1 -1
  85. package/dist/index.mjs +1157 -173
  86. package/dist/index.mjs.map +1 -1
  87. package/dist/registration/index.d.mts +1 -1
  88. package/dist/registration/index.d.ts +1 -1
  89. package/dist/registration/index.js +1 -1
  90. package/dist/registration/index.js.map +1 -1
  91. package/dist/registration/index.mjs +1 -1
  92. package/dist/registration/index.mjs.map +1 -1
  93. package/dist/transport/index.js +3 -2
  94. package/dist/transport/index.js.map +1 -1
  95. package/dist/transport/index.mjs +3 -2
  96. package/dist/transport/index.mjs.map +1 -1
  97. package/dist/{types-3ObBpsnf.d.mts → types-BoGjyon0.d.mts} +81 -2
  98. package/dist/{types-Cns4l8gX.d.mts → types-DqWPkiYl.d.mts} +90 -12
  99. package/dist/{types-Cns4l8gX.d.ts → types-DqWPkiYl.d.ts} +90 -12
  100. package/dist/{types-BoxZEYPS.d.ts → types-eWyZuz-3.d.ts} +81 -2
  101. package/dist/ui/index.d.mts +1 -1
  102. package/dist/ui/index.d.ts +1 -1
  103. package/dist/verify.d.mts +23 -2
  104. package/dist/verify.d.ts +23 -2
  105. package/dist/verify.js +23 -4
  106. package/dist/verify.js.map +1 -1
  107. package/dist/verify.mjs +23 -4
  108. package/dist/verify.mjs.map +1 -1
  109. package/dist/webhooks.d.mts +16 -3
  110. package/dist/webhooks.d.ts +16 -3
  111. package/dist/webhooks.js +24 -13
  112. package/dist/webhooks.js.map +1 -1
  113. package/dist/webhooks.mjs +24 -13
  114. package/dist/webhooks.mjs.map +1 -1
  115. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -30,7 +30,7 @@ function getTrustLevel(score) {
30
30
  }
31
31
 
32
32
  // src/version.ts
33
- var SDK_VERSION = "5.18.0";
33
+ var SDK_VERSION = "5.19.0";
34
34
 
35
35
  // src/http.ts
36
36
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -282,7 +282,12 @@ function getCacheKey(request, counterpartyId) {
282
282
  // its start_checkout quote was served the cached quote-leg grant (no
283
283
  // settlementOutcome) and never reached the backend. Same F-A1-07 bug class.
284
284
  request.commercePhase || "",
285
- request.checkoutSessionId || ""
285
+ request.checkoutSessionId || "",
286
+ // 5.19.0: a route_config purpose claim is checked server-side against
287
+ // the stored rule for this method + path, so it changes the verdict.
288
+ request.purposeSource || "",
289
+ request.requestMethod || "",
290
+ request.requestPath || ""
286
291
  ].join("|");
287
292
  }
288
293
  function getCachedResult(request, counterpartyId) {
@@ -496,6 +501,11 @@ async function callVerifyAccessAPI(config, request) {
496
501
  if (requestData.buyerEmail) body.buyerEmail = requestData.buyerEmail;
497
502
  if (requestData.commerceArtifacts) body.commerceArtifacts = requestData.commerceArtifacts;
498
503
  if (requestData.attemptId) body.attemptId = requestData.attemptId;
504
+ if (requestData.purposeSource) body.purposeSource = requestData.purposeSource;
505
+ if (requestData.agentArtifacts) body.agentArtifacts = requestData.agentArtifacts;
506
+ if (requestData.metadata) body.metadata = requestData.metadata;
507
+ if (requestData.requestMethod) body.requestMethod = requestData.requestMethod;
508
+ if (requestData.requestPath) body.requestPath = requestData.requestPath;
499
509
  if (requestData.considerationSet) body.considerationSet = requestData.considerationSet;
500
510
  body.sdkVersion = SDK_VERSION;
501
511
  if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
@@ -685,9 +695,18 @@ async function verify(config, request, options) {
685
695
  if (!apiResponse.access?.allowed) {
686
696
  const aggregatedFailures = apiResponse.access?.failures;
687
697
  const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
698
+ const denyAgent = idVerifiedFromBackend && apiResponse.agent ? {
699
+ astraId: apiResponse.agent.astraId,
700
+ name: apiResponse.agent.name,
701
+ trustScore: apiResponse.agent.trustScore,
702
+ trustLevel: getTrustLevel(apiResponse.agent.trustScore),
703
+ blockchainAnchored: apiResponse.agent.blockchainStatus === "anchored",
704
+ status: apiResponse.agent.agentStatus
705
+ } : void 0;
688
706
  const result2 = {
689
707
  identityVerified: idVerifiedFromBackend,
690
708
  policyAllowed: false,
709
+ ...denyAgent && { agent: denyAgent },
691
710
  // 3.6.0: the server now emits top-level denialReasons on every deny
692
711
  // path — prefer it verbatim; the failure-message reconstruction remains
693
712
  // as the fallback for older backends.
@@ -879,7 +898,7 @@ async function reportAttempt(config, report, options = {}) {
879
898
  }
880
899
  var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
881
900
  var refusedPolicyReads = /* @__PURE__ */ new Set();
882
- async function fetchRoutes(config, counterpartyId) {
901
+ async function fetchRoutes(config, counterpartyId, options = {}) {
883
902
  if (!counterpartyId) return null;
884
903
  const headers = { "Content-Type": "application/json" };
885
904
  if (config.apiKey) {
@@ -888,7 +907,7 @@ async function fetchRoutes(config, counterpartyId) {
888
907
  }
889
908
  try {
890
909
  const response = await sdkFetch(
891
- `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
910
+ `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes` + (options.codeGates?.length ? `?codeGates=${options.codeGates.map(encodeURIComponent).join(",")}` : ""),
892
911
  { method: "GET", headers }
893
912
  );
894
913
  if (!response.ok) {
@@ -1571,6 +1590,42 @@ async function detectFetchCommerce(request) {
1571
1590
  );
1572
1591
  }
1573
1592
 
1593
+ // src/adapters/routes-cache.ts
1594
+ var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
1595
+ function createRoutesCache(config, opts = {}) {
1596
+ const refreshMs = opts.refreshMs ?? DEFAULT_ROUTES_REFRESH_MS;
1597
+ let cached = [];
1598
+ let lastFetchAt = 0;
1599
+ let refreshing = null;
1600
+ async function refresh() {
1601
+ if (!config.counterpartyId) {
1602
+ opts.onNoCounterparty?.();
1603
+ return;
1604
+ }
1605
+ const fetched = await fetchRoutes(config, config.counterpartyId, opts.fetchOptions);
1606
+ if (fetched) {
1607
+ cached = fetched;
1608
+ lastFetchAt = Date.now();
1609
+ opts.onFetched?.(fetched);
1610
+ }
1611
+ }
1612
+ function start() {
1613
+ refreshing = refresh().catch(() => void 0).finally(() => {
1614
+ refreshing = null;
1615
+ });
1616
+ }
1617
+ start();
1618
+ return {
1619
+ async current() {
1620
+ if (refreshing) await refreshing.catch(() => {
1621
+ });
1622
+ if (config.counterpartyId && Date.now() - lastFetchAt > refreshMs) start();
1623
+ return cached;
1624
+ },
1625
+ peek: () => cached
1626
+ };
1627
+ }
1628
+
1574
1629
  // src/transport/http.ts
1575
1630
  var HEADER_PREFIX = "X-Astra-";
1576
1631
  function setHttpHeaders(headers, credentials) {
@@ -1747,6 +1802,20 @@ function resolveHttpPdlss(input) {
1747
1802
  }
1748
1803
  return { purpose, action, purposeSource, actionSource };
1749
1804
  }
1805
+ function wirePurposeSource(source) {
1806
+ switch (source) {
1807
+ case "route_config":
1808
+ return "route_config";
1809
+ case "header":
1810
+ case "legacy_header":
1811
+ case "query":
1812
+ return "header";
1813
+ case "custom_extractor":
1814
+ return "body";
1815
+ default:
1816
+ return "default";
1817
+ }
1818
+ }
1750
1819
 
1751
1820
  // src/pdlss-pre-check.ts
1752
1821
  function performCounterpartyPreCheck(routeConfig, astraCreds, purpose) {
@@ -2246,12 +2315,12 @@ var BeaconBuffer = class {
2246
2315
  }
2247
2316
  /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
2248
2317
  takeBatch(now) {
2249
- const first = this.queue.find((q) => q.eligibleAt <= now);
2250
- if (!first) return [];
2318
+ const first2 = this.queue.find((q) => q.eligibleAt <= now);
2319
+ if (!first2) return [];
2251
2320
  const batch = [];
2252
2321
  const rest = [];
2253
2322
  for (const q of this.queue) {
2254
- if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
2323
+ if (batch.length < this.opts.maxBatch && q.destKey === first2.destKey && q.eligibleAt <= now) {
2255
2324
  q.inFlight = true;
2256
2325
  batch.push(q);
2257
2326
  } else {
@@ -2528,7 +2597,6 @@ function defaultOnDenied(result, _req, res) {
2528
2597
  }
2529
2598
  });
2530
2599
  }
2531
- var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
2532
2600
  function createMiddleware(options) {
2533
2601
  if ("routes" in options) {
2534
2602
  console.warn(
@@ -2544,7 +2612,7 @@ function createMiddleware(options) {
2544
2612
  recordDecisions,
2545
2613
  enableRuntimeChallenge = true,
2546
2614
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
2547
- failOnError = "open",
2615
+ failOnError = "closed",
2548
2616
  caseInsensitiveRouteMatch = false,
2549
2617
  anonymousBeacon = true,
2550
2618
  trustVerifiedHop = false,
@@ -2568,26 +2636,19 @@ function createMiddleware(options) {
2568
2636
  rawHeaders: req.headers
2569
2637
  });
2570
2638
  } : void 0;
2571
- let cachedRoutes = [];
2572
- let lastFetchAt = 0;
2573
- let refreshing = null;
2574
2639
  let warnedNoCounterparty = false;
2575
2640
  let warnedEmptyRoutes = false;
2576
- async function refreshRoutes() {
2577
- if (!config.counterpartyId) {
2578
- if (!warnedNoCounterparty) {
2579
- console.warn(
2580
- "[VerificationGateway] No counterpartyId configured \u2014 falling through (allow all). Per-route policy lives in the AstraSync dashboard now; register the endpoint and set counterpartyId in your middleware config to enforce policy."
2581
- );
2582
- warnedNoCounterparty = true;
2583
- }
2584
- return;
2585
- }
2586
- const fetched = await fetchRoutes(config, config.counterpartyId);
2587
- if (fetched) {
2588
- cachedRoutes = fetched;
2589
- lastFetchAt = Date.now();
2590
- if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
2641
+ const routesCache = createRoutesCache(config, {
2642
+ refreshMs: routesRefreshMs,
2643
+ onNoCounterparty: () => {
2644
+ if (warnedNoCounterparty) return;
2645
+ console.warn(
2646
+ "[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."
2647
+ );
2648
+ warnedNoCounterparty = true;
2649
+ },
2650
+ onFetched: (routes) => {
2651
+ if (routes.length === 0 && !warnedEmptyRoutes) {
2591
2652
  const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
2592
2653
  console.warn(
2593
2654
  `[VerificationGateway] No route policy configured for ${config.counterpartyId}. Gateway is in pass-through mode for ALL traffic until you add at least one route. Configure at ${dashboard}/dashboard/endpoints/${config.counterpartyId}/routes`
@@ -2595,9 +2656,6 @@ function createMiddleware(options) {
2595
2656
  warnedEmptyRoutes = true;
2596
2657
  }
2597
2658
  }
2598
- }
2599
- refreshing = refreshRoutes().finally(() => {
2600
- refreshing = null;
2601
2659
  });
2602
2660
  return async (req, res, next) => {
2603
2661
  try {
@@ -2605,15 +2663,9 @@ function createMiddleware(options) {
2605
2663
  if (shouldSkip) {
2606
2664
  return next();
2607
2665
  }
2608
- if (refreshing) {
2609
- await refreshing.catch(() => {
2610
- });
2611
- }
2612
- if (config.counterpartyId && Date.now() - lastFetchAt > routesRefreshMs) {
2613
- refreshing = refreshRoutes().finally(() => {
2614
- refreshing = null;
2615
- });
2616
- }
2666
+ const cachedRoutes = (await routesCache.current()).filter(
2667
+ (r) => typeof r.pattern === "string" && !r.tool
2668
+ );
2617
2669
  const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
2618
2670
  const matchedRoute = findRouteConfig(cachedRoutes, req.path, req.method, {
2619
2671
  caseInsensitive: caseInsensitiveRouteMatch,
@@ -2737,6 +2789,12 @@ function createMiddleware(options) {
2737
2789
  ...commerceArtifacts && { commerceArtifacts },
2738
2790
  purpose,
2739
2791
  action: pdlssPair.action,
2792
+ // 5.19.0: the source + the request line let verify-access check a
2793
+ // route purpose against the stored rule (item 20: an MPP paid leg
2794
+ // keeps `shopping.purchase` instead of `commerce.payment.execute`).
2795
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
2796
+ requestMethod: req.method,
2797
+ requestPath: req.path,
2740
2798
  resource: req.path,
2741
2799
  createSession: shouldRecordDecisions,
2742
2800
  counterpartyUrl,
@@ -2814,7 +2872,8 @@ function createMiddleware(options) {
2814
2872
  return;
2815
2873
  }
2816
2874
  if (routeConfig.minTrustScore && result.agent) {
2817
- if (result.agent.trustScore < routeConfig.minTrustScore) {
2875
+ const score = result.agent.trustScore;
2876
+ if (typeof score !== "number" || score < routeConfig.minTrustScore) {
2818
2877
  const trustFailure = {
2819
2878
  dimension: "endpoint.trust",
2820
2879
  message: "Trust below the route requirement for this endpoint.",
@@ -3258,6 +3317,10 @@ function createMiddleware2(options) {
3258
3317
  ...commerceArtifacts && { commerceArtifacts },
3259
3318
  purpose,
3260
3319
  action: pdlssPair.action,
3320
+ // 5.19.0: route purpose claim + request line (checked server-side).
3321
+ purposeSource: wirePurposeSource(pdlssPair.purposeSource),
3322
+ requestMethod: request.method,
3323
+ requestPath: pathname,
3261
3324
  resource: pathname,
3262
3325
  counterpartyUrl,
3263
3326
  counterpartyType: config.counterpartyType || "website",
@@ -3458,6 +3521,7 @@ function isRetryableSettlementError(err) {
3458
3521
  }
3459
3522
 
3460
3523
  // src/adapters/sdk.ts
3524
+ var warnedPhaselessValue = false;
3461
3525
  var VerificationGatewayClient = class {
3462
3526
  constructor(options) {
3463
3527
  this.config = {
@@ -3489,9 +3553,16 @@ var VerificationGatewayClient = class {
3489
3553
  apiKey: options.apiKey,
3490
3554
  jwt: options.jwt
3491
3555
  };
3556
+ if (options.transactionValue && !options.commercePhase && !warnedPhaselessValue) {
3557
+ warnedPhaselessValue = true;
3558
+ console.warn(
3559
+ "[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."
3560
+ );
3561
+ }
3492
3562
  return this.executeWithRetry(
3493
3563
  () => verify(this.config, {
3494
3564
  credentials,
3565
+ ...options.commercePhase && { commercePhase: options.commercePhase },
3495
3566
  purpose: options.purpose,
3496
3567
  action: options.action,
3497
3568
  resourceType: options.resourceType,
@@ -3507,6 +3578,43 @@ var VerificationGatewayClient = class {
3507
3578
  })
3508
3579
  );
3509
3580
  }
3581
+ /**
3582
+ * 5.19.0: the QUOTE leg of a checkout — a price / limit check before the
3583
+ * purchase. Sends `commercePhase: 'quote'`: verify-access evaluates the
3584
+ * agent's policy at this value and tells you whether a purchase would be
3585
+ * granted, held for the owner's approval, or denied, but records no order,
3586
+ * moves no money and never redeems an approval. Always reaches the backend
3587
+ * (no verdict cache).
3588
+ *
3589
+ * Quotes count toward limit-probing detection: a run of rising quotes is
3590
+ * how an agent (or a merchant) maps a spending limit. Quote what the buyer
3591
+ * asked for, not a ladder of values.
3592
+ */
3593
+ async quoteCheckout(options) {
3594
+ const credentials = {
3595
+ astraId: options.astraId,
3596
+ apiKey: options.apiKey,
3597
+ jwt: options.jwt
3598
+ };
3599
+ return this.executeWithRetry(
3600
+ () => verify(
3601
+ this.config,
3602
+ {
3603
+ credentials,
3604
+ purpose: options.purpose ?? "shopping",
3605
+ action: options.action ?? "shopping.purchase",
3606
+ transactionValue: options.transactionValue,
3607
+ currency: options.currency,
3608
+ commercePhase: "quote",
3609
+ ...options.checkoutSessionId && { checkoutSessionId: options.checkoutSessionId },
3610
+ ...options.attemptId && { attemptId: options.attemptId },
3611
+ counterpartyUrl: options.counterpartyUrl,
3612
+ counterpartyType: options.counterpartyType
3613
+ },
3614
+ { bypassCache: true }
3615
+ )
3616
+ );
3617
+ }
3510
3618
  /**
3511
3619
  * 5.4.0 (astra-pay): confirm-leg of a checkout — the ONLY call that may fire
3512
3620
  * first-party charge-at-redeem (server-side settlement on the owner's saved
@@ -3545,6 +3653,14 @@ var VerificationGatewayClient = class {
3545
3653
  * is matched to — and only to — this confirm. A held cart's approval is
3546
3654
  * bound to its checkout session and exact value; a confirm for any other
3547
3655
  * cart is held for its own approval.
3656
+ *
3657
+ * 5.19.0 checkout kits: pass the kit's `agentArtifacts` (the inbound agent
3658
+ * request — signed RFC 9421 headers with the public URL and raw-body
3659
+ * digest, or `{ unsigned: true }`) and `metadata` (`{ kitSessionId,
3660
+ * protocol }`) from the `verifyPurchase` context. The platform attributes
3661
+ * the confirm to the agent from the artifacts, and stores the metadata on
3662
+ * any approval this confirm creates so the approval event
3663
+ * (`handleApprovalEvents`) can find the kit session.
3548
3664
  */
3549
3665
  async confirmCheckout(options) {
3550
3666
  const credentials = {
@@ -3567,6 +3683,8 @@ var VerificationGatewayClient = class {
3567
3683
  ...options.checkoutJwt && { checkoutJwt: options.checkoutJwt },
3568
3684
  ...options.attemptId && { attemptId: options.attemptId },
3569
3685
  buyerEmail: options.buyerEmail,
3686
+ ...options.agentArtifacts && { agentArtifacts: options.agentArtifacts },
3687
+ ...options.metadata && { metadata: options.metadata },
3570
3688
  counterpartyUrl: options.counterpartyUrl,
3571
3689
  counterpartyType: options.counterpartyType
3572
3690
  },
@@ -4494,9 +4612,9 @@ function extractFulfillmentOption(body) {
4494
4612
  if (direct) return direct;
4495
4613
  const options = body.fulfillment_options;
4496
4614
  if (Array.isArray(options) && options.length > 0) {
4497
- const first = options[0];
4498
- if (first && typeof first === "object") {
4499
- const id = coerceString3(first.id);
4615
+ const first2 = options[0];
4616
+ if (first2 && typeof first2 === "object") {
4617
+ const id = coerceString3(first2.id);
4500
4618
  if (id) return id;
4501
4619
  }
4502
4620
  }
@@ -5503,7 +5621,8 @@ function verifyMPP(input) {
5503
5621
  }
5504
5622
  let replayOk = true;
5505
5623
  if (challenge?.digest && expiryOk) {
5506
- const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
5624
+ const c = challenge;
5625
+ const replayKey = `mpp:${challenge.digest}:${c.id ?? ""}:${c.nonce ?? ""}`;
5507
5626
  const expiresAt = (nowSec + tolerance) * 1e3;
5508
5627
  if (nonceStore.seen(replayKey, expiresAt)) {
5509
5628
  replayOk = false;
@@ -6063,11 +6182,11 @@ function resolveTransactionValue(input) {
6063
6182
  if (v) return v;
6064
6183
  }
6065
6184
  if (input.x402?.paymentRequired) {
6066
- const first = input.x402.paymentRequired.accepts[0];
6067
- if (first) {
6185
+ const first2 = input.x402.paymentRequired.accepts[0];
6186
+ if (first2) {
6068
6187
  const v = extractX402TransactionValue({
6069
- maxAmountRequired: Number(first.amount),
6070
- asset: first.asset
6188
+ maxAmountRequired: Number(first2.amount),
6189
+ asset: first2.asset
6071
6190
  });
6072
6191
  if (v) return v;
6073
6192
  }
@@ -6525,6 +6644,21 @@ function defaultMcpDenied(result, req, res) {
6525
6644
  }
6526
6645
  });
6527
6646
  }
6647
+ function dashboardToolRules(routes) {
6648
+ const out = /* @__PURE__ */ new Map();
6649
+ for (const r of routes) {
6650
+ if (typeof r.tool !== "string" || !r.tool || r.legacy === true) continue;
6651
+ if (out.has(r.tool)) continue;
6652
+ out.set(r.tool, {
6653
+ tool: r.tool,
6654
+ ...r.purpose && { purpose: r.purpose },
6655
+ ...r.action && { action: r.action },
6656
+ ...r.resource && { resource: r.resource },
6657
+ ...r.observe === true && { observe: true }
6658
+ });
6659
+ }
6660
+ return out;
6661
+ }
6528
6662
  function resolveGate(parsed, opts) {
6529
6663
  if (!parsed.toolName) {
6530
6664
  const methodGate = opts.methodGates?.[parsed.method];
@@ -6534,8 +6668,30 @@ function resolveGate(parsed, opts) {
6534
6668
  return { enforce: false, source: "discovery_default" };
6535
6669
  }
6536
6670
  const toolGate = opts.toolGates?.[parsed.toolName];
6671
+ const rule = opts.dashboardRules?.get(parsed.toolName);
6672
+ const fromRule = (r) => r.observe ? { enforce: false, source: "dashboard" } : {
6673
+ enforce: true,
6674
+ config: {
6675
+ ...r.purpose && { purpose: r.purpose },
6676
+ ...r.action && { action: r.action },
6677
+ ...r.resource && { resource: r.resource }
6678
+ },
6679
+ source: "dashboard"
6680
+ };
6681
+ const codeGated = toolGate !== void 0 && toolGate !== "observe";
6682
+ if (rule && (opts.toolGatePrecedence === "dashboard" || !codeGated)) return fromRule(rule);
6537
6683
  if (toolGate !== void 0) {
6538
- return toolGate === "observe" ? { enforce: false, source: "toolGate" } : { enforce: true, config: toolGate, source: "toolGate" };
6684
+ if (toolGate === "observe") return { enforce: false, source: "toolGate" };
6685
+ if (rule && !rule.observe && rule.purpose && (rule.purpose !== toolGate.purpose || rule.action && rule.action !== toolGate.action)) {
6686
+ const extra = fromRule(rule).config;
6687
+ return {
6688
+ enforce: true,
6689
+ config: toolGate,
6690
+ additional: { ...extra, ...!extra.resource && toolGate.resource && { resource: toolGate.resource } },
6691
+ source: "toolGate"
6692
+ };
6693
+ }
6694
+ return { enforce: true, config: toolGate, source: "toolGate" };
6539
6695
  }
6540
6696
  return { enforce: mcpDefaultEnforced(parsed), source: "tier" };
6541
6697
  }
@@ -6550,13 +6706,20 @@ function createMcpMiddleware(options) {
6550
6706
  verifiedHopMaxAgeMs,
6551
6707
  recordDecisions,
6552
6708
  enableRuntimeChallenge = true,
6553
- failOnError = "open",
6709
+ failOnError = "closed",
6710
+ toolGatePrecedence = "code",
6711
+ routesRefreshMs,
6554
6712
  ...config
6555
6713
  } = options;
6556
6714
  if (config.apiBaseUrl) {
6557
6715
  prefetchWellKnown(config.apiBaseUrl).catch(() => {
6558
6716
  });
6559
6717
  }
6718
+ const codeGatedTools = Object.entries(toolGates ?? {}).filter(([, g]) => g !== "observe").map(([name]) => name);
6719
+ const rulesCache = config.apiKey && config.counterpartyId ? createRoutesCache(config, {
6720
+ ...routesRefreshMs !== void 0 && { refreshMs: routesRefreshMs },
6721
+ ...codeGatedTools.length && { fetchOptions: { codeGates: codeGatedTools } }
6722
+ }) : void 0;
6560
6723
  return async (req, res, next) => {
6561
6724
  try {
6562
6725
  if (skip) return next();
@@ -6605,9 +6768,12 @@ function createMcpMiddleware(options) {
6605
6768
  return next();
6606
6769
  }
6607
6770
  }
6771
+ const dashboardRules = rulesCache ? dashboardToolRules(await rulesCache.current()) : void 0;
6608
6772
  const gateDecision = resolveGate(parsed, {
6609
6773
  toolGates,
6610
- methodGates
6774
+ methodGates,
6775
+ dashboardRules,
6776
+ toolGatePrecedence
6611
6777
  });
6612
6778
  const credentials = extractCredentials(
6613
6779
  req.headers,
@@ -6705,6 +6871,20 @@ function createMcpMiddleware(options) {
6705
6871
  result.stepUpOutcome = outcome;
6706
6872
  }
6707
6873
  }
6874
+ if (shouldEnforce && gateDecision.additional?.purpose && result.policyAllowed && !requiresHumanApproval(result)) {
6875
+ const extra = mcpToPdlss(parsed, req.path, void 0, void 0, gateDecision.additional);
6876
+ const extraResult = await verify(config, {
6877
+ ...verificationRequest,
6878
+ purpose: extra.purpose,
6879
+ action: extra.action,
6880
+ resource: extra.resource
6881
+ });
6882
+ if (!extraResult.policyAllowed || requiresHumanApproval(extraResult)) {
6883
+ result = extraResult;
6884
+ req.agentVerification = result;
6885
+ sessionId = result.sessionId;
6886
+ }
6887
+ }
6708
6888
  if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
6709
6889
  res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6710
6890
  return next();
@@ -10011,15 +10191,15 @@ function requireDumper() {
10011
10191
  return !isWhitespace(c) && c !== CHAR_COLON;
10012
10192
  }
10013
10193
  function codePointAt(string, pos) {
10014
- const first = string.charCodeAt(pos);
10194
+ const first2 = string.charCodeAt(pos);
10015
10195
  let second;
10016
- if (first >= 55296 && first <= 56319 && pos + 1 < string.length) {
10196
+ if (first2 >= 55296 && first2 <= 56319 && pos + 1 < string.length) {
10017
10197
  second = string.charCodeAt(pos + 1);
10018
10198
  if (second >= 56320 && second <= 57343) {
10019
- return (first - 55296) * 1024 + second - 56320 + 65536;
10199
+ return (first2 - 55296) * 1024 + second - 56320 + 65536;
10020
10200
  }
10021
10201
  }
10022
- return first;
10202
+ return first2;
10023
10203
  }
10024
10204
  function needIndentIndicator(string) {
10025
10205
  const leadingSpaceRe = /^\n* /;
@@ -11678,6 +11858,12 @@ async function finishIdempotent(opts, decision, response) {
11678
11858
  } catch {
11679
11859
  }
11680
11860
  }
11861
+ async function abandonIdempotent(opts, decision) {
11862
+ try {
11863
+ await opts.store.delete(decision.storeKey);
11864
+ } catch {
11865
+ }
11866
+ }
11681
11867
  var KEPT_HEADERS = ["content-type", "location"];
11682
11868
  function withHeader(res, name, value) {
11683
11869
  const headers = new Headers(res.headers);
@@ -11833,6 +12019,87 @@ function warnInMemoryStores(kit, missing) {
11833
12019
  `[astrasync] ${kit} checkout kit is using the in-memory ${missing.join(" and ")} (sessions are lost on restart and not shared across instances). Pass ${missing.map((m) => m.startsWith("idempotency") ? "idempotencyStore" : "store").join(" and ")} in production.`
11834
12020
  );
11835
12021
  }
12022
+ var SECRET_KEY = /token|secret|credential|cvc|cvv|number|pan|password|key|cryptogram|signature/i;
12023
+ function scrubPaymentInput(input) {
12024
+ if (!input || typeof input !== "object") return void 0;
12025
+ const out = {};
12026
+ for (const [k, v] of Object.entries(input)) {
12027
+ if (SECRET_KEY.test(k)) continue;
12028
+ if (v === null || ["string", "number", "boolean"].includes(typeof v)) out[k] = v;
12029
+ }
12030
+ return out;
12031
+ }
12032
+ var NEVER_FORWARD = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "proxy-authorization"]);
12033
+ function coveredHeaders(signatureInput) {
12034
+ const names = /* @__PURE__ */ new Set();
12035
+ for (const m of signatureInput.matchAll(/\(([^)]*)\)/g)) {
12036
+ for (const q of m[1].matchAll(/"([^"]+)"/g)) {
12037
+ const name = q[1].toLowerCase();
12038
+ if (!name.startsWith("@")) names.add(name);
12039
+ }
12040
+ }
12041
+ return [...names];
12042
+ }
12043
+ function buildAgentArtifacts(req, rawBody, opts) {
12044
+ const signature = req.headers.get("signature");
12045
+ const signatureInput = req.headers.get("signature-input");
12046
+ if (opts.protocol === "acp" || !signature || !signatureInput) {
12047
+ return { unsigned: true, protocol: opts.protocol };
12048
+ }
12049
+ const headers = {};
12050
+ const names = [
12051
+ "signature",
12052
+ "signature-input",
12053
+ "signature-agent",
12054
+ "ucp-agent",
12055
+ "content-digest",
12056
+ ...coveredHeaders(signatureInput)
12057
+ ];
12058
+ for (const name of names) {
12059
+ if (NEVER_FORWARD.has(name)) continue;
12060
+ const v = req.headers.get(name);
12061
+ if (v !== null) headers[name] = v;
12062
+ }
12063
+ const u = new URL(req.url);
12064
+ const origin = opts.publicOrigin ? opts.publicOrigin.replace(/\/+$/, "") : u.origin;
12065
+ return {
12066
+ protocol: opts.protocol,
12067
+ method: req.method,
12068
+ url: `${origin}${u.pathname}${u.search}`,
12069
+ headers,
12070
+ ...rawBody && { contentDigest: contentDigest(rawBody) }
12071
+ };
12072
+ }
12073
+ async function withReportRetries(fn, opts = {}, isRetryable) {
12074
+ const attempts = Math.max(1, opts.attempts ?? 3);
12075
+ const backoff = opts.backoffMs ?? 250;
12076
+ let last;
12077
+ for (let i = 0; i < attempts; i++) {
12078
+ try {
12079
+ return await fn();
12080
+ } catch (err) {
12081
+ last = err;
12082
+ if (i === attempts - 1 || !isRetryable(err)) break;
12083
+ await new Promise((r) => setTimeout(r, backoff * 2 ** i));
12084
+ }
12085
+ }
12086
+ throw last;
12087
+ }
12088
+ function defaultOnReportError(err, ctx) {
12089
+ console.error(
12090
+ `[astrasync] checkout kit: ${ctx.kind === "order_webhook" ? "order webhook" : "settlement report"} failed for session ${ctx.sessionId}${ctx.orderId ? ` (order ${ctx.orderId})` : ""}:`,
12091
+ err
12092
+ );
12093
+ }
12094
+ var NON_TERMINAL = /* @__PURE__ */ new Set(["busy", "still_held"]);
12095
+ function isTerminalOutcome(outcome) {
12096
+ return !NON_TERMINAL.has(outcome);
12097
+ }
12098
+ var resumeResult = (outcome, session) => ({
12099
+ outcome,
12100
+ terminal: isTerminalOutcome(outcome),
12101
+ session
12102
+ });
11836
12103
 
11837
12104
  // src/ucp/checkout-kit.ts
11838
12105
  var MemoryCheckoutStore = class {
@@ -11857,6 +12124,15 @@ var MemoryCheckoutStore = class {
11857
12124
  const id = this.orders.get(orderId);
11858
12125
  return id ? this.m.get(id) ?? null : null;
11859
12126
  }
12127
+ async findByCheckoutSessionId(checkoutSessionId) {
12128
+ for (const s of this.m.values()) {
12129
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
12130
+ }
12131
+ return this.m.get(checkoutSessionId) ?? null;
12132
+ }
12133
+ async listHeld() {
12134
+ return [...this.m.values()].filter((s) => s.status === "requires_escalation");
12135
+ }
11860
12136
  };
11861
12137
  function buildUcpOrder(session, opts = {}) {
11862
12138
  if (!session.order) return null;
@@ -11879,11 +12155,20 @@ function buildUcpOrder(session, opts = {}) {
11879
12155
  totals: session.totals
11880
12156
  };
11881
12157
  }
12158
+ function heldBody(text) {
12159
+ try {
12160
+ return JSON.parse(text).status === "requires_escalation";
12161
+ } catch {
12162
+ return false;
12163
+ }
12164
+ }
11882
12165
  var json2 = (status, body, headers = {}) => new Response(JSON.stringify(body), {
11883
12166
  status,
11884
12167
  headers: { "content-type": "application/json", ...headers }
11885
12168
  });
11886
12169
  var total = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
12170
+ var clone = (v) => JSON.parse(JSON.stringify(v));
12171
+ var inFlight = (st) => st === "complete_in_progress" || st === "finishing";
11887
12172
  function createUcpCheckoutKit(opts) {
11888
12173
  const profile = buildUcpProfile(opts.profile);
11889
12174
  const store = opts.store ?? new MemoryCheckoutStore();
@@ -11909,6 +12194,11 @@ function createUcpCheckoutKit(opts) {
11909
12194
  ...entries.map((e) => e.id)
11910
12195
  ])
11911
12196
  );
12197
+ const publicOrigin = (opts.publicOrigin ?? new URL(opts.profile.endpoint).origin).replace(
12198
+ /\/+$/,
12199
+ ""
12200
+ );
12201
+ const onReportError = opts.onReportError ?? defaultOnReportError;
11912
12202
  const fetchProfile = opts.fetchProfile ?? (async (url, fresh) => {
11913
12203
  const r = await fetchUcpProfile(url, { fresh });
11914
12204
  return r.ok ? r.profile : r;
@@ -11930,7 +12220,7 @@ function createUcpCheckoutKit(opts) {
11930
12220
  payment_handlers: profile.ucp.payment_handlers
11931
12221
  },
11932
12222
  id: s.id,
11933
- status: s.status,
12223
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
11934
12224
  currency: s.currency,
11935
12225
  line_items: s.line_items,
11936
12226
  totals: s.totals,
@@ -11976,6 +12266,23 @@ function createUcpCheckoutKit(opts) {
11976
12266
  "ephemeral"
11977
12267
  );
11978
12268
  }
12269
+ function heldNoEdit(s) {
12270
+ return render(
12271
+ {
12272
+ ...s,
12273
+ messages: [
12274
+ ...s.messages ?? [],
12275
+ ucpError(
12276
+ "checkout_awaiting_approval",
12277
+ "This checkout is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over.",
12278
+ "requires_buyer_review"
12279
+ )
12280
+ ]
12281
+ },
12282
+ 409,
12283
+ "ephemeral"
12284
+ );
12285
+ }
11979
12286
  async function save(s) {
11980
12287
  if (s.order) orderIndex.set(s.order.id, s.id);
11981
12288
  await store.put(s);
@@ -12006,31 +12313,44 @@ function createUcpCheckoutKit(opts) {
12006
12313
  return r;
12007
12314
  }
12008
12315
  function emitOrder(s) {
12009
- void deliverOrder(s).catch(() => void 0);
12010
- }
12011
- async function reportOutcome(p, settled) {
12012
- if (!opts.reportSettlement || !p.orderId) return;
12013
- await opts.reportSettlement({
12014
- orderId: p.orderId,
12015
- status: settled.status,
12016
- ...p.report,
12017
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12018
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12019
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12020
- }).catch(() => void 0);
12316
+ const ctx = { sessionId: s.id, orderId: s.order?.id, kind: "order_webhook" };
12317
+ void deliverOrder(s).then((r) => {
12318
+ if (r && !r.ok) onReportError(new Error(`order webhook failed (HTTP ${r.status})`), ctx);
12319
+ }).catch((err) => onReportError(err, ctx));
12320
+ }
12321
+ async function reportOutcome(s, p, settled) {
12322
+ const report = opts.reportSettlement;
12323
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
12324
+ if (!report || !ref) return;
12325
+ await withReportRetries(
12326
+ () => report({
12327
+ ...ref,
12328
+ status: settled.status,
12329
+ ...p.report,
12330
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12331
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12332
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12333
+ }),
12334
+ opts.reportRetry,
12335
+ isRetryableSettlementError
12336
+ ).catch(
12337
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
12338
+ );
12021
12339
  }
12022
12340
  async function finish(s, result, p, settled) {
12023
- await reportOutcome(p, settled);
12341
+ await reportOutcome(s, p, settled);
12024
12342
  delete s._pending;
12343
+ delete s._held;
12344
+ delete s._approvalId;
12025
12345
  if (settled.status === "failed") {
12026
12346
  s.status = "ready_for_complete";
12027
12347
  s.messages = [
12028
- ucpError("payment_failed", "The payment did not go through.", "requires_buyer_input")
12348
+ ucpError("payment_failed", "The payment did not go through.", "recoverable")
12029
12349
  ];
12030
12350
  await save(s);
12031
12351
  return;
12032
12352
  }
12033
- const order = await opts.fulfil({ session: s, result });
12353
+ const order = await opts.fulfil({ session: s, result, settled });
12034
12354
  s.order = {
12035
12355
  ...order,
12036
12356
  ...!order.permalink_url && opts.permalinkBase && {
@@ -12064,7 +12384,8 @@ function createUcpCheckoutKit(opts) {
12064
12384
  const out = [];
12065
12385
  const messages = [];
12066
12386
  for (const [i, l] of lines.entries()) {
12067
- const item = l.item?.id ? await opts.catalog(l.item.id) : null;
12387
+ const q = Math.max(1, Math.floor(l.quantity ?? 1));
12388
+ const item = l.item?.id ? await opts.catalog(l.item.id, q) : null;
12068
12389
  if (!item) {
12069
12390
  messages.push(
12070
12391
  ucpError(
@@ -12076,7 +12397,6 @@ function createUcpCheckoutKit(opts) {
12076
12397
  );
12077
12398
  continue;
12078
12399
  }
12079
- const q = Math.max(1, Math.floor(l.quantity ?? 1));
12080
12400
  const amount = item.price * q;
12081
12401
  out.push({
12082
12402
  id: `li_${i + 1}`,
@@ -12098,6 +12418,19 @@ function createUcpCheckoutKit(opts) {
12098
12418
  ]
12099
12419
  };
12100
12420
  }
12421
+ async function validate(s) {
12422
+ if (!opts.validateCart) return { messages: [], blocked: false };
12423
+ const found = await opts.validateCart(s);
12424
+ const messages = found.map(
12425
+ (m) => (m.type ?? "error") === "error" ? ucpError(m.code, m.content, "recoverable", m.path) : {
12426
+ type: m.type,
12427
+ code: m.code,
12428
+ content: m.content,
12429
+ ...m.path && { path: m.path }
12430
+ }
12431
+ );
12432
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
12433
+ }
12101
12434
  async function platform(req, body) {
12102
12435
  const profileUrl = parseUcpAgentHeader(req.headers.get("ucp-agent"));
12103
12436
  if (!profileUrl) {
@@ -12169,17 +12502,20 @@ function createUcpCheckoutKit(opts) {
12169
12502
  });
12170
12503
  const res = await run();
12171
12504
  if (d.kind === "proceed") {
12172
- await finishIdempotent(idem, d, {
12173
- status: res.status,
12174
- headers: { "content-type": "application/json" },
12175
- body: await res.clone().text()
12176
- });
12505
+ const text = await res.clone().text();
12506
+ if (heldBody(text)) await abandonIdempotent(idem, d);
12507
+ else
12508
+ await finishIdempotent(idem, d, {
12509
+ status: res.status,
12510
+ headers: { "content-type": "application/json" },
12511
+ body: text
12512
+ });
12177
12513
  }
12178
12514
  return res;
12179
12515
  }
12180
- async function complete(req, s, body) {
12516
+ async function complete(req, s, body, agentArtifacts) {
12181
12517
  if (s.status === "completed" || s.status === "canceled") return render(s);
12182
- if (s.status === "complete_in_progress") return busy(s);
12518
+ if (inFlight(s.status)) return busy(s);
12183
12519
  const ap2 = body.ap2;
12184
12520
  const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
12185
12521
  if (s._ap2) {
@@ -12195,13 +12531,13 @@ function createUcpCheckoutKit(opts) {
12195
12531
  return render(s);
12196
12532
  }
12197
12533
  const kidResolver = async (kid) => opts.mandateKey ? opts.mandateKey(kid, s) : (s._platformKeys ?? []).find((k) => k.kid === kid) ?? null;
12198
- const v = await verifyAp2Mandate(ap2.checkout_mandate, {
12534
+ const v2 = await verifyAp2Mandate(ap2.checkout_mandate, {
12199
12535
  resolveRootKey: kidResolver,
12200
12536
  expectedVct: AP2_VCT.checkout,
12201
12537
  ...checkoutJwt && { checkoutJwt }
12202
12538
  });
12203
- if (!v.ok) {
12204
- s.messages = [ucpError(v.code, v.error, "unrecoverable")];
12539
+ if (!v2.ok) {
12540
+ s.messages = [ucpError(v2.code, v2.error, "unrecoverable")];
12205
12541
  await store.put(s);
12206
12542
  return render(s);
12207
12543
  }
@@ -12220,19 +12556,32 @@ function createUcpCheckoutKit(opts) {
12220
12556
  await store.put(s);
12221
12557
  return render(s);
12222
12558
  }
12559
+ const v = await validate(s);
12560
+ if (v.blocked) {
12561
+ s.status = "incomplete";
12562
+ s.messages = v.messages;
12563
+ delete s._held;
12564
+ delete s._approvalId;
12565
+ await store.put(s);
12566
+ return render(s);
12567
+ }
12223
12568
  const from = s.status;
12224
12569
  if (!await claim2(s, from)) {
12225
12570
  const current = await store.get(s.id);
12226
12571
  return busy(current ?? s);
12227
12572
  }
12573
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
12574
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
12228
12575
  try {
12229
12576
  const result = await opts.verifyPurchase({
12230
12577
  session: s,
12231
12578
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
12232
- astraId: req.headers.get("x-astra-id") ?? void 0,
12233
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
12579
+ astraId,
12580
+ attemptId,
12234
12581
  ...checkoutJwt && { checkoutJwt },
12235
- valueMajor: total(s) / 100
12582
+ valueMajor: total(s) / 100,
12583
+ agentArtifacts,
12584
+ metadata: { kitSessionId: s.id, protocol: "ucp" }
12236
12585
  });
12237
12586
  const binding2 = await bindAgent(req, result);
12238
12587
  if (binding2) {
@@ -12244,6 +12593,16 @@ function createUcpCheckoutKit(opts) {
12244
12593
  if (isHeld(result)) {
12245
12594
  s.status = "requires_escalation";
12246
12595
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
12596
+ s._approvalId = result.stepUpApproval?.approvalId;
12597
+ s._astraId = astraId ?? result.agent?.astraId;
12598
+ s._attemptId = attemptId ?? result.attemptId;
12599
+ s._held = {
12600
+ line_items: clone(s.line_items),
12601
+ totals: clone(s.totals),
12602
+ ...scrubPaymentInput(instrument) && { instrument: scrubPaymentInput(instrument) },
12603
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
12604
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
12605
+ };
12247
12606
  s.messages = [
12248
12607
  ucpError(
12249
12608
  "buyer_approval_required",
@@ -12266,21 +12625,7 @@ function createUcpCheckoutKit(opts) {
12266
12625
  await store.put(s);
12267
12626
  return render(s);
12268
12627
  }
12269
- const settled = await opts.settle({ session: s, result, instrument });
12270
- const pending = {
12271
- ...result.settlementOutcome?.orderId && {
12272
- orderId: result.settlementOutcome.orderId
12273
- },
12274
- report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
12275
- result: pendingResult(result)
12276
- };
12277
- if (settled.status === "pending") {
12278
- s._pending = pending;
12279
- s.messages = [];
12280
- await store.put(s);
12281
- return render(s);
12282
- }
12283
- await finish(s, result, pending, settled);
12628
+ await settleAndFinish(s, result, instrument);
12284
12629
  return render(s);
12285
12630
  } catch (e) {
12286
12631
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -12290,13 +12635,184 @@ function createUcpCheckoutKit(opts) {
12290
12635
  throw e;
12291
12636
  }
12292
12637
  }
12638
+ async function settleAndFinish(s, result, instrument) {
12639
+ const settled = await opts.settle({ session: s, result, instrument });
12640
+ const enhanced = result;
12641
+ const pending = {
12642
+ ...result.settlementOutcome?.orderId && {
12643
+ orderId: result.settlementOutcome.orderId
12644
+ },
12645
+ ...enhanced.sessionId && { sessionId: enhanced.sessionId },
12646
+ report: orderReportAmount(result, { amountMinor: total(s), currency: s.currency }),
12647
+ result: pendingResult(result)
12648
+ };
12649
+ if (settled.status === "pending") {
12650
+ s._pending = pending;
12651
+ s.messages = [];
12652
+ delete s._held;
12653
+ delete s._approvalId;
12654
+ await store.put(s);
12655
+ return "pending";
12656
+ }
12657
+ await finish(s, result, pending, settled);
12658
+ return settled.status === "failed" ? "payment_failed" : "completed";
12659
+ }
12293
12660
  async function resolvePending(sessionId, outcome) {
12294
12661
  const s = await store.get(sessionId);
12295
12662
  if (!s) return null;
12296
12663
  if (s.status !== "complete_in_progress" || !s._pending) return s;
12297
- await finish(s, s._pending.result, s._pending, outcome);
12664
+ if (store.claim) {
12665
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
12666
+ return await store.get(sessionId) ?? s;
12667
+ }
12668
+ s.status = "finishing";
12669
+ } else {
12670
+ s.status = "finishing";
12671
+ await store.put(s);
12672
+ }
12673
+ const pending = s._pending;
12674
+ try {
12675
+ await finish(s, pending.result, pending, outcome);
12676
+ } catch (e) {
12677
+ s.status = "complete_in_progress";
12678
+ s._pending = pending;
12679
+ await store.put(s).catch(() => void 0);
12680
+ throw e;
12681
+ }
12298
12682
  return s;
12299
12683
  }
12684
+ async function resumeApproved(sessionId, event) {
12685
+ const s = await store.get(sessionId);
12686
+ if (!s) return resumeResult("not_found", null);
12687
+ if (s.status === "completed") return resumeResult("already_completed", s);
12688
+ if (s.status === "canceled") return resumeResult("canceled", s);
12689
+ if (inFlight(s.status)) return resumeResult("busy", s);
12690
+ if (s.status !== "requires_escalation" || !s._held) return resumeResult("not_held", s);
12691
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
12692
+ return resumeResult("stale", s);
12693
+ if (!await claim2(s, "requires_escalation")) {
12694
+ return resumeResult("busy", await store.get(sessionId) ?? s);
12695
+ }
12696
+ const held = s._held;
12697
+ const backToHeld = async () => {
12698
+ s.status = "requires_escalation";
12699
+ s._held = held;
12700
+ await store.put(s).catch(() => void 0);
12701
+ };
12702
+ try {
12703
+ s.line_items = clone(held.line_items);
12704
+ s.totals = clone(held.totals);
12705
+ const v = await validate(s);
12706
+ if (v.blocked) {
12707
+ s.status = "incomplete";
12708
+ s.messages = v.messages;
12709
+ delete s._held;
12710
+ delete s._approvalId;
12711
+ delete s.continue_url;
12712
+ await store.put(s);
12713
+ return resumeResult("cart_invalid", s);
12714
+ }
12715
+ const checkoutJwt = s._ap2 ? s._checkoutJwt : void 0;
12716
+ const result = await opts.verifyPurchase({
12717
+ session: s,
12718
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
12719
+ astraId: s._astraId,
12720
+ attemptId: s._attemptId,
12721
+ ...checkoutJwt && { checkoutJwt },
12722
+ valueMajor: total(s) / 100,
12723
+ metadata: { kitSessionId: s.id, protocol: "ucp" },
12724
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
12725
+ });
12726
+ if (isHeld(result)) {
12727
+ await backToHeld();
12728
+ return resumeResult("still_held", s);
12729
+ }
12730
+ if (!result.identityVerified || !result.policyAllowed) {
12731
+ s.status = "ready_for_complete";
12732
+ s.messages = [
12733
+ ucpError(
12734
+ "agent_not_authorized",
12735
+ "The agent is not authorized for this purchase.",
12736
+ "unrecoverable"
12737
+ )
12738
+ ];
12739
+ delete s._held;
12740
+ delete s._approvalId;
12741
+ delete s.continue_url;
12742
+ await store.put(s);
12743
+ return resumeResult("not_authorized", s);
12744
+ }
12745
+ delete s.continue_url;
12746
+ const outcome = await settleAndFinish(s, result, held.instrument);
12747
+ return resumeResult(outcome, s);
12748
+ } catch (e) {
12749
+ if (!s._pending && inFlight(s.status)) await backToHeld();
12750
+ throw e;
12751
+ }
12752
+ }
12753
+ async function resumeDeclined(sessionId, reason = "denied", event) {
12754
+ const s = await store.get(sessionId);
12755
+ if (!s) return resumeResult("not_found", null);
12756
+ if (s.status === "completed") return resumeResult("already_completed", s);
12757
+ if (s.status === "canceled") return resumeResult("canceled", s);
12758
+ if (inFlight(s.status)) return resumeResult("busy", s);
12759
+ if (s.status !== "requires_escalation") return resumeResult("not_held", s);
12760
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
12761
+ return resumeResult("stale", s);
12762
+ if (store.claim && !await store.claim(s.id, "requires_escalation", "ready_for_complete")) {
12763
+ return resumeResult("busy", await store.get(sessionId) ?? s);
12764
+ }
12765
+ s.status = "ready_for_complete";
12766
+ delete s._held;
12767
+ delete s._approvalId;
12768
+ delete s.continue_url;
12769
+ s.messages = [
12770
+ ucpError(
12771
+ `approval_${reason}`,
12772
+ 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.",
12773
+ "requires_buyer_review"
12774
+ )
12775
+ ];
12776
+ await store.put(s);
12777
+ return resumeResult("declined", s);
12778
+ }
12779
+ async function findForEvent(event) {
12780
+ if (event.kitSessionId) {
12781
+ const s = await store.get(event.kitSessionId);
12782
+ if (s) return s;
12783
+ }
12784
+ if (store.findByCheckoutSessionId)
12785
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
12786
+ return store.get(event.checkoutSessionId);
12787
+ }
12788
+ async function applyApprovalEvent(event) {
12789
+ const s = await findForEvent(event);
12790
+ if (!s) return resumeResult("not_found", null);
12791
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
12792
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
12793
+ }
12794
+ async function pollHeldSessions(o = {}) {
12795
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
12796
+ (x) => Boolean(x)
12797
+ ) : store.listHeld ? await store.listHeld() : [];
12798
+ const out = [];
12799
+ for (const s of sessions) {
12800
+ if (s.status !== "requires_escalation" || !s._held?.pollUrl || !s._approvalId) continue;
12801
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
12802
+ approvalId: s._approvalId,
12803
+ pollUrl: s._held.pollUrl,
12804
+ expiresAt: s._held.expiresAt ?? ""
12805
+ });
12806
+ if (status === "approved" || status === "consumed") {
12807
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
12808
+ } else if (status === "denied" || status === "expired") {
12809
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
12810
+ } else {
12811
+ out.push({ sessionId: s.id, outcome: "pending" });
12812
+ }
12813
+ }
12814
+ return out;
12815
+ }
12300
12816
  async function sendOrderWebhook(sessionId) {
12301
12817
  const s = await store.get(sessionId);
12302
12818
  return s ? deliverOrder(s) : null;
@@ -12315,6 +12831,7 @@ function createUcpCheckoutKit(opts) {
12315
12831
  const p = await platform(req, body);
12316
12832
  if ("error" in p && p.error) return p.error;
12317
12833
  const ctx = p;
12834
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "ucp", publicOrigin });
12318
12835
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId) ?? fallbackScope("ucp-profile", ctx.profileUrl);
12319
12836
  if (req.method === "POST" && path === "/checkout-sessions") {
12320
12837
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12347,6 +12864,11 @@ function createUcpCheckoutKit(opts) {
12347
12864
  )
12348
12865
  );
12349
12866
  }
12867
+ if (priced.lines.length && !priced.messages.length) {
12868
+ const v = await validate(s2);
12869
+ s2.messages = [...s2.messages ?? [], ...v.messages];
12870
+ if (v.blocked) s2.status = "incomplete";
12871
+ }
12350
12872
  await store.put(s2);
12351
12873
  return render(s2, 201);
12352
12874
  });
@@ -12372,13 +12894,19 @@ function createUcpCheckoutKit(opts) {
12372
12894
  if (req.method === "PUT" && !m[2]) {
12373
12895
  return withIdempotency2(req, body, path, caller, async () => {
12374
12896
  if (s.status === "completed" || s.status === "canceled") return render(s);
12375
- if (s.status === "complete_in_progress") return busy(s);
12897
+ if (inFlight(s.status)) return busy(s);
12898
+ if (s.status === "requires_escalation") return heldNoEdit(s);
12376
12899
  if (parsed.line_items) {
12377
12900
  const priced = await priceLines(parsed.line_items);
12378
12901
  s.line_items = priced.lines;
12379
12902
  s.totals = priced.totals;
12380
12903
  s.messages = priced.messages;
12381
12904
  s.status = priced.lines.length && !priced.messages.length ? "ready_for_complete" : "incomplete";
12905
+ if (s.status === "ready_for_complete") {
12906
+ const v = await validate(s);
12907
+ s.messages = [...s.messages, ...v.messages];
12908
+ if (v.blocked) s.status = "incomplete";
12909
+ }
12382
12910
  }
12383
12911
  if (parsed.buyer) s.buyer = parsed.buyer;
12384
12912
  await store.put(s);
@@ -12386,7 +12914,13 @@ function createUcpCheckoutKit(opts) {
12386
12914
  });
12387
12915
  }
12388
12916
  if (req.method === "POST" && m[2] === "/complete") {
12389
- return withIdempotency2(req, body, path, caller, () => complete(req, s, parsed));
12917
+ return withIdempotency2(
12918
+ req,
12919
+ body,
12920
+ path,
12921
+ caller,
12922
+ () => complete(req, s, parsed, agentArtifacts)
12923
+ );
12390
12924
  }
12391
12925
  if (req.method === "POST" && m[2] === "/cancel") {
12392
12926
  return withIdempotency2(req, body, path, caller, async () => {
@@ -12395,7 +12929,7 @@ function createUcpCheckoutKit(opts) {
12395
12929
  code: "method_not_allowed",
12396
12930
  content: `A ${s.status} checkout can't be canceled.`
12397
12931
  });
12398
- if (s.status === "complete_in_progress") return busy(s);
12932
+ if (inFlight(s.status)) return busy(s);
12399
12933
  s.status = "canceled";
12400
12934
  await store.put(s);
12401
12935
  return render(s);
@@ -12408,10 +12942,156 @@ function createUcpCheckoutKit(opts) {
12408
12942
  profile,
12409
12943
  profileHandler: () => json2(200, profile, { "cache-control": "public, max-age=300" }),
12410
12944
  resolvePending,
12411
- sendOrderWebhook
12945
+ sendOrderWebhook,
12946
+ /** 5.19.0 */
12947
+ protocol: "ucp",
12948
+ publicOrigin,
12949
+ resumeApproved,
12950
+ resumeDeclined,
12951
+ applyApprovalEvent,
12952
+ pollHeldSessions
12412
12953
  };
12413
12954
  }
12414
12955
 
12956
+ // src/webhooks.ts
12957
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
12958
+ var DEFAULT_TOLERANCE_SECONDS = 300;
12959
+ function parseSignatureHeader(header) {
12960
+ let t;
12961
+ const signatures = [];
12962
+ for (const segment of header.split(",")) {
12963
+ const eq = segment.indexOf("=");
12964
+ if (eq < 0) continue;
12965
+ const k = segment.slice(0, eq).trim();
12966
+ const v = segment.slice(eq + 1).trim();
12967
+ if (k === "t") t = v;
12968
+ else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
12969
+ }
12970
+ return { t, signatures };
12971
+ }
12972
+ function computeHmac(secret, signedPayload) {
12973
+ return createHmac2("sha256", secret).update(signedPayload, "utf8").digest("hex");
12974
+ }
12975
+ function constantTimeEquals(a, b) {
12976
+ const aBuf = Buffer.from(a, "utf8");
12977
+ const bBuf = Buffer.from(b, "utf8");
12978
+ if (aBuf.length !== bBuf.length) return false;
12979
+ return timingSafeEqual2(aBuf, bBuf);
12980
+ }
12981
+ function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
12982
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
12983
+ (x) => typeof x === "string" && x.length > 0
12984
+ );
12985
+ if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
12986
+ const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
12987
+ if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
12988
+ const headerValue3 = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
12989
+ const { t: timestamp2, signatures } = parseSignatureHeader(headerValue3);
12990
+ if (!timestamp2 || !signatures.length) {
12991
+ return { ok: false, reason: "malformed_signature_header" };
12992
+ }
12993
+ const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
12994
+ const now = options.nowMs ?? Date.now();
12995
+ const tsSeconds = Number(timestamp2);
12996
+ if (!Number.isFinite(tsSeconds)) {
12997
+ return { ok: false, reason: "invalid_timestamp" };
12998
+ }
12999
+ const ageSeconds = Math.abs(now / 1e3 - tsSeconds);
13000
+ if (ageSeconds > tolerance) {
13001
+ return { ok: false, reason: "timestamp_outside_tolerance" };
13002
+ }
13003
+ for (const key of secrets) {
13004
+ const expectedSignature = computeHmac(key, `${timestamp2}.${rawBody}`);
13005
+ if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
13006
+ return { ok: true };
13007
+ }
13008
+ }
13009
+ return { ok: false, reason: "signature_mismatch" };
13010
+ }
13011
+
13012
+ // src/ucp/approval-events.ts
13013
+ var MemorySeenEvents = class {
13014
+ constructor(max = 1e4) {
13015
+ this.max = max;
13016
+ this.ids = /* @__PURE__ */ new Set();
13017
+ }
13018
+ async has(eventId) {
13019
+ return this.ids.has(eventId);
13020
+ }
13021
+ async add(eventId) {
13022
+ if (this.ids.size >= this.max) {
13023
+ const first2 = this.ids.values().next().value;
13024
+ if (first2 !== void 0) this.ids.delete(first2);
13025
+ }
13026
+ this.ids.add(eventId);
13027
+ }
13028
+ };
13029
+ var defaultSeen = new MemorySeenEvents();
13030
+ var TYPES = /* @__PURE__ */ new Set(["step_up.approved", "step_up.denied", "step_up.expired", "step_up.lapsed"]);
13031
+ function toHeaderRecord(headers) {
13032
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
13033
+ const out2 = {};
13034
+ headers.forEach((v, k) => out2[k.toLowerCase()] = v);
13035
+ return out2;
13036
+ }
13037
+ const out = {};
13038
+ for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
13039
+ return out;
13040
+ }
13041
+ async function handleApprovalEvents(rawBody, headers, secret, opts) {
13042
+ const raw = typeof rawBody === "string" ? rawBody : Buffer.from(rawBody).toString("utf8");
13043
+ const sig = verifyAstraSyncWebhook(raw, toHeaderRecord(headers), secret, {
13044
+ ...opts.toleranceSeconds !== void 0 && { toleranceSeconds: opts.toleranceSeconds },
13045
+ ...opts.nowMs !== void 0 && { nowMs: opts.nowMs }
13046
+ });
13047
+ if (!sig.ok) return { status: 401, body: { ok: false, error: sig.reason } };
13048
+ let event;
13049
+ try {
13050
+ event = JSON.parse(raw);
13051
+ } catch {
13052
+ return { status: 400, body: { ok: false, error: "invalid_json" } };
13053
+ }
13054
+ if (!event || typeof event.eventId !== "string" || typeof event.type !== "string") {
13055
+ return { status: 400, body: { ok: false, error: "invalid_event" } };
13056
+ }
13057
+ if (!TYPES.has(event.type)) {
13058
+ opts.onEvent?.(event, "ignored");
13059
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "ignored" } };
13060
+ }
13061
+ const seen = opts.seen ?? defaultSeen;
13062
+ const has = typeof seen === "function" ? seen : (id) => seen.has(id);
13063
+ try {
13064
+ if (await has(event.eventId)) {
13065
+ opts.onEvent?.(event, "duplicate");
13066
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: "duplicate" } };
13067
+ }
13068
+ const candidates = event.protocol ? opts.kits.filter((k) => k.protocol === event.protocol) : opts.kits;
13069
+ let last = "not_found";
13070
+ let terminal = true;
13071
+ for (const kit of candidates) {
13072
+ const r = await kit.applyApprovalEvent(event);
13073
+ last = r.outcome;
13074
+ terminal = r.terminal;
13075
+ if (r.outcome !== "not_found") break;
13076
+ }
13077
+ opts.onEvent?.(event, last);
13078
+ if (!terminal) {
13079
+ return { status: 503, body: { ok: false, eventId: event.eventId, outcome: last } };
13080
+ }
13081
+ if (typeof seen !== "function") await seen.add(event.eventId);
13082
+ return { status: 200, body: { ok: true, eventId: event.eventId, outcome: last } };
13083
+ } catch (err) {
13084
+ return {
13085
+ status: 503,
13086
+ body: {
13087
+ ok: false,
13088
+ eventId: event.eventId,
13089
+ error: err instanceof Error ? err.message : "transient_error"
13090
+ }
13091
+ };
13092
+ }
13093
+ }
13094
+
12415
13095
  // src/payments/x402.ts
12416
13096
  var CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
12417
13097
  var b64 = (v) => Buffer.from(JSON.stringify(v), "utf8").toString("base64");
@@ -12461,7 +13141,7 @@ function x402PaymentMatchesOffer(payment, offered) {
12461
13141
  }
12462
13142
 
12463
13143
  // src/payments/mpp.ts
12464
- import { createHash as createHash11, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
13144
+ import { createHash as createHash11, createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
12465
13145
  var b64url = (s) => Buffer.from(s, "utf8").toString("base64url");
12466
13146
  function canonical(value) {
12467
13147
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
@@ -12482,7 +13162,7 @@ function binding(c, secret) {
12482
13162
  ];
12483
13163
  if (c.header && c.header.toLowerCase() !== "authorization") slots.push(c.header);
12484
13164
  slots.push(c.opaque ?? "");
12485
- return createHmac2("sha256", secret).update(slots.join("|")).digest("base64url");
13165
+ return createHmac3("sha256", secret).update(slots.join("|")).digest("base64url");
12486
13166
  }
12487
13167
  var quote = (v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
12488
13168
  function buildMppChallenge(input, hmacSecret) {
@@ -12530,7 +13210,7 @@ function checkChallenge(challenge, hmacSecret, now) {
12530
13210
  const { id, ...rest } = challenge;
12531
13211
  const expected = Buffer.from(binding(rest, hmacSecret));
12532
13212
  const got = Buffer.from(String(id ?? ""));
12533
- if (expected.length !== got.length || !timingSafeEqual2(expected, got))
13213
+ if (expected.length !== got.length || !timingSafeEqual3(expected, got))
12534
13214
  return { ok: false, reason: "invalid_id" };
12535
13215
  if (challenge.expires && Date.parse(challenge.expires) < now.getTime())
12536
13216
  return { ok: false, reason: "expired" };
@@ -12546,7 +13226,7 @@ function verifyMppChallenge(challenge, hmacSecret, nowOrOptions = /* @__PURE__ *
12546
13226
  if (opts.body === void 0) return { ok: false, reason: "body_required" };
12547
13227
  const want = Buffer.from(challenge.digest);
12548
13228
  const got = Buffer.from(mppBodyDigest(opts.body));
12549
- if (want.length !== got.length || !timingSafeEqual2(want, got))
13229
+ if (want.length !== got.length || !timingSafeEqual3(want, got))
12550
13230
  return { ok: false, reason: "digest_mismatch" };
12551
13231
  }
12552
13232
  if (opts.store) {
@@ -12852,6 +13532,23 @@ function buildAcpWellKnown(opts) {
12852
13532
  var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
12853
13533
  var apiError = (status, code, message, param) => json3(status, { type: "invalid_request", code, message, ...param && { param } });
12854
13534
  var total2 = (s) => s.totals.find((t) => t.type === "total")?.amount ?? 0;
13535
+ var clone2 = (v) => JSON.parse(JSON.stringify(v));
13536
+ var inFlight2 = (st) => st === "complete_in_progress" || st === "finishing";
13537
+ var ACP_CODES = /* @__PURE__ */ new Set([
13538
+ "missing",
13539
+ "invalid",
13540
+ "out_of_stock",
13541
+ "payment_declined",
13542
+ "requires_sign_in",
13543
+ "requires_3ds"
13544
+ ]);
13545
+ function heldBody2(text) {
13546
+ try {
13547
+ return JSON.parse(text).status === "pending_approval";
13548
+ } catch {
13549
+ return false;
13550
+ }
13551
+ }
12855
13552
  function createAcpCheckoutKit(opts) {
12856
13553
  const mem = /* @__PURE__ */ new Map();
12857
13554
  const store = opts.store ?? {
@@ -12862,7 +13559,14 @@ function createAcpCheckoutKit(opts) {
12862
13559
  if (!s || s.status !== from) return false;
12863
13560
  s.status = to;
12864
13561
  return true;
12865
- }
13562
+ },
13563
+ findByCheckoutSessionId: async (checkoutSessionId) => {
13564
+ for (const s of mem.values()) {
13565
+ if ((s._astraCheckoutSessionId ?? s.id) === checkoutSessionId) return s;
13566
+ }
13567
+ return mem.get(checkoutSessionId) ?? null;
13568
+ },
13569
+ listHeld: async () => [...mem.values()].filter((s) => s.status === "pending_approval")
12866
13570
  };
12867
13571
  const idem = {
12868
13572
  store: opts.idempotencyStore ?? new MemoryIdempotencyStore(),
@@ -12880,16 +13584,19 @@ function createAcpCheckoutKit(opts) {
12880
13584
  );
12881
13585
  }
12882
13586
  const handlers = acpPaymentHandlers(opts.paymentHandlers);
13587
+ const handlerIds = new Set(handlers.flatMap((h) => [h.id, h.name]));
13588
+ const publicOrigin = opts.publicOrigin?.replace(/\/+$/, "");
13589
+ const onReportError = opts.onReportError ?? defaultOnReportError;
12883
13590
  function view(s) {
12884
13591
  return {
12885
13592
  id: s.id,
12886
- status: s.status,
13593
+ status: s.status === "finishing" ? "complete_in_progress" : s.status,
12887
13594
  currency: s.currency.toLowerCase(),
12888
13595
  line_items: s.line_items,
12889
13596
  totals: s.totals,
12890
13597
  fulfillment_options: [],
12891
13598
  messages: s.messages,
12892
- links: [],
13599
+ links: opts.links ?? [],
12893
13600
  capabilities: { payment: { handlers } },
12894
13601
  ...s.continue_url && s.status === "pending_approval" && { continue_url: s.continue_url },
12895
13602
  ...s.order && { order: s.order }
@@ -12899,7 +13606,8 @@ function createAcpCheckoutKit(opts) {
12899
13606
  const lines = [];
12900
13607
  const messages = [];
12901
13608
  for (const [i, it] of items.entries()) {
12902
- const c = it.id ? await opts.catalog(it.id) : null;
13609
+ const q = Math.max(1, Math.floor(it.quantity ?? 1));
13610
+ const c = it.id ? await opts.catalog(it.id, q) : null;
12903
13611
  if (!c) {
12904
13612
  messages.push({
12905
13613
  type: "error",
@@ -12910,7 +13618,6 @@ function createAcpCheckoutKit(opts) {
12910
13618
  });
12911
13619
  continue;
12912
13620
  }
12913
- const q = Math.max(1, Math.floor(it.quantity ?? 1));
12914
13621
  const amount = c.price * q;
12915
13622
  lines.push({
12916
13623
  id: `li_${i + 1}`,
@@ -12934,6 +13641,21 @@ function createAcpCheckoutKit(opts) {
12934
13641
  ]
12935
13642
  };
12936
13643
  }
13644
+ async function validate(s) {
13645
+ if (!opts.validateCart) return { messages: [], blocked: false };
13646
+ const found = await opts.validateCart(s);
13647
+ const messages = found.map((m) => {
13648
+ const error = (m.type ?? "error") === "error";
13649
+ return {
13650
+ type: m.type ?? "error",
13651
+ code: error ? ACP_CODES.has(m.code) ? m.code : "invalid" : m.code,
13652
+ content: m.content,
13653
+ ...error && { resolution: "recoverable" },
13654
+ ...m.path && { param: m.path }
13655
+ };
13656
+ });
13657
+ return { messages, blocked: found.some((m) => (m.type ?? "error") === "error") };
13658
+ }
12937
13659
  async function idempotent(req, body, path, caller, run) {
12938
13660
  const d = await beginIdempotent(idem, {
12939
13661
  method: req.method,
@@ -12949,29 +13671,43 @@ function createAcpCheckoutKit(opts) {
12949
13671
  headers: { ...d.headers, "idempotent-replayed": "true" }
12950
13672
  });
12951
13673
  const res = await run();
12952
- if (d.kind === "proceed")
12953
- await finishIdempotent(idem, d, {
12954
- status: res.status,
12955
- headers: { "content-type": "application/json" },
12956
- body: await res.clone().text()
12957
- });
13674
+ if (d.kind === "proceed") {
13675
+ const text = await res.clone().text();
13676
+ if (heldBody2(text)) await abandonIdempotent(idem, d);
13677
+ else
13678
+ await finishIdempotent(idem, d, {
13679
+ status: res.status,
13680
+ headers: { "content-type": "application/json" },
13681
+ body: text
13682
+ });
13683
+ }
12958
13684
  return res;
12959
13685
  }
12960
13686
  const inProgress = () => apiError(409, "checkout_in_progress", "This checkout session is already being completed.");
12961
- async function reportOutcome(p, settled) {
12962
- if (!opts.reportSettlement || !p.orderId) return;
12963
- await opts.reportSettlement({
12964
- orderId: p.orderId,
12965
- status: settled.status,
12966
- ...p.report,
12967
- ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
12968
- ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
12969
- ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
12970
- }).catch(() => void 0);
13687
+ async function reportOutcome(s, p, settled) {
13688
+ const report = opts.reportSettlement;
13689
+ const ref = p.orderId ? { orderId: p.orderId } : p.sessionId ? { sessionId: p.sessionId } : null;
13690
+ if (!report || !ref) return;
13691
+ await withReportRetries(
13692
+ () => report({
13693
+ ...ref,
13694
+ status: settled.status,
13695
+ ...p.report,
13696
+ ...settled.status === "settled" && settled.processorRef && { processorRef: settled.processorRef },
13697
+ ...settled.status === "settled" && settled.txHash && { txHash: settled.txHash, chainId: settled.chainId },
13698
+ ...settled.status === "failed" && settled.failureCode && { failureCode: settled.failureCode }
13699
+ }),
13700
+ opts.reportRetry,
13701
+ isRetryableSettlementError
13702
+ ).catch(
13703
+ (err) => onReportError(err, { sessionId: s.id, orderId: p.orderId, kind: "settlement_report" })
13704
+ );
12971
13705
  }
12972
13706
  async function finish(s, result, p, settled) {
12973
- await reportOutcome(p, settled);
13707
+ await reportOutcome(s, p, settled);
12974
13708
  delete s._pending;
13709
+ delete s._held;
13710
+ delete s._approvalId;
12975
13711
  if (settled.status === "failed") {
12976
13712
  s.status = "ready_for_payment";
12977
13713
  s.messages = [
@@ -12980,13 +13716,13 @@ function createAcpCheckoutKit(opts) {
12980
13716
  code: "payment_declined",
12981
13717
  content: "The payment was declined.",
12982
13718
  severity: "high",
12983
- resolution: "requires_buyer_input"
13719
+ resolution: "recoverable"
12984
13720
  }
12985
13721
  ];
12986
13722
  await store.put(s);
12987
13723
  return;
12988
13724
  }
12989
- const order = await opts.fulfil({ session: s, result });
13725
+ const order = await opts.fulfil({ session: s, result, settled });
12990
13726
  const permalink = order.permalink_url ?? (opts.permalinkBase && `${opts.permalinkBase.replace(/\/+$/, "")}/${encodeURIComponent(order.id)}`);
12991
13727
  s.order = {
12992
13728
  id: order.id,
@@ -13009,11 +13745,29 @@ function createAcpCheckoutKit(opts) {
13009
13745
  s.messages = [];
13010
13746
  await store.put(s);
13011
13747
  }
13012
- async function complete(req, s, body) {
13748
+ async function complete(req, s, body, agentArtifacts) {
13013
13749
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13014
- if (s.status === "complete_in_progress") return inProgress();
13750
+ if (inFlight2(s.status)) return inProgress();
13015
13751
  if (!body.payment_data)
13016
13752
  return apiError(400, "missing", "payment_data is required.", "$.payment_data");
13753
+ const paymentData = body.payment_data;
13754
+ if (Array.isArray(paymentData) || typeof paymentData !== "object" || typeof paymentData.handler_id !== "string" || !handlerIds.has(paymentData.handler_id)) {
13755
+ return apiError(
13756
+ 400,
13757
+ "invalid",
13758
+ "payment_data must be one object whose handler_id is the id or name of a payment handler this merchant declares.",
13759
+ "$.payment_data.handler_id"
13760
+ );
13761
+ }
13762
+ const v = await validate(s);
13763
+ if (v.blocked) {
13764
+ s.status = "not_ready_for_payment";
13765
+ s.messages = v.messages;
13766
+ delete s._held;
13767
+ delete s._approvalId;
13768
+ await store.put(s);
13769
+ return json3(200, view(s));
13770
+ }
13017
13771
  const from = s.status;
13018
13772
  if (store.claim) {
13019
13773
  if (!await store.claim(s.id, from, "complete_in_progress")) return inProgress();
@@ -13022,17 +13776,31 @@ function createAcpCheckoutKit(opts) {
13022
13776
  s.status = "complete_in_progress";
13023
13777
  await store.put(s);
13024
13778
  }
13779
+ const astraId = req.headers.get("x-astra-id") ?? void 0;
13780
+ const attemptId = req.headers.get("x-astra-attempt-id") ?? s._attemptId;
13025
13781
  try {
13026
13782
  const result = await opts.verifyPurchase({
13027
13783
  session: s,
13028
13784
  checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
13029
- astraId: req.headers.get("x-astra-id") ?? void 0,
13030
- attemptId: req.headers.get("x-astra-attempt-id") ?? void 0,
13031
- valueMajor: total2(s) / 100
13785
+ astraId,
13786
+ attemptId,
13787
+ valueMajor: total2(s) / 100,
13788
+ agentArtifacts,
13789
+ metadata: { kitSessionId: s.id, protocol: "acp" }
13032
13790
  });
13033
13791
  if (isHeld(result)) {
13034
13792
  s.status = "pending_approval";
13035
13793
  s.continue_url = result.stepUpApproval?.approvalUrl ?? opts.fallbackContinueUrl;
13794
+ s._approvalId = result.stepUpApproval?.approvalId;
13795
+ s._astraId = astraId ?? result.agent?.astraId;
13796
+ s._attemptId = attemptId ?? result.attemptId;
13797
+ s._held = {
13798
+ line_items: clone2(s.line_items),
13799
+ totals: clone2(s.totals),
13800
+ ...scrubPaymentInput(paymentData) && { paymentData: scrubPaymentInput(paymentData) },
13801
+ ...result.stepUpApproval?.pollUrl && { pollUrl: result.stepUpApproval.pollUrl },
13802
+ ...result.stepUpApproval?.expiresAt && { expiresAt: result.stepUpApproval.expiresAt }
13803
+ };
13036
13804
  s.messages = [
13037
13805
  {
13038
13806
  type: "error",
@@ -13047,36 +13815,11 @@ function createAcpCheckoutKit(opts) {
13047
13815
  }
13048
13816
  if (!result.identityVerified || !result.policyAllowed) {
13049
13817
  s.status = from;
13050
- s.messages = [
13051
- {
13052
- type: "error",
13053
- code: "intervention_required",
13054
- content: "The agent is not authorized for this purchase.",
13055
- severity: "high",
13056
- resolution: "requires_buyer_review"
13057
- }
13058
- ];
13818
+ s.messages = [notAuthorized()];
13059
13819
  await store.put(s);
13060
13820
  return json3(200, view(s));
13061
13821
  }
13062
- const settled = await opts.settle({
13063
- session: s,
13064
- result,
13065
- paymentData: body.payment_data
13066
- });
13067
- const orderId = result.settlementOutcome?.orderId;
13068
- const pending = {
13069
- ...orderId && { orderId },
13070
- report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
13071
- result: pendingResult(result)
13072
- };
13073
- if (settled.status === "pending") {
13074
- s._pending = pending;
13075
- s.messages = [];
13076
- await store.put(s);
13077
- return json3(200, view(s));
13078
- }
13079
- await finish(s, result, pending, settled);
13822
+ await settleAndFinish(s, result, paymentData);
13080
13823
  return json3(200, view(s));
13081
13824
  } catch (e) {
13082
13825
  if (s.status === "complete_in_progress" && !s._pending) {
@@ -13086,13 +13829,188 @@ function createAcpCheckoutKit(opts) {
13086
13829
  throw e;
13087
13830
  }
13088
13831
  }
13832
+ const notAuthorized = () => ({
13833
+ type: "error",
13834
+ code: "intervention_required",
13835
+ content: "The agent is not authorized for this purchase.",
13836
+ severity: "high",
13837
+ resolution: "requires_buyer_review"
13838
+ });
13839
+ async function settleAndFinish(s, result, paymentData) {
13840
+ const settled = await opts.settle({ session: s, result, paymentData });
13841
+ const orderId = result.settlementOutcome?.orderId;
13842
+ const sessionId = result.sessionId;
13843
+ const pending = {
13844
+ ...orderId && { orderId },
13845
+ ...sessionId && { sessionId },
13846
+ report: orderReportAmount(result, { amountMinor: total2(s), currency: s.currency }),
13847
+ result: pendingResult(result)
13848
+ };
13849
+ if (settled.status === "pending") {
13850
+ s._pending = pending;
13851
+ s.messages = [];
13852
+ delete s._held;
13853
+ delete s._approvalId;
13854
+ await store.put(s);
13855
+ return "pending";
13856
+ }
13857
+ await finish(s, result, pending, settled);
13858
+ return settled.status === "failed" ? "payment_failed" : "completed";
13859
+ }
13089
13860
  async function resolvePending(sessionId, outcome) {
13090
13861
  const s = await store.get(sessionId);
13091
13862
  if (!s) return null;
13092
13863
  if (s.status !== "complete_in_progress" || !s._pending) return s;
13093
- await finish(s, s._pending.result, s._pending, outcome);
13864
+ if (store.claim) {
13865
+ if (!await store.claim(s.id, "complete_in_progress", "finishing")) {
13866
+ return await store.get(sessionId) ?? s;
13867
+ }
13868
+ s.status = "finishing";
13869
+ } else {
13870
+ s.status = "finishing";
13871
+ await store.put(s);
13872
+ }
13873
+ const pending = s._pending;
13874
+ try {
13875
+ await finish(s, pending.result, pending, outcome);
13876
+ } catch (e) {
13877
+ s.status = "complete_in_progress";
13878
+ s._pending = pending;
13879
+ await store.put(s).catch(() => void 0);
13880
+ throw e;
13881
+ }
13094
13882
  return s;
13095
13883
  }
13884
+ async function resumeApproved(sessionId, event) {
13885
+ const s = await store.get(sessionId);
13886
+ if (!s) return resumeResult("not_found", null);
13887
+ if (s.status === "completed") return resumeResult("already_completed", s);
13888
+ if (s.status === "canceled") return resumeResult("canceled", s);
13889
+ if (inFlight2(s.status)) return resumeResult("busy", s);
13890
+ if (s.status !== "pending_approval" || !s._held) return resumeResult("not_held", s);
13891
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
13892
+ return resumeResult("stale", s);
13893
+ if (store.claim) {
13894
+ if (!await store.claim(s.id, "pending_approval", "complete_in_progress"))
13895
+ return resumeResult("busy", await store.get(sessionId) ?? s);
13896
+ s.status = "complete_in_progress";
13897
+ } else {
13898
+ s.status = "complete_in_progress";
13899
+ await store.put(s);
13900
+ }
13901
+ const held = s._held;
13902
+ const backToHeld = async () => {
13903
+ s.status = "pending_approval";
13904
+ s._held = held;
13905
+ await store.put(s).catch(() => void 0);
13906
+ };
13907
+ try {
13908
+ s.line_items = clone2(held.line_items);
13909
+ s.totals = clone2(held.totals);
13910
+ const v = await validate(s);
13911
+ if (v.blocked) {
13912
+ s.status = "not_ready_for_payment";
13913
+ s.messages = v.messages;
13914
+ delete s._held;
13915
+ delete s._approvalId;
13916
+ delete s.continue_url;
13917
+ await store.put(s);
13918
+ return resumeResult("cart_invalid", s);
13919
+ }
13920
+ const result = await opts.verifyPurchase({
13921
+ session: s,
13922
+ checkoutSessionId: s._astraCheckoutSessionId ?? s.id,
13923
+ astraId: s._astraId,
13924
+ attemptId: s._attemptId,
13925
+ valueMajor: total2(s) / 100,
13926
+ metadata: { kitSessionId: s.id, protocol: "acp" },
13927
+ resumed: { ...s._approvalId && { approvalId: s._approvalId } }
13928
+ });
13929
+ if (isHeld(result)) {
13930
+ await backToHeld();
13931
+ return resumeResult("still_held", s);
13932
+ }
13933
+ if (!result.identityVerified || !result.policyAllowed) {
13934
+ s.status = "ready_for_payment";
13935
+ s.messages = [notAuthorized()];
13936
+ delete s._held;
13937
+ delete s._approvalId;
13938
+ delete s.continue_url;
13939
+ await store.put(s);
13940
+ return resumeResult("not_authorized", s);
13941
+ }
13942
+ delete s.continue_url;
13943
+ return resumeResult(await settleAndFinish(s, result, held.paymentData), s);
13944
+ } catch (e) {
13945
+ if (!s._pending && inFlight2(s.status)) await backToHeld();
13946
+ throw e;
13947
+ }
13948
+ }
13949
+ async function resumeDeclined(sessionId, reason = "denied", event) {
13950
+ const s = await store.get(sessionId);
13951
+ if (!s) return resumeResult("not_found", null);
13952
+ if (s.status === "completed") return resumeResult("already_completed", s);
13953
+ if (s.status === "canceled") return resumeResult("canceled", s);
13954
+ if (inFlight2(s.status)) return resumeResult("busy", s);
13955
+ if (s.status !== "pending_approval") return resumeResult("not_held", s);
13956
+ if (event?.approvalId && s._approvalId && event.approvalId !== s._approvalId)
13957
+ return resumeResult("stale", s);
13958
+ if (store.claim && !await store.claim(s.id, "pending_approval", "ready_for_payment")) {
13959
+ return resumeResult("busy", await store.get(sessionId) ?? s);
13960
+ }
13961
+ s.status = "ready_for_payment";
13962
+ delete s._held;
13963
+ delete s._approvalId;
13964
+ delete s.continue_url;
13965
+ s.messages = [
13966
+ {
13967
+ type: "error",
13968
+ code: `approval_${reason}`,
13969
+ 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.",
13970
+ severity: "medium",
13971
+ resolution: "requires_buyer_review"
13972
+ }
13973
+ ];
13974
+ await store.put(s);
13975
+ return resumeResult("declined", s);
13976
+ }
13977
+ async function findForEvent(event) {
13978
+ if (event.kitSessionId) {
13979
+ const s = await store.get(event.kitSessionId);
13980
+ if (s) return s;
13981
+ }
13982
+ if (store.findByCheckoutSessionId)
13983
+ return store.findByCheckoutSessionId(event.checkoutSessionId);
13984
+ return store.get(event.checkoutSessionId);
13985
+ }
13986
+ async function applyApprovalEvent(event) {
13987
+ const s = await findForEvent(event);
13988
+ if (!s) return resumeResult("not_found", null);
13989
+ if (event.type === "step_up.approved") return resumeApproved(s.id, event);
13990
+ return resumeDeclined(s.id, event.type.slice("step_up.".length), event);
13991
+ }
13992
+ async function pollHeldSessions(o = {}) {
13993
+ const sessions = o.sessionIds ? (await Promise.all(o.sessionIds.map((id) => store.get(id)))).filter(
13994
+ (x) => Boolean(x)
13995
+ ) : store.listHeld ? await store.listHeld() : [];
13996
+ const out = [];
13997
+ for (const s of sessions) {
13998
+ if (s.status !== "pending_approval" || !s._held?.pollUrl || !s._approvalId) continue;
13999
+ const status = await pollStepUpStatus({ apiBaseUrl: o.apiBaseUrl ?? "" }, {
14000
+ approvalId: s._approvalId,
14001
+ pollUrl: s._held.pollUrl,
14002
+ expiresAt: s._held.expiresAt ?? ""
14003
+ });
14004
+ if (status === "approved" || status === "consumed") {
14005
+ out.push({ sessionId: s.id, outcome: (await resumeApproved(s.id)).outcome });
14006
+ } else if (status === "denied" || status === "expired") {
14007
+ out.push({ sessionId: s.id, outcome: (await resumeDeclined(s.id, status)).outcome });
14008
+ } else {
14009
+ out.push({ sessionId: s.id, outcome: "pending" });
14010
+ }
14011
+ }
14012
+ return out;
14013
+ }
13096
14014
  async function route(req, reqCtx) {
13097
14015
  const version = req.headers.get("api-version");
13098
14016
  if (!version) return apiError(400, "missing", "The API-Version header is required.");
@@ -13117,6 +14035,7 @@ function createAcpCheckoutKit(opts) {
13117
14035
  const caller = await resolveCallerScope(req, reqCtx, opts.resolveCallerId);
13118
14036
  if (!caller)
13119
14037
  return apiError(401, "unauthorized", "Send an Authorization header (or X-Astra-Id).");
14038
+ const agentArtifacts = buildAgentArtifacts(req, body, { protocol: "acp", publicOrigin });
13120
14039
  if (req.method === "POST" && path === "/checkout_sessions") {
13121
14040
  return idempotent(req, body, path, caller, async () => {
13122
14041
  const items = parsed.line_items ?? parsed.items;
@@ -13135,6 +14054,11 @@ function createAcpCheckoutKit(opts) {
13135
14054
  _astraCheckoutSessionId: req.headers.get("x-astra-checkout-session").slice(0, 64)
13136
14055
  }
13137
14056
  };
14057
+ if (s2.status === "ready_for_payment") {
14058
+ const v = await validate(s2);
14059
+ s2.messages = [...s2.messages, ...v.messages];
14060
+ if (v.blocked) s2.status = "not_ready_for_payment";
14061
+ }
13138
14062
  await store.put(s2);
13139
14063
  return json3(201, view(s2));
13140
14064
  });
@@ -13148,7 +14072,13 @@ function createAcpCheckoutKit(opts) {
13148
14072
  if (!m[2]) {
13149
14073
  return idempotent(req, body, path, caller, async () => {
13150
14074
  if (s.status === "completed" || s.status === "canceled") return json3(200, view(s));
13151
- if (s.status === "complete_in_progress") return inProgress();
14075
+ if (inFlight2(s.status)) return inProgress();
14076
+ if (s.status === "pending_approval")
14077
+ return apiError(
14078
+ 409,
14079
+ "checkout_awaiting_approval",
14080
+ "This session is waiting for the buyer\u2019s approval and can\u2019t be changed. Cancel it to start over."
14081
+ );
13152
14082
  const items = parsed.line_items ?? parsed.items;
13153
14083
  if (items) {
13154
14084
  const p = await price(items);
@@ -13158,17 +14088,22 @@ function createAcpCheckoutKit(opts) {
13158
14088
  messages: p.messages,
13159
14089
  status: p.lines.length && !p.messages.length ? "ready_for_payment" : "not_ready_for_payment"
13160
14090
  });
14091
+ if (s.status === "ready_for_payment") {
14092
+ const v = await validate(s);
14093
+ s.messages = [...s.messages, ...v.messages];
14094
+ if (v.blocked) s.status = "not_ready_for_payment";
14095
+ }
13161
14096
  }
13162
14097
  await store.put(s);
13163
14098
  return json3(200, view(s));
13164
14099
  });
13165
14100
  }
13166
14101
  if (m[2] === "/complete")
13167
- return idempotent(req, body, path, caller, () => complete(req, s, parsed));
14102
+ return idempotent(req, body, path, caller, () => complete(req, s, parsed, agentArtifacts));
13168
14103
  return idempotent(req, body, path, caller, async () => {
13169
14104
  if (s.status === "completed" || s.status === "canceled")
13170
14105
  return apiError(405, "method_not_allowed", `A ${s.status} session can't be canceled.`);
13171
- if (s.status === "complete_in_progress") return inProgress();
14106
+ if (inFlight2(s.status)) return inProgress();
13172
14107
  s.status = "canceled";
13173
14108
  await store.put(s);
13174
14109
  return json3(200, view(s));
@@ -13184,7 +14119,17 @@ function createAcpCheckoutKit(opts) {
13184
14119
  if (requestId) headers.set("request-id", requestId);
13185
14120
  return new Response(res.body, { status: res.status, headers });
13186
14121
  }
13187
- return { handle, resolvePending };
14122
+ return {
14123
+ handle,
14124
+ resolvePending,
14125
+ /** 5.19.0 */
14126
+ protocol: "acp",
14127
+ publicOrigin,
14128
+ resumeApproved,
14129
+ resumeDeclined,
14130
+ applyApprovalEvent,
14131
+ pollHeldSessions
14132
+ };
13188
14133
  }
13189
14134
 
13190
14135
  // src/adapters/kit-express.ts
@@ -13206,11 +14151,46 @@ function rawBodyOf(req) {
13206
14151
  }
13207
14152
  return void 0;
13208
14153
  }
14154
+ var first = (v) => (Array.isArray(v) ? v[0] : v)?.split(",")[0]?.trim() || void 0;
14155
+ function originOf(req, publicOrigin) {
14156
+ if (publicOrigin) return publicOrigin.replace(/\/+$/, "");
14157
+ const trustProxy = Boolean(req.app?.get("trust proxy"));
14158
+ const host = trustProxy && first(req.headers["x-forwarded-host"]) || req.get?.("host") || req.headers.host || "localhost";
14159
+ const proto = trustProxy && first(req.headers["x-forwarded-proto"]) || req.protocol || "http";
14160
+ return `${proto}://${host}`;
14161
+ }
14162
+ var isKit = (k) => typeof k.applyApprovalEvent === "function" && (k.protocol === "ucp" || k.protocol === "acp");
14163
+ async function writeEvents(req, res, secret, kits, seen) {
14164
+ const raw = rawBodyOf(req);
14165
+ const text = raw === void 0 ? "" : typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
14166
+ const out = secret ? await handleApprovalEvents(text, req.headers, secret, { kits, ...seen && { seen } }) : { status: 500, body: { ok: false, error: "webhook_secret_not_configured" } };
14167
+ res.status(out.status);
14168
+ res.setHeader("content-type", "application/json");
14169
+ res.end(JSON.stringify(out.body));
14170
+ }
14171
+ function approvalEventsHandler(options) {
14172
+ return async (req, res, next) => {
14173
+ try {
14174
+ await writeEvents(req, res, options.secret, options.kits, options.seen);
14175
+ } catch (err) {
14176
+ next(err);
14177
+ }
14178
+ };
14179
+ }
13209
14180
  function expressHandler(kit, options = {}) {
13210
14181
  return async (req, res, next) => {
13211
14182
  try {
13212
- const host = req.get?.("host") ?? req.headers.host ?? "localhost";
13213
- const url = `${req.protocol ?? "http"}://${host}${req.originalUrl ?? req.url}`;
14183
+ if (options.approvalEventsPath && req.method === "POST") {
14184
+ const p = options.approvalEventsPath.replace(/\/+$/, "");
14185
+ const full = (req.originalUrl ?? req.url).split("?")[0].replace(/\/+$/, "");
14186
+ const local = req.url.split("?")[0].replace(/\/+$/, "");
14187
+ if (full === p || local === p) {
14188
+ const kits = options.approvalEventKits ?? (isKit(kit) ? [kit] : []);
14189
+ await writeEvents(req, res, options.webhookSecret, kits, options.seenEvents);
14190
+ return;
14191
+ }
14192
+ }
14193
+ const url = `${originOf(req, kit.publicOrigin)}${req.originalUrl ?? req.url}`;
13214
14194
  const headers = new Headers();
13215
14195
  for (const [k, v] of Object.entries(req.headers)) {
13216
14196
  if (v === void 0) continue;
@@ -13260,6 +14240,7 @@ export {
13260
14240
  MemoryCheckoutStore,
13261
14241
  MemoryIdempotencyStore,
13262
14242
  MemoryMppChallengeStore,
14243
+ MemorySeenEvents,
13263
14244
  PLATFORM_AGENT_SIGNATURES,
13264
14245
  RUNTIME_SIGNATURES,
13265
14246
  RedisIdempotencyStore,
@@ -13274,6 +14255,7 @@ export {
13274
14255
  VERIFICATION_STATUS_HEADER,
13275
14256
  SDK_VERSION as VERSION,
13276
14257
  _resetEdgeConfigCache,
14258
+ abandonIdempotent,
13277
14259
  acpPaymentHandlers,
13278
14260
  aepCanonicalJson,
13279
14261
  aepHash,
@@ -13285,6 +14267,7 @@ export {
13285
14267
  aepTimestamp,
13286
14268
  aepVerifySignature,
13287
14269
  agent_exports as agent,
14270
+ approvalEventsHandler,
13288
14271
  astrasyncPaymentHandlers,
13289
14272
  authorizeSettlement,
13290
14273
  awaitStepUpApproval,
@@ -13331,6 +14314,7 @@ export {
13331
14314
  getEdgeConfig,
13332
14315
  getTrustLevel,
13333
14316
  getWellKnownUrls,
14317
+ handleApprovalEvents,
13334
14318
  hasCredentials,
13335
14319
  idempotencyMiddleware,
13336
14320
  isEdgeConfig,