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