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