@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.
- package/CHANGELOG.md +178 -0
- package/README.md +168 -4
- package/dist/adapters/express.d.mts +9 -1
- package/dist/adapters/express.d.ts +9 -1
- package/dist/adapters/express.js +96 -37
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +96 -37
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/http-pdlss.d.mts +7 -1
- package/dist/adapters/http-pdlss.d.ts +7 -1
- package/dist/adapters/http-pdlss.js +18 -2
- package/dist/adapters/http-pdlss.js.map +1 -1
- package/dist/adapters/http-pdlss.mjs +16 -1
- package/dist/adapters/http-pdlss.mjs.map +1 -1
- package/dist/adapters/kit-express.d.mts +93 -1
- package/dist/adapters/kit-express.d.ts +93 -1
- package/dist/adapters/kit-express.js +181 -2
- package/dist/adapters/kit-express.js.map +1 -1
- package/dist/adapters/kit-express.mjs +180 -2
- package/dist/adapters/kit-express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +80 -9
- package/dist/adapters/mcp.d.ts +80 -9
- package/dist/adapters/mcp.js +153 -5
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +151 -5
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +1 -1
- package/dist/adapters/nextjs.d.ts +1 -1
- package/dist/adapters/nextjs.js +41 -4
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +41 -4
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +44 -1
- package/dist/adapters/sdk.d.ts +44 -1
- package/dist/adapters/sdk.js +76 -2
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +76 -2
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +1 -1
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +46 -6
- package/dist/bin/astrasync-codex-hook.js +46 -6
- package/dist/bin/astrasync-guard.js +375 -117
- package/dist/bin/astrasync.js +417 -117
- package/dist/browser/background.js +46 -6
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +46 -6
- package/dist/browser/background.mjs.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.js +46 -6
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +46 -6
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +46 -6
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +46 -6
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +1 -1
- package/dist/edge-config.d.ts +1 -1
- package/dist/edge-config.js +1 -1
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs +1 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +1 -1
- package/dist/edge-core/index.d.ts +1 -1
- package/dist/edge-core/index.js +21 -2
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +21 -2
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/gateway/gateway.js +46 -6
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +46 -6
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +1 -1
- package/dist/git-trigger/git-hooks.d.ts +1 -1
- package/dist/index.d.mts +542 -30
- package/dist/index.d.ts +542 -30
- package/dist/index.js +1171 -183
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1157 -173
- package/dist/index.mjs.map +1 -1
- package/dist/registration/index.d.mts +1 -1
- package/dist/registration/index.d.ts +1 -1
- package/dist/registration/index.js +1 -1
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs +1 -1
- package/dist/registration/index.mjs.map +1 -1
- package/dist/transport/index.js +3 -2
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +3 -2
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-3ObBpsnf.d.mts → types-BoGjyon0.d.mts} +81 -2
- package/dist/{types-Cns4l8gX.d.mts → types-DqWPkiYl.d.mts} +90 -12
- package/dist/{types-Cns4l8gX.d.ts → types-DqWPkiYl.d.ts} +90 -12
- package/dist/{types-BoxZEYPS.d.ts → types-eWyZuz-3.d.ts} +81 -2
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +23 -2
- package/dist/verify.d.ts +23 -2
- package/dist/verify.js +23 -4
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +23 -4
- package/dist/verify.mjs.map +1 -1
- package/dist/webhooks.d.mts +16 -3
- package/dist/webhooks.d.ts +16 -3
- package/dist/webhooks.js +24 -13
- package/dist/webhooks.js.map +1 -1
- package/dist/webhooks.mjs +24 -13
- package/dist/webhooks.mjs.map +1 -1
- 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.
|
|
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
|
|
2250
|
-
if (!
|
|
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 ===
|
|
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 = "
|
|
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
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
);
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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
|
-
|
|
2609
|
-
|
|
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
|
-
|
|
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
|
|
4498
|
-
if (
|
|
4499
|
-
const id = coerceString3(
|
|
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
|
|
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
|
|
6067
|
-
if (
|
|
6185
|
+
const first2 = input.x402.paymentRequired.accepts[0];
|
|
6186
|
+
if (first2) {
|
|
6068
6187
|
const v = extractX402TransactionValue({
|
|
6069
|
-
maxAmountRequired: Number(
|
|
6070
|
-
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
|
-
|
|
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 = "
|
|
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
|
|
10194
|
+
const first2 = string.charCodeAt(pos);
|
|
10015
10195
|
let second;
|
|
10016
|
-
if (
|
|
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 (
|
|
10199
|
+
return (first2 - 55296) * 1024 + second - 56320 + 65536;
|
|
10020
10200
|
}
|
|
10021
10201
|
}
|
|
10022
|
-
return
|
|
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
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
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.", "
|
|
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
|
|
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
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
12204
|
-
s.messages = [ucpError(
|
|
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
|
|
12233
|
-
attemptId
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 || !
|
|
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 || !
|
|
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
|
|
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
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
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
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
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: "
|
|
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
|
|
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
|
|
13030
|
-
attemptId
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
13213
|
-
|
|
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,
|