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