@agent-score/commerce 1.8.0 → 2.0.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/README.md +73 -9
- package/dist/{_response-BMt2y4Or.d.mts → _response-BFYN3b6i.d.mts} +19 -22
- package/dist/{_response-DyJ3mWI3.d.ts → _response-_iPD5AIj.d.ts} +19 -22
- package/dist/challenge/index.d.mts +106 -198
- package/dist/challenge/index.d.ts +106 -198
- package/dist/challenge/index.js +238 -111
- package/dist/challenge/index.js.map +1 -1
- package/dist/challenge/index.mjs +238 -111
- package/dist/challenge/index.mjs.map +1 -1
- package/dist/checkout-BoFwnVsj.d.ts +931 -0
- package/dist/checkout-DRbQ0Fsh.d.mts +931 -0
- package/dist/core.d.mts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +1 -1
- package/dist/core.mjs.map +1 -1
- package/dist/discovery/index.d.mts +453 -51
- package/dist/discovery/index.d.ts +453 -51
- package/dist/discovery/index.js +1092 -58
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/index.mjs +1060 -57
- package/dist/discovery/index.mjs.map +1 -1
- package/dist/identity/express.d.mts +3 -3
- package/dist/identity/express.d.ts +3 -3
- package/dist/identity/express.js +30 -19
- package/dist/identity/express.js.map +1 -1
- package/dist/identity/express.mjs +30 -19
- package/dist/identity/express.mjs.map +1 -1
- package/dist/identity/fastify.d.mts +4 -4
- package/dist/identity/fastify.d.ts +4 -4
- package/dist/identity/fastify.js +30 -19
- package/dist/identity/fastify.js.map +1 -1
- package/dist/identity/fastify.mjs +30 -19
- package/dist/identity/fastify.mjs.map +1 -1
- package/dist/identity/hono.d.mts +3 -3
- package/dist/identity/hono.d.ts +3 -3
- package/dist/identity/hono.js +30 -19
- package/dist/identity/hono.js.map +1 -1
- package/dist/identity/hono.mjs +30 -19
- package/dist/identity/hono.mjs.map +1 -1
- package/dist/identity/nextjs.d.mts +6 -7
- package/dist/identity/nextjs.d.ts +6 -7
- package/dist/identity/nextjs.js +30 -19
- package/dist/identity/nextjs.js.map +1 -1
- package/dist/identity/nextjs.mjs +30 -19
- package/dist/identity/nextjs.mjs.map +1 -1
- package/dist/identity/policy.d.mts +41 -4
- package/dist/identity/policy.d.ts +41 -4
- package/dist/identity/policy.js +3662 -18
- package/dist/identity/policy.js.map +1 -1
- package/dist/identity/policy.mjs +3648 -3
- package/dist/identity/policy.mjs.map +1 -1
- package/dist/identity/web.d.mts +3 -3
- package/dist/identity/web.d.ts +3 -3
- package/dist/identity/web.js +30 -19
- package/dist/identity/web.js.map +1 -1
- package/dist/identity/web.mjs +30 -19
- package/dist/identity/web.mjs.map +1 -1
- package/dist/index.d.mts +72 -329
- package/dist/index.d.ts +72 -329
- package/dist/index.js +3651 -373
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3628 -361
- package/dist/index.mjs.map +1 -1
- package/dist/payment/index.d.mts +257 -266
- package/dist/payment/index.d.ts +257 -266
- package/dist/payment/index.js +586 -149
- package/dist/payment/index.js.map +1 -1
- package/dist/payment/index.mjs +573 -148
- package/dist/payment/index.mjs.map +1 -1
- package/dist/{agent_instructions-DiMSGkdm.d.mts → pricing-CQ9DIFaw.d.ts} +109 -56
- package/dist/{agent_instructions-DiMSGkdm.d.ts → pricing-CxzwyiO6.d.mts} +109 -56
- package/dist/rail_spec-XP0wKgJV.d.mts +132 -0
- package/dist/rail_spec-XP0wKgJV.d.ts +132 -0
- package/dist/{signer-CFVQsWjL.d.mts → signer-3FAit11j.d.mts} +27 -1
- package/dist/{signer-CFVQsWjL.d.ts → signer-3FAit11j.d.ts} +27 -1
- package/dist/solana-Cds87OTu.d.mts +67 -0
- package/dist/solana-Cds87OTu.d.ts +67 -0
- package/dist/stripe-multichain/index.d.mts +56 -67
- package/dist/stripe-multichain/index.d.ts +56 -67
- package/dist/stripe-multichain/index.js +68 -42
- package/dist/stripe-multichain/index.js.map +1 -1
- package/dist/stripe-multichain/index.mjs +68 -41
- package/dist/stripe-multichain/index.mjs.map +1 -1
- package/dist/{wwwauthenticate-CU1eNvMQ.d.mts → wwwauthenticate-D_FMnPgU.d.mts} +9 -10
- package/dist/{wwwauthenticate-CU1eNvMQ.d.ts → wwwauthenticate-D_FMnPgU.d.ts} +9 -10
- package/dist/x402_server-hgQzWQwB.d.mts +81 -0
- package/dist/x402_server-hgQzWQwB.d.ts +81 -0
- package/package.json +13 -9
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,1151 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/identity/ucp.ts
|
|
12
|
+
function ucpSigningKeyFromJWKImpl(jwk) {
|
|
13
|
+
if (!jwk || typeof jwk !== "object") {
|
|
14
|
+
throw new Error(`UCPSigningKey.fromJWK expected a non-null object; got ${typeof jwk}.`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof jwk.kid !== "string" || !jwk.kid) {
|
|
17
|
+
throw new Error("UCPSigningKey.fromJWK: JWK missing required field `kid` (or non-string).");
|
|
18
|
+
}
|
|
19
|
+
if (typeof jwk.kty !== "string" || !jwk.kty) {
|
|
20
|
+
throw new Error("UCPSigningKey.fromJWK: JWK missing required field `kty` (or non-string).");
|
|
21
|
+
}
|
|
22
|
+
if (jwk.kty !== "OKP" && jwk.kty !== "EC" && jwk.kty !== "RSA") {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`UCPSigningKey.fromJWK: kty=${JSON.stringify(jwk.kty)} is not a supported asymmetric key type (expected OKP, EC, or RSA). Symmetric \`oct\` keys are rejected because they cannot publicly verify a JWS in the trust-mode UCP flow.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if ((jwk.kty === "EC" || jwk.kty === "OKP") && (typeof jwk.crv !== "string" || !jwk.crv)) {
|
|
28
|
+
throw new Error(`UCPSigningKey.fromJWK: kty=${jwk.kty} requires a non-empty \`crv\` field (e.g., "P-256" for EC, "Ed25519" for OKP).`);
|
|
29
|
+
}
|
|
30
|
+
return jwk;
|
|
31
|
+
}
|
|
32
|
+
function buildUCPProfile(input) {
|
|
33
|
+
for (const [name, bindings] of Object.entries(input.services ?? {})) {
|
|
34
|
+
for (const binding of bindings) {
|
|
35
|
+
if ((binding.transport === "rest" || binding.transport === "mcp" || binding.transport === "a2a") && (binding.endpoint === void 0 || binding.endpoint === null || binding.endpoint === "")) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`buildUCPProfile: service "${name}" transport=${binding.transport} requires \`endpoint\`. Per UCP spec service.json business_schema, rest/mcp/a2a bindings MUST carry an endpoint URL.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const paymentHandlers = {};
|
|
43
|
+
for (const [name, bindings] of Object.entries(input.payment_handlers ?? {})) {
|
|
44
|
+
paymentHandlers[name] = bindings.map((binding) => {
|
|
45
|
+
if (Array.isArray(binding.available_instruments) && binding.available_instruments.length === 0) {
|
|
46
|
+
const { available_instruments: _drop, ...rest } = binding;
|
|
47
|
+
return rest;
|
|
48
|
+
}
|
|
49
|
+
return binding;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const capabilities = {};
|
|
53
|
+
for (const [name, bindings] of Object.entries(input.capabilities ?? {})) {
|
|
54
|
+
capabilities[name] = [...bindings];
|
|
55
|
+
}
|
|
56
|
+
if (input.agentscore_gate) {
|
|
57
|
+
const gateConfig = { ...input.agentscore_gate };
|
|
58
|
+
const agentscoreBinding = {
|
|
59
|
+
version: AGENTSCORE_CAPABILITY_VERSION,
|
|
60
|
+
spec: input.agentscore_spec_url ?? AGENTSCORE_DEFAULT_SPEC_URL,
|
|
61
|
+
schema: input.agentscore_schema_url ?? AGENTSCORE_DEFAULT_SCHEMA_URL,
|
|
62
|
+
extends: AGENTSCORE_EXTENDS
|
|
63
|
+
};
|
|
64
|
+
if (Object.keys(gateConfig).length > 0) agentscoreBinding.config = gateConfig;
|
|
65
|
+
const existing = capabilities[AGENTSCORE_CAPABILITY_NAME];
|
|
66
|
+
if (existing) existing.push(agentscoreBinding);
|
|
67
|
+
else capabilities[AGENTSCORE_CAPABILITY_NAME] = [agentscoreBinding];
|
|
68
|
+
}
|
|
69
|
+
const ucp = {
|
|
70
|
+
version: input.version ?? DEFAULT_VERSION,
|
|
71
|
+
services: input.services ?? {},
|
|
72
|
+
capabilities,
|
|
73
|
+
payment_handlers: paymentHandlers
|
|
74
|
+
};
|
|
75
|
+
if (input.name !== void 0) ucp.name = input.name;
|
|
76
|
+
if (input.supported_versions !== void 0) ucp.supported_versions = input.supported_versions;
|
|
77
|
+
if (input.ucp_extras) {
|
|
78
|
+
for (const k of Object.keys(input.ucp_extras)) {
|
|
79
|
+
if (RESERVED_UCP_FIELDS.has(k)) {
|
|
80
|
+
throw new Error(`buildUCPProfile: ucp_extras key "${k}" collides with a reserved \`ucp\` field; rejected.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
Object.assign(ucp, input.ucp_extras);
|
|
84
|
+
}
|
|
85
|
+
const profile = {
|
|
86
|
+
ucp,
|
|
87
|
+
signing_keys: input.signing_keys
|
|
88
|
+
};
|
|
89
|
+
if (input.extras) {
|
|
90
|
+
for (const k of Object.keys(input.extras)) {
|
|
91
|
+
if (RESERVED_TOP_LEVEL.has(k)) {
|
|
92
|
+
throw new Error(`buildUCPProfile: extras key "${k}" collides with a reserved profile field; rejected.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
Object.assign(profile, input.extras);
|
|
96
|
+
}
|
|
97
|
+
return profile;
|
|
98
|
+
}
|
|
99
|
+
function ucpNetworkName(caip2OrUcp, fallback) {
|
|
100
|
+
if (caip2OrUcp === void 0) return fallback;
|
|
101
|
+
return CAIP2_TO_UCP_NETWORK[caip2OrUcp] ?? caip2OrUcp;
|
|
102
|
+
}
|
|
103
|
+
function staticRecipient(r) {
|
|
104
|
+
return typeof r === "string" && r.length > 0 ? r : void 0;
|
|
105
|
+
}
|
|
106
|
+
function tempoToNetworkEntry(spec) {
|
|
107
|
+
const entry = {
|
|
108
|
+
network: spec.testnet ? "tempo-testnet" : spec.network ?? "tempo-mainnet",
|
|
109
|
+
chain_id: spec.chainId ?? 4217
|
|
110
|
+
};
|
|
111
|
+
const recipient = staticRecipient(spec.recipient);
|
|
112
|
+
if (recipient !== void 0) entry.recipient = recipient;
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
function solanaMppToNetworkEntry(spec) {
|
|
116
|
+
const entry = {
|
|
117
|
+
network: ucpNetworkName(spec.network, "solana-mainnet-beta")
|
|
118
|
+
};
|
|
119
|
+
const recipient = staticRecipient(spec.recipient);
|
|
120
|
+
if (recipient !== void 0) entry.recipient = recipient;
|
|
121
|
+
return entry;
|
|
122
|
+
}
|
|
123
|
+
function tempoSessionToNetworkEntry(spec) {
|
|
124
|
+
const entry = {
|
|
125
|
+
network: spec.testnet ? "tempo-testnet" : "tempo-mainnet",
|
|
126
|
+
escrow_contract: spec.escrowContract
|
|
127
|
+
};
|
|
128
|
+
const recipient = staticRecipient(spec.recipient);
|
|
129
|
+
if (recipient !== void 0) entry.recipient = recipient;
|
|
130
|
+
return entry;
|
|
131
|
+
}
|
|
132
|
+
function isTempoRailSpec(s) {
|
|
133
|
+
return !("escrowContract" in s) && !("rpcUrl" in s) && !("tokenProgram" in s);
|
|
134
|
+
}
|
|
135
|
+
function isTempoSessionRailSpec(s) {
|
|
136
|
+
return "escrowContract" in s && "store" in s;
|
|
137
|
+
}
|
|
138
|
+
function mppRailToNetworkEntry(spec) {
|
|
139
|
+
if (isTempoSessionRailSpec(spec)) return tempoSessionToNetworkEntry(spec);
|
|
140
|
+
if ("rpcUrl" in spec || "tokenProgram" in spec || (spec.network?.startsWith("solana:") ?? false)) {
|
|
141
|
+
return solanaMppToNetworkEntry(spec);
|
|
142
|
+
}
|
|
143
|
+
if (isTempoRailSpec(spec)) return tempoToNetworkEntry(spec);
|
|
144
|
+
return tempoToNetworkEntry(spec);
|
|
145
|
+
}
|
|
146
|
+
function mppPaymentHandler({
|
|
147
|
+
networks: networks2
|
|
148
|
+
}) {
|
|
149
|
+
return {
|
|
150
|
+
"sh.agentscore.payment.mpp": [{
|
|
151
|
+
id: "mpp",
|
|
152
|
+
version: HANDLER_VERSION,
|
|
153
|
+
spec: `${SPEC_BASE}/mpp`,
|
|
154
|
+
schema: `${SCHEMA_BASE}/mpp.json`,
|
|
155
|
+
config: { networks: networks2.map(mppRailToNetworkEntry) }
|
|
156
|
+
}]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function x402RailToNetworkEntry(spec) {
|
|
160
|
+
const entry = {
|
|
161
|
+
network: ucpNetworkName(spec.network, "base-8453")
|
|
162
|
+
};
|
|
163
|
+
const recipient = staticRecipient(spec.recipient);
|
|
164
|
+
if (recipient !== void 0) entry.recipient = recipient;
|
|
165
|
+
return entry;
|
|
166
|
+
}
|
|
167
|
+
function x402PaymentHandler({
|
|
168
|
+
networks: networks2
|
|
169
|
+
}) {
|
|
170
|
+
return {
|
|
171
|
+
"sh.agentscore.payment.x402": [{
|
|
172
|
+
id: "x402",
|
|
173
|
+
version: HANDLER_VERSION,
|
|
174
|
+
spec: `${SPEC_BASE}/x402`,
|
|
175
|
+
schema: `${SCHEMA_BASE}/x402.json`,
|
|
176
|
+
config: { networks: networks2.map(x402RailToNetworkEntry) }
|
|
177
|
+
}]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function stripeSptPaymentHandler({
|
|
181
|
+
spec
|
|
182
|
+
}) {
|
|
183
|
+
return {
|
|
184
|
+
"sh.agentscore.payment.stripe_spt": [{
|
|
185
|
+
id: "stripe-spt",
|
|
186
|
+
version: HANDLER_VERSION,
|
|
187
|
+
spec: `${SPEC_BASE}/stripe_spt`,
|
|
188
|
+
schema: `${SCHEMA_BASE}/stripe_spt.json`,
|
|
189
|
+
config: { rail: "stripe-spt", profile_id: spec.profileId ?? null }
|
|
190
|
+
}]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
var UCPSigningKey, DEFAULT_VERSION, AGENTSCORE_CAPABILITY_NAME, AGENTSCORE_CAPABILITY_VERSION, AGENTSCORE_DEFAULT_SPEC_URL, AGENTSCORE_DEFAULT_SCHEMA_URL, AGENTSCORE_EXTENDS, RESERVED_TOP_LEVEL, RESERVED_UCP_FIELDS, AGENTSCORE_UCP_CAPABILITY, HANDLER_VERSION, SPEC_BASE, SCHEMA_BASE, CAIP2_TO_UCP_NETWORK;
|
|
194
|
+
var init_ucp = __esm({
|
|
195
|
+
"src/identity/ucp.ts"() {
|
|
196
|
+
"use strict";
|
|
197
|
+
UCPSigningKey = {
|
|
198
|
+
fromJWK: ucpSigningKeyFromJWKImpl
|
|
199
|
+
};
|
|
200
|
+
DEFAULT_VERSION = "2026-04-08";
|
|
201
|
+
AGENTSCORE_CAPABILITY_NAME = "sh.agentscore.identity";
|
|
202
|
+
AGENTSCORE_CAPABILITY_VERSION = "2026-04-08";
|
|
203
|
+
AGENTSCORE_DEFAULT_SPEC_URL = "https://agentscore.sh/specification/identity";
|
|
204
|
+
AGENTSCORE_DEFAULT_SCHEMA_URL = "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json";
|
|
205
|
+
AGENTSCORE_EXTENDS = ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"];
|
|
206
|
+
RESERVED_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
207
|
+
"ucp",
|
|
208
|
+
"signing_keys",
|
|
209
|
+
"signature",
|
|
210
|
+
"__proto__",
|
|
211
|
+
"constructor",
|
|
212
|
+
"prototype"
|
|
213
|
+
]);
|
|
214
|
+
RESERVED_UCP_FIELDS = /* @__PURE__ */ new Set([
|
|
215
|
+
"version",
|
|
216
|
+
"name",
|
|
217
|
+
"services",
|
|
218
|
+
"capabilities",
|
|
219
|
+
"payment_handlers",
|
|
220
|
+
"supported_versions",
|
|
221
|
+
"__proto__",
|
|
222
|
+
"constructor",
|
|
223
|
+
"prototype"
|
|
224
|
+
]);
|
|
225
|
+
AGENTSCORE_UCP_CAPABILITY = AGENTSCORE_CAPABILITY_NAME;
|
|
226
|
+
HANDLER_VERSION = "2026-04-08";
|
|
227
|
+
SPEC_BASE = "https://agentscore.sh/specification/payment-handlers";
|
|
228
|
+
SCHEMA_BASE = "https://agentscore.sh/schemas/payment-handlers";
|
|
229
|
+
CAIP2_TO_UCP_NETWORK = {
|
|
230
|
+
"eip155:8453": "base-8453",
|
|
231
|
+
"eip155:84532": "base-84532",
|
|
232
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": "solana-mainnet-beta",
|
|
233
|
+
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": "solana-devnet"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// src/identity/ucp-jwks.ts
|
|
239
|
+
async function loadJose() {
|
|
240
|
+
try {
|
|
241
|
+
return await import("jose");
|
|
242
|
+
} catch (err) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`UCP signing requires the \`jose\` library, which is an optional peer dependency. ${JOSE_INSTALL_HINT}
|
|
245
|
+
Original error: ${err instanceof Error ? err.message : String(err)}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function canonicalizeProfile(profile) {
|
|
250
|
+
const stripped = { ...profile };
|
|
251
|
+
delete stripped.signature;
|
|
252
|
+
return stableStringify(stripped);
|
|
253
|
+
}
|
|
254
|
+
function stableStringify(value) {
|
|
255
|
+
if (value === void 0) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
"stableStringify: undefined values are not allowed in canonicalized JSON. Object fields with no value must be omitted."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
261
|
+
throw new Error(`stableStringify: ${typeof value} values are not allowed in canonicalized JSON.`);
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === "bigint") {
|
|
264
|
+
throw new Error("stableStringify: BigInt values are not allowed; use a decimal string.");
|
|
265
|
+
}
|
|
266
|
+
if (value instanceof Date) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"stableStringify: Date instances are not allowed; serialize to an ISO string before passing."
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`stableStringify: ${value.constructor.name} values are not allowed; convert to a plain object/array first.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (ArrayBuffer.isView(value)) {
|
|
277
|
+
throw new Error("stableStringify: typed arrays are not allowed; convert to a plain array first.");
|
|
278
|
+
}
|
|
279
|
+
if (typeof value === "number") {
|
|
280
|
+
if (!Number.isFinite(value)) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`UCP profile canonicalization rejects non-finite Number ${value}. Use a decimal string for any value that may be NaN/Infinity.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (!Number.isInteger(value)) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`UCP profile canonicalization rejects non-integer Number ${value}. Use a decimal string (e.g. "9.99") for monetary or fractional fields to preserve cross-language byte-parity.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!Number.isSafeInteger(value)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`stableStringify: integer ${value} exceeds Number.MAX_SAFE_INTEGER. For values >2^53, use a decimal string to preserve cross-language byte parity.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (typeof value === "string") {
|
|
297
|
+
if (value.includes("\u2028") || value.includes("\u2029")) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
"stableStringify: strings containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language byte parity requires neither be present (Node JSON.stringify on older V8 escapes them; Python json.dumps with ensure_ascii=False does not)."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return JSON.stringify(value);
|
|
303
|
+
}
|
|
304
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
305
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
306
|
+
const obj = value;
|
|
307
|
+
const keys = Object.keys(obj).sort((a, b) => {
|
|
308
|
+
const aPoints = [...a].map((c) => c.codePointAt(0));
|
|
309
|
+
const bPoints = [...b].map((c) => c.codePointAt(0));
|
|
310
|
+
const len = Math.min(aPoints.length, bPoints.length);
|
|
311
|
+
for (let i = 0; i < len; i += 1) {
|
|
312
|
+
if (aPoints[i] !== bPoints[i]) return aPoints[i] - bPoints[i];
|
|
313
|
+
}
|
|
314
|
+
return aPoints.length - bPoints.length;
|
|
315
|
+
});
|
|
316
|
+
for (const k of keys) {
|
|
317
|
+
if (k.includes("\u2028") || k.includes("\u2029")) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"stableStringify: object keys containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language byte parity (Node JSON.stringify on older V8 escapes them; Python json.dumps with ensure_ascii=False does not)."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const pairs = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
|
|
324
|
+
return `{${pairs.join(",")}}`;
|
|
325
|
+
}
|
|
326
|
+
async function generateUCPSigningKey(opts) {
|
|
327
|
+
const jose = await loadJose();
|
|
328
|
+
const alg = opts.alg ?? "EdDSA";
|
|
329
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: true });
|
|
330
|
+
const exportedJwk = await jose.exportJWK(publicKey);
|
|
331
|
+
const publicJWK = {
|
|
332
|
+
kid: opts.kid,
|
|
333
|
+
alg,
|
|
334
|
+
use: "sig",
|
|
335
|
+
...exportedJwk
|
|
336
|
+
};
|
|
337
|
+
return { privateKey, publicJWK };
|
|
338
|
+
}
|
|
339
|
+
async function signUCPProfile(profile, {
|
|
340
|
+
signingKey,
|
|
341
|
+
kid,
|
|
342
|
+
alg = "EdDSA"
|
|
343
|
+
}) {
|
|
344
|
+
const jose = await loadJose();
|
|
345
|
+
if (!ALLOWED_ALGS.includes(alg)) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`signUCPProfile: alg ${JSON.stringify(alg)} is not in the supported set [${ALLOWED_ALGS.join(", ")}].`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (typeof kid !== "string" || !kid) {
|
|
351
|
+
throw new Error("signUCPProfile: kid must be a non-empty string.");
|
|
352
|
+
}
|
|
353
|
+
const kids = (profile.signing_keys ?? []).map((k) => k.kid);
|
|
354
|
+
if (!kids.includes(kid)) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`signUCPProfile: kid ${JSON.stringify(kid)} is not present in profile.signing_keys[] (declared kids: ${JSON.stringify(kids)}). Verifiers will not find the key.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const canonicalBody = canonicalizeProfile(profile);
|
|
360
|
+
const payloadBytes = new TextEncoder().encode(canonicalBody);
|
|
361
|
+
const signature = await new jose.CompactSign(payloadBytes).setProtectedHeader({ alg, kid, typ: PROFILE_TYP }).sign(signingKey);
|
|
362
|
+
return { ...profile, signature };
|
|
363
|
+
}
|
|
364
|
+
async function verifyUCPProfile(profile, jwks) {
|
|
365
|
+
if (profile === null || typeof profile !== "object" || Array.isArray(profile)) {
|
|
366
|
+
throw new UCPVerificationError(
|
|
367
|
+
"no_signature",
|
|
368
|
+
`UCP profile must be a JSON object; got ${profile === null ? "null" : Array.isArray(profile) ? "array" : typeof profile}.`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const jose = await loadJose();
|
|
372
|
+
if (!jwks || typeof jwks !== "object" || !Array.isArray(jwks.keys)) {
|
|
373
|
+
throw new UCPVerificationError(
|
|
374
|
+
"malformed_jwks",
|
|
375
|
+
`UCP verifier expected JWKS shape { keys: [...] }; got ${jwks === null ? "null" : typeof jwks === "object" ? "object without keys[] array" : typeof jwks}.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const stripped = { ...profile };
|
|
379
|
+
const sig = stripped.signature;
|
|
380
|
+
delete stripped.signature;
|
|
381
|
+
if (typeof sig !== "string" || !sig) {
|
|
382
|
+
throw new UCPVerificationError(
|
|
383
|
+
"no_signature",
|
|
384
|
+
`UCP profile signature must be a non-empty string; got ${sig === void 0 ? "undefined" : typeof sig}.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
let header;
|
|
388
|
+
try {
|
|
389
|
+
const protectedB64 = sig.split(".")[0];
|
|
390
|
+
if (!protectedB64) throw new Error("JWS protected header segment is empty.");
|
|
391
|
+
const headerJson = new TextDecoder().decode(jose.base64url.decode(protectedB64));
|
|
392
|
+
const parsed = JSON.parse(headerJson);
|
|
393
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
394
|
+
throw new Error("JWS protected header is not a JSON object.");
|
|
395
|
+
}
|
|
396
|
+
header = parsed;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
throw new UCPVerificationError(
|
|
399
|
+
"malformed_jws",
|
|
400
|
+
`JWS protected header is not valid base64url-encoded JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (header.typ !== PROFILE_TYP) {
|
|
404
|
+
throw new UCPVerificationError("wrong_typ", `UCP signature typ must be "${PROFILE_TYP}"; got ${String(header.typ)}.`);
|
|
405
|
+
}
|
|
406
|
+
if (!ALLOWED_ALGS.includes(header.alg)) {
|
|
407
|
+
throw new UCPVerificationError("unsupported_alg", `UCP signing alg must be one of ${ALLOWED_ALGS.join(", ")}; got ${String(header.alg)}.`);
|
|
408
|
+
}
|
|
409
|
+
if (typeof header.kid !== "string" || !header.kid) {
|
|
410
|
+
throw new UCPVerificationError(
|
|
411
|
+
"missing_kid",
|
|
412
|
+
`UCP signature header kid must be a non-empty string; got ${header.kid === void 0 ? "undefined" : typeof header.kid}.`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if ("crit" in header) {
|
|
416
|
+
const crit = header.crit;
|
|
417
|
+
if (!Array.isArray(crit) || crit.length === 0 || !crit.every((c) => typeof c === "string")) {
|
|
418
|
+
throw new UCPVerificationError(
|
|
419
|
+
"malformed_jws",
|
|
420
|
+
`JWS protected header crit must be a non-empty array of strings; got ${JSON.stringify(crit)}.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
throw new UCPVerificationError(
|
|
424
|
+
"unrecognized_critical_header",
|
|
425
|
+
`JWS protected header advertises unrecognized crit headers: ${JSON.stringify(crit)}.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
let signedPayload;
|
|
429
|
+
try {
|
|
430
|
+
const verified = await jose.compactVerify(
|
|
431
|
+
sig,
|
|
432
|
+
async (h) => {
|
|
433
|
+
const kid = h.kid;
|
|
434
|
+
if (typeof kid !== "string" || !kid) {
|
|
435
|
+
throw new UCPVerificationError(
|
|
436
|
+
"missing_kid",
|
|
437
|
+
`UCP signature header kid must be a non-empty string; got ${kid === void 0 ? "undefined" : typeof kid}.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const matches = jwks.keys.filter(
|
|
441
|
+
(k) => k != null && typeof k === "object" && k.kid === kid
|
|
442
|
+
);
|
|
443
|
+
if (matches.length === 0) throw new UCPVerificationError("kid_not_found", `No JWK in JWKS matching kid=${JSON.stringify(kid)}.`);
|
|
444
|
+
if (matches.length > 1) throw new UCPVerificationError("duplicate_kid", `JWKS contains ${matches.length} keys with kid=${JSON.stringify(kid)}; expected exactly one.`);
|
|
445
|
+
const matchedKey = matches[0];
|
|
446
|
+
if (matchedKey.use != null && matchedKey.use !== "sig") {
|
|
447
|
+
throw new UCPVerificationError("unusable_key", `JWK with kid=${kid} has use=${JSON.stringify(matchedKey.use)}; expected "sig".`);
|
|
448
|
+
}
|
|
449
|
+
if (matchedKey.alg != null && matchedKey.alg !== h.alg) {
|
|
450
|
+
throw new UCPVerificationError(
|
|
451
|
+
"unusable_key",
|
|
452
|
+
`JWK alg ${JSON.stringify(matchedKey.alg)} does not match JWS header alg ${JSON.stringify(h.alg)}.`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return jose.importJWK(matches[0], h.alg);
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
signedPayload = verified.payload;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if (err instanceof UCPVerificationError) throw err;
|
|
461
|
+
if (err instanceof Error && err.name === "JOSEAlgNotAllowed") {
|
|
462
|
+
throw new UCPVerificationError("unsupported_alg", `UCP signing alg not allowed: ${err.message}`);
|
|
463
|
+
}
|
|
464
|
+
if (err instanceof Error && err.name === "JWSSignatureVerificationFailed") {
|
|
465
|
+
throw new UCPVerificationError("signature_invalid", `UCP signature verification failed: ${err.message}`);
|
|
466
|
+
}
|
|
467
|
+
if (err instanceof Error && err.name === "JWSInvalid") {
|
|
468
|
+
throw new UCPVerificationError("malformed_jws", `Malformed JWS: ${err.message}`);
|
|
469
|
+
}
|
|
470
|
+
if (err instanceof Error && err.name === "JOSENotSupported") {
|
|
471
|
+
throw new UCPVerificationError("unrecognized_critical_header", `UCP signing rejected unrecognized critical header: ${err.message}`);
|
|
472
|
+
}
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
let canonicalBody;
|
|
476
|
+
try {
|
|
477
|
+
canonicalBody = canonicalizeProfile(stripped);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
throw new UCPVerificationError(
|
|
480
|
+
"body_mismatch",
|
|
481
|
+
`Failed to canonicalize received profile for verification: ${err instanceof Error ? err.message : String(err)}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
const expectedPayload = new TextEncoder().encode(canonicalBody);
|
|
485
|
+
if (!constantTimeEqual(signedPayload, expectedPayload)) {
|
|
486
|
+
throw new UCPVerificationError("body_mismatch", "UCP profile body does not match the signed payload (tampered or non-canonical).");
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
function constantTimeEqual(a, b) {
|
|
491
|
+
if (a.length !== b.length) return false;
|
|
492
|
+
let diff = 0;
|
|
493
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
494
|
+
diff |= a[i] ^ b[i];
|
|
495
|
+
}
|
|
496
|
+
return diff === 0;
|
|
497
|
+
}
|
|
498
|
+
function buildJWKSResponse(keys) {
|
|
499
|
+
return { keys };
|
|
500
|
+
}
|
|
501
|
+
function readEnvTrimmed(name) {
|
|
502
|
+
const raw = process.env[name];
|
|
503
|
+
if (raw === void 0) return void 0;
|
|
504
|
+
const trimmed = raw.trim();
|
|
505
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
506
|
+
}
|
|
507
|
+
function detectAlgFromJwk(jwk) {
|
|
508
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "EdDSA";
|
|
509
|
+
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
function cacheKey(opts) {
|
|
513
|
+
return `${opts.envJwkVar}|${opts.envKidVar}|${opts.envAlgVar}|${opts.defaultKid}|${opts.defaultAlg}`;
|
|
514
|
+
}
|
|
515
|
+
async function buildEnvSigningKey(opts) {
|
|
516
|
+
const kidDefault = readEnvTrimmed(opts.envKidVar) ?? opts.defaultKid;
|
|
517
|
+
const rawAlg = (readEnvTrimmed(opts.envAlgVar) ?? "").toUpperCase();
|
|
518
|
+
const algFallback = rawAlg === "ES256" ? "ES256" : opts.defaultAlg;
|
|
519
|
+
const envJwk = readEnvTrimmed(opts.envJwkVar);
|
|
520
|
+
if (envJwk) {
|
|
521
|
+
let jwkDict;
|
|
522
|
+
try {
|
|
523
|
+
jwkDict = JSON.parse(envJwk);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`${opts.envJwkVar} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (!jwkDict || typeof jwkDict !== "object" || Array.isArray(jwkDict) || Object.keys(jwkDict).length === 0) {
|
|
530
|
+
throw new Error(`${opts.envJwkVar} must be a non-empty JWK object.`);
|
|
531
|
+
}
|
|
532
|
+
const detectedAlg = detectAlgFromJwk(jwkDict);
|
|
533
|
+
if (!detectedAlg) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`${opts.envJwkVar} has unsupported kty/crv (got kty=${String(jwkDict.kty)} crv=${String(jwkDict.crv)}); expected OKP+Ed25519 or EC+P-256.`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const canonicalPrivateJwk = detectedAlg === "EdDSA" ? { kty: jwkDict.kty, crv: jwkDict.crv, x: jwkDict.x, d: jwkDict.d } : { kty: jwkDict.kty, crv: jwkDict.crv, x: jwkDict.x, y: jwkDict.y, d: jwkDict.d };
|
|
539
|
+
const { importJWK } = await import("jose");
|
|
540
|
+
const { createPublicKey } = await import("crypto");
|
|
541
|
+
let privateKey;
|
|
542
|
+
let publicNodeKey;
|
|
543
|
+
try {
|
|
544
|
+
privateKey = await importJWK(
|
|
545
|
+
canonicalPrivateJwk,
|
|
546
|
+
detectedAlg
|
|
547
|
+
);
|
|
548
|
+
publicNodeKey = createPublicKey({ key: canonicalPrivateJwk, format: "jwk" });
|
|
549
|
+
} catch (err) {
|
|
550
|
+
const className = err instanceof Error ? err.constructor.name : typeof err;
|
|
551
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : null;
|
|
552
|
+
const codeSuffix = code ? ` [${code}]` : "";
|
|
553
|
+
throw new Error(
|
|
554
|
+
`${opts.envJwkVar} has malformed key material (${className}${codeSuffix}). Verify the JWK is well-formed and matches the declared kty/crv. Underlying details suppressed to avoid leaking key bytes.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const publicJWK = publicNodeKey.export({ format: "jwk" });
|
|
558
|
+
publicJWK.kid = jwkDict.kid || kidDefault;
|
|
559
|
+
publicJWK.alg = detectedAlg;
|
|
560
|
+
publicJWK.use = "sig";
|
|
561
|
+
return { privateKey, publicJWK };
|
|
562
|
+
}
|
|
563
|
+
return generateUCPSigningKey({ kid: kidDefault, alg: algFallback });
|
|
564
|
+
}
|
|
565
|
+
async function loadUCPSigningKeyFromEnv({
|
|
566
|
+
envJwkVar,
|
|
567
|
+
envKidVar,
|
|
568
|
+
envAlgVar,
|
|
569
|
+
defaultKid,
|
|
570
|
+
defaultAlg
|
|
571
|
+
} = {}) {
|
|
572
|
+
const resolved = {
|
|
573
|
+
...DEFAULT_LOAD_OPTS,
|
|
574
|
+
...envJwkVar !== void 0 && { envJwkVar },
|
|
575
|
+
...envKidVar !== void 0 && { envKidVar },
|
|
576
|
+
...envAlgVar !== void 0 && { envAlgVar },
|
|
577
|
+
...defaultKid !== void 0 && { defaultKid },
|
|
578
|
+
...defaultAlg !== void 0 && { defaultAlg }
|
|
579
|
+
};
|
|
580
|
+
const key = cacheKey(resolved);
|
|
581
|
+
let cached = envLoaderCache.get(key);
|
|
582
|
+
if (cached) return cached;
|
|
583
|
+
cached = buildEnvSigningKey(resolved).catch((err) => {
|
|
584
|
+
envLoaderCache.delete(key);
|
|
585
|
+
throw err;
|
|
586
|
+
});
|
|
587
|
+
envLoaderCache.set(key, cached);
|
|
588
|
+
return cached;
|
|
589
|
+
}
|
|
590
|
+
var JOSE_INSTALL_HINT, ALLOWED_ALGS, PROFILE_TYP, UCPVerificationError, DEFAULT_LOAD_OPTS, envLoaderCache;
|
|
591
|
+
var init_ucp_jwks = __esm({
|
|
592
|
+
"src/identity/ucp-jwks.ts"() {
|
|
593
|
+
"use strict";
|
|
594
|
+
JOSE_INSTALL_HINT = "Install the optional peer dependency: `npm install jose@^6` (or `bun add jose`). Tested against jose v6.x.";
|
|
595
|
+
ALLOWED_ALGS = ["EdDSA", "ES256"];
|
|
596
|
+
PROFILE_TYP = "agentscore-profile+jws";
|
|
597
|
+
UCPVerificationError = class extends Error {
|
|
598
|
+
constructor(code, message) {
|
|
599
|
+
super(message);
|
|
600
|
+
this.code = code;
|
|
601
|
+
this.name = "UCPVerificationError";
|
|
602
|
+
}
|
|
603
|
+
code;
|
|
604
|
+
};
|
|
605
|
+
DEFAULT_LOAD_OPTS = {
|
|
606
|
+
envJwkVar: "UCP_SIGNING_KEY_JWK_PRIVATE",
|
|
607
|
+
envKidVar: "UCP_SIGNING_KEY_KID",
|
|
608
|
+
envAlgVar: "UCP_SIGNING_KEY_ALG",
|
|
609
|
+
defaultKid: "merchant-default",
|
|
610
|
+
defaultAlg: "EdDSA"
|
|
611
|
+
};
|
|
612
|
+
envLoaderCache = /* @__PURE__ */ new Map();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// src/payment/usdc.ts
|
|
617
|
+
var USDC;
|
|
618
|
+
var init_usdc = __esm({
|
|
619
|
+
"src/payment/usdc.ts"() {
|
|
620
|
+
"use strict";
|
|
621
|
+
USDC = {
|
|
622
|
+
base: {
|
|
623
|
+
mainnet: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6 },
|
|
624
|
+
sepolia: { address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", decimals: 6 }
|
|
625
|
+
},
|
|
626
|
+
solana: {
|
|
627
|
+
mainnet: { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6 },
|
|
628
|
+
devnet: { mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", decimals: 6 }
|
|
629
|
+
},
|
|
630
|
+
tempo: {
|
|
631
|
+
mainnet: { address: "0x20C000000000000000000000b9537d11c60E8b50", decimals: 6 },
|
|
632
|
+
testnet: { address: "0x20c0000000000000000000000000000000000000", decimals: 6 }
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// src/payment/wwwauthenticate.ts
|
|
639
|
+
function paymentRequiredHeader({
|
|
640
|
+
x402Version,
|
|
641
|
+
accepts,
|
|
642
|
+
resource
|
|
643
|
+
}) {
|
|
644
|
+
return Buffer.from(JSON.stringify({ x402Version, accepts, ...resource ? { resource } : {} })).toString("base64");
|
|
645
|
+
}
|
|
646
|
+
var init_wwwauthenticate = __esm({
|
|
647
|
+
"src/payment/wwwauthenticate.ts"() {
|
|
648
|
+
"use strict";
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// src/payment/networks.ts
|
|
653
|
+
var networks;
|
|
654
|
+
var init_networks = __esm({
|
|
655
|
+
"src/payment/networks.ts"() {
|
|
656
|
+
"use strict";
|
|
657
|
+
networks = {
|
|
658
|
+
base: {
|
|
659
|
+
mainnet: { caip2: "eip155:8453", chainId: 8453 },
|
|
660
|
+
sepolia: { caip2: "eip155:84532", chainId: 84532 }
|
|
661
|
+
},
|
|
662
|
+
solana: {
|
|
663
|
+
mainnet: { caip2: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" },
|
|
664
|
+
devnet: { caip2: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" }
|
|
665
|
+
},
|
|
666
|
+
tempo: {
|
|
667
|
+
mainnet: { caip2: "eip155:4217", chainId: 4217 },
|
|
668
|
+
testnet: { caip2: "eip155:42431", chainId: 42431 }
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// src/payment/rails.ts
|
|
675
|
+
function lookupRail(name) {
|
|
676
|
+
return rails[name];
|
|
677
|
+
}
|
|
678
|
+
var rails;
|
|
679
|
+
var init_rails = __esm({
|
|
680
|
+
"src/payment/rails.ts"() {
|
|
681
|
+
"use strict";
|
|
682
|
+
init_networks();
|
|
683
|
+
init_usdc();
|
|
684
|
+
rails = {
|
|
685
|
+
"tempo-mainnet": {
|
|
686
|
+
method: "tempo",
|
|
687
|
+
network: networks.tempo.mainnet.caip2,
|
|
688
|
+
chainId: networks.tempo.mainnet.chainId,
|
|
689
|
+
currency: USDC.tempo.mainnet.address,
|
|
690
|
+
decimals: USDC.tempo.mainnet.decimals,
|
|
691
|
+
asset: USDC.tempo.mainnet.address
|
|
692
|
+
},
|
|
693
|
+
"tempo-testnet": {
|
|
694
|
+
method: "tempo",
|
|
695
|
+
network: networks.tempo.testnet.caip2,
|
|
696
|
+
chainId: networks.tempo.testnet.chainId,
|
|
697
|
+
currency: USDC.tempo.testnet.address,
|
|
698
|
+
decimals: USDC.tempo.testnet.decimals,
|
|
699
|
+
asset: USDC.tempo.testnet.address
|
|
700
|
+
},
|
|
701
|
+
"x402-base-mainnet": {
|
|
702
|
+
method: "x402",
|
|
703
|
+
network: networks.base.mainnet.caip2,
|
|
704
|
+
chainId: networks.base.mainnet.chainId,
|
|
705
|
+
currency: USDC.base.mainnet.address,
|
|
706
|
+
decimals: USDC.base.mainnet.decimals,
|
|
707
|
+
asset: USDC.base.mainnet.address
|
|
708
|
+
},
|
|
709
|
+
"x402-base-sepolia": {
|
|
710
|
+
method: "x402",
|
|
711
|
+
network: networks.base.sepolia.caip2,
|
|
712
|
+
chainId: networks.base.sepolia.chainId,
|
|
713
|
+
currency: USDC.base.sepolia.address,
|
|
714
|
+
decimals: USDC.base.sepolia.decimals,
|
|
715
|
+
asset: USDC.base.sepolia.address
|
|
716
|
+
},
|
|
717
|
+
// Upto rails — pay UP TO a max amount (Permit2-based, vs EIP-3009 for exact). Use for
|
|
718
|
+
// variable-cost APIs where the actual cost depends on output (LLM tokens, bandwidth, etc.).
|
|
719
|
+
// Only available on EVM networks; Solana svm doesn't ship an upto scheme yet.
|
|
720
|
+
"x402-base-mainnet-upto": {
|
|
721
|
+
method: "x402-upto",
|
|
722
|
+
network: networks.base.mainnet.caip2,
|
|
723
|
+
chainId: networks.base.mainnet.chainId,
|
|
724
|
+
currency: USDC.base.mainnet.address,
|
|
725
|
+
decimals: USDC.base.mainnet.decimals,
|
|
726
|
+
asset: USDC.base.mainnet.address
|
|
727
|
+
},
|
|
728
|
+
"x402-base-sepolia-upto": {
|
|
729
|
+
method: "x402-upto",
|
|
730
|
+
network: networks.base.sepolia.caip2,
|
|
731
|
+
chainId: networks.base.sepolia.chainId,
|
|
732
|
+
currency: USDC.base.sepolia.address,
|
|
733
|
+
decimals: USDC.base.sepolia.decimals,
|
|
734
|
+
asset: USDC.base.sepolia.address
|
|
735
|
+
},
|
|
736
|
+
"mpp-solana-mainnet": {
|
|
737
|
+
method: "solana",
|
|
738
|
+
network: networks.solana.mainnet.caip2,
|
|
739
|
+
currency: USDC.solana.mainnet.mint,
|
|
740
|
+
decimals: USDC.solana.mainnet.decimals,
|
|
741
|
+
asset: USDC.solana.mainnet.mint
|
|
742
|
+
},
|
|
743
|
+
"mpp-solana-devnet": {
|
|
744
|
+
method: "solana",
|
|
745
|
+
network: networks.solana.devnet.caip2,
|
|
746
|
+
currency: USDC.solana.devnet.mint,
|
|
747
|
+
decimals: USDC.solana.devnet.decimals,
|
|
748
|
+
asset: USDC.solana.devnet.mint
|
|
749
|
+
},
|
|
750
|
+
"stripe-spt": {
|
|
751
|
+
method: "stripe",
|
|
752
|
+
currency: "usd",
|
|
753
|
+
decimals: 2
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// src/payment/directive.ts
|
|
760
|
+
function buildPaymentRequestBlob({
|
|
761
|
+
rail,
|
|
762
|
+
amountUsd,
|
|
763
|
+
currency,
|
|
764
|
+
decimals,
|
|
765
|
+
recipient,
|
|
766
|
+
chainId,
|
|
767
|
+
networkId
|
|
768
|
+
}) {
|
|
769
|
+
const railDef = rail ? lookupRail(rail) : void 0;
|
|
770
|
+
const decimalsResolved = decimals ?? railDef?.decimals ?? 6;
|
|
771
|
+
const currencyResolved = currency ?? railDef?.currency ?? "usd";
|
|
772
|
+
const chainIdResolved = chainId ?? railDef?.chainId;
|
|
773
|
+
const amountNum = typeof amountUsd === "string" ? Number(amountUsd) : amountUsd;
|
|
774
|
+
const amountRaw = BigInt(Math.round(amountNum * 10 ** decimalsResolved)).toString();
|
|
775
|
+
const blob = { amount: amountRaw, currency: currencyResolved, decimals: decimalsResolved };
|
|
776
|
+
if (recipient) blob.recipient = recipient;
|
|
777
|
+
const methodDetails = {};
|
|
778
|
+
if (chainIdResolved !== void 0) methodDetails.chainId = chainIdResolved;
|
|
779
|
+
if (networkId) methodDetails.networkId = networkId;
|
|
780
|
+
if (Object.keys(methodDetails).length > 0) blob.methodDetails = methodDetails;
|
|
781
|
+
return Buffer.from(JSON.stringify(blob)).toString("base64url");
|
|
782
|
+
}
|
|
783
|
+
function paymentDirective({
|
|
784
|
+
rail,
|
|
785
|
+
id,
|
|
786
|
+
realm,
|
|
787
|
+
method,
|
|
788
|
+
intent,
|
|
789
|
+
expires,
|
|
790
|
+
request
|
|
791
|
+
}) {
|
|
792
|
+
const railDef = rail ? lookupRail(rail) : void 0;
|
|
793
|
+
const methodResolved = method ?? railDef?.method ?? "unknown";
|
|
794
|
+
const intentResolved = intent ?? "charge";
|
|
795
|
+
const expiresResolved = expires ?? new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
796
|
+
return `Payment id="${id}", realm="${realm}", method="${methodResolved}", intent="${intentResolved}", expires="${expiresResolved}", request="${request}"`;
|
|
797
|
+
}
|
|
798
|
+
var init_directive = __esm({
|
|
799
|
+
"src/payment/directive.ts"() {
|
|
800
|
+
"use strict";
|
|
801
|
+
init_rails();
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// src/discovery/probe.ts
|
|
806
|
+
var probe_exports = {};
|
|
807
|
+
__export(probe_exports, {
|
|
808
|
+
buildDiscoveryProbeResponse: () => buildDiscoveryProbeResponse,
|
|
809
|
+
isDiscoveryProbeRequest: () => isDiscoveryProbeRequest,
|
|
810
|
+
sampleX402AcceptForNetwork: () => sampleX402AcceptForNetwork
|
|
811
|
+
});
|
|
812
|
+
function sampleX402AcceptForNetwork(caip2, amountAtomic = "1000000") {
|
|
813
|
+
if (caip2 === networks.base.mainnet.caip2) {
|
|
814
|
+
return {
|
|
815
|
+
scheme: "exact",
|
|
816
|
+
network: caip2,
|
|
817
|
+
amount: amountAtomic,
|
|
818
|
+
asset: USDC.base.mainnet.address,
|
|
819
|
+
payTo: ZERO_EVM_PAYTO,
|
|
820
|
+
maxTimeoutSeconds: 300,
|
|
821
|
+
// ``extra.name`` mirrors the on-chain USDC contract's ``name()`` because
|
|
822
|
+
// EIP-712 domain hashes include this string. Wrong name → every signed
|
|
823
|
+
// payload fails facilitator verify with ``invalid_exact_evm_payload_signature``.
|
|
824
|
+
// Base mainnet USDC returns "USD Coin"; base sepolia USDC returns "USDC".
|
|
825
|
+
extra: { name: "USD Coin", version: "2" }
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (caip2 === networks.base.sepolia.caip2) {
|
|
829
|
+
return {
|
|
830
|
+
scheme: "exact",
|
|
831
|
+
network: caip2,
|
|
832
|
+
amount: amountAtomic,
|
|
833
|
+
asset: USDC.base.sepolia.address,
|
|
834
|
+
payTo: ZERO_EVM_PAYTO,
|
|
835
|
+
maxTimeoutSeconds: 300,
|
|
836
|
+
extra: { name: "USDC", version: "2" }
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (caip2 === networks.solana.mainnet.caip2) {
|
|
840
|
+
return {
|
|
841
|
+
scheme: "exact",
|
|
842
|
+
network: caip2,
|
|
843
|
+
amount: amountAtomic,
|
|
844
|
+
asset: USDC.solana.mainnet.mint,
|
|
845
|
+
payTo: ZERO_SOLANA_PAYTO,
|
|
846
|
+
maxTimeoutSeconds: 300
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (caip2 === networks.solana.devnet.caip2) {
|
|
850
|
+
return {
|
|
851
|
+
scheme: "exact",
|
|
852
|
+
network: caip2,
|
|
853
|
+
amount: amountAtomic,
|
|
854
|
+
asset: USDC.solana.devnet.mint,
|
|
855
|
+
payTo: ZERO_SOLANA_PAYTO,
|
|
856
|
+
maxTimeoutSeconds: 300
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
function buildDiscoveryProbeResponse(opts) {
|
|
862
|
+
const probeId = `probe_${Date.now()}`;
|
|
863
|
+
const expires = new Date(Date.now() + (opts.ttlSeconds ?? 300) * 1e3).toISOString();
|
|
864
|
+
const request = buildPaymentRequestBlob({
|
|
865
|
+
rail: opts.sampleRail,
|
|
866
|
+
amountUsd: opts.sampleAmountUsd,
|
|
867
|
+
recipient: opts.sampleRecipient
|
|
868
|
+
});
|
|
869
|
+
const directive = paymentDirective({
|
|
870
|
+
rail: opts.sampleRail,
|
|
871
|
+
id: probeId,
|
|
872
|
+
realm: opts.realm,
|
|
873
|
+
intent: opts.intent,
|
|
874
|
+
expires,
|
|
875
|
+
request
|
|
876
|
+
});
|
|
877
|
+
const bodyObj = {
|
|
878
|
+
error: {
|
|
879
|
+
code: "payment_required",
|
|
880
|
+
message: opts.message ?? "This endpoint requires payment. Send a valid request body to receive a full challenge."
|
|
881
|
+
},
|
|
882
|
+
discovery: true,
|
|
883
|
+
...opts.docsUrl ? { docs: opts.docsUrl } : {}
|
|
884
|
+
};
|
|
885
|
+
const headers = {
|
|
886
|
+
"content-type": "application/json",
|
|
887
|
+
"www-authenticate": directive
|
|
888
|
+
};
|
|
889
|
+
if (opts.x402Sample) {
|
|
890
|
+
const x402Version = opts.x402Sample.version ?? 2;
|
|
891
|
+
const sampleAccepts = opts.x402Sample.accepts ?? (opts.x402Sample.networks ?? []).map((n) => sampleX402AcceptForNetwork(n, opts.x402Sample.amountAtomic ?? "1000000")).filter((e) => e !== null);
|
|
892
|
+
headers["payment-required"] = paymentRequiredHeader({
|
|
893
|
+
x402Version,
|
|
894
|
+
accepts: sampleAccepts,
|
|
895
|
+
...opts.x402Sample.resourceUrl ? { resource: { url: opts.x402Sample.resourceUrl, mimeType: "application/json" } } : {}
|
|
896
|
+
});
|
|
897
|
+
bodyObj.x402Version = x402Version;
|
|
898
|
+
const headerJson = JSON.parse(Buffer.from(headers["payment-required"], "base64").toString("utf-8"));
|
|
899
|
+
bodyObj.accepts = headerJson.accepts;
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
status: 402,
|
|
903
|
+
headers,
|
|
904
|
+
body: JSON.stringify(bodyObj)
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
async function isDiscoveryProbeRequest(req) {
|
|
908
|
+
if (req.method !== "POST") return false;
|
|
909
|
+
const auth = req.headers.get("authorization");
|
|
910
|
+
if (auth?.startsWith("Payment ")) return false;
|
|
911
|
+
const body = await req.clone().text();
|
|
912
|
+
return !body || body === "{}";
|
|
913
|
+
}
|
|
914
|
+
var ZERO_EVM_PAYTO, ZERO_SOLANA_PAYTO;
|
|
915
|
+
var init_probe = __esm({
|
|
916
|
+
"src/discovery/probe.ts"() {
|
|
917
|
+
"use strict";
|
|
918
|
+
init_directive();
|
|
919
|
+
init_networks();
|
|
920
|
+
init_usdc();
|
|
921
|
+
init_wwwauthenticate();
|
|
922
|
+
ZERO_EVM_PAYTO = "0x0000000000000000000000000000000000000000";
|
|
923
|
+
ZERO_SOLANA_PAYTO = "11111111111111111111111111111111";
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// src/discovery/well_known.ts
|
|
928
|
+
var well_known_exports = {};
|
|
929
|
+
__export(well_known_exports, {
|
|
930
|
+
bootstrapUcpSigningKey: () => bootstrapUcpSigningKey,
|
|
931
|
+
buildSignedJwksResponse: () => buildSignedJwksResponse,
|
|
932
|
+
buildSignedUcpResponse: () => buildSignedUcpResponse,
|
|
933
|
+
defaultA2aServices: () => defaultA2aServices,
|
|
934
|
+
signedResponseExpress: () => signedResponseExpress,
|
|
935
|
+
signedResponseFastify: () => signedResponseFastify,
|
|
936
|
+
signedResponseHono: () => signedResponseHono,
|
|
937
|
+
signedResponseNextjs: () => signedResponseNextjs,
|
|
938
|
+
signedResponseWeb: () => signedResponseWeb,
|
|
939
|
+
wellKnownCorsPreflightHeaders: () => wellKnownCorsPreflightHeaders,
|
|
940
|
+
wellKnownPreflightResponse: () => wellKnownPreflightResponse
|
|
941
|
+
});
|
|
942
|
+
function requestId(headers) {
|
|
943
|
+
if (headers === void 0) return void 0;
|
|
944
|
+
if (headers instanceof Headers) return headers.get("x-request-id") ?? void 0;
|
|
945
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
946
|
+
if (k.toLowerCase() === "x-request-id") return v;
|
|
947
|
+
}
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
function attachRequestId(headers, requestHeaders) {
|
|
951
|
+
const rid = requestId(requestHeaders);
|
|
952
|
+
if (rid !== void 0) headers["X-Request-ID"] = rid;
|
|
953
|
+
}
|
|
954
|
+
function isTempoSession(s) {
|
|
955
|
+
return "escrowContract" in s && "store" in s;
|
|
956
|
+
}
|
|
957
|
+
function isStripe(s) {
|
|
958
|
+
return !("recipient" in s);
|
|
959
|
+
}
|
|
960
|
+
function railHasRecipientField(spec) {
|
|
961
|
+
return Object.hasOwn(spec, "recipient");
|
|
962
|
+
}
|
|
963
|
+
function composeHandlers(checkout) {
|
|
964
|
+
const handlers = {};
|
|
965
|
+
const mpp = [];
|
|
966
|
+
const x402 = [];
|
|
967
|
+
const stripe = [];
|
|
968
|
+
for (const spec of Object.values(checkout.rails)) {
|
|
969
|
+
if (isStripe(spec)) {
|
|
970
|
+
stripe.push(spec);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (isTempoSession(spec)) {
|
|
974
|
+
if (railHasRecipientField(spec)) mpp.push(spec);
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const network = spec.network ?? "";
|
|
978
|
+
if (network.startsWith("eip155:") || "mode" in spec) {
|
|
979
|
+
if (railHasRecipientField(spec)) x402.push(spec);
|
|
980
|
+
} else if (network.startsWith("solana:") || "rpcUrl" in spec) {
|
|
981
|
+
if (railHasRecipientField(spec)) mpp.push(spec);
|
|
982
|
+
} else {
|
|
983
|
+
if (railHasRecipientField(spec)) mpp.push(spec);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (mpp.length > 0) Object.assign(handlers, mppPaymentHandler({ networks: mpp }));
|
|
987
|
+
if (x402.length > 0) Object.assign(handlers, x402PaymentHandler({ networks: x402 }));
|
|
988
|
+
for (const spec of stripe) Object.assign(handlers, stripeSptPaymentHandler({ spec }));
|
|
989
|
+
return handlers;
|
|
990
|
+
}
|
|
991
|
+
function misconfiguredResponse(requestHeaders) {
|
|
992
|
+
const body = {
|
|
993
|
+
error: {
|
|
994
|
+
code: "ucp_misconfigured",
|
|
995
|
+
message: "Merchant has no configured payment handlers."
|
|
996
|
+
},
|
|
997
|
+
next_steps: {
|
|
998
|
+
action: "contact_merchant",
|
|
999
|
+
user_message: "This merchant is temporarily unable to accept agent payments."
|
|
1000
|
+
},
|
|
1001
|
+
agent_instructions: {
|
|
1002
|
+
action: "contact_merchant",
|
|
1003
|
+
steps: [
|
|
1004
|
+
"Surface a transient error to the user.",
|
|
1005
|
+
"Retry later; the merchant operator will repair the configuration."
|
|
1006
|
+
],
|
|
1007
|
+
user_message: "Merchant temporarily offline for agent payments."
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const headers = {
|
|
1011
|
+
"Access-Control-Allow-Origin": "*",
|
|
1012
|
+
"Cache-Control": `public, max-age=${UCP_CACHE_SECONDS}`
|
|
1013
|
+
};
|
|
1014
|
+
attachRequestId(headers, requestHeaders);
|
|
1015
|
+
return {
|
|
1016
|
+
body: JSON.stringify(body),
|
|
1017
|
+
mediaType: "application/json",
|
|
1018
|
+
headers,
|
|
1019
|
+
status: 503
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async function buildSignedUcpResponse(opts) {
|
|
1023
|
+
const {
|
|
1024
|
+
checkout,
|
|
1025
|
+
name,
|
|
1026
|
+
wellKnownUcpUrl,
|
|
1027
|
+
services,
|
|
1028
|
+
requestHeaders,
|
|
1029
|
+
signingKid = "merchant-default",
|
|
1030
|
+
agentscoreGate
|
|
1031
|
+
} = opts;
|
|
1032
|
+
const handlers = composeHandlers(checkout);
|
|
1033
|
+
if (Object.keys(handlers).length === 0) {
|
|
1034
|
+
return misconfiguredResponse(requestHeaders);
|
|
1035
|
+
}
|
|
1036
|
+
const key = await loadUCPSigningKeyFromEnv({ defaultKid: signingKid });
|
|
1037
|
+
const signingKeyEntry = UCPSigningKey.fromJWK(key.publicJWK);
|
|
1038
|
+
const profile = buildUCPProfile({
|
|
1039
|
+
name,
|
|
1040
|
+
supported_versions: { "2026-04-08": wellKnownUcpUrl },
|
|
1041
|
+
agentscore_gate: agentscoreGate,
|
|
1042
|
+
services,
|
|
1043
|
+
payment_handlers: handlers,
|
|
1044
|
+
signing_keys: [signingKeyEntry]
|
|
1045
|
+
});
|
|
1046
|
+
const signed = await signUCPProfile(profile, {
|
|
1047
|
+
signingKey: key.privateKey,
|
|
1048
|
+
kid: key.publicJWK.kid,
|
|
1049
|
+
alg: key.publicJWK.alg ?? "EdDSA"
|
|
1050
|
+
});
|
|
1051
|
+
const headers = {
|
|
1052
|
+
"Cache-Control": `public, max-age=${UCP_CACHE_SECONDS}`,
|
|
1053
|
+
"Access-Control-Allow-Origin": "*"
|
|
1054
|
+
};
|
|
1055
|
+
attachRequestId(headers, requestHeaders);
|
|
1056
|
+
return {
|
|
1057
|
+
body: JSON.stringify(signed),
|
|
1058
|
+
mediaType: "application/json",
|
|
1059
|
+
headers,
|
|
1060
|
+
status: 200
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
async function buildSignedJwksResponse(opts) {
|
|
1064
|
+
const { requestHeaders, signingKid = "merchant-default" } = opts ?? {};
|
|
1065
|
+
const key = await loadUCPSigningKeyFromEnv({ defaultKid: signingKid });
|
|
1066
|
+
const jwks = buildJWKSResponse([UCPSigningKey.fromJWK(key.publicJWK)]);
|
|
1067
|
+
const headers = {
|
|
1068
|
+
"Cache-Control": `public, max-age=${JWKS_CACHE_SECONDS}`,
|
|
1069
|
+
"Access-Control-Allow-Origin": "*"
|
|
1070
|
+
};
|
|
1071
|
+
attachRequestId(headers, requestHeaders);
|
|
1072
|
+
return {
|
|
1073
|
+
body: JSON.stringify(jwks),
|
|
1074
|
+
mediaType: "application/jwk-set+json",
|
|
1075
|
+
headers,
|
|
1076
|
+
status: 200
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function wellKnownCorsPreflightHeaders(requestHeaders) {
|
|
1080
|
+
const headers = {
|
|
1081
|
+
"Access-Control-Allow-Origin": "*",
|
|
1082
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
1083
|
+
"Access-Control-Max-Age": "86400",
|
|
1084
|
+
Vary: "Access-Control-Request-Headers"
|
|
1085
|
+
};
|
|
1086
|
+
if (requestHeaders === void 0) return headers;
|
|
1087
|
+
const acrh = requestHeaders instanceof Headers ? requestHeaders.get("access-control-request-headers") : Object.entries(requestHeaders).find(([k]) => k.toLowerCase() === "access-control-request-headers")?.[1];
|
|
1088
|
+
if (acrh) headers["Access-Control-Allow-Headers"] = acrh;
|
|
1089
|
+
return headers;
|
|
1090
|
+
}
|
|
1091
|
+
function wellKnownPreflightResponse(requestHeaders) {
|
|
1092
|
+
return new Response(null, {
|
|
1093
|
+
status: 204,
|
|
1094
|
+
headers: wellKnownCorsPreflightHeaders(requestHeaders)
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
function defaultA2aServices(opts) {
|
|
1098
|
+
return {
|
|
1099
|
+
"dev.ucp.shopping": [
|
|
1100
|
+
{
|
|
1101
|
+
version: "2026-04-08",
|
|
1102
|
+
spec: UCP_SHOPPING_SPEC_2026_04_08,
|
|
1103
|
+
transport: "a2a",
|
|
1104
|
+
endpoint: opts.agentCardUrl
|
|
1105
|
+
}
|
|
1106
|
+
]
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async function bootstrapUcpSigningKey(opts) {
|
|
1110
|
+
const defaultKid = opts?.defaultKid ?? "merchant-default";
|
|
1111
|
+
await loadUCPSigningKeyFromEnv({ defaultKid });
|
|
1112
|
+
}
|
|
1113
|
+
function signedResponseHono(resp) {
|
|
1114
|
+
return new Response(resp.body, {
|
|
1115
|
+
status: resp.status,
|
|
1116
|
+
headers: { ...resp.headers, "Content-Type": resp.mediaType }
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
function signedResponseNextjs(resp) {
|
|
1120
|
+
return signedResponseHono(resp);
|
|
1121
|
+
}
|
|
1122
|
+
function signedResponseWeb(resp) {
|
|
1123
|
+
return signedResponseHono(resp);
|
|
1124
|
+
}
|
|
1125
|
+
function signedResponseExpress(res, resp) {
|
|
1126
|
+
res.status(resp.status);
|
|
1127
|
+
res.set(resp.headers);
|
|
1128
|
+
res.type(resp.mediaType);
|
|
1129
|
+
res.send(resp.body);
|
|
1130
|
+
}
|
|
1131
|
+
function signedResponseFastify(reply, resp) {
|
|
1132
|
+
reply.code(resp.status);
|
|
1133
|
+
for (const [k, v] of Object.entries(resp.headers)) reply.header(k, v);
|
|
1134
|
+
reply.type(resp.mediaType);
|
|
1135
|
+
return reply.send(resp.body);
|
|
1136
|
+
}
|
|
1137
|
+
var UCP_CACHE_SECONDS, JWKS_CACHE_SECONDS, UCP_SHOPPING_SPEC_2026_04_08;
|
|
1138
|
+
var init_well_known = __esm({
|
|
1139
|
+
"src/discovery/well_known.ts"() {
|
|
1140
|
+
"use strict";
|
|
1141
|
+
init_ucp();
|
|
1142
|
+
init_ucp_jwks();
|
|
1143
|
+
UCP_CACHE_SECONDS = 60;
|
|
1144
|
+
JWKS_CACHE_SECONDS = 300;
|
|
1145
|
+
UCP_SHOPPING_SPEC_2026_04_08 = "https://ucp.dev/2026-04-08/specification/overview";
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1
1149
|
// src/core.ts
|
|
2
1150
|
import {
|
|
3
1151
|
AgentScore,
|
|
@@ -23,13 +1171,16 @@ function denialReasonStatus(reason) {
|
|
|
23
1171
|
if (reason.code === "api_error") return 503;
|
|
24
1172
|
return 403;
|
|
25
1173
|
}
|
|
26
|
-
function buildSignerMismatchBody(
|
|
27
|
-
|
|
1174
|
+
function buildSignerMismatchBody({
|
|
1175
|
+
result,
|
|
1176
|
+
userMessage,
|
|
1177
|
+
learnMoreUrl
|
|
1178
|
+
}) {
|
|
28
1179
|
if (result.kind === "pass") return null;
|
|
29
|
-
const
|
|
1180
|
+
const learnMoreUrlResolved = learnMoreUrl ?? "https://docs.agentscore.sh/guides/agent-identity";
|
|
30
1181
|
if (result.kind === "wallet_signer_mismatch") {
|
|
31
1182
|
const linkedWallets = result.linkedWallets ?? [];
|
|
32
|
-
const
|
|
1183
|
+
const userMessageResolved = userMessage ?? (linkedWallets.length > 0 ? `Sign the payment with one of the wallets linked to this operator: ${linkedWallets.join(", ")}. Then retry.` : "Sign the payment with the same wallet you claimed via X-Wallet-Address, or switch to X-Operator-Token for rail-independent identity.");
|
|
33
1184
|
return {
|
|
34
1185
|
error: {
|
|
35
1186
|
code: "wallet_signer_mismatch",
|
|
@@ -42,8 +1193,8 @@ function buildSignerMismatchBody(input) {
|
|
|
42
1193
|
linked_wallets: linkedWallets,
|
|
43
1194
|
next_steps: {
|
|
44
1195
|
action: "regenerate_payment_from_linked_wallet",
|
|
45
|
-
user_message:
|
|
46
|
-
learn_more_url:
|
|
1196
|
+
user_message: userMessageResolved,
|
|
1197
|
+
learn_more_url: learnMoreUrlResolved
|
|
47
1198
|
}
|
|
48
1199
|
};
|
|
49
1200
|
}
|
|
@@ -54,8 +1205,8 @@ function buildSignerMismatchBody(input) {
|
|
|
54
1205
|
},
|
|
55
1206
|
next_steps: {
|
|
56
1207
|
action: "switch_to_operator_token",
|
|
57
|
-
user_message:
|
|
58
|
-
learn_more_url:
|
|
1208
|
+
user_message: userMessage ?? "Drop the X-Wallet-Address header and retry with X-Operator-Token (works on every payment rail).",
|
|
1209
|
+
learn_more_url: learnMoreUrlResolved
|
|
59
1210
|
}
|
|
60
1211
|
};
|
|
61
1212
|
}
|
|
@@ -63,27 +1214,35 @@ function buildContactSupportNextSteps(supportEmail, message) {
|
|
|
63
1214
|
return {
|
|
64
1215
|
action: "contact_support",
|
|
65
1216
|
support_email: supportEmail,
|
|
66
|
-
user_message: message ?? `If you believe this denial is in error, contact support at ${supportEmail} with your
|
|
1217
|
+
user_message: message ?? `If you believe this denial is in error, contact support at ${supportEmail} with the details of your request.`
|
|
67
1218
|
};
|
|
68
1219
|
}
|
|
69
|
-
function verificationAgentInstructions(
|
|
1220
|
+
function verificationAgentInstructions({
|
|
1221
|
+
userAction,
|
|
1222
|
+
retryStep,
|
|
1223
|
+
extraSteps,
|
|
1224
|
+
pollIntervalSeconds = 5,
|
|
1225
|
+
timeoutSeconds = 3600,
|
|
1226
|
+
orderTtl,
|
|
1227
|
+
extra
|
|
1228
|
+
} = {}) {
|
|
70
1229
|
const baseSteps = [
|
|
71
1230
|
"Present the verify_url directly to the user \u2014 it is a complete, ready-to-open URL with the session token already embedded (e.g. https://agentscore.sh/verify?session=sess_...). Do NOT modify or construct the URL yourself.",
|
|
72
|
-
`Immediately begin polling poll_url every ${
|
|
1231
|
+
`Immediately begin polling poll_url every ${pollIntervalSeconds} seconds with header X-Poll-Secret set to poll_secret. The user will complete verification in their browser while you poll in the background.`,
|
|
73
1232
|
"The user visits the URL, signs in, completes identity verification (photo ID + selfie via Stripe Identity), and closes the tab. They do NOT need to copy or paste anything back to you.",
|
|
74
1233
|
'When your poll returns status "verified", extract operator_token from the response. This is a one-time value \u2014 save it immediately. Subsequent polls return status "consumed" without the token.',
|
|
75
|
-
|
|
1234
|
+
retryStep ?? "Retry the original merchant request with header X-Operator-Token set to the operator_token value."
|
|
76
1235
|
];
|
|
77
1236
|
return {
|
|
78
1237
|
action: "poll_for_credential",
|
|
79
|
-
user_action:
|
|
80
|
-
steps:
|
|
81
|
-
poll_interval_seconds:
|
|
1238
|
+
user_action: userAction ?? "The user must visit verify_url to complete identity verification before this request can proceed",
|
|
1239
|
+
steps: extraSteps ? [...baseSteps, ...extraSteps] : baseSteps,
|
|
1240
|
+
poll_interval_seconds: pollIntervalSeconds,
|
|
82
1241
|
poll_secret_header: "X-Poll-Secret",
|
|
83
1242
|
retry_token_header: "X-Operator-Token",
|
|
84
|
-
timeout_seconds:
|
|
85
|
-
...
|
|
86
|
-
...
|
|
1243
|
+
timeout_seconds: timeoutSeconds,
|
|
1244
|
+
...orderTtl ? { order_ttl: orderTtl } : {},
|
|
1245
|
+
...extra ?? {}
|
|
87
1246
|
};
|
|
88
1247
|
}
|
|
89
1248
|
|
|
@@ -204,7 +1363,72 @@ function denialReasonToBody(reason) {
|
|
|
204
1363
|
return body;
|
|
205
1364
|
}
|
|
206
1365
|
|
|
1366
|
+
// src/address.ts
|
|
1367
|
+
var SOLANA_BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
1368
|
+
var isSolanaAddress = (address) => SOLANA_BASE58_RE.test(address) && !address.startsWith("0x");
|
|
1369
|
+
var normalizeAddress = (address) => {
|
|
1370
|
+
if (isSolanaAddress(address)) {
|
|
1371
|
+
return address;
|
|
1372
|
+
}
|
|
1373
|
+
return address.toLowerCase();
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/cache.ts
|
|
1377
|
+
var TTLCache = class {
|
|
1378
|
+
constructor(defaultTtlMs, maxSize = 1e4) {
|
|
1379
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
1380
|
+
this.maxSize = maxSize;
|
|
1381
|
+
}
|
|
1382
|
+
defaultTtlMs;
|
|
1383
|
+
store = /* @__PURE__ */ new Map();
|
|
1384
|
+
maxSize;
|
|
1385
|
+
get(key) {
|
|
1386
|
+
const entry = this.store.get(key);
|
|
1387
|
+
if (!entry) return void 0;
|
|
1388
|
+
if (Date.now() > entry.expiresAt) {
|
|
1389
|
+
this.store.delete(key);
|
|
1390
|
+
return void 0;
|
|
1391
|
+
}
|
|
1392
|
+
return entry.value;
|
|
1393
|
+
}
|
|
1394
|
+
set(key, value, ttlMs) {
|
|
1395
|
+
if (this.store.size >= this.maxSize) {
|
|
1396
|
+
this.sweep();
|
|
1397
|
+
}
|
|
1398
|
+
if (this.store.size >= this.maxSize) {
|
|
1399
|
+
this.evictOldest(this.store.size - this.maxSize + 1);
|
|
1400
|
+
}
|
|
1401
|
+
this.store.set(key, {
|
|
1402
|
+
value,
|
|
1403
|
+
expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs)
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
/** Remove all expired entries. */
|
|
1407
|
+
sweep() {
|
|
1408
|
+
const now = Date.now();
|
|
1409
|
+
for (const [k, v] of this.store) {
|
|
1410
|
+
if (now > v.expiresAt) {
|
|
1411
|
+
this.store.delete(k);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/** Evict the oldest `count` entries by insertion order. */
|
|
1416
|
+
evictOldest(count) {
|
|
1417
|
+
let removed = 0;
|
|
1418
|
+
for (const key of this.store.keys()) {
|
|
1419
|
+
if (removed >= count) break;
|
|
1420
|
+
this.store.delete(key);
|
|
1421
|
+
removed++;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
207
1426
|
// src/core.ts
|
|
1427
|
+
function stripTrailingSlashes(s) {
|
|
1428
|
+
let end = s.length;
|
|
1429
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
1430
|
+
return end === s.length ? s : s.slice(0, end);
|
|
1431
|
+
}
|
|
208
1432
|
var CANONICAL_AGENTSCORE_API = "https://api.agentscore.sh";
|
|
209
1433
|
var WALLET_SIGNER_MISMATCH_INSTRUCTIONS = JSON.stringify({
|
|
210
1434
|
action: "resign_or_switch_to_operator_token",
|
|
@@ -247,6 +1471,317 @@ function buildAgentMemoryHint() {
|
|
|
247
1471
|
persist_in_credential_store: ["operator_token"]
|
|
248
1472
|
};
|
|
249
1473
|
}
|
|
1474
|
+
function createAgentScoreCore(options) {
|
|
1475
|
+
if (!options.apiKey) {
|
|
1476
|
+
throw new Error("AgentScore API key is required. Get one at https://agentscore.sh/sign-up");
|
|
1477
|
+
}
|
|
1478
|
+
const {
|
|
1479
|
+
apiKey,
|
|
1480
|
+
requireKyc,
|
|
1481
|
+
requireSanctionsClear,
|
|
1482
|
+
minAge,
|
|
1483
|
+
blockedJurisdictions,
|
|
1484
|
+
allowedJurisdictions,
|
|
1485
|
+
failOpen = false,
|
|
1486
|
+
cacheSeconds = 300,
|
|
1487
|
+
baseUrl: rawBaseUrl = "https://api.agentscore.sh",
|
|
1488
|
+
chain: gateChain,
|
|
1489
|
+
userAgent,
|
|
1490
|
+
createSessionOnMissing
|
|
1491
|
+
} = options;
|
|
1492
|
+
const baseUrl = stripTrailingSlashes(rawBaseUrl);
|
|
1493
|
+
const agentMemoryHint = buildAgentMemoryHint();
|
|
1494
|
+
const defaultUa = `@agent-score/commerce@${"2.0.0"}`;
|
|
1495
|
+
const userAgentHeader = userAgent ? `${userAgent} (${defaultUa})` : defaultUa;
|
|
1496
|
+
const sdk = new AgentScore({ apiKey, baseUrl, userAgent: userAgentHeader });
|
|
1497
|
+
const sessionSdkCache = /* @__PURE__ */ new Map();
|
|
1498
|
+
function getSessionSdk(sessionApiKey, sessionBaseUrl) {
|
|
1499
|
+
const key = `${sessionApiKey}|${sessionBaseUrl ?? ""}`;
|
|
1500
|
+
let s = sessionSdkCache.get(key);
|
|
1501
|
+
if (!s) {
|
|
1502
|
+
s = new AgentScore({
|
|
1503
|
+
apiKey: sessionApiKey,
|
|
1504
|
+
baseUrl: sessionBaseUrl ?? baseUrl,
|
|
1505
|
+
userAgent: userAgentHeader
|
|
1506
|
+
});
|
|
1507
|
+
sessionSdkCache.set(key, s);
|
|
1508
|
+
}
|
|
1509
|
+
return s;
|
|
1510
|
+
}
|
|
1511
|
+
const cache = new TTLCache(cacheSeconds * 1e3);
|
|
1512
|
+
async function tryMintSessionDenial(ctx) {
|
|
1513
|
+
if (!createSessionOnMissing) return void 0;
|
|
1514
|
+
try {
|
|
1515
|
+
const sessionBody = {};
|
|
1516
|
+
if (createSessionOnMissing.context != null) sessionBody.context = createSessionOnMissing.context;
|
|
1517
|
+
if (createSessionOnMissing.productName != null) sessionBody.product_name = createSessionOnMissing.productName;
|
|
1518
|
+
if (createSessionOnMissing.getSessionOptions && ctx !== void 0) {
|
|
1519
|
+
try {
|
|
1520
|
+
const dynamic = await createSessionOnMissing.getSessionOptions(ctx);
|
|
1521
|
+
if (dynamic?.context != null) sessionBody.context = dynamic.context;
|
|
1522
|
+
if (dynamic?.productName != null) sessionBody.product_name = dynamic.productName;
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
console.warn("[gate] createSessionOnMissing.getSessionOptions hook failed:", err instanceof Error ? err.message : err);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const sessionSdk = getSessionSdk(createSessionOnMissing.apiKey, createSessionOnMissing.baseUrl);
|
|
1528
|
+
const data = await sessionSdk.createSession({
|
|
1529
|
+
...sessionBody.context !== void 0 ? { context: sessionBody.context } : {},
|
|
1530
|
+
...sessionBody.product_name !== void 0 ? { product_name: sessionBody.product_name } : {}
|
|
1531
|
+
});
|
|
1532
|
+
if (typeof data.session_id !== "string" || typeof data.poll_secret !== "string" || typeof data.verify_url !== "string") {
|
|
1533
|
+
console.warn("[gate] /v1/sessions returned 200 without required fields \u2014 falling back to bare denial");
|
|
1534
|
+
return void 0;
|
|
1535
|
+
}
|
|
1536
|
+
let extra;
|
|
1537
|
+
if (createSessionOnMissing.onBeforeSession && ctx !== void 0) {
|
|
1538
|
+
try {
|
|
1539
|
+
const sessionMeta = {
|
|
1540
|
+
session_id: data.session_id,
|
|
1541
|
+
verify_url: data.verify_url,
|
|
1542
|
+
poll_secret: data.poll_secret,
|
|
1543
|
+
poll_url: data.poll_url,
|
|
1544
|
+
expires_at: data.expires_at
|
|
1545
|
+
};
|
|
1546
|
+
const result = await createSessionOnMissing.onBeforeSession(ctx, sessionMeta);
|
|
1547
|
+
if (result && typeof result === "object") extra = result;
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
console.warn("[gate] createSessionOnMissing.onBeforeSession hook failed:", err instanceof Error ? err.message : err);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
const apiNextSteps = data.next_steps;
|
|
1553
|
+
return {
|
|
1554
|
+
code: "identity_verification_required",
|
|
1555
|
+
verify_url: data.verify_url,
|
|
1556
|
+
session_id: data.session_id,
|
|
1557
|
+
poll_secret: data.poll_secret,
|
|
1558
|
+
poll_url: data.poll_url,
|
|
1559
|
+
agent_instructions: apiNextSteps ? JSON.stringify(apiNextSteps) : void 0,
|
|
1560
|
+
agent_memory: agentMemoryHint,
|
|
1561
|
+
...extra && { extra }
|
|
1562
|
+
};
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
console.warn("[gate] createSessionOnMissing path failed \u2014 falling back to bare denial:", err instanceof Error ? err.message : err);
|
|
1565
|
+
return void 0;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function evaluate(identity, ctx, signer) {
|
|
1569
|
+
if (!identity || !identity.address && !identity.operatorToken) {
|
|
1570
|
+
if (failOpen) return { kind: "allow" };
|
|
1571
|
+
const sessionReason = await tryMintSessionDenial(ctx);
|
|
1572
|
+
if (sessionReason) return { kind: "deny", reason: sessionReason };
|
|
1573
|
+
const missingIdentityInstructions = JSON.stringify({
|
|
1574
|
+
action: "probe_identity_then_session",
|
|
1575
|
+
steps: [
|
|
1576
|
+
"If you have a wallet AND this merchant accepts a wallet-signing rail (Tempo MPP or x402 \u2014 see openapi.json x-payment-info): send X-Wallet-Address and retry. If the wallet is already linked to an AgentScore operator (via prior SIWE claim or prior captureWallet), this passes in one round trip. If the wallet is unlinked or the account has no KYC, the 403 will include a verify_url \u2014 share it with the user to claim the wallet + complete KYC, then retry.",
|
|
1577
|
+
"If step 1 is denied or you already have a stored operator_token (valid + not expired): send X-Operator-Token: opc_... and retry.",
|
|
1578
|
+
"If neither applies: retry with NO identity header. Merchants that auto-create verification sessions (most AgentScore merchants do) return verify_url + session_id + poll_secret in the 403 body \u2014 share verify_url with the user, then poll poll_url every 5s with the X-Poll-Secret header until status=verified (the poll returns a one-time operator_token). If the retry returns the same bare 403, this merchant does not support self-service session bootstrapping \u2014 direct the user to https://agentscore.sh/sign-up to create an AgentScore identity and mint an operator_token from their dashboard (https://agentscore.sh/dashboard/verify). The user hands the opc_... to you, and you retry with X-Operator-Token."
|
|
1579
|
+
],
|
|
1580
|
+
user_message: "Try X-Wallet-Address first if you have a wallet and the merchant accepts Tempo/x402; fall back to a stored X-Operator-Token, then to the session/verify flow described in agent_memory.bootstrap."
|
|
1581
|
+
});
|
|
1582
|
+
return {
|
|
1583
|
+
kind: "deny",
|
|
1584
|
+
reason: {
|
|
1585
|
+
code: "missing_identity",
|
|
1586
|
+
agent_instructions: missingIdentityInstructions,
|
|
1587
|
+
agent_memory: agentMemoryHint
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const cacheKey2 = identity.operatorToken?.toLowerCase() ?? (identity.address ? normalizeAddress(identity.address) : "");
|
|
1592
|
+
const cached = cache.get(cacheKey2);
|
|
1593
|
+
if (cached) {
|
|
1594
|
+
if (cached.allow) {
|
|
1595
|
+
const cachedRaw = cached.raw;
|
|
1596
|
+
const cachedQuota = cachedRaw?.quota;
|
|
1597
|
+
return {
|
|
1598
|
+
kind: "allow",
|
|
1599
|
+
data: cachedRaw,
|
|
1600
|
+
...cachedQuota !== void 0 && { quota: cachedQuota }
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
if (isFixableDenial(cached.reasons)) {
|
|
1604
|
+
const sessionReason = await tryMintSessionDenial(ctx);
|
|
1605
|
+
if (sessionReason) return { kind: "deny", reason: sessionReason };
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
kind: "deny",
|
|
1609
|
+
reason: {
|
|
1610
|
+
code: "wallet_not_trusted",
|
|
1611
|
+
decision: cached.decision,
|
|
1612
|
+
reasons: cached.reasons,
|
|
1613
|
+
verify_url: cached.raw?.verify_url,
|
|
1614
|
+
data: cached.raw
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
const policy = {};
|
|
1619
|
+
if (requireKyc != null) policy.require_kyc = requireKyc;
|
|
1620
|
+
if (requireSanctionsClear != null) policy.require_sanctions_clear = requireSanctionsClear;
|
|
1621
|
+
if (minAge != null) policy.min_age = minAge;
|
|
1622
|
+
if (blockedJurisdictions != null) policy.blocked_jurisdictions = blockedJurisdictions;
|
|
1623
|
+
if (allowedJurisdictions != null) policy.allowed_jurisdictions = allowedJurisdictions;
|
|
1624
|
+
let data;
|
|
1625
|
+
try {
|
|
1626
|
+
const opts = {
|
|
1627
|
+
chain: gateChain,
|
|
1628
|
+
...Object.keys(policy).length > 0 ? { policy } : {},
|
|
1629
|
+
// Pre-extracted payment signer (by the adapter middleware). When present, the API
|
|
1630
|
+
// composes BOTH signer_match (wallet-binding) and signer_sanctions (OFAC SDN wallet
|
|
1631
|
+
// check) verdicts on the response in one round trip. Under
|
|
1632
|
+
// policy.require_sanctions_clear, a signer_sanctions hit flips decision -> deny inline.
|
|
1633
|
+
...signer && { signer: { address: signer.address, network: signer.network } }
|
|
1634
|
+
};
|
|
1635
|
+
const result = identity.address ? await sdk.assess(identity.address, { ...opts, operatorToken: identity.operatorToken }) : await sdk.assess(null, { ...opts, operatorToken: identity.operatorToken });
|
|
1636
|
+
data = result;
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
if (err instanceof PaymentRequiredError) {
|
|
1639
|
+
if (failOpen) return { kind: "allow" };
|
|
1640
|
+
return { kind: "deny", reason: { code: "payment_required" } };
|
|
1641
|
+
}
|
|
1642
|
+
if (err instanceof TokenExpiredError) {
|
|
1643
|
+
return {
|
|
1644
|
+
kind: "deny",
|
|
1645
|
+
reason: {
|
|
1646
|
+
code: "token_expired",
|
|
1647
|
+
data: err.details,
|
|
1648
|
+
...err.verifyUrl ? { verify_url: err.verifyUrl } : {},
|
|
1649
|
+
...err.sessionId ? { session_id: err.sessionId } : {},
|
|
1650
|
+
...err.pollSecret ? { poll_secret: err.pollSecret } : {},
|
|
1651
|
+
...err.pollUrl ? { poll_url: err.pollUrl } : {},
|
|
1652
|
+
...err.nextSteps ? { agent_instructions: JSON.stringify(err.nextSteps) } : {},
|
|
1653
|
+
...err.agentMemory ? { agent_memory: err.agentMemory } : {}
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
if (err instanceof InvalidCredentialError) {
|
|
1658
|
+
return {
|
|
1659
|
+
kind: "deny",
|
|
1660
|
+
reason: {
|
|
1661
|
+
code: "invalid_credential",
|
|
1662
|
+
agent_instructions: INVALID_CREDENTIAL_INSTRUCTIONS,
|
|
1663
|
+
agent_memory: agentMemoryHint
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
if (err instanceof QuotaExceededError) {
|
|
1668
|
+
console.warn("[gate] /v1/assess returned 429 quota_exceeded");
|
|
1669
|
+
if (failOpen) return { kind: "allow", degraded: true, infraReason: "quota_exceeded" };
|
|
1670
|
+
return {
|
|
1671
|
+
kind: "deny",
|
|
1672
|
+
reason: { code: "api_error", agent_instructions: QUOTA_EXCEEDED_INSTRUCTIONS }
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
if (err instanceof SdkTimeoutError) {
|
|
1676
|
+
console.warn("[gate] /v1/assess timed out:", err.message);
|
|
1677
|
+
if (failOpen) return { kind: "allow", degraded: true, infraReason: "network_timeout" };
|
|
1678
|
+
return { kind: "deny", reason: { code: "api_error" } };
|
|
1679
|
+
}
|
|
1680
|
+
const status = err?.status;
|
|
1681
|
+
const errName = err instanceof Error ? err.name : "";
|
|
1682
|
+
if (status === 429) {
|
|
1683
|
+
console.warn("[gate] /v1/assess returned 429 (untyped \u2014 defensive)");
|
|
1684
|
+
if (failOpen) return { kind: "allow", degraded: true, infraReason: "quota_exceeded" };
|
|
1685
|
+
return {
|
|
1686
|
+
kind: "deny",
|
|
1687
|
+
reason: { code: "api_error", agent_instructions: QUOTA_EXCEEDED_INSTRUCTIONS }
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
if (errName === "TimeoutError" || errName === "AbortError") {
|
|
1691
|
+
console.warn("[gate] /v1/assess timed out (by Error.name):", err instanceof Error ? err.message : err);
|
|
1692
|
+
if (failOpen) return { kind: "allow", degraded: true, infraReason: "network_timeout" };
|
|
1693
|
+
return { kind: "deny", reason: { code: "api_error" } };
|
|
1694
|
+
}
|
|
1695
|
+
const errCode = err?.code;
|
|
1696
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1697
|
+
const detail = errCode ? `${errCode}: ${msg}` : msg;
|
|
1698
|
+
console.warn(`[gate] /v1/assess call failed \u2014 surfacing as api_error: ${detail}`);
|
|
1699
|
+
if (failOpen) return { kind: "allow", degraded: true, infraReason: "api_error" };
|
|
1700
|
+
return { kind: "deny", reason: { code: "api_error" } };
|
|
1701
|
+
}
|
|
1702
|
+
const decision = data.decision;
|
|
1703
|
+
const decisionReasons = data.decision_reasons ?? [];
|
|
1704
|
+
const allow = decision === "allow" || decision == null;
|
|
1705
|
+
cache.set(cacheKey2, { allow, decision: decision ?? void 0, reasons: decisionReasons, raw: data });
|
|
1706
|
+
if (allow) {
|
|
1707
|
+
const quota = data.quota;
|
|
1708
|
+
return {
|
|
1709
|
+
kind: "allow",
|
|
1710
|
+
data,
|
|
1711
|
+
...quota !== void 0 && { quota }
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
if (isFixableDenial(decisionReasons)) {
|
|
1715
|
+
const sessionReason = await tryMintSessionDenial(ctx);
|
|
1716
|
+
if (sessionReason) return { kind: "deny", reason: sessionReason };
|
|
1717
|
+
}
|
|
1718
|
+
return {
|
|
1719
|
+
kind: "deny",
|
|
1720
|
+
reason: {
|
|
1721
|
+
code: "wallet_not_trusted",
|
|
1722
|
+
decision: decision ?? void 0,
|
|
1723
|
+
reasons: decisionReasons,
|
|
1724
|
+
verify_url: data.verify_url,
|
|
1725
|
+
data
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
async function captureWallet(options2) {
|
|
1730
|
+
try {
|
|
1731
|
+
await sdk.associateWallet({
|
|
1732
|
+
operatorToken: options2.operatorToken,
|
|
1733
|
+
walletAddress: options2.walletAddress,
|
|
1734
|
+
network: options2.network,
|
|
1735
|
+
...options2.idempotencyKey ? { idempotencyKey: options2.idempotencyKey } : {}
|
|
1736
|
+
});
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
console.warn("[agentscore-commerce] captureWallet failed:", err instanceof Error ? err.message : err);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function projectSignerMatch(sm, claimedNorm, signerNorm) {
|
|
1742
|
+
const kind = sm.kind;
|
|
1743
|
+
if (kind === "pass") {
|
|
1744
|
+
return {
|
|
1745
|
+
kind: "pass",
|
|
1746
|
+
claimedOperator: sm.claimed_operator ?? null,
|
|
1747
|
+
signerOperator: sm.signer_operator ?? null
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (kind === "wallet_auth_requires_wallet_signing") {
|
|
1751
|
+
return {
|
|
1752
|
+
kind: "wallet_auth_requires_wallet_signing",
|
|
1753
|
+
claimedWallet: sm.claimed_wallet ?? claimedNorm,
|
|
1754
|
+
agentInstructions: sm.agent_instructions ?? WALLET_AUTH_REQUIRES_WALLET_SIGNING_INSTRUCTIONS
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
const linked = sm.linked_wallets;
|
|
1758
|
+
return {
|
|
1759
|
+
kind: "wallet_signer_mismatch",
|
|
1760
|
+
claimedOperator: sm.claimed_operator ?? null,
|
|
1761
|
+
actualSignerOperator: sm.signer_operator ?? null,
|
|
1762
|
+
expectedSigner: sm.expected_signer ?? claimedNorm,
|
|
1763
|
+
actualSigner: sm.actual_signer ?? signerNorm,
|
|
1764
|
+
linkedWallets: Array.isArray(linked) ? linked.filter((w) => typeof w === "string") : [],
|
|
1765
|
+
agentInstructions: sm.agent_instructions ?? WALLET_SIGNER_MISMATCH_INSTRUCTIONS
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
function getSignerVerdict(claimedAddress) {
|
|
1769
|
+
const claimedNorm = normalizeAddress(claimedAddress);
|
|
1770
|
+
const cached = cache.get(claimedNorm);
|
|
1771
|
+
if (!cached) return void 0;
|
|
1772
|
+
const raw = cached.raw;
|
|
1773
|
+
if (!raw) return void 0;
|
|
1774
|
+
const rawMatch = raw.signer_match;
|
|
1775
|
+
const rawSanctions = raw.signer_sanctions;
|
|
1776
|
+
if (!rawMatch && !rawSanctions) return void 0;
|
|
1777
|
+
const signerNorm = rawMatch?.actual_signer ?? claimedNorm;
|
|
1778
|
+
return {
|
|
1779
|
+
signer_match: rawMatch ? projectSignerMatch(rawMatch, claimedNorm, signerNorm) : null,
|
|
1780
|
+
signer_sanctions: rawSanctions ?? null
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
return { evaluate, captureWallet, getSignerVerdict };
|
|
1784
|
+
}
|
|
250
1785
|
|
|
251
1786
|
// src/signer.ts
|
|
252
1787
|
var TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
@@ -321,9 +1856,33 @@ async function extractPaymentSigner(request, x402PaymentHeader) {
|
|
|
321
1856
|
}
|
|
322
1857
|
return null;
|
|
323
1858
|
}
|
|
1859
|
+
async function extractPaymentSignerFromAuth(authHeader, x402PaymentHeader) {
|
|
1860
|
+
const request = new Request("http://internal.gate/", {
|
|
1861
|
+
headers: authHeader ? { authorization: authHeader } : {}
|
|
1862
|
+
});
|
|
1863
|
+
return extractPaymentSigner(request, x402PaymentHeader);
|
|
1864
|
+
}
|
|
324
1865
|
function readX402PaymentHeader(request) {
|
|
325
1866
|
return request.headers.get("payment-signature") ?? request.headers.get("x-payment") ?? void 0;
|
|
326
1867
|
}
|
|
1868
|
+
function lowerHeaders(headers) {
|
|
1869
|
+
const out = {};
|
|
1870
|
+
for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1873
|
+
async function extractSignerForPrecheck(headers) {
|
|
1874
|
+
const lower = lowerHeaders(headers);
|
|
1875
|
+
const x402 = lower["payment-signature"] ?? lower["x-payment"];
|
|
1876
|
+
if (x402) {
|
|
1877
|
+
const signer = await extractPaymentSignerFromAuth(void 0, x402);
|
|
1878
|
+
if (signer !== null) return signer;
|
|
1879
|
+
}
|
|
1880
|
+
const authorization = lower["authorization"];
|
|
1881
|
+
if (authorization && authorization.toLowerCase().startsWith("payment ")) {
|
|
1882
|
+
return await extractPaymentSignerFromAuth(authorization);
|
|
1883
|
+
}
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
327
1886
|
|
|
328
1887
|
// src/identity/a2a.ts
|
|
329
1888
|
var PROTOCOL_VERSION = "1.0";
|
|
@@ -379,434 +1938,2062 @@ function buildA2AAgentCard(input) {
|
|
|
379
1938
|
return card;
|
|
380
1939
|
}
|
|
381
1940
|
|
|
382
|
-
// src/
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1941
|
+
// src/index.ts
|
|
1942
|
+
init_ucp();
|
|
1943
|
+
init_ucp_jwks();
|
|
1944
|
+
|
|
1945
|
+
// src/checkout.ts
|
|
1946
|
+
import { randomUUID } from "crypto";
|
|
1947
|
+
|
|
1948
|
+
// src/payment/rail_spec.ts
|
|
1949
|
+
init_usdc();
|
|
1950
|
+
async function resolveRecipient(r) {
|
|
1951
|
+
if (typeof r === "string") return r;
|
|
1952
|
+
return Promise.resolve(r());
|
|
1953
|
+
}
|
|
1954
|
+
var RAIL_SPEC_DEFAULTS = {
|
|
1955
|
+
tempo: {
|
|
1956
|
+
network: "tempo-mainnet",
|
|
1957
|
+
chainId: 4217,
|
|
1958
|
+
token: USDC.tempo.mainnet.address,
|
|
1959
|
+
symbol: "USDC.e",
|
|
1960
|
+
decimals: 6,
|
|
1961
|
+
testnet: false,
|
|
1962
|
+
recommend: "both"
|
|
1963
|
+
},
|
|
1964
|
+
x402Base: {
|
|
1965
|
+
network: "eip155:8453",
|
|
1966
|
+
chainId: 8453,
|
|
1967
|
+
token: USDC.base.mainnet.address,
|
|
1968
|
+
symbol: "USDC",
|
|
1969
|
+
decimals: 6,
|
|
1970
|
+
mode: "exact"
|
|
1971
|
+
},
|
|
1972
|
+
solanaMpp: {
|
|
1973
|
+
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
1974
|
+
token: USDC.solana.mainnet.mint,
|
|
1975
|
+
symbol: "USDC",
|
|
1976
|
+
decimals: 6
|
|
1977
|
+
},
|
|
1978
|
+
stripe: {
|
|
1979
|
+
rails: ["card", "link", "shared_payment_token"]
|
|
1980
|
+
},
|
|
1981
|
+
tempoSession: {
|
|
1982
|
+
currency: USDC.tempo.mainnet.address,
|
|
1983
|
+
testnet: false
|
|
386
1984
|
}
|
|
387
|
-
|
|
388
|
-
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
// src/challenge/accepted_methods.ts
|
|
1988
|
+
async function buildAcceptedMethods({
|
|
1989
|
+
tempo,
|
|
1990
|
+
x402_base,
|
|
1991
|
+
solana_mpp,
|
|
1992
|
+
stripe
|
|
1993
|
+
}) {
|
|
1994
|
+
const out = [];
|
|
1995
|
+
if (tempo) {
|
|
1996
|
+
out.push({
|
|
1997
|
+
method: "tempo/charge",
|
|
1998
|
+
network: tempo.network ?? RAIL_SPEC_DEFAULTS.tempo.network,
|
|
1999
|
+
chain_id: tempo.chainId ?? RAIL_SPEC_DEFAULTS.tempo.chainId,
|
|
2000
|
+
token: tempo.token ?? RAIL_SPEC_DEFAULTS.tempo.token,
|
|
2001
|
+
symbol: tempo.symbol ?? RAIL_SPEC_DEFAULTS.tempo.symbol,
|
|
2002
|
+
decimals: tempo.decimals ?? RAIL_SPEC_DEFAULTS.tempo.decimals,
|
|
2003
|
+
pay_to: await resolveRecipient(tempo.recipient)
|
|
2004
|
+
});
|
|
389
2005
|
}
|
|
390
|
-
if (
|
|
391
|
-
|
|
2006
|
+
if (x402_base) {
|
|
2007
|
+
out.push({
|
|
2008
|
+
method: "x402/exact",
|
|
2009
|
+
network: x402_base.network ?? RAIL_SPEC_DEFAULTS.x402Base.network,
|
|
2010
|
+
chain_id: x402_base.chainId ?? RAIL_SPEC_DEFAULTS.x402Base.chainId,
|
|
2011
|
+
token: x402_base.token ?? RAIL_SPEC_DEFAULTS.x402Base.token,
|
|
2012
|
+
symbol: x402_base.symbol ?? RAIL_SPEC_DEFAULTS.x402Base.symbol,
|
|
2013
|
+
decimals: x402_base.decimals ?? RAIL_SPEC_DEFAULTS.x402Base.decimals,
|
|
2014
|
+
pay_to: await resolveRecipient(x402_base.recipient)
|
|
2015
|
+
});
|
|
392
2016
|
}
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
2017
|
+
if (solana_mpp) {
|
|
2018
|
+
out.push({
|
|
2019
|
+
method: "solana/charge",
|
|
2020
|
+
network: solana_mpp.network ?? RAIL_SPEC_DEFAULTS.solanaMpp.network,
|
|
2021
|
+
token: solana_mpp.token ?? RAIL_SPEC_DEFAULTS.solanaMpp.token,
|
|
2022
|
+
symbol: solana_mpp.symbol ?? RAIL_SPEC_DEFAULTS.solanaMpp.symbol,
|
|
2023
|
+
decimals: solana_mpp.decimals ?? RAIL_SPEC_DEFAULTS.solanaMpp.decimals,
|
|
2024
|
+
pay_to: await resolveRecipient(solana_mpp.recipient)
|
|
2025
|
+
});
|
|
397
2026
|
}
|
|
398
|
-
if (
|
|
399
|
-
|
|
2027
|
+
if (stripe) {
|
|
2028
|
+
out.push({
|
|
2029
|
+
method: "stripe/charge",
|
|
2030
|
+
rails: stripe.rails ?? [...RAIL_SPEC_DEFAULTS.stripe.rails],
|
|
2031
|
+
profile_id: stripe.profileId ?? null
|
|
2032
|
+
});
|
|
400
2033
|
}
|
|
401
|
-
return
|
|
2034
|
+
return out;
|
|
402
2035
|
}
|
|
403
|
-
|
|
404
|
-
|
|
2036
|
+
|
|
2037
|
+
// src/challenge/agent_instructions.ts
|
|
2038
|
+
var TEMPO_WARNING = "Do NOT use `tempo wallet transfer`. That moves USDC on-chain without completing the protocol handshake, so the request will not complete. Use `tempo request` instead.";
|
|
2039
|
+
var X402_WARNING = "Do NOT send USDC manually to the deposit addresses. Use `agentscore-pay pay` so the credential is signed and submitted; otherwise the request will not complete even though the deposit lands.";
|
|
2040
|
+
var TEMPO_TOOL = "`tempo request` for Tempo USDC";
|
|
2041
|
+
var AGENTSCORE_PAY_TOOL = "`agentscore-pay` \u2014 Base + Solana + Tempo from one CLI";
|
|
2042
|
+
var DEFAULT_WALLET_COMPATIBILITY = "Any client that can produce a valid MPP credential (Authorization: Payment) or x402 X-Payment header. Use the CLI commands above; sign-it-yourself is also fine.";
|
|
2043
|
+
function defaultRecommendedTools(howToPay) {
|
|
2044
|
+
const tools = [];
|
|
2045
|
+
if (howToPay.tempo) tools.push(TEMPO_TOOL);
|
|
2046
|
+
if (howToPay.tempo || howToPay.x402_base || howToPay.solana_mpp) tools.push(AGENTSCORE_PAY_TOOL);
|
|
2047
|
+
return tools;
|
|
2048
|
+
}
|
|
2049
|
+
function defaultWarnings(howToPay) {
|
|
2050
|
+
const w = [];
|
|
2051
|
+
if (howToPay.tempo) w.push(TEMPO_WARNING);
|
|
2052
|
+
if (howToPay.x402_base) w.push(X402_WARNING);
|
|
2053
|
+
return w;
|
|
2054
|
+
}
|
|
2055
|
+
var RAIL_CLIENTS = {
|
|
2056
|
+
tempo_mpp: ["agentscore-pay", "tempo request", "x402-proxy"],
|
|
2057
|
+
x402_base: ["agentscore-pay", "x402-proxy", "purl (omit --network flag)"],
|
|
2058
|
+
solana_mpp: ["agentscore-pay"],
|
|
2059
|
+
stripe: ["link-cli"]
|
|
405
2060
|
};
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
2061
|
+
function compatibleClientsByRails(rails2) {
|
|
2062
|
+
const out = {};
|
|
2063
|
+
for (const r of rails2) out[r] = [...RAIL_CLIENTS[r]];
|
|
2064
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
2065
|
+
}
|
|
2066
|
+
function defaultCompatibleClients(howToPay) {
|
|
2067
|
+
const rails2 = [];
|
|
2068
|
+
if (howToPay.tempo) rails2.push("tempo_mpp");
|
|
2069
|
+
if (howToPay.x402_base) rails2.push("x402_base");
|
|
2070
|
+
if (howToPay.solana_mpp) rails2.push("solana_mpp");
|
|
2071
|
+
if (howToPay.stripe) rails2.push("stripe");
|
|
2072
|
+
return compatibleClientsByRails(rails2);
|
|
2073
|
+
}
|
|
2074
|
+
function buildAgentInstructions({
|
|
2075
|
+
howToPay,
|
|
2076
|
+
recommendedTools,
|
|
2077
|
+
walletCompatibility,
|
|
2078
|
+
timeoutSeconds,
|
|
2079
|
+
warnings,
|
|
2080
|
+
extraWarnings,
|
|
2081
|
+
recommended,
|
|
2082
|
+
compatibleClients,
|
|
2083
|
+
extra
|
|
2084
|
+
}) {
|
|
2085
|
+
const compatibleClientsOut = compatibleClients ?? defaultCompatibleClients(howToPay);
|
|
2086
|
+
return {
|
|
2087
|
+
how_to_pay: howToPay,
|
|
2088
|
+
recommended_tools: recommendedTools ?? defaultRecommendedTools(howToPay),
|
|
2089
|
+
wallet_compatibility: walletCompatibility ?? DEFAULT_WALLET_COMPATIBILITY,
|
|
2090
|
+
timeout_seconds: timeoutSeconds ?? 300,
|
|
2091
|
+
warnings: warnings ?? [...defaultWarnings(howToPay), ...extraWarnings ?? []],
|
|
2092
|
+
...recommended ? { recommended } : {},
|
|
2093
|
+
...compatibleClientsOut ? { compatible_clients: compatibleClientsOut } : {},
|
|
2094
|
+
...extra ?? {}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/challenge/agent_memory.ts
|
|
2099
|
+
function firstEncounterAgentMemory({
|
|
2100
|
+
firstEncounter
|
|
2101
|
+
}) {
|
|
2102
|
+
if (!firstEncounter) return void 0;
|
|
2103
|
+
return buildAgentMemoryHint();
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// src/challenge/body.ts
|
|
2107
|
+
function build402Body({
|
|
2108
|
+
acceptedMethods,
|
|
2109
|
+
agentInstructions,
|
|
2110
|
+
identityMetadata,
|
|
2111
|
+
agentMemory,
|
|
2112
|
+
pricing,
|
|
2113
|
+
amountUsd,
|
|
2114
|
+
currency,
|
|
2115
|
+
orderId,
|
|
2116
|
+
product,
|
|
2117
|
+
retryBody,
|
|
2118
|
+
recommended,
|
|
2119
|
+
x402,
|
|
2120
|
+
extra
|
|
2121
|
+
}) {
|
|
2122
|
+
const body = {
|
|
2123
|
+
payment_required: true,
|
|
2124
|
+
accepted_methods: acceptedMethods
|
|
2125
|
+
};
|
|
2126
|
+
if (x402) {
|
|
2127
|
+
body.x402Version = x402.version ?? 2;
|
|
2128
|
+
body.accepts = x402.accepts;
|
|
2129
|
+
if (x402.extensions !== void 0 && Object.keys(x402.extensions).length > 0) {
|
|
2130
|
+
body.extensions = x402.extensions;
|
|
439
2131
|
}
|
|
440
2132
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
2133
|
+
if (amountUsd !== void 0) body.amount_usd = amountUsd;
|
|
2134
|
+
if (currency) body.currency = currency;
|
|
2135
|
+
if (pricing) body.pricing = pricing;
|
|
2136
|
+
if (orderId !== void 0) body.order_id = orderId;
|
|
2137
|
+
if (product) body.product = product;
|
|
2138
|
+
if (recommended) body.recommended = recommended;
|
|
2139
|
+
if (retryBody !== void 0) body.retry_body = retryBody;
|
|
2140
|
+
if (identityMetadata) {
|
|
2141
|
+
Object.assign(body, identityMetadata);
|
|
450
2142
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
2143
|
+
if (agentInstructions) body.agent_instructions = agentInstructions;
|
|
2144
|
+
if (agentMemory !== void 0) body.agent_memory = agentMemory;
|
|
2145
|
+
if (extra) Object.assign(body, extra);
|
|
2146
|
+
return body;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// src/challenge/how_to_pay.ts
|
|
2150
|
+
var TEMPO_SETUP = [
|
|
2151
|
+
"curl -fsSL https://tempo.xyz/install | bash",
|
|
2152
|
+
"tempo wallet login",
|
|
2153
|
+
"tempo wallet whoami",
|
|
2154
|
+
"tempo wallet fund # if balance is zero"
|
|
2155
|
+
];
|
|
2156
|
+
var PAY_SETUP_BASE = [
|
|
2157
|
+
"npm install -g @agent-score/pay # or: brew install agentscore/tap/agentscore-pay",
|
|
2158
|
+
"agentscore-pay wallet create --chain base",
|
|
2159
|
+
"agentscore-pay balance --chain base # fund the printed address with USDC on Base"
|
|
2160
|
+
];
|
|
2161
|
+
var PAY_SETUP_SOLANA = [
|
|
2162
|
+
"npm install -g @agent-score/pay # or: brew install agentscore/tap/agentscore-pay",
|
|
2163
|
+
"agentscore-pay wallet create --chain solana",
|
|
2164
|
+
"agentscore-pay balance --chain solana # fund the printed address with USDC on Solana"
|
|
2165
|
+
];
|
|
2166
|
+
function buildHowToPay({
|
|
2167
|
+
url,
|
|
2168
|
+
retryBodyJson,
|
|
2169
|
+
totalUsd,
|
|
2170
|
+
rails: rails2,
|
|
2171
|
+
opTokenPlaceholder,
|
|
2172
|
+
maxSpend
|
|
2173
|
+
}) {
|
|
2174
|
+
const totalNum = typeof totalUsd === "string" ? Number(totalUsd) : totalUsd;
|
|
2175
|
+
const maxSpendStr = String(maxSpend ?? (Math.ceil(totalNum) + 1).toFixed(2));
|
|
2176
|
+
const opToken = opTokenPlaceholder ?? "<your_opc_token>";
|
|
2177
|
+
const block = {};
|
|
2178
|
+
if (rails2.tempo) {
|
|
2179
|
+
const networkName = rails2.tempo.testnet ? "tempo-testnet" : rails2.tempo.network ?? RAIL_SPEC_DEFAULTS.tempo.network;
|
|
2180
|
+
const chainId = rails2.tempo.chainId ?? RAIL_SPEC_DEFAULTS.tempo.chainId;
|
|
2181
|
+
const recommend = rails2.tempo.recommend ?? RAIL_SPEC_DEFAULTS.tempo.recommend;
|
|
2182
|
+
const tempoCommand = `tempo request -X POST -H 'X-Operator-Token: ${opToken}' -H 'Content-Type: application/json' --json '${retryBodyJson}' --max-spend ${maxSpendStr} ${url}`;
|
|
2183
|
+
const payCommand = `agentscore-pay pay POST ${url} --chain tempo -H 'X-Operator-Token: ${opToken}' -H 'Content-Type: application/json' -d '${retryBodyJson}' --max-spend ${maxSpendStr}`;
|
|
2184
|
+
block.tempo = {
|
|
2185
|
+
setup: TEMPO_SETUP,
|
|
2186
|
+
prerequisite: `Run \`tempo wallet whoami\` and confirm USDC.e balance on ${networkName} (chain ${chainId}) is at least $${maxSpendStr}. If the tempo CLI is not installed, run the setup commands above first.`,
|
|
2187
|
+
command: recommend === "agentscore-pay" ? payCommand : tempoCommand,
|
|
2188
|
+
...recommend === "both" ? { alternative_command: payCommand } : recommend === "agentscore-pay" ? { alternative_command: tempoCommand } : {},
|
|
2189
|
+
what_it_does: `Pays via Tempo USDC on ${networkName}.`
|
|
2190
|
+
};
|
|
454
2191
|
}
|
|
455
|
-
if (
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
2192
|
+
if (rails2.x402_base) {
|
|
2193
|
+
const network = rails2.x402_base.network ?? RAIL_SPEC_DEFAULTS.x402Base.network;
|
|
2194
|
+
block.x402_base = {
|
|
2195
|
+
setup: PAY_SETUP_BASE,
|
|
2196
|
+
prerequisite: `Run \`agentscore-pay balance --chain base\` and confirm USDC balance on Base (${network}) is at least $${maxSpendStr}. If the CLI is not installed, run the setup commands above first.`,
|
|
2197
|
+
command: `agentscore-pay pay POST ${url} --chain base -H 'X-Operator-Token: ${opToken}' -H 'Content-Type: application/json' -d '${retryBodyJson}' --max-spend ${maxSpendStr}`,
|
|
2198
|
+
what_it_does: "Pays via USDC on Base."
|
|
462
2199
|
};
|
|
463
|
-
if (Object.keys(gateConfig).length > 0) agentscoreBinding.config = gateConfig;
|
|
464
|
-
const existing = capabilities[AGENTSCORE_CAPABILITY_NAME];
|
|
465
|
-
if (existing) existing.push(agentscoreBinding);
|
|
466
|
-
else capabilities[AGENTSCORE_CAPABILITY_NAME] = [agentscoreBinding];
|
|
467
2200
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (input.ucp_extras) {
|
|
477
|
-
for (const k of Object.keys(input.ucp_extras)) {
|
|
478
|
-
if (RESERVED_UCP_FIELDS.has(k)) {
|
|
479
|
-
throw new Error(`buildUCPProfile: ucp_extras key "${k}" collides with a reserved \`ucp\` field; rejected.`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
Object.assign(ucp, input.ucp_extras);
|
|
2201
|
+
if (rails2.solana_mpp) {
|
|
2202
|
+
const network = rails2.solana_mpp.network ?? RAIL_SPEC_DEFAULTS.solanaMpp.network;
|
|
2203
|
+
block.solana_mpp = {
|
|
2204
|
+
setup: PAY_SETUP_SOLANA,
|
|
2205
|
+
prerequisite: `Run \`agentscore-pay balance --chain solana\` and confirm USDC balance on Solana (${network}) is at least $${maxSpendStr}. If the CLI is not installed, run the setup commands above first.`,
|
|
2206
|
+
command: `agentscore-pay pay POST ${url} --chain solana -H 'X-Operator-Token: ${opToken}' -H 'Content-Type: application/json' -d '${retryBodyJson}' --max-spend ${maxSpendStr}`,
|
|
2207
|
+
what_it_does: "Pays via USDC on Solana."
|
|
2208
|
+
};
|
|
483
2209
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
2210
|
+
if (rails2.stripe) {
|
|
2211
|
+
const stripeCfg = rails2.stripe;
|
|
2212
|
+
const amountCents = Math.round(totalNum * 100);
|
|
2213
|
+
const linkCliBlocked = amountCents > 5e4;
|
|
2214
|
+
const productName = stripeCfg.productName ?? "this purchase";
|
|
2215
|
+
const sptContext = `Purchasing "${productName}" via the agent commerce API. The user authorized this purchase through their AI agent for $${totalNum}; charge to be settled via shared payment token over the Machine Payments Protocol.`;
|
|
2216
|
+
const stripe = {
|
|
2217
|
+
prerequisite: "Either your own Stripe account with Shared Payment Token acceptance, OR a Stripe Link wallet (any user with link.com).",
|
|
2218
|
+
instructions: "Mint a SharedPaymentToken scoped to the profile_id advertised in accepted_methods, then submit via Authorization: Payment MPP header with method=stripe/charge."
|
|
2219
|
+
};
|
|
2220
|
+
if (stripeCfg.profileId && !linkCliBlocked) {
|
|
2221
|
+
stripe.setup_link_cli = [
|
|
2222
|
+
"npm install -g @stripe/link-cli # or use npx -y @stripe/link-cli for one-shot",
|
|
2223
|
+
"link-cli auth login # one-time, opens your Link wallet",
|
|
2224
|
+
"link-cli payment-methods list --output-json # copy a csmrpd_... id"
|
|
2225
|
+
];
|
|
2226
|
+
stripe.command_link_cli = [
|
|
2227
|
+
`SPEND_ID=$(link-cli spend-request create --payment-method-id <csmrpd_id_from_payment_methods_list> --credential-type shared_payment_token --network-id ${stripeCfg.profileId} --amount ${amountCents} --context "${sptContext}" --request-approval --output-json | jq -r .id)`,
|
|
2228
|
+
`link-cli mpp pay ${url} --spend-request-id $SPEND_ID --method POST --data '${retryBodyJson}' --header 'X-Operator-Token: ${opToken}' --output-json`
|
|
2229
|
+
];
|
|
2230
|
+
stripe.what_it_does_link_cli = "Mints a one-time-use SharedPaymentToken scoped to this purchase (user approves in Link wallet), then submits it as the payment credential.";
|
|
2231
|
+
} else if (linkCliBlocked) {
|
|
2232
|
+
stripe.note = `link-cli SPT path not available for this purchase \u2014 Stripe link-cli caps spend requests at $500.00 ($50000 cents); your total is $${totalNum}. Use your own Stripe account with the SharedPaymentToken API instead.`;
|
|
493
2233
|
}
|
|
494
|
-
|
|
2234
|
+
block.stripe = stripe;
|
|
495
2235
|
}
|
|
496
|
-
return
|
|
497
|
-
}
|
|
498
|
-
var AGENTSCORE_UCP_CAPABILITY = AGENTSCORE_CAPABILITY_NAME;
|
|
499
|
-
var HANDLER_VERSION = "2026-04-08";
|
|
500
|
-
var SPEC_BASE = "https://agentscore.sh/specification/payment-handlers";
|
|
501
|
-
var SCHEMA_BASE = "https://agentscore.sh/schemas/payment-handlers";
|
|
502
|
-
function mppPaymentHandler(input) {
|
|
503
|
-
return {
|
|
504
|
-
"sh.agentscore.payment.mpp": [{
|
|
505
|
-
id: "mpp",
|
|
506
|
-
version: HANDLER_VERSION,
|
|
507
|
-
spec: `${SPEC_BASE}/mpp`,
|
|
508
|
-
schema: `${SCHEMA_BASE}/mpp.json`,
|
|
509
|
-
config: { networks: input.networks }
|
|
510
|
-
}]
|
|
511
|
-
};
|
|
2236
|
+
return block;
|
|
512
2237
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
};
|
|
2238
|
+
|
|
2239
|
+
// src/challenge/identity.ts
|
|
2240
|
+
function buildIdentityMetadata({
|
|
2241
|
+
mode,
|
|
2242
|
+
wallet,
|
|
2243
|
+
signerMatchResult,
|
|
2244
|
+
linkedWallets,
|
|
2245
|
+
signerConstraint
|
|
2246
|
+
}) {
|
|
2247
|
+
const block = { identity_mode: mode };
|
|
2248
|
+
if (mode !== "wallet") return block;
|
|
2249
|
+
if (wallet) {
|
|
2250
|
+
block.required_signer = signerMatchResult?.expectedSigner ?? wallet;
|
|
2251
|
+
}
|
|
2252
|
+
if (linkedWallets && linkedWallets.length > 0) {
|
|
2253
|
+
block.linked_wallets = linkedWallets;
|
|
2254
|
+
}
|
|
2255
|
+
block.signer_constraint = signerConstraint ?? "Payment must be signed with the claimed wallet OR any same-operator linked wallet listed in linked_wallets.";
|
|
2256
|
+
return block;
|
|
523
2257
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
2258
|
+
|
|
2259
|
+
// src/challenge/pricing.ts
|
|
2260
|
+
function buildPricingBlock({
|
|
2261
|
+
subtotalCents,
|
|
2262
|
+
taxCents = 0,
|
|
2263
|
+
shippingCents,
|
|
2264
|
+
discountCents,
|
|
2265
|
+
totalCents,
|
|
2266
|
+
taxRate,
|
|
2267
|
+
taxState,
|
|
2268
|
+
currency
|
|
2269
|
+
}) {
|
|
2270
|
+
const shipping = shippingCents ?? 0;
|
|
2271
|
+
const discount = discountCents ?? 0;
|
|
2272
|
+
const total = totalCents ?? Math.max(0, subtotalCents + taxCents + shipping - discount);
|
|
2273
|
+
const block = {
|
|
2274
|
+
subtotal: formatCents(subtotalCents),
|
|
2275
|
+
tax: formatCents(taxCents),
|
|
2276
|
+
total: formatCents(total)
|
|
533
2277
|
};
|
|
2278
|
+
if (shippingCents !== void 0) block.shipping = formatCents(shipping);
|
|
2279
|
+
if (discountCents !== void 0) block.discount = formatCents(discount);
|
|
2280
|
+
if (taxRate !== void 0) block.tax_rate = taxRate;
|
|
2281
|
+
if (taxState !== void 0) block.tax_state = taxState;
|
|
2282
|
+
if (currency !== void 0) block.currency = currency;
|
|
2283
|
+
return block;
|
|
2284
|
+
}
|
|
2285
|
+
function formatCents(cents) {
|
|
2286
|
+
return (cents / 100).toFixed(2);
|
|
534
2287
|
}
|
|
535
2288
|
|
|
536
|
-
// src/
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
2289
|
+
// src/challenge/respond_402.ts
|
|
2290
|
+
init_wwwauthenticate();
|
|
2291
|
+
function respond402({
|
|
2292
|
+
mppxChallengeHeaders,
|
|
2293
|
+
body,
|
|
2294
|
+
x402
|
|
2295
|
+
}) {
|
|
2296
|
+
const headers = {};
|
|
2297
|
+
for (const [k, v] of Object.entries(mppxChallengeHeaders)) {
|
|
2298
|
+
headers[k.toLowerCase()] = v;
|
|
545
2299
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
return await import("jose");
|
|
551
|
-
} catch (err) {
|
|
552
|
-
throw new Error(
|
|
553
|
-
`UCP signing requires the \`jose\` library, which is an optional peer dependency. ${JOSE_INSTALL_HINT}
|
|
554
|
-
Original error: ${err instanceof Error ? err.message : String(err)}`
|
|
555
|
-
);
|
|
2300
|
+
headers["content-type"] = "application/json";
|
|
2301
|
+
if (x402) {
|
|
2302
|
+
headers["payment-required"] = paymentRequiredHeader(x402);
|
|
556
2303
|
}
|
|
2304
|
+
return { body, headers, status: 402 };
|
|
557
2305
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
2306
|
+
|
|
2307
|
+
// src/challenge/validation_error.ts
|
|
2308
|
+
function buildValidationError({
|
|
2309
|
+
code,
|
|
2310
|
+
message,
|
|
2311
|
+
requiredFields,
|
|
2312
|
+
exampleBody,
|
|
2313
|
+
nextSteps,
|
|
2314
|
+
extra
|
|
2315
|
+
}) {
|
|
2316
|
+
const body = {
|
|
2317
|
+
error: { code, message }
|
|
2318
|
+
};
|
|
2319
|
+
if (requiredFields) body.required_fields = requiredFields;
|
|
2320
|
+
if (exampleBody !== void 0) body.example_body = exampleBody;
|
|
2321
|
+
if (nextSteps) body.next_steps = nextSteps;
|
|
2322
|
+
if (extra) Object.assign(body, extra);
|
|
2323
|
+
return body;
|
|
562
2324
|
}
|
|
563
|
-
|
|
564
|
-
|
|
2325
|
+
|
|
2326
|
+
// src/stripe-multichain/mppx_stripe.ts
|
|
2327
|
+
async function createMppxStripe({
|
|
2328
|
+
profileId,
|
|
2329
|
+
secretKey,
|
|
2330
|
+
paymentMethodTypes
|
|
2331
|
+
}) {
|
|
2332
|
+
const moduleName = "mppx/server";
|
|
2333
|
+
const mppx = await import(moduleName).catch(() => null);
|
|
2334
|
+
if (!mppx?.stripe?.charge) {
|
|
565
2335
|
throw new Error(
|
|
566
|
-
"
|
|
2336
|
+
"mppx not installed \u2014 install with `npm install mppx` to use createMppxStripe."
|
|
567
2337
|
);
|
|
568
2338
|
}
|
|
569
|
-
|
|
570
|
-
|
|
2339
|
+
return mppx.stripe.charge({
|
|
2340
|
+
networkId: profileId,
|
|
2341
|
+
paymentMethodTypes: paymentMethodTypes ?? ["card", "link"],
|
|
2342
|
+
secretKey
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/payment/mppx_server.ts
|
|
2347
|
+
init_networks();
|
|
2348
|
+
init_usdc();
|
|
2349
|
+
function isStripeRailSpec(s) {
|
|
2350
|
+
return !("recipient" in s);
|
|
2351
|
+
}
|
|
2352
|
+
function isTempoSessionRailSpec2(s) {
|
|
2353
|
+
return "escrowContract" in s && "store" in s;
|
|
2354
|
+
}
|
|
2355
|
+
function isSolanaMppRailSpec(s) {
|
|
2356
|
+
if (!("recipient" in s)) return false;
|
|
2357
|
+
if ("escrowContract" in s) return false;
|
|
2358
|
+
if ("rpcUrl" in s || "tokenProgram" in s) return true;
|
|
2359
|
+
return s.network?.startsWith("solana:") ?? false;
|
|
2360
|
+
}
|
|
2361
|
+
function solanaNetworkFromCAIP2(caip2) {
|
|
2362
|
+
if (caip2 === networks.solana.devnet.caip2) return "devnet";
|
|
2363
|
+
return "mainnet-beta";
|
|
2364
|
+
}
|
|
2365
|
+
function solanaDefaultRpcUrl(network) {
|
|
2366
|
+
if (network === "mainnet-beta") return "https://api.mainnet-beta.solana.com";
|
|
2367
|
+
if (network === "devnet") return "https://api.devnet.solana.com";
|
|
2368
|
+
return "http://localhost:8899";
|
|
2369
|
+
}
|
|
2370
|
+
async function createMppxServer({
|
|
2371
|
+
rails: rails2,
|
|
2372
|
+
methods: extraMethods,
|
|
2373
|
+
secretKey
|
|
2374
|
+
}) {
|
|
2375
|
+
const mppx = await dynamicImport("mppx/server");
|
|
2376
|
+
if (!mppx?.Mppx?.create) {
|
|
2377
|
+
throw new Error("mppx not installed \u2014 `npm install mppx` to use createMppxServer.");
|
|
571
2378
|
}
|
|
572
|
-
|
|
573
|
-
|
|
2379
|
+
const methods = [...extraMethods ?? []];
|
|
2380
|
+
for (const [name, spec] of Object.entries(rails2 ?? {})) {
|
|
2381
|
+
if (isStripeRailSpec(spec)) {
|
|
2382
|
+
methods.push(await registerStripe(spec));
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (isTempoSessionRailSpec2(spec)) {
|
|
2386
|
+
methods.push(await registerTempoSession(mppx, spec));
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
if (isSolanaMppRailSpec(spec)) {
|
|
2390
|
+
methods.push(await registerSolana(spec));
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
methods.push(registerTempo(mppx, spec, name));
|
|
574
2394
|
}
|
|
575
|
-
|
|
2395
|
+
return mppx.Mppx.create({ methods, secretKey });
|
|
2396
|
+
}
|
|
2397
|
+
function registerTempo(mppx, spec, _name) {
|
|
2398
|
+
if (!mppx.tempo?.charge) {
|
|
2399
|
+
throw new Error("mppx.tempo.charge not available \u2014 check installed mppx version.");
|
|
2400
|
+
}
|
|
2401
|
+
const defaultCurrency = spec.testnet ? USDC.tempo.testnet.address : USDC.tempo.mainnet.address;
|
|
2402
|
+
if (typeof spec.recipient !== "string") {
|
|
2403
|
+
throw new TypeError(
|
|
2404
|
+
"createMppxServer: TempoRailSpec requires a string recipient (per-order factories not supported here)."
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
return mppx.tempo.charge({
|
|
2408
|
+
currency: spec.token ?? defaultCurrency,
|
|
2409
|
+
recipient: spec.recipient,
|
|
2410
|
+
testnet: spec.testnet ?? false
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
async function registerTempoSession(mppx, spec) {
|
|
2414
|
+
if (!mppx.tempo?.session) {
|
|
576
2415
|
throw new Error(
|
|
577
|
-
"
|
|
2416
|
+
"mppx.tempo.session not available \u2014 your mppx version may not support sessions yet. Upgrade with `npm install mppx@latest`."
|
|
578
2417
|
);
|
|
579
2418
|
}
|
|
580
|
-
|
|
2419
|
+
const defaultCurrency = spec.testnet ? USDC.tempo.testnet.address : USDC.tempo.mainnet.address;
|
|
2420
|
+
return mppx.tempo.session({
|
|
2421
|
+
currency: spec.currency ?? defaultCurrency,
|
|
2422
|
+
recipient: await resolveRecipient(spec.recipient),
|
|
2423
|
+
escrowContract: spec.escrowContract,
|
|
2424
|
+
store: spec.store,
|
|
2425
|
+
testnet: spec.testnet ?? false,
|
|
2426
|
+
...spec.chains ? { chains: spec.chains } : {}
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
async function registerSolana(spec) {
|
|
2430
|
+
const solanaMpp = await dynamicImport("@solana/mpp/server");
|
|
2431
|
+
if (!solanaMpp?.charge) {
|
|
581
2432
|
throw new Error(
|
|
582
|
-
|
|
2433
|
+
"@solana/mpp not installed \u2014 `npm install @solana/mpp @solana/kit` to use the solana rail."
|
|
583
2434
|
);
|
|
584
2435
|
}
|
|
585
|
-
|
|
586
|
-
|
|
2436
|
+
const network = solanaNetworkFromCAIP2(spec.network);
|
|
2437
|
+
const defaultMint = network === "mainnet-beta" ? USDC.solana.mainnet.mint : USDC.solana.devnet.mint;
|
|
2438
|
+
const defaultDecimals = network === "mainnet-beta" ? USDC.solana.mainnet.decimals : USDC.solana.devnet.decimals;
|
|
2439
|
+
if (typeof spec.recipient !== "string") {
|
|
2440
|
+
throw new TypeError(
|
|
2441
|
+
"createMppxServer: SolanaMppRailSpec requires a string recipient (per-order factories not supported here)."
|
|
2442
|
+
);
|
|
587
2443
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
2444
|
+
const baseMethod = solanaMpp.charge({
|
|
2445
|
+
recipient: spec.recipient,
|
|
2446
|
+
currency: spec.token ?? defaultMint,
|
|
2447
|
+
decimals: spec.decimals ?? defaultDecimals,
|
|
2448
|
+
network,
|
|
2449
|
+
...spec.rpcUrl ? { rpcUrl: spec.rpcUrl } : {},
|
|
2450
|
+
...spec.signer ? { signer: spec.signer } : {},
|
|
2451
|
+
...spec.tokenProgram ? { tokenProgram: spec.tokenProgram } : {}
|
|
2452
|
+
});
|
|
2453
|
+
return wrapSolanaChargeWithFinalizedBlockhash(baseMethod, spec.rpcUrl ?? solanaDefaultRpcUrl(network));
|
|
2454
|
+
}
|
|
2455
|
+
async function registerStripe(spec) {
|
|
2456
|
+
if (!spec.profileId || !spec.secretKey) {
|
|
2457
|
+
throw new Error(
|
|
2458
|
+
"createMppxServer: StripeRailSpec requires both profileId and secretKey."
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
return createMppxStripe({
|
|
2462
|
+
profileId: spec.profileId,
|
|
2463
|
+
secretKey: spec.secretKey,
|
|
2464
|
+
paymentMethodTypes: spec.paymentMethodTypes
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
async function dynamicImport(moduleName) {
|
|
2468
|
+
try {
|
|
2469
|
+
return await import(moduleName);
|
|
2470
|
+
} catch {
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
function wrapSolanaChargeWithFinalizedBlockhash(baseMethod, rpcUrl) {
|
|
2475
|
+
return {
|
|
2476
|
+
...baseMethod,
|
|
2477
|
+
async request(args) {
|
|
2478
|
+
const orig = await baseMethod.request(args);
|
|
2479
|
+
if (args.credential || !orig || typeof orig !== "object") return orig;
|
|
2480
|
+
try {
|
|
2481
|
+
const res = await fetch(rpcUrl, {
|
|
2482
|
+
method: "POST",
|
|
2483
|
+
headers: { "Content-Type": "application/json" },
|
|
2484
|
+
body: JSON.stringify({
|
|
2485
|
+
id: 1,
|
|
2486
|
+
jsonrpc: "2.0",
|
|
2487
|
+
method: "getLatestBlockhash",
|
|
2488
|
+
params: [{ commitment: "finalized" }]
|
|
2489
|
+
})
|
|
2490
|
+
});
|
|
2491
|
+
const data = await res.json();
|
|
2492
|
+
const finalized = data?.result?.value?.blockhash;
|
|
2493
|
+
if (finalized) {
|
|
2494
|
+
return {
|
|
2495
|
+
...orig,
|
|
2496
|
+
methodDetails: { ...orig.methodDetails ?? {}, recentBlockhash: finalized }
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
return orig;
|
|
603
2502
|
}
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// src/payment/x402_server.ts
|
|
2507
|
+
init_networks();
|
|
2508
|
+
|
|
2509
|
+
// src/payment/x402.ts
|
|
2510
|
+
function registerX402SchemesV1V2(server, network, scheme) {
|
|
2511
|
+
server.register(network, scheme);
|
|
2512
|
+
if (typeof server.registerV1 === "function") {
|
|
2513
|
+
server.registerV1(network, scheme);
|
|
604
2514
|
}
|
|
605
|
-
|
|
606
|
-
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/payment/x402_server.ts
|
|
2518
|
+
async function createX402Server(opts = {}) {
|
|
2519
|
+
const x402Core = await dynamicImport2("@x402/core/server") ?? null;
|
|
2520
|
+
if (!x402Core) {
|
|
2521
|
+
throw new Error(
|
|
2522
|
+
"@x402/core not installed \u2014 `npm install @x402/core` to use createX402Server."
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
let facilitator;
|
|
2526
|
+
const facilitatorChoice = opts.facilitator ?? (process.env.CDP_API_KEY_ID && process.env.CDP_API_KEY_SECRET ? "coinbase" : "http");
|
|
2527
|
+
if (facilitatorChoice === "coinbase") {
|
|
2528
|
+
const cb = await dynamicImport2("@coinbase/x402");
|
|
2529
|
+
if (!cb?.facilitator) {
|
|
607
2530
|
throw new Error(
|
|
608
|
-
|
|
2531
|
+
'@coinbase/x402 not installed \u2014 `npm install @coinbase/x402` for facilitator: "coinbase".'
|
|
609
2532
|
);
|
|
610
2533
|
}
|
|
611
|
-
|
|
2534
|
+
facilitator = new x402Core.HTTPFacilitatorClient(cb.facilitator);
|
|
2535
|
+
} else if (facilitatorChoice === "http") {
|
|
2536
|
+
facilitator = new x402Core.HTTPFacilitatorClient();
|
|
2537
|
+
} else {
|
|
2538
|
+
facilitator = facilitatorChoice;
|
|
612
2539
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (
|
|
2540
|
+
const server = new x402Core.x402ResourceServer(facilitator);
|
|
2541
|
+
let evmExactModule = null;
|
|
2542
|
+
let evmUptoModule = null;
|
|
2543
|
+
for (const rail of opts.rails ?? []) {
|
|
2544
|
+
const isUpto = rail.endsWith("-upto");
|
|
2545
|
+
if (rail.startsWith("x402-base")) {
|
|
2546
|
+
const baseRail = isUpto ? rail.slice(0, -5) : rail;
|
|
2547
|
+
const network = baseRail === "x402-base-mainnet" ? networks.base.mainnet.caip2 : networks.base.sepolia.caip2;
|
|
2548
|
+
if (isUpto) {
|
|
2549
|
+
evmUptoModule ??= await dynamicImport2("@x402/evm/upto/server");
|
|
2550
|
+
if (!evmUptoModule?.UptoEvmScheme) {
|
|
2551
|
+
throw new Error("@x402/evm not installed \u2014 `npm install @x402/evm` for x402 base upto rails.");
|
|
2552
|
+
}
|
|
2553
|
+
registerX402SchemesV1V2(server, network, new evmUptoModule.UptoEvmScheme());
|
|
2554
|
+
} else {
|
|
2555
|
+
evmExactModule ??= await dynamicImport2("@x402/evm/exact/server");
|
|
2556
|
+
if (!evmExactModule?.ExactEvmScheme) {
|
|
2557
|
+
throw new Error("@x402/evm not installed \u2014 `npm install @x402/evm` for x402 base rails.");
|
|
2558
|
+
}
|
|
2559
|
+
registerX402SchemesV1V2(server, network, new evmExactModule.ExactEvmScheme());
|
|
2560
|
+
}
|
|
622
2561
|
}
|
|
623
|
-
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
|
|
2562
|
+
}
|
|
2563
|
+
for (const { network, scheme } of opts.schemes ?? []) {
|
|
2564
|
+
registerX402SchemesV1V2(server, network, scheme);
|
|
2565
|
+
}
|
|
2566
|
+
if (opts.bazaar) {
|
|
2567
|
+
const bazaar = await dynamicImport2("@x402/extensions/bazaar");
|
|
2568
|
+
if (!bazaar?.bazaarResourceServerExtension) {
|
|
627
2569
|
throw new Error(
|
|
628
|
-
"
|
|
2570
|
+
"@x402/extensions not installed \u2014 `npm install @x402/extensions` for bazaar discovery."
|
|
629
2571
|
);
|
|
630
2572
|
}
|
|
2573
|
+
server.registerExtension(bazaar.bazaarResourceServerExtension);
|
|
631
2574
|
}
|
|
632
|
-
|
|
633
|
-
|
|
2575
|
+
if (opts.initialize !== false) {
|
|
2576
|
+
await server.initialize();
|
|
2577
|
+
}
|
|
2578
|
+
return server;
|
|
634
2579
|
}
|
|
635
|
-
async function
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
2580
|
+
async function buildX402AcceptsFor402(server, opts) {
|
|
2581
|
+
const requirements = await server.buildPaymentRequirements(
|
|
2582
|
+
{
|
|
2583
|
+
scheme: opts.scheme ?? "exact",
|
|
2584
|
+
network: opts.network,
|
|
2585
|
+
price: opts.price,
|
|
2586
|
+
payTo: opts.payTo,
|
|
2587
|
+
maxTimeoutSeconds: opts.maxTimeoutSeconds ?? 300
|
|
2588
|
+
},
|
|
2589
|
+
opts.extensions
|
|
2590
|
+
);
|
|
2591
|
+
return Array.isArray(requirements) ? requirements : [];
|
|
2592
|
+
}
|
|
2593
|
+
async function dynamicImport2(moduleName) {
|
|
2594
|
+
try {
|
|
2595
|
+
return await import(moduleName);
|
|
2596
|
+
} catch {
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/payment/lazy.ts
|
|
2602
|
+
function x402RailName(spec) {
|
|
2603
|
+
const network = spec.network ?? "eip155:8453";
|
|
2604
|
+
if (network === "eip155:8453") return "x402-base-mainnet";
|
|
2605
|
+
if (network === "eip155:84532") return "x402-base-sepolia";
|
|
2606
|
+
throw new Error(
|
|
2607
|
+
`lazyX402Server: unsupported X402BaseRailSpec.network=${JSON.stringify(network)}`
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
function lazyX402Server(opts) {
|
|
2611
|
+
const { spec, cdpApiKeyId, cdpApiKeySecret } = opts;
|
|
2612
|
+
const railName = x402RailName(spec);
|
|
2613
|
+
const useCdp = Boolean(cdpApiKeyId && cdpApiKeySecret);
|
|
2614
|
+
const facilitator = useCdp ? "coinbase" : "http";
|
|
2615
|
+
let cached;
|
|
2616
|
+
let pending;
|
|
2617
|
+
return async () => {
|
|
2618
|
+
if (cached !== void 0) return cached;
|
|
2619
|
+
if (pending !== void 0) return pending;
|
|
2620
|
+
pending = (async () => {
|
|
2621
|
+
const server = await createX402Server({ facilitator, rails: [railName] });
|
|
2622
|
+
cached = server;
|
|
2623
|
+
pending = void 0;
|
|
2624
|
+
return server;
|
|
2625
|
+
})();
|
|
2626
|
+
return pending;
|
|
645
2627
|
};
|
|
646
|
-
return { privateKey, publicJWK };
|
|
647
2628
|
}
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
2629
|
+
function lazyMppxServer(opts) {
|
|
2630
|
+
const { rails: rails2, secretKey } = opts;
|
|
2631
|
+
let cached;
|
|
2632
|
+
let pending;
|
|
2633
|
+
return async () => {
|
|
2634
|
+
if (cached !== void 0) return cached;
|
|
2635
|
+
if (pending !== void 0) return pending;
|
|
2636
|
+
pending = (async () => {
|
|
2637
|
+
const server = await createMppxServer({ secretKey, rails: rails2 });
|
|
2638
|
+
cached = server;
|
|
2639
|
+
pending = void 0;
|
|
2640
|
+
return server;
|
|
2641
|
+
})();
|
|
2642
|
+
return pending;
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/payment/x402_settle.ts
|
|
2647
|
+
function classifyX402SettleResult(result) {
|
|
2648
|
+
if (result.success) return null;
|
|
2649
|
+
switch (result.phase) {
|
|
2650
|
+
case "no_requirements":
|
|
2651
|
+
return {
|
|
2652
|
+
status: 500,
|
|
2653
|
+
code: "payment_internal_error",
|
|
2654
|
+
message: "Failed to build x402 payment requirements for this configuration",
|
|
2655
|
+
nextSteps: {
|
|
2656
|
+
action: "contact_support",
|
|
2657
|
+
user_message: "The merchant could not produce a payment challenge for this request. Try again later or contact support."
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
case "verify_failed":
|
|
2661
|
+
return {
|
|
2662
|
+
status: 400,
|
|
2663
|
+
code: "payment_proof_invalid",
|
|
2664
|
+
message: "Payment credential failed verification; regenerate from a fresh 402 challenge",
|
|
2665
|
+
nextSteps: {
|
|
2666
|
+
action: "regenerate_payment_credential",
|
|
2667
|
+
user_message: "The payment credential was rejected at verify time. Discard it, fetch a fresh 402 challenge, and re-sign."
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
case "facilitator_error":
|
|
2671
|
+
return {
|
|
2672
|
+
status: 503,
|
|
2673
|
+
code: "payment_provider_unavailable",
|
|
2674
|
+
message: "Payment provider could not process this network configuration",
|
|
2675
|
+
nextSteps: {
|
|
2676
|
+
action: "try_different_rail",
|
|
2677
|
+
user_message: "This rail is currently unavailable. Pick a different rail from the 402 challenge and retry."
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
case "settle_failed":
|
|
2681
|
+
return {
|
|
2682
|
+
status: 503,
|
|
2683
|
+
code: "payment_provider_unavailable",
|
|
2684
|
+
message: "Payment credential verified but on-chain settlement failed",
|
|
2685
|
+
nextSteps: {
|
|
2686
|
+
action: "retry_or_swap_method",
|
|
2687
|
+
retry_after_seconds: 10,
|
|
2688
|
+
user_message: "Transient settlement error. Retry in a few seconds, or pick a different rail from the 402 challenge."
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
async function processX402Settle({
|
|
2694
|
+
x402Server,
|
|
2695
|
+
payload,
|
|
2696
|
+
resourceConfig,
|
|
2697
|
+
resourceMeta,
|
|
2698
|
+
extension,
|
|
2699
|
+
transportContext
|
|
2700
|
+
}) {
|
|
2701
|
+
const server = x402Server;
|
|
2702
|
+
let builtRequirements;
|
|
2703
|
+
try {
|
|
2704
|
+
builtRequirements = await server.buildPaymentRequirements(resourceConfig);
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
console.warn("[x402_settle] build_requirements failed:", err instanceof Error ? err.message : err);
|
|
2707
|
+
return { success: false, phase: "facilitator_error", step: "build_requirements", error: err };
|
|
2708
|
+
}
|
|
2709
|
+
const matchedRequirement = builtRequirements[0];
|
|
2710
|
+
if (!matchedRequirement) {
|
|
2711
|
+
return { success: false, phase: "no_requirements", reason: "x402Server.buildPaymentRequirements returned empty" };
|
|
2712
|
+
}
|
|
2713
|
+
const resolvedTransportContext = transportContext ?? (() => {
|
|
2714
|
+
const path = new URL(resourceMeta.url).pathname;
|
|
2715
|
+
return { method: "POST", adapter: { getPath: () => path }, routePattern: path };
|
|
2716
|
+
})();
|
|
2717
|
+
let enrichedExt;
|
|
2718
|
+
try {
|
|
2719
|
+
enrichedExt = extension !== void 0 ? server.enrichExtensions(extension, resolvedTransportContext) : void 0;
|
|
2720
|
+
} catch (err) {
|
|
2721
|
+
console.warn("[x402_settle] enrich_extensions failed:", err instanceof Error ? err.message : err);
|
|
2722
|
+
return { success: false, phase: "facilitator_error", step: "enrich_extensions", error: err };
|
|
2723
|
+
}
|
|
2724
|
+
let verifyResult;
|
|
2725
|
+
try {
|
|
2726
|
+
verifyResult = await server.verifyPayment(
|
|
2727
|
+
payload,
|
|
2728
|
+
matchedRequirement,
|
|
2729
|
+
enrichedExt,
|
|
2730
|
+
resolvedTransportContext
|
|
654
2731
|
);
|
|
2732
|
+
} catch (err) {
|
|
2733
|
+
console.warn("[x402_settle] verify_payment failed:", err instanceof Error ? err.message : err);
|
|
2734
|
+
return { success: false, phase: "facilitator_error", step: "verify_payment", error: err };
|
|
655
2735
|
}
|
|
656
|
-
|
|
657
|
-
|
|
2736
|
+
const verifyOk = verifyResult.isValid === true || verifyResult.success === true;
|
|
2737
|
+
if (!verifyOk) {
|
|
2738
|
+
return { success: false, phase: "verify_failed", verifyResult };
|
|
658
2739
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
2740
|
+
try {
|
|
2741
|
+
const settleResult = await server.settlePayment(
|
|
2742
|
+
payload,
|
|
2743
|
+
matchedRequirement,
|
|
2744
|
+
enrichedExt,
|
|
2745
|
+
resolvedTransportContext
|
|
663
2746
|
);
|
|
2747
|
+
const paymentResponseHeader = settleResult ? Buffer.from(JSON.stringify(settleResult)).toString("base64") : void 0;
|
|
2748
|
+
return {
|
|
2749
|
+
success: true,
|
|
2750
|
+
matchedRequirement,
|
|
2751
|
+
settleResult,
|
|
2752
|
+
paymentResponseHeader,
|
|
2753
|
+
verifyResult
|
|
2754
|
+
};
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
return { success: false, phase: "settle_failed", error: err, matchedRequirement };
|
|
664
2757
|
}
|
|
665
|
-
const canonicalBody = canonicalizeProfile(profile);
|
|
666
|
-
const payloadBytes = new TextEncoder().encode(canonicalBody);
|
|
667
|
-
const signature = await new jose.CompactSign(payloadBytes).setProtectedHeader({ alg, kid: opts.kid, typ: PROFILE_TYP }).sign(opts.signingKey);
|
|
668
|
-
return { ...profile, signature };
|
|
669
2758
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
2759
|
+
|
|
2760
|
+
// src/payment/x402_validation.ts
|
|
2761
|
+
init_networks();
|
|
2762
|
+
var X402_SUPPORTED_BASE_NETWORKS = /* @__PURE__ */ new Set([
|
|
2763
|
+
networks.base.mainnet.caip2,
|
|
2764
|
+
networks.base.sepolia.caip2
|
|
2765
|
+
]);
|
|
2766
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
2767
|
+
var REGENERATE_WARNING = "Use `agentscore-pay pay --chain base` (or `tempo request` for Tempo USDC) so the credential is signed and submitted via the protocol handshake. Do NOT use `tempo wallet transfer` \u2014 that sends USDC on-chain but does not complete the handshake.";
|
|
2768
|
+
function regenerateBody(message, userMessage) {
|
|
2769
|
+
return {
|
|
2770
|
+
error: { code: "payment_proof_invalid", message },
|
|
2771
|
+
next_steps: {
|
|
2772
|
+
action: "regenerate_payment_credential",
|
|
2773
|
+
user_message: userMessage,
|
|
2774
|
+
warning: REGENERATE_WARNING
|
|
2775
|
+
}
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
async function verifyX402Request({
|
|
2779
|
+
request,
|
|
2780
|
+
isCachedAddress,
|
|
2781
|
+
acceptedNetwork
|
|
2782
|
+
}) {
|
|
2783
|
+
const headerValue = request.headers.get("payment-signature") ?? request.headers.get("x-payment");
|
|
2784
|
+
if (!headerValue) {
|
|
2785
|
+
return {
|
|
2786
|
+
ok: false,
|
|
2787
|
+
status: 400,
|
|
2788
|
+
body: regenerateBody(
|
|
2789
|
+
"X-Payment header missing",
|
|
2790
|
+
"No X-Payment header was sent. Generate the credential from the 402 challenge and resubmit on the same endpoint."
|
|
2791
|
+
)
|
|
2792
|
+
};
|
|
676
2793
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
2794
|
+
let payload;
|
|
2795
|
+
try {
|
|
2796
|
+
payload = JSON.parse(Buffer.from(headerValue, "base64").toString());
|
|
2797
|
+
} catch {
|
|
2798
|
+
return {
|
|
2799
|
+
ok: false,
|
|
2800
|
+
status: 400,
|
|
2801
|
+
body: regenerateBody(
|
|
2802
|
+
"X-Payment header is not valid base64 JSON",
|
|
2803
|
+
"The payment credential could not be decoded. Reconstruct the credential from the 402 challenge and retry."
|
|
2804
|
+
)
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
const signedNetwork = payload.accepted?.network;
|
|
2808
|
+
const signedPayTo = payload.accepted?.payTo;
|
|
2809
|
+
if (!signedNetwork || signedNetwork !== acceptedNetwork) {
|
|
2810
|
+
if (signedNetwork && signedNetwork.toLowerCase().startsWith("solana:")) {
|
|
2811
|
+
return {
|
|
2812
|
+
ok: false,
|
|
2813
|
+
status: 400,
|
|
2814
|
+
body: regenerateBody(
|
|
2815
|
+
`x402 on ${signedNetwork} is not accepted; Solana payments must use the \`solana/charge\` rail advertised in the 402 challenge. This server accepts x402 on ${acceptedNetwork} only.`,
|
|
2816
|
+
"Solana payments are not accepted over x402 at this merchant. Pick the `solana/charge` rail from the 402 challenge and re-sign."
|
|
2817
|
+
)
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
return {
|
|
2821
|
+
ok: false,
|
|
2822
|
+
status: 400,
|
|
2823
|
+
body: regenerateBody(
|
|
2824
|
+
`Unsupported x402 network ${signedNetwork ?? "<missing>"}; this server accepts ${acceptedNetwork}.`,
|
|
2825
|
+
"The credential signed for an unsupported network. Pick the accepted network from the 402 challenge and re-sign."
|
|
2826
|
+
)
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
const addressShapeOk = typeof signedPayTo === "string" && EVM_ADDRESS_RE.test(signedPayTo);
|
|
2830
|
+
if (!signedPayTo || !addressShapeOk) {
|
|
2831
|
+
return {
|
|
2832
|
+
ok: false,
|
|
2833
|
+
status: 400,
|
|
2834
|
+
body: regenerateBody(
|
|
2835
|
+
`Payment payload missing or malformed accepted.payTo address for network ${signedNetwork}`,
|
|
2836
|
+
"The credential payload is missing or malformed payTo for the signed network. Reconstruct the credential from the 402 challenge."
|
|
2837
|
+
)
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
if (!await isCachedAddress(signedPayTo)) {
|
|
2841
|
+
return {
|
|
2842
|
+
ok: false,
|
|
2843
|
+
status: 400,
|
|
2844
|
+
body: regenerateBody(
|
|
2845
|
+
"payTo address not found in cache or expired. Request a fresh 402 challenge and retry.",
|
|
2846
|
+
"The deposit address is unknown or expired on this server. Request a fresh 402 challenge and re-sign against the new payTo."
|
|
2847
|
+
)
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
return { ok: true, payload, signedNetwork, signedPayTo };
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/payment/zero-settle.ts
|
|
2854
|
+
var EVM_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
2855
|
+
var SOLANA_BASE58_RE2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
2856
|
+
var NULL_RESULT = {
|
|
2857
|
+
signerAddress: null,
|
|
2858
|
+
signerNetwork: null,
|
|
2859
|
+
txHash: null
|
|
2860
|
+
};
|
|
2861
|
+
function zeroAmountCarveOut({
|
|
2862
|
+
rail,
|
|
2863
|
+
payload,
|
|
2864
|
+
authorizationHeader
|
|
2865
|
+
}) {
|
|
2866
|
+
if (rail === "x402-base") {
|
|
2867
|
+
return x402SignerFromPayload(payload);
|
|
2868
|
+
}
|
|
2869
|
+
if (rail === "tempo" || rail === "solana") {
|
|
2870
|
+
return mppSignerFromAuth(authorizationHeader);
|
|
2871
|
+
}
|
|
2872
|
+
return NULL_RESULT;
|
|
2873
|
+
}
|
|
2874
|
+
function x402SignerFromPayload(payload) {
|
|
2875
|
+
if (!payload || typeof payload !== "object") return NULL_RESULT;
|
|
2876
|
+
const inner = payload.payload;
|
|
2877
|
+
if (!inner || typeof inner !== "object") return NULL_RESULT;
|
|
2878
|
+
const authorization = inner.authorization;
|
|
2879
|
+
if (!authorization || typeof authorization !== "object") return NULL_RESULT;
|
|
2880
|
+
const fromAddr = authorization.from;
|
|
2881
|
+
if (typeof fromAddr !== "string" || !EVM_RE.test(fromAddr)) return NULL_RESULT;
|
|
2882
|
+
return {
|
|
2883
|
+
signerAddress: fromAddr.toLowerCase(),
|
|
2884
|
+
signerNetwork: "evm",
|
|
2885
|
+
txHash: null
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
function mppSignerFromAuth(authorizationHeader) {
|
|
2889
|
+
if (typeof authorizationHeader !== "string") return NULL_RESULT;
|
|
2890
|
+
if (!authorizationHeader.toLowerCase().startsWith("payment ")) return NULL_RESULT;
|
|
2891
|
+
const token = authorizationHeader.slice("payment ".length).trim();
|
|
2892
|
+
if (!token) return NULL_RESULT;
|
|
2893
|
+
let credential;
|
|
2894
|
+
try {
|
|
2895
|
+
credential = JSON.parse(atob(token));
|
|
2896
|
+
} catch {
|
|
2897
|
+
return NULL_RESULT;
|
|
2898
|
+
}
|
|
2899
|
+
if (!credential || typeof credential !== "object") return NULL_RESULT;
|
|
2900
|
+
let source = credential.source;
|
|
2901
|
+
if (typeof source !== "string") {
|
|
2902
|
+
const challenge = credential.challenge;
|
|
2903
|
+
if (challenge && typeof challenge === "object") {
|
|
2904
|
+
source = challenge.source;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
if (typeof source !== "string") return NULL_RESULT;
|
|
2908
|
+
const parts = source.split(":");
|
|
2909
|
+
if (parts.length < 4 || parts[0] !== "did" || parts[1] !== "pkh") return NULL_RESULT;
|
|
2910
|
+
const family = parts[2];
|
|
2911
|
+
const addr = parts[parts.length - 1] ?? "";
|
|
2912
|
+
if (family === "eip155" && EVM_RE.test(addr)) {
|
|
2913
|
+
return { signerAddress: addr.toLowerCase(), signerNetwork: "evm", txHash: null };
|
|
2914
|
+
}
|
|
2915
|
+
if (family === "solana" && SOLANA_BASE58_RE2.test(addr)) {
|
|
2916
|
+
return { signerAddress: addr, signerNetwork: "solana", txHash: null };
|
|
2917
|
+
}
|
|
2918
|
+
return NULL_RESULT;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
// src/checkout.ts
|
|
2922
|
+
function pricingResult(opts) {
|
|
2923
|
+
const currency = opts.currency ?? "USD";
|
|
2924
|
+
if (opts.subtotalCents !== void 0) {
|
|
2925
|
+
const totalCents = Math.max(
|
|
2926
|
+
0,
|
|
2927
|
+
opts.subtotalCents + (opts.taxCents ?? 0) + (opts.shippingCents ?? 0) - (opts.discountCents ?? 0)
|
|
682
2928
|
);
|
|
2929
|
+
const derivedAmount = opts.amountUsd ?? totalCents / 100;
|
|
2930
|
+
const block = buildPricingBlock({
|
|
2931
|
+
subtotalCents: opts.subtotalCents,
|
|
2932
|
+
taxCents: opts.taxCents ?? 0,
|
|
2933
|
+
...opts.shippingCents !== void 0 && { shippingCents: opts.shippingCents },
|
|
2934
|
+
...opts.discountCents !== void 0 && { discountCents: opts.discountCents },
|
|
2935
|
+
...opts.taxRate !== void 0 && { taxRate: opts.taxRate },
|
|
2936
|
+
...opts.taxState !== void 0 && { taxState: opts.taxState },
|
|
2937
|
+
currency
|
|
2938
|
+
});
|
|
2939
|
+
return {
|
|
2940
|
+
amountUsd: derivedAmount,
|
|
2941
|
+
currency,
|
|
2942
|
+
block,
|
|
2943
|
+
...opts.product !== void 0 && { product: opts.product },
|
|
2944
|
+
...opts.bodyExtras !== void 0 && { bodyExtras: opts.bodyExtras }
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
if (opts.amountUsd === void 0) {
|
|
2948
|
+
throw new Error("pricingResult requires either `subtotalCents` or `amountUsd`.");
|
|
2949
|
+
}
|
|
2950
|
+
return {
|
|
2951
|
+
amountUsd: opts.amountUsd,
|
|
2952
|
+
currency,
|
|
2953
|
+
...opts.product !== void 0 && { product: opts.product },
|
|
2954
|
+
...opts.bodyExtras !== void 0 && { bodyExtras: opts.bodyExtras }
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
function getIdentityStatus(ctx) {
|
|
2958
|
+
const assess = ctx.request.assess;
|
|
2959
|
+
if (assess === null || assess === void 0) return "anonymous";
|
|
2960
|
+
const decision = assess.decision;
|
|
2961
|
+
if (decision === "allow") return "verified";
|
|
2962
|
+
return "unverified";
|
|
2963
|
+
}
|
|
2964
|
+
var CheckoutValidationError = class extends Error {
|
|
2965
|
+
code;
|
|
2966
|
+
action;
|
|
2967
|
+
status;
|
|
2968
|
+
extra;
|
|
2969
|
+
constructor(opts) {
|
|
2970
|
+
super(opts.message);
|
|
2971
|
+
this.name = "CheckoutValidationError";
|
|
2972
|
+
this.code = opts.code;
|
|
2973
|
+
this.action = opts.action ?? "fix_request";
|
|
2974
|
+
this.status = opts.status ?? 400;
|
|
2975
|
+
this.extra = opts.extra;
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
function lowerHeaders2(headers) {
|
|
2979
|
+
const out = {};
|
|
2980
|
+
for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
|
|
2981
|
+
return out;
|
|
2982
|
+
}
|
|
2983
|
+
function hasX402Header(headers) {
|
|
2984
|
+
const h = lowerHeaders2(headers);
|
|
2985
|
+
return Boolean(h["payment-signature"] ?? h["x-payment"]);
|
|
2986
|
+
}
|
|
2987
|
+
function hasMppxHeader(headers) {
|
|
2988
|
+
const h = lowerHeaders2(headers);
|
|
2989
|
+
return (h["authorization"] ?? "").startsWith("Payment ");
|
|
2990
|
+
}
|
|
2991
|
+
function resolveIdentityMetadata(ctx) {
|
|
2992
|
+
const h = lowerHeaders2(ctx.request.headers);
|
|
2993
|
+
const wallet = h["x-wallet-address"];
|
|
2994
|
+
if (!wallet) return void 0;
|
|
2995
|
+
let linkedWallets;
|
|
2996
|
+
const assess = ctx.request.assess;
|
|
2997
|
+
if (assess && typeof assess === "object") {
|
|
2998
|
+
const identity = assess["identity"];
|
|
2999
|
+
if (identity && typeof identity === "object") {
|
|
3000
|
+
const lw = identity["linked_wallets"];
|
|
3001
|
+
if (Array.isArray(lw) && lw.every((x) => typeof x === "string")) {
|
|
3002
|
+
linkedWallets = lw;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return buildIdentityMetadata({
|
|
3007
|
+
mode: "wallet",
|
|
3008
|
+
wallet,
|
|
3009
|
+
...linkedWallets !== void 0 ? { linkedWallets } : {}
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
function isStripeRailSpec2(s) {
|
|
3013
|
+
return !("recipient" in s);
|
|
3014
|
+
}
|
|
3015
|
+
function isTempoSessionRailSpec3(s) {
|
|
3016
|
+
return "escrowContract" in s && "store" in s;
|
|
3017
|
+
}
|
|
3018
|
+
function specRailKey(spec) {
|
|
3019
|
+
if (isStripeRailSpec2(spec)) return "stripe";
|
|
3020
|
+
if (isTempoSessionRailSpec3(spec)) return "tempo_mpp";
|
|
3021
|
+
const network = spec.network ?? "";
|
|
3022
|
+
if (network.startsWith("eip155:")) return "x402_base";
|
|
3023
|
+
if (network.startsWith("solana:") || "rpcUrl" in spec) return "solana_mpp";
|
|
3024
|
+
return "tempo_mpp";
|
|
3025
|
+
}
|
|
3026
|
+
function specMethodName(spec) {
|
|
3027
|
+
if (isStripeRailSpec2(spec)) return "stripe/spt";
|
|
3028
|
+
if (isTempoSessionRailSpec3(spec)) return "tempo/charge";
|
|
3029
|
+
const network = spec.network ?? "";
|
|
3030
|
+
if (network.startsWith("eip155:")) return "x402/exact (base)";
|
|
3031
|
+
if (network.startsWith("solana:") || "rpcUrl" in spec) return "solana/charge";
|
|
3032
|
+
return "tempo/charge";
|
|
3033
|
+
}
|
|
3034
|
+
function makeMppxComposeHook(opts) {
|
|
3035
|
+
return async (ctx) => {
|
|
3036
|
+
if (ctx.pricing === null) return { status: 402 };
|
|
3037
|
+
const mpp = await opts.serverGetter();
|
|
3038
|
+
const lower = lowerHeaders2(ctx.request.headers);
|
|
3039
|
+
const authorization = lower["authorization"];
|
|
3040
|
+
const amountStr = ctx.pricing.amountUsd.toFixed(2);
|
|
3041
|
+
let result;
|
|
3042
|
+
try {
|
|
3043
|
+
result = await mpp.charge({ authorization, amount: amountStr });
|
|
3044
|
+
} catch {
|
|
3045
|
+
return { status: 402 };
|
|
3046
|
+
}
|
|
3047
|
+
if (!Array.isArray(result)) {
|
|
3048
|
+
const challenge = result;
|
|
3049
|
+
const realm = mpp.realm ?? "";
|
|
3050
|
+
const headers = typeof challenge.toWwwAuthenticate === "function" ? { "www-authenticate": challenge.toWwwAuthenticate(realm) } : {};
|
|
3051
|
+
return { status: 402, headers };
|
|
3052
|
+
}
|
|
3053
|
+
const [credential, receipt] = result;
|
|
3054
|
+
const txHash = receipt.reference ?? receipt.transaction ?? null;
|
|
3055
|
+
let signerAddress = null;
|
|
3056
|
+
let signerNetwork = null;
|
|
3057
|
+
const source = credential.source;
|
|
3058
|
+
if (typeof source === "string") {
|
|
3059
|
+
const parts = source.split(":");
|
|
3060
|
+
if (parts.length >= 4 && parts[0] === "did" && parts[1] === "pkh") {
|
|
3061
|
+
const family = parts[2];
|
|
3062
|
+
const addr = parts[parts.length - 1] ?? null;
|
|
3063
|
+
if (family === "eip155" && addr !== null) {
|
|
3064
|
+
signerAddress = addr.toLowerCase();
|
|
3065
|
+
signerNetwork = "evm";
|
|
3066
|
+
} else if (family === "solana" && addr !== null) {
|
|
3067
|
+
signerAddress = addr;
|
|
3068
|
+
signerNetwork = "solana";
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return {
|
|
3073
|
+
status: 200,
|
|
3074
|
+
txHash,
|
|
3075
|
+
signerAddress,
|
|
3076
|
+
signerNetwork,
|
|
3077
|
+
raw: { credential, receipt }
|
|
3078
|
+
};
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
function applyRecipientOverrides(rails2, overrides) {
|
|
3082
|
+
const out = {};
|
|
3083
|
+
for (const [key, spec] of Object.entries(rails2)) {
|
|
3084
|
+
if (isStripeRailSpec2(spec)) {
|
|
3085
|
+
out[key] = spec;
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
const override = overrides[key];
|
|
3089
|
+
const finalRecipient = override ?? spec.recipient;
|
|
3090
|
+
if (finalRecipient === "" || finalRecipient === void 0) continue;
|
|
3091
|
+
out[key] = override !== void 0 ? { ...spec, recipient: override } : spec;
|
|
3092
|
+
}
|
|
3093
|
+
return out;
|
|
3094
|
+
}
|
|
3095
|
+
function pickRail(rails2, key) {
|
|
3096
|
+
const spec = rails2[key];
|
|
3097
|
+
return spec === void 0 ? void 0 : spec;
|
|
3098
|
+
}
|
|
3099
|
+
var Checkout = class {
|
|
3100
|
+
rails;
|
|
3101
|
+
url;
|
|
3102
|
+
merchantName;
|
|
3103
|
+
computePricing;
|
|
3104
|
+
preValidate;
|
|
3105
|
+
x402Server;
|
|
3106
|
+
composeMppx;
|
|
3107
|
+
mintRecipients;
|
|
3108
|
+
mintReferenceId;
|
|
3109
|
+
onSettled;
|
|
3110
|
+
isCachedAddress;
|
|
3111
|
+
zeroSettleCarveOut;
|
|
3112
|
+
gate;
|
|
3113
|
+
discoveryExtensions;
|
|
3114
|
+
discoveryProbe;
|
|
3115
|
+
_x402ServerGetter;
|
|
3116
|
+
constructor(opts) {
|
|
3117
|
+
const x402Server = opts.x402Server;
|
|
3118
|
+
let x402ServerGetter;
|
|
3119
|
+
if (x402Server === void 0) {
|
|
3120
|
+
const baseSpec = Object.values(opts.rails).find(
|
|
3121
|
+
(s) => !isTempoSessionRailSpec3(s) && !isStripeRailSpec2(s) && "recipient" in s && (s.network ?? "").startsWith("eip155:")
|
|
3122
|
+
);
|
|
3123
|
+
if (baseSpec !== void 0) {
|
|
3124
|
+
x402ServerGetter = lazyX402Server({
|
|
3125
|
+
spec: baseSpec,
|
|
3126
|
+
cdpApiKeyId: opts.cdpApiKeyId,
|
|
3127
|
+
cdpApiKeySecret: opts.cdpApiKeySecret
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
} else {
|
|
3131
|
+
const baseSpec = opts.rails["x402_base"];
|
|
3132
|
+
if (baseSpec === void 0 || !("recipient" in baseSpec)) {
|
|
3133
|
+
throw new Error(
|
|
3134
|
+
"Checkout: x402Server requires an X402BaseRailSpec in rails['x402_base'] (the rail's `network` field supplies the CAIP-2)."
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
let composeMppx = opts.composeMppx;
|
|
3139
|
+
if (composeMppx === void 0 && opts.mppxSecretKey !== void 0) {
|
|
3140
|
+
const mppRails = {};
|
|
3141
|
+
for (const [k, v] of Object.entries(opts.rails)) {
|
|
3142
|
+
if (isStripeRailSpec2(v) || isTempoSessionRailSpec3(v) || "recipient" in v) {
|
|
3143
|
+
mppRails[k] = v;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
const getter = lazyMppxServer({
|
|
3147
|
+
rails: mppRails,
|
|
3148
|
+
secretKey: opts.mppxSecretKey
|
|
3149
|
+
});
|
|
3150
|
+
composeMppx = makeMppxComposeHook({ serverGetter: getter });
|
|
3151
|
+
}
|
|
3152
|
+
this.rails = opts.rails;
|
|
3153
|
+
this.url = opts.url;
|
|
3154
|
+
this.merchantName = opts.gate?.merchantName;
|
|
3155
|
+
this.computePricing = opts.computePricing;
|
|
3156
|
+
this.preValidate = opts.preValidate;
|
|
3157
|
+
this.x402Server = x402Server;
|
|
3158
|
+
this._x402ServerGetter = x402ServerGetter;
|
|
3159
|
+
this.composeMppx = composeMppx;
|
|
3160
|
+
this.mintRecipients = opts.mintRecipients;
|
|
3161
|
+
this.mintReferenceId = opts.mintReferenceId;
|
|
3162
|
+
this.onSettled = opts.onSettled;
|
|
3163
|
+
this.isCachedAddress = opts.isCachedAddress;
|
|
3164
|
+
this.zeroSettleCarveOut = opts.zeroSettleCarveOut ?? false;
|
|
3165
|
+
this.gate = opts.gate;
|
|
3166
|
+
this.discoveryExtensions = opts.discoveryExtensions;
|
|
3167
|
+
this.discoveryProbe = opts.discoveryProbe;
|
|
683
3168
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
)
|
|
3169
|
+
/** Canonical `RailKey` list derived from the configured rails dict. Each
|
|
3170
|
+
* `*RailSpec` type maps to one `RailKey` (Tempo and TempoSession both fold
|
|
3171
|
+
* to `"tempo_mpp"`). Dedupes so listing is per protocol, not per recipient.
|
|
3172
|
+
* Use in `.well-known/mpp.json`, skill.md / llms.txt discovery responses. */
|
|
3173
|
+
get acceptedRails() {
|
|
3174
|
+
const out = [];
|
|
3175
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3176
|
+
for (const spec of Object.values(this.rails)) {
|
|
3177
|
+
const key = specRailKey(spec);
|
|
3178
|
+
if (seen.has(key)) continue;
|
|
3179
|
+
seen.add(key);
|
|
3180
|
+
out.push(key);
|
|
3181
|
+
}
|
|
3182
|
+
return out;
|
|
692
3183
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
3184
|
+
/** Protocol-shaped method-name list (`"tempo/charge"`, `"x402/exact (base)"`).
|
|
3185
|
+
* Suitable for the `methods: [...]` array of `.well-known/mpp.json`. */
|
|
3186
|
+
get acceptedMethodNames() {
|
|
3187
|
+
const out = [];
|
|
3188
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3189
|
+
for (const spec of Object.values(this.rails)) {
|
|
3190
|
+
const name = specMethodName(spec);
|
|
3191
|
+
if (seen.has(name)) continue;
|
|
3192
|
+
seen.add(name);
|
|
3193
|
+
out.push(name);
|
|
701
3194
|
}
|
|
702
|
-
|
|
703
|
-
} catch (err) {
|
|
704
|
-
throw new UCPVerificationError(
|
|
705
|
-
"malformed_jws",
|
|
706
|
-
`JWS protected header is not valid base64url-encoded JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
707
|
-
);
|
|
3195
|
+
return out;
|
|
708
3196
|
}
|
|
709
|
-
|
|
710
|
-
|
|
3197
|
+
/** Resolve the x402 server, awaiting the lazy getter on first use. */
|
|
3198
|
+
async getX402Server() {
|
|
3199
|
+
if (this.x402Server !== void 0) return this.x402Server;
|
|
3200
|
+
if (this._x402ServerGetter === void 0) return void 0;
|
|
3201
|
+
this.x402Server = await this._x402ServerGetter();
|
|
3202
|
+
return this.x402Server;
|
|
711
3203
|
}
|
|
712
|
-
|
|
713
|
-
|
|
3204
|
+
x402ServerAvailable() {
|
|
3205
|
+
return this.x402Server !== void 0 || this._x402ServerGetter !== void 0;
|
|
714
3206
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
3207
|
+
/** Return the rails-dict key for the X402BaseRailSpec entry. Defaults to
|
|
3208
|
+
* `"x402_base"` when no match found. */
|
|
3209
|
+
x402RailKey() {
|
|
3210
|
+
for (const [k, v] of Object.entries(this.rails)) {
|
|
3211
|
+
if (!isStripeRailSpec2(v) && !isTempoSessionRailSpec3(v) && (v.network ?? "").startsWith("eip155:")) {
|
|
3212
|
+
return k;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
return "x402_base";
|
|
720
3216
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
`JWS protected header crit must be a non-empty array of strings; got ${JSON.stringify(crit)}.`
|
|
727
|
-
);
|
|
3217
|
+
/** Return the rails-dict key for the primary MPP rail. */
|
|
3218
|
+
mppRailKey() {
|
|
3219
|
+
for (const [k, v] of Object.entries(this.rails)) {
|
|
3220
|
+
const network = v.network ?? "";
|
|
3221
|
+
if (!isStripeRailSpec2(v) && !network.startsWith("eip155:")) return k;
|
|
728
3222
|
}
|
|
729
|
-
|
|
730
|
-
"unrecognized_critical_header",
|
|
731
|
-
`JWS protected header advertises unrecognized crit headers: ${JSON.stringify(crit)}.`
|
|
732
|
-
);
|
|
3223
|
+
return "tempo";
|
|
733
3224
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
3225
|
+
/** CAIP-2 read from `rails['x402_base'].network` (or its default).
|
|
3226
|
+
* Defined only when an `X402BaseRailSpec` is present in rails AND a server
|
|
3227
|
+
* is configured (explicit or auto-derived); otherwise `null`. */
|
|
3228
|
+
get x402BaseNetwork() {
|
|
3229
|
+
if (!this.x402ServerAvailable()) return null;
|
|
3230
|
+
for (const spec of Object.values(this.rails)) {
|
|
3231
|
+
if (!isStripeRailSpec2(spec) && !isTempoSessionRailSpec3(spec) && (spec.network ?? "").startsWith("eip155:")) {
|
|
3232
|
+
return spec.network ?? "eip155:8453";
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return null;
|
|
3236
|
+
}
|
|
3237
|
+
async handle(request) {
|
|
3238
|
+
const referenceId = await this.mintRefId(request);
|
|
3239
|
+
const ctx = {
|
|
3240
|
+
request,
|
|
3241
|
+
referenceId,
|
|
3242
|
+
pricing: null,
|
|
3243
|
+
recipients: {},
|
|
3244
|
+
state: {}
|
|
3245
|
+
};
|
|
3246
|
+
if (this.discoveryProbe !== void 0 && request.method === "POST") {
|
|
3247
|
+
const auth = request.headers["authorization"] ?? request.headers["Authorization"];
|
|
3248
|
+
const isProbe = !auth?.startsWith("Payment ") && !hasX402Header(request.headers) && !hasMppxHeader(request.headers) && (request.body === void 0 || request.body === null || typeof request.body === "object" && Object.keys(request.body).length === 0);
|
|
3249
|
+
if (isProbe) {
|
|
3250
|
+
const { buildDiscoveryProbeResponse: buildDiscoveryProbeResponse2 } = await Promise.resolve().then(() => (init_probe(), probe_exports));
|
|
3251
|
+
const cfg = this.discoveryProbe;
|
|
3252
|
+
const probe = buildDiscoveryProbeResponse2({
|
|
3253
|
+
realm: cfg.realm,
|
|
3254
|
+
sampleRail: cfg.sampleRail,
|
|
3255
|
+
sampleAmountUsd: cfg.sampleAmountUsd,
|
|
3256
|
+
sampleRecipient: cfg.sampleRecipient,
|
|
3257
|
+
...cfg.intent !== void 0 && { intent: cfg.intent },
|
|
3258
|
+
...cfg.ttlSeconds !== void 0 && { ttlSeconds: cfg.ttlSeconds },
|
|
3259
|
+
...cfg.docsUrl !== void 0 && { docsUrl: cfg.docsUrl },
|
|
3260
|
+
...cfg.message !== void 0 && { message: cfg.message },
|
|
3261
|
+
...cfg.x402Sample !== void 0 && { x402Sample: cfg.x402Sample }
|
|
3262
|
+
});
|
|
3263
|
+
return {
|
|
3264
|
+
status: probe.status,
|
|
3265
|
+
body: JSON.parse(probe.body),
|
|
3266
|
+
headers: probe.headers,
|
|
3267
|
+
referenceId: ctx.referenceId,
|
|
3268
|
+
settled: false
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
if (this.preValidate !== void 0) {
|
|
3273
|
+
try {
|
|
3274
|
+
const state = await this.preValidate(ctx);
|
|
3275
|
+
if (state && typeof state === "object") Object.assign(ctx.state, state);
|
|
3276
|
+
} catch (err) {
|
|
3277
|
+
if (err instanceof CheckoutValidationError) {
|
|
3278
|
+
return this.validationErrorResult(ctx, err);
|
|
745
3279
|
}
|
|
746
|
-
|
|
747
|
-
|
|
3280
|
+
throw err;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const hasPaymentHeader = hasX402Header(request.headers) || hasMppxHeader(request.headers);
|
|
3284
|
+
if (this.gate !== void 0 && hasPaymentHeader) {
|
|
3285
|
+
const denial = await this.runGate(ctx);
|
|
3286
|
+
if (denial !== null) {
|
|
3287
|
+
return {
|
|
3288
|
+
status: denial.status,
|
|
3289
|
+
body: denial.body,
|
|
3290
|
+
headers: { ...denial.headers ?? {} },
|
|
3291
|
+
referenceId: ctx.referenceId,
|
|
3292
|
+
settled: false
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
ctx.pricing = await this.computePricing(ctx);
|
|
3297
|
+
await this.resolveRecipientsForCtx(ctx);
|
|
3298
|
+
const x402ServerOk = this.x402ServerAvailable() && this.x402BaseNetwork !== null;
|
|
3299
|
+
if (hasX402Header(request.headers) && x402ServerOk) {
|
|
3300
|
+
const zero = await this.handleZeroSettle(ctx, "x402-base");
|
|
3301
|
+
if (zero !== null) return zero;
|
|
3302
|
+
return await this.handleX402(ctx);
|
|
3303
|
+
}
|
|
3304
|
+
if (hasMppxHeader(request.headers) && this.composeMppx !== void 0) {
|
|
3305
|
+
const zero = await this.handleZeroSettle(ctx, "tempo");
|
|
3306
|
+
if (zero !== null) return zero;
|
|
3307
|
+
return await this.handleMppx(ctx);
|
|
3308
|
+
}
|
|
3309
|
+
await this.resolveRecipientsForCtx(ctx);
|
|
3310
|
+
let mppxHeaders = {};
|
|
3311
|
+
if (this.composeMppx !== void 0) {
|
|
3312
|
+
try {
|
|
3313
|
+
const preComposed = await this.composeMppx(ctx);
|
|
3314
|
+
if (preComposed.status === 402) {
|
|
3315
|
+
mppxHeaders = { ...preComposed.headers ?? {} };
|
|
3316
|
+
}
|
|
3317
|
+
} catch {
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
return await this.emit402(ctx, mppxHeaders);
|
|
3321
|
+
}
|
|
3322
|
+
validationErrorResult(ctx, err) {
|
|
3323
|
+
const body = buildValidationError({
|
|
3324
|
+
code: err.code,
|
|
3325
|
+
message: err.message,
|
|
3326
|
+
nextSteps: { action: err.action, user_message: err.message },
|
|
3327
|
+
extra: err.extra
|
|
3328
|
+
});
|
|
3329
|
+
return {
|
|
3330
|
+
status: err.status,
|
|
3331
|
+
body,
|
|
3332
|
+
headers: {},
|
|
3333
|
+
referenceId: ctx.referenceId,
|
|
3334
|
+
settled: false
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
async runGate(ctx) {
|
|
3338
|
+
const gate = this.gate;
|
|
3339
|
+
if (gate === void 0) return null;
|
|
3340
|
+
if (gate.runGate !== void 0) {
|
|
3341
|
+
const result = await gate.runGate(ctx);
|
|
3342
|
+
if (result === void 0 || result === null) return null;
|
|
3343
|
+
if (typeof result !== "object" || typeof result.status !== "number") {
|
|
3344
|
+
throw new TypeError(
|
|
3345
|
+
"gate.runGate must return null/undefined (allow) or an object { status, body, headers? } (deny)"
|
|
748
3346
|
);
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
3347
|
+
}
|
|
3348
|
+
return result;
|
|
3349
|
+
}
|
|
3350
|
+
if (gate.apiKey === void 0) return null;
|
|
3351
|
+
let policyOverride;
|
|
3352
|
+
if (gate.perRequestPolicy !== void 0) {
|
|
3353
|
+
policyOverride = await gate.perRequestPolicy(ctx);
|
|
3354
|
+
if (policyOverride === null) return null;
|
|
3355
|
+
}
|
|
3356
|
+
const coreOpts = {
|
|
3357
|
+
apiKey: gate.apiKey,
|
|
3358
|
+
...gate.baseUrl !== void 0 && { baseUrl: gate.baseUrl },
|
|
3359
|
+
...gate.userAgent !== void 0 && { userAgent: gate.userAgent },
|
|
3360
|
+
...gate.requireKyc !== void 0 && { requireKyc: gate.requireKyc },
|
|
3361
|
+
...gate.requireSanctionsClear !== void 0 && { requireSanctionsClear: gate.requireSanctionsClear },
|
|
3362
|
+
...gate.minAge !== void 0 && { minAge: gate.minAge },
|
|
3363
|
+
...gate.blockedJurisdictions !== void 0 && { blockedJurisdictions: gate.blockedJurisdictions },
|
|
3364
|
+
...gate.allowedJurisdictions !== void 0 && { allowedJurisdictions: gate.allowedJurisdictions },
|
|
3365
|
+
...gate.failOpen !== void 0 && { failOpen: gate.failOpen },
|
|
3366
|
+
...gate.cacheSeconds !== void 0 && { cacheSeconds: gate.cacheSeconds },
|
|
3367
|
+
...gate.chain !== void 0 && { chain: gate.chain },
|
|
3368
|
+
...gate.createSessionOnMissing !== void 0 && {
|
|
3369
|
+
createSessionOnMissing: gate.createSessionOnMissing
|
|
3370
|
+
},
|
|
3371
|
+
...policyOverride ?? {}
|
|
3372
|
+
};
|
|
3373
|
+
const core = createAgentScoreCore(coreOpts);
|
|
3374
|
+
const headers = lowerHeaders2(ctx.request.headers);
|
|
3375
|
+
const walletAddress = headers["x-wallet-address"];
|
|
3376
|
+
const operatorToken = headers["x-operator-token"];
|
|
3377
|
+
const identity = walletAddress !== void 0 || operatorToken !== void 0 ? {
|
|
3378
|
+
...walletAddress !== void 0 && { address: walletAddress },
|
|
3379
|
+
...operatorToken !== void 0 && { operatorToken }
|
|
3380
|
+
} : void 0;
|
|
3381
|
+
const x402Header = headers["payment-signature"] ?? headers["x-payment"];
|
|
3382
|
+
const signer = await extractPaymentSignerFromAuth(headers["authorization"], x402Header);
|
|
3383
|
+
const outcome = await core.evaluate(identity, ctx, signer);
|
|
3384
|
+
if (outcome.kind === "allow") {
|
|
3385
|
+
if (operatorToken !== void 0) {
|
|
3386
|
+
const opToken = operatorToken;
|
|
3387
|
+
ctx.captureWallet = async (opts) => {
|
|
3388
|
+
await core.captureWallet({
|
|
3389
|
+
operatorToken: opToken,
|
|
3390
|
+
walletAddress: opts.walletAddress,
|
|
3391
|
+
network: opts.network,
|
|
3392
|
+
...opts.idempotencyKey !== void 0 && { idempotencyKey: opts.idempotencyKey }
|
|
3393
|
+
});
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
if (walletAddress !== void 0) {
|
|
3397
|
+
const verdict = core.getSignerVerdict(walletAddress);
|
|
3398
|
+
const sm = verdict?.signer_match;
|
|
3399
|
+
if (sm && sm.kind !== "pass") {
|
|
3400
|
+
const reason2 = sm.kind === "wallet_auth_requires_wallet_signing" ? {
|
|
3401
|
+
code: "wallet_auth_requires_wallet_signing",
|
|
3402
|
+
expected_signer: sm.claimedWallet,
|
|
3403
|
+
agent_instructions: sm.agentInstructions
|
|
3404
|
+
} : {
|
|
3405
|
+
code: "wallet_signer_mismatch",
|
|
3406
|
+
...sm.claimedOperator !== null && { claimed_operator: sm.claimedOperator },
|
|
3407
|
+
actual_signer_operator: sm.actualSignerOperator,
|
|
3408
|
+
expected_signer: sm.expectedSigner,
|
|
3409
|
+
actual_signer: sm.actualSigner,
|
|
3410
|
+
...sm.linkedWallets.length > 0 && { linked_wallets: sm.linkedWallets },
|
|
3411
|
+
agent_instructions: sm.agentInstructions
|
|
3412
|
+
};
|
|
3413
|
+
if (gate.onDenied !== void 0) {
|
|
3414
|
+
const custom = await gate.onDenied(ctx, reason2);
|
|
3415
|
+
if (custom !== null) return custom;
|
|
3416
|
+
}
|
|
3417
|
+
const body2 = denialReasonToBody(reason2);
|
|
3418
|
+
return { status: 403, body: body2 };
|
|
754
3419
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
3420
|
+
}
|
|
3421
|
+
return null;
|
|
3422
|
+
}
|
|
3423
|
+
const reason = outcome.reason;
|
|
3424
|
+
if (gate.onDenied !== void 0) {
|
|
3425
|
+
const custom = await gate.onDenied(ctx, reason);
|
|
3426
|
+
if (custom !== null) return custom;
|
|
3427
|
+
}
|
|
3428
|
+
const body = denialReasonToBody(reason);
|
|
3429
|
+
const status = reason.code === "token_expired" || reason.code === "invalid_credential" ? 401 : reason.code === "api_error" ? 503 : 403;
|
|
3430
|
+
return { status, body };
|
|
3431
|
+
}
|
|
3432
|
+
async handleZeroSettle(ctx, rail) {
|
|
3433
|
+
if (!this.zeroSettleCarveOut || ctx.pricing === null) return null;
|
|
3434
|
+
const cents = Math.round(ctx.pricing.amountUsd * 100);
|
|
3435
|
+
if (cents !== 0) return null;
|
|
3436
|
+
const headers = lowerHeaders2(ctx.request.headers);
|
|
3437
|
+
let zero;
|
|
3438
|
+
if (rail === "x402-base") {
|
|
3439
|
+
const x402Header = headers["payment-signature"] ?? headers["x-payment"];
|
|
3440
|
+
let payload = null;
|
|
3441
|
+
if (typeof x402Header === "string" && x402Header.length > 0) {
|
|
3442
|
+
try {
|
|
3443
|
+
payload = JSON.parse(atob(x402Header));
|
|
3444
|
+
} catch {
|
|
3445
|
+
payload = null;
|
|
760
3446
|
}
|
|
761
|
-
return jose.importJWK(matches[0], h.alg);
|
|
762
3447
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (err instanceof UCPVerificationError) throw err;
|
|
767
|
-
if (err instanceof Error && err.name === "JOSEAlgNotAllowed") {
|
|
768
|
-
throw new UCPVerificationError("unsupported_alg", `UCP signing alg not allowed: ${err.message}`);
|
|
3448
|
+
zero = zeroAmountCarveOut({ rail, payload });
|
|
3449
|
+
} else {
|
|
3450
|
+
zero = zeroAmountCarveOut({ rail, authorizationHeader: headers["authorization"] });
|
|
769
3451
|
}
|
|
770
|
-
|
|
771
|
-
|
|
3452
|
+
const railKey = rail === "x402-base" ? this.x402RailKey() : this.mppRailKey();
|
|
3453
|
+
const outcome = {
|
|
3454
|
+
rail: rail === "x402-base" ? "x402" : "mpp",
|
|
3455
|
+
paymentResponseHeader: null,
|
|
3456
|
+
raw: zero,
|
|
3457
|
+
txHash: null,
|
|
3458
|
+
signerAddress: zero.signerAddress,
|
|
3459
|
+
signerNetwork: zero.signerNetwork,
|
|
3460
|
+
railKey
|
|
3461
|
+
};
|
|
3462
|
+
return await this.buildSuccess(ctx, outcome);
|
|
3463
|
+
}
|
|
3464
|
+
async mintRefId(request) {
|
|
3465
|
+
if (this.mintReferenceId === void 0) return randomUUID();
|
|
3466
|
+
const seedCtx = { request, referenceId: "", pricing: null, recipients: {}, state: {} };
|
|
3467
|
+
return await this.mintReferenceId(seedCtx);
|
|
3468
|
+
}
|
|
3469
|
+
async resolveRecipientsForCtx(ctx) {
|
|
3470
|
+
if (this.mintRecipients === void 0) return ctx.recipients;
|
|
3471
|
+
if (Object.keys(ctx.recipients).length > 0) return ctx.recipients;
|
|
3472
|
+
ctx.recipients = { ...await this.mintRecipients(ctx) };
|
|
3473
|
+
return ctx.recipients;
|
|
3474
|
+
}
|
|
3475
|
+
async asyncIsCachedAddress(address) {
|
|
3476
|
+
if (this.isCachedAddress === void 0) return true;
|
|
3477
|
+
return Promise.resolve(this.isCachedAddress(address));
|
|
3478
|
+
}
|
|
3479
|
+
async handleX402(ctx) {
|
|
3480
|
+
const x402Server = await this.getX402Server();
|
|
3481
|
+
if (ctx.pricing === null || this.x402BaseNetwork === null || x402Server === void 0) {
|
|
3482
|
+
throw new Error("Checkout.handleX402: missing pricing or x402 rail config");
|
|
772
3483
|
}
|
|
773
|
-
|
|
774
|
-
|
|
3484
|
+
const fakeRequest = new Request(ctx.request.url, {
|
|
3485
|
+
method: ctx.request.method,
|
|
3486
|
+
headers: ctx.request.headers
|
|
3487
|
+
});
|
|
3488
|
+
const verified = await verifyX402Request({
|
|
3489
|
+
request: fakeRequest,
|
|
3490
|
+
isCachedAddress: this.asyncIsCachedAddress.bind(this),
|
|
3491
|
+
acceptedNetwork: this.x402BaseNetwork
|
|
3492
|
+
});
|
|
3493
|
+
if (!verified.ok) {
|
|
3494
|
+
return {
|
|
3495
|
+
status: verified.status,
|
|
3496
|
+
body: verified.body,
|
|
3497
|
+
headers: {},
|
|
3498
|
+
referenceId: ctx.referenceId,
|
|
3499
|
+
settled: false,
|
|
3500
|
+
settlePhase: "verify_failed"
|
|
3501
|
+
};
|
|
775
3502
|
}
|
|
776
|
-
|
|
777
|
-
|
|
3503
|
+
const settle = await processX402Settle({
|
|
3504
|
+
x402Server,
|
|
3505
|
+
payload: verified.payload,
|
|
3506
|
+
resourceConfig: {
|
|
3507
|
+
scheme: "exact",
|
|
3508
|
+
network: verified.signedNetwork,
|
|
3509
|
+
price: `$${ctx.pricing.amountUsd.toFixed(2)}`,
|
|
3510
|
+
payTo: verified.signedPayTo,
|
|
3511
|
+
maxTimeoutSeconds: 300
|
|
3512
|
+
},
|
|
3513
|
+
resourceMeta: {
|
|
3514
|
+
url: ctx.request.url,
|
|
3515
|
+
description: "Agent purchase via x402",
|
|
3516
|
+
mimeType: "application/json"
|
|
3517
|
+
}
|
|
3518
|
+
});
|
|
3519
|
+
if (!settle.success) {
|
|
3520
|
+
const classified = classifyX402SettleResult(settle);
|
|
3521
|
+
const responseHeaders = classified !== null && classified.status >= 500 ? { "Cache-Control": "no-store" } : {};
|
|
3522
|
+
if (classified !== null) {
|
|
3523
|
+
return {
|
|
3524
|
+
status: classified.status,
|
|
3525
|
+
body: {
|
|
3526
|
+
error: { code: classified.code, message: classified.message },
|
|
3527
|
+
next_steps: classified.nextSteps
|
|
3528
|
+
},
|
|
3529
|
+
headers: responseHeaders,
|
|
3530
|
+
referenceId: ctx.referenceId,
|
|
3531
|
+
settled: false,
|
|
3532
|
+
settlePhase: settle.phase ?? "settle_failed"
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
return {
|
|
3536
|
+
status: 400,
|
|
3537
|
+
body: buildValidationError({
|
|
3538
|
+
code: "payment_proof_invalid",
|
|
3539
|
+
message: `Payment failed during settlement (phase: ${settle.phase ?? "unknown"}).`,
|
|
3540
|
+
nextSteps: { action: "regenerate_payment_credential" },
|
|
3541
|
+
extra: { phase: settle.phase }
|
|
3542
|
+
}),
|
|
3543
|
+
headers: {},
|
|
3544
|
+
referenceId: ctx.referenceId,
|
|
3545
|
+
settled: false,
|
|
3546
|
+
settlePhase: settle.phase ?? "settle_failed"
|
|
3547
|
+
};
|
|
778
3548
|
}
|
|
779
|
-
|
|
3549
|
+
const settleRes = settle.settleResult ?? {};
|
|
3550
|
+
const verifiedFrom = verified.payload.payload?.authorization?.from ?? null;
|
|
3551
|
+
const signerAddress = verifiedFrom !== null ? verifiedFrom.toLowerCase() : settleRes.payer ?? null;
|
|
3552
|
+
const outcome = {
|
|
3553
|
+
rail: "x402",
|
|
3554
|
+
paymentResponseHeader: settle.paymentResponseHeader ?? null,
|
|
3555
|
+
raw: settle,
|
|
3556
|
+
txHash: settleRes.transaction ?? null,
|
|
3557
|
+
signerAddress,
|
|
3558
|
+
signerNetwork: signerAddress !== null ? "evm" : null,
|
|
3559
|
+
railKey: this.x402RailKey()
|
|
3560
|
+
};
|
|
3561
|
+
return await this.buildSuccess(ctx, outcome);
|
|
780
3562
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
3563
|
+
async handleMppx(ctx) {
|
|
3564
|
+
if (this.composeMppx === void 0) {
|
|
3565
|
+
throw new Error("Checkout.handleMppx: composeMppx hook not configured");
|
|
3566
|
+
}
|
|
3567
|
+
const composed = await this.composeMppx(ctx);
|
|
3568
|
+
if (composed.status === 200) {
|
|
3569
|
+
const outcome = {
|
|
3570
|
+
rail: "mpp",
|
|
3571
|
+
paymentResponseHeader: composed.paymentResponseHeader ?? null,
|
|
3572
|
+
raw: composed.raw,
|
|
3573
|
+
txHash: composed.txHash ?? null,
|
|
3574
|
+
signerAddress: composed.signerAddress ?? null,
|
|
3575
|
+
signerNetwork: composed.signerNetwork ?? null,
|
|
3576
|
+
railKey: composed.railKey ?? this.mppRailKey()
|
|
3577
|
+
};
|
|
3578
|
+
return await this.buildSuccess(ctx, outcome);
|
|
3579
|
+
}
|
|
3580
|
+
return {
|
|
3581
|
+
status: 400,
|
|
3582
|
+
body: buildValidationError({
|
|
3583
|
+
code: "payment_proof_invalid",
|
|
3584
|
+
message: "MPP credential rejected; regenerate from a fresh 402 challenge.",
|
|
3585
|
+
nextSteps: { action: "regenerate_payment_credential" }
|
|
3586
|
+
}),
|
|
3587
|
+
headers: { ...composed.headers ?? {} },
|
|
3588
|
+
referenceId: ctx.referenceId,
|
|
3589
|
+
settled: false,
|
|
3590
|
+
settlePhase: "verify_failed"
|
|
3591
|
+
};
|
|
789
3592
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
3593
|
+
async emit402(ctx, mppxHeaders = {}) {
|
|
3594
|
+
if (ctx.pricing === null) {
|
|
3595
|
+
throw new Error("Checkout.emit402: pricing not computed");
|
|
3596
|
+
}
|
|
3597
|
+
await this.resolveRecipientsForCtx(ctx);
|
|
3598
|
+
const emitRails = applyRecipientOverrides(this.rails, ctx.recipients);
|
|
3599
|
+
const accepted = await buildAcceptedMethods({
|
|
3600
|
+
tempo: pickRail(emitRails, "tempo"),
|
|
3601
|
+
x402_base: pickRail(emitRails, "x402_base"),
|
|
3602
|
+
solana_mpp: pickRail(emitRails, "solana_mpp"),
|
|
3603
|
+
stripe: pickRail(emitRails, "stripe")
|
|
3604
|
+
});
|
|
3605
|
+
const howToPayRails = {};
|
|
3606
|
+
for (const [k, v] of Object.entries(emitRails)) {
|
|
3607
|
+
if (!isTempoSessionRailSpec3(v)) {
|
|
3608
|
+
howToPayRails[k] = v;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
const howToPay = buildHowToPay({
|
|
3612
|
+
url: this.url,
|
|
3613
|
+
retryBodyJson: JSON.stringify(ctx.request.body),
|
|
3614
|
+
totalUsd: ctx.pricing.amountUsd.toFixed(2),
|
|
3615
|
+
rails: howToPayRails
|
|
3616
|
+
});
|
|
3617
|
+
const pricingBlock = ctx.pricing.block ?? buildPricingBlock({
|
|
3618
|
+
subtotalCents: Math.round(ctx.pricing.amountUsd * 100),
|
|
3619
|
+
currency: ctx.pricing.currency ?? "USD"
|
|
3620
|
+
});
|
|
3621
|
+
let x402Accepts = [];
|
|
3622
|
+
let x402Resource;
|
|
3623
|
+
const baseNetwork = this.x402BaseNetwork;
|
|
3624
|
+
const x402Server = await this.getX402Server();
|
|
3625
|
+
if (x402Server !== void 0 && baseNetwork !== null) {
|
|
3626
|
+
const baseSpec = emitRails["x402_base"];
|
|
3627
|
+
if (baseSpec !== void 0) {
|
|
3628
|
+
const recipient = await resolveRecipientValue(baseSpec.recipient);
|
|
3629
|
+
try {
|
|
3630
|
+
x402Accepts = await buildX402AcceptsFor402(x402Server, {
|
|
3631
|
+
network: baseNetwork,
|
|
3632
|
+
price: `$${ctx.pricing.amountUsd.toFixed(2)}`,
|
|
3633
|
+
payTo: recipient,
|
|
3634
|
+
maxTimeoutSeconds: 300
|
|
3635
|
+
});
|
|
3636
|
+
x402Resource = { url: ctx.request.url, mimeType: "application/json" };
|
|
3637
|
+
} catch {
|
|
3638
|
+
x402Accepts = [];
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
const identityMetadata = resolveIdentityMetadata(ctx);
|
|
3643
|
+
const body = build402Body({
|
|
3644
|
+
acceptedMethods: accepted,
|
|
3645
|
+
agentInstructions: buildAgentInstructions({ howToPay }),
|
|
3646
|
+
...identityMetadata !== void 0 ? { identityMetadata } : {},
|
|
3647
|
+
pricing: pricingBlock,
|
|
3648
|
+
amountUsd: ctx.pricing.amountUsd.toFixed(2),
|
|
3649
|
+
retryBody: ctx.request.body,
|
|
3650
|
+
agentMemory: firstEncounterAgentMemory({ firstEncounter: true }),
|
|
3651
|
+
...ctx.pricing.product ? { product: ctx.pricing.product } : {},
|
|
3652
|
+
...ctx.pricing.bodyExtras ? { extra: ctx.pricing.bodyExtras } : {},
|
|
3653
|
+
...x402Accepts.length > 0 ? {
|
|
3654
|
+
x402: {
|
|
3655
|
+
accepts: x402Accepts,
|
|
3656
|
+
...this.discoveryExtensions !== void 0 && Object.keys(this.discoveryExtensions).length > 0 ? { extensions: this.discoveryExtensions } : {}
|
|
3657
|
+
}
|
|
3658
|
+
} : {}
|
|
3659
|
+
});
|
|
3660
|
+
let x402Block;
|
|
3661
|
+
if (x402Accepts.length > 0) {
|
|
3662
|
+
x402Block = {
|
|
3663
|
+
x402Version: 2,
|
|
3664
|
+
accepts: x402Accepts,
|
|
3665
|
+
...x402Resource ? { resource: x402Resource } : {}
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
const respond = respond402({
|
|
3669
|
+
mppxChallengeHeaders: mppxHeaders,
|
|
3670
|
+
body,
|
|
3671
|
+
x402: x402Block
|
|
3672
|
+
});
|
|
3673
|
+
return {
|
|
3674
|
+
status: respond.status,
|
|
3675
|
+
body: respond.body,
|
|
3676
|
+
headers: respond.headers,
|
|
3677
|
+
referenceId: ctx.referenceId,
|
|
3678
|
+
settled: false
|
|
3679
|
+
};
|
|
793
3680
|
}
|
|
794
|
-
|
|
3681
|
+
async buildSuccess(ctx, outcome) {
|
|
3682
|
+
let customBody = null;
|
|
3683
|
+
if (this.onSettled !== void 0) {
|
|
3684
|
+
const result = await this.onSettled(ctx, outcome);
|
|
3685
|
+
if (result !== null && typeof result === "object") customBody = result;
|
|
3686
|
+
}
|
|
3687
|
+
const body = customBody ?? { ok: true };
|
|
3688
|
+
if (!("reference_id" in body)) body.reference_id = ctx.referenceId;
|
|
3689
|
+
const headers = {};
|
|
3690
|
+
if (outcome.paymentResponseHeader) {
|
|
3691
|
+
headers["payment-response"] = outcome.paymentResponseHeader;
|
|
3692
|
+
}
|
|
3693
|
+
return {
|
|
3694
|
+
status: 200,
|
|
3695
|
+
body,
|
|
3696
|
+
headers,
|
|
3697
|
+
referenceId: ctx.referenceId,
|
|
3698
|
+
settled: true
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
async function resolveRecipientValue(r) {
|
|
3703
|
+
return await resolveRecipient(r);
|
|
795
3704
|
}
|
|
796
|
-
function
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
3705
|
+
function validationEnvelope(opts) {
|
|
3706
|
+
const action = opts.action ?? "fix_request";
|
|
3707
|
+
return buildValidationError({
|
|
3708
|
+
code: opts.code,
|
|
3709
|
+
message: opts.message,
|
|
3710
|
+
nextSteps: { action, user_message: opts.message },
|
|
3711
|
+
extra: opts.extra
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
function validationResponseHono(input) {
|
|
3715
|
+
const status = input.status ?? 400;
|
|
3716
|
+
const body = validationEnvelope(input);
|
|
3717
|
+
return new Response(JSON.stringify(body), {
|
|
3718
|
+
status,
|
|
3719
|
+
headers: { "Content-Type": "application/json" }
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
function validationResponseExpress(res, input) {
|
|
3723
|
+
const status = input.status ?? 400;
|
|
3724
|
+
const body = validationEnvelope(input);
|
|
3725
|
+
res.status(status);
|
|
3726
|
+
res.json(body);
|
|
3727
|
+
}
|
|
3728
|
+
function validationResponseFastify(reply, input) {
|
|
3729
|
+
const status = input.status ?? 400;
|
|
3730
|
+
const body = validationEnvelope(input);
|
|
3731
|
+
reply.code(status);
|
|
3732
|
+
return reply.send(body);
|
|
3733
|
+
}
|
|
3734
|
+
function validationResponseNextjs(input) {
|
|
3735
|
+
return validationResponseHono(input);
|
|
3736
|
+
}
|
|
3737
|
+
function validationResponseWeb(input) {
|
|
3738
|
+
return validationResponseHono(input);
|
|
3739
|
+
}
|
|
3740
|
+
function invalidBodyEnvelope() {
|
|
3741
|
+
return validationEnvelope({
|
|
3742
|
+
code: "invalid_body",
|
|
3743
|
+
message: "Request body must be valid JSON."
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
function stripContentType(headers) {
|
|
3747
|
+
const out = {};
|
|
3748
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
3749
|
+
if (k.toLowerCase() !== "content-type") out[k] = v;
|
|
801
3750
|
}
|
|
802
|
-
return
|
|
3751
|
+
return out;
|
|
803
3752
|
}
|
|
804
|
-
function
|
|
805
|
-
return {
|
|
3753
|
+
function headersToRecord(h) {
|
|
3754
|
+
if (h === void 0) return {};
|
|
3755
|
+
if (h instanceof Headers) {
|
|
3756
|
+
const out = {};
|
|
3757
|
+
h.forEach((v, k) => {
|
|
3758
|
+
out[k] = v;
|
|
3759
|
+
});
|
|
3760
|
+
return out;
|
|
3761
|
+
}
|
|
3762
|
+
return { ...h };
|
|
806
3763
|
}
|
|
3764
|
+
Checkout.prototype.handleHono = async function(c, body) {
|
|
3765
|
+
let parsedBody;
|
|
3766
|
+
if (body !== void 0) {
|
|
3767
|
+
parsedBody = body;
|
|
3768
|
+
} else {
|
|
3769
|
+
try {
|
|
3770
|
+
parsedBody = await c.req.json();
|
|
3771
|
+
} catch {
|
|
3772
|
+
return new Response(JSON.stringify(invalidBodyEnvelope()), {
|
|
3773
|
+
status: 400,
|
|
3774
|
+
headers: { "Content-Type": "application/json" }
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
const rawHeaders = c.req.header();
|
|
3779
|
+
const headers = headersToRecord(rawHeaders);
|
|
3780
|
+
const result = await this.handle({
|
|
3781
|
+
method: c.req.method,
|
|
3782
|
+
url: c.req.url,
|
|
3783
|
+
headers,
|
|
3784
|
+
body: parsedBody,
|
|
3785
|
+
assess: null,
|
|
3786
|
+
raw: c.req.raw ?? c
|
|
3787
|
+
});
|
|
3788
|
+
return new Response(JSON.stringify(result.body), {
|
|
3789
|
+
status: result.status,
|
|
3790
|
+
headers: { "Content-Type": "application/json", ...stripContentType(result.headers) }
|
|
3791
|
+
});
|
|
3792
|
+
};
|
|
3793
|
+
Checkout.prototype.handleExpress = async function(req, res, body) {
|
|
3794
|
+
const parsedBody = body ?? (typeof req.body === "object" && req.body !== null ? req.body : null);
|
|
3795
|
+
if (parsedBody === null) {
|
|
3796
|
+
res.status(400);
|
|
3797
|
+
res.json(invalidBodyEnvelope());
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const headers = {};
|
|
3801
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
3802
|
+
if (typeof v === "string") headers[k] = v;
|
|
3803
|
+
else if (Array.isArray(v) && v[0] !== void 0) headers[k] = v[0];
|
|
3804
|
+
}
|
|
3805
|
+
const url = req.originalUrl ?? req.url ?? "/";
|
|
3806
|
+
const result = await this.handle({
|
|
3807
|
+
method: req.method,
|
|
3808
|
+
url,
|
|
3809
|
+
headers,
|
|
3810
|
+
body: parsedBody,
|
|
3811
|
+
assess: null,
|
|
3812
|
+
raw: req
|
|
3813
|
+
});
|
|
3814
|
+
for (const [k, v] of Object.entries(stripContentType(result.headers))) res.setHeader(k, v);
|
|
3815
|
+
res.status(result.status);
|
|
3816
|
+
res.json(result.body);
|
|
3817
|
+
};
|
|
3818
|
+
Checkout.prototype.handleFastify = async function(request, reply, body) {
|
|
3819
|
+
const parsedBody = body ?? (typeof request.body === "object" && request.body !== null ? request.body : null);
|
|
3820
|
+
if (parsedBody === null) {
|
|
3821
|
+
reply.code(400);
|
|
3822
|
+
return reply.send(invalidBodyEnvelope());
|
|
3823
|
+
}
|
|
3824
|
+
const headers = {};
|
|
3825
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
3826
|
+
if (typeof v === "string") headers[k] = v;
|
|
3827
|
+
else if (Array.isArray(v) && v[0] !== void 0) headers[k] = v[0];
|
|
3828
|
+
}
|
|
3829
|
+
const result = await this.handle({
|
|
3830
|
+
method: request.method,
|
|
3831
|
+
url: request.url,
|
|
3832
|
+
headers,
|
|
3833
|
+
body: parsedBody,
|
|
3834
|
+
assess: null,
|
|
3835
|
+
raw: request
|
|
3836
|
+
});
|
|
3837
|
+
for (const [k, v] of Object.entries(stripContentType(result.headers))) reply.header(k, v);
|
|
3838
|
+
reply.code(result.status);
|
|
3839
|
+
return reply.send(result.body);
|
|
3840
|
+
};
|
|
3841
|
+
Checkout.prototype.handleNextjs = async function(request, body) {
|
|
3842
|
+
let parsedBody;
|
|
3843
|
+
if (body !== void 0) {
|
|
3844
|
+
parsedBody = body;
|
|
3845
|
+
} else {
|
|
3846
|
+
try {
|
|
3847
|
+
parsedBody = await request.json();
|
|
3848
|
+
} catch {
|
|
3849
|
+
return new Response(JSON.stringify(invalidBodyEnvelope()), {
|
|
3850
|
+
status: 400,
|
|
3851
|
+
headers: { "Content-Type": "application/json" }
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
const headers = {};
|
|
3856
|
+
request.headers.forEach((v, k) => {
|
|
3857
|
+
headers[k] = v;
|
|
3858
|
+
});
|
|
3859
|
+
const result = await this.handle({
|
|
3860
|
+
method: request.method,
|
|
3861
|
+
url: request.url,
|
|
3862
|
+
headers,
|
|
3863
|
+
body: parsedBody,
|
|
3864
|
+
assess: null,
|
|
3865
|
+
raw: request
|
|
3866
|
+
});
|
|
3867
|
+
return new Response(JSON.stringify(result.body), {
|
|
3868
|
+
status: result.status,
|
|
3869
|
+
headers: { "Content-Type": "application/json", ...stripContentType(result.headers) }
|
|
3870
|
+
});
|
|
3871
|
+
};
|
|
3872
|
+
Checkout.prototype.handleWeb = Checkout.prototype.handleNextjs;
|
|
3873
|
+
async function _ucpSignedResp(checkout, reqHeaders, opts) {
|
|
3874
|
+
const { buildSignedUcpResponse: buildSignedUcpResponse2 } = await Promise.resolve().then(() => (init_well_known(), well_known_exports));
|
|
3875
|
+
return await buildSignedUcpResponse2({
|
|
3876
|
+
checkout,
|
|
3877
|
+
name: opts.name,
|
|
3878
|
+
wellKnownUcpUrl: opts.wellKnownUcpUrl,
|
|
3879
|
+
services: opts.services,
|
|
3880
|
+
requestHeaders: reqHeaders,
|
|
3881
|
+
...opts.signingKid !== void 0 && { signingKid: opts.signingKid },
|
|
3882
|
+
...opts.agentscoreGate !== void 0 && {
|
|
3883
|
+
agentscoreGate: opts.agentscoreGate
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
async function _jwksSignedResp(reqHeaders, opts) {
|
|
3888
|
+
const { buildSignedJwksResponse: buildSignedJwksResponse2 } = await Promise.resolve().then(() => (init_well_known(), well_known_exports));
|
|
3889
|
+
return await buildSignedJwksResponse2({
|
|
3890
|
+
requestHeaders: reqHeaders,
|
|
3891
|
+
...opts.signingKid !== void 0 && { signingKid: opts.signingKid }
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
function _preflightResp(reqHeaders) {
|
|
3895
|
+
return new Response(null, {
|
|
3896
|
+
status: 204,
|
|
3897
|
+
headers: _preflightHeaders(reqHeaders)
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
function _preflightHeaders(reqHeaders) {
|
|
3901
|
+
const headers = {
|
|
3902
|
+
"Access-Control-Allow-Origin": "*",
|
|
3903
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
3904
|
+
"Access-Control-Max-Age": "86400",
|
|
3905
|
+
Vary: "Access-Control-Request-Headers"
|
|
3906
|
+
};
|
|
3907
|
+
const acrh = reqHeaders.get("access-control-request-headers");
|
|
3908
|
+
if (acrh) headers["Access-Control-Allow-Headers"] = acrh;
|
|
3909
|
+
return headers;
|
|
3910
|
+
}
|
|
3911
|
+
Checkout.prototype.mountUcpRoutesHono = function(app, opts) {
|
|
3912
|
+
const ucpPath = opts.ucpPath ?? "/.well-known/ucp";
|
|
3913
|
+
const jwksPath = opts.jwksPath ?? "/.well-known/jwks.json";
|
|
3914
|
+
const checkout = this;
|
|
3915
|
+
app.get(ucpPath, async (c) => {
|
|
3916
|
+
const resp = await _ucpSignedResp(checkout, c.req.raw.headers, opts);
|
|
3917
|
+
return new Response(resp.body, {
|
|
3918
|
+
status: resp.status,
|
|
3919
|
+
headers: { ...resp.headers, "Content-Type": resp.mediaType }
|
|
3920
|
+
});
|
|
3921
|
+
});
|
|
3922
|
+
app.get(jwksPath, async (c) => {
|
|
3923
|
+
const resp = await _jwksSignedResp(c.req.raw.headers, opts);
|
|
3924
|
+
return new Response(resp.body, {
|
|
3925
|
+
status: resp.status,
|
|
3926
|
+
headers: { ...resp.headers, "Content-Type": resp.mediaType }
|
|
3927
|
+
});
|
|
3928
|
+
});
|
|
3929
|
+
app.options(ucpPath, (c) => _preflightResp(c.req.raw.headers));
|
|
3930
|
+
app.options(jwksPath, (c) => _preflightResp(c.req.raw.headers));
|
|
3931
|
+
};
|
|
3932
|
+
function _headersFromExpressLike(raw) {
|
|
3933
|
+
const out = new Headers();
|
|
3934
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
3935
|
+
if (v === void 0) continue;
|
|
3936
|
+
out.set(k, Array.isArray(v) ? v.join(",") : v);
|
|
3937
|
+
}
|
|
3938
|
+
return out;
|
|
3939
|
+
}
|
|
3940
|
+
Checkout.prototype.mountUcpRoutesExpress = function(app, opts) {
|
|
3941
|
+
const ucpPath = opts.ucpPath ?? "/.well-known/ucp";
|
|
3942
|
+
const jwksPath = opts.jwksPath ?? "/.well-known/jwks.json";
|
|
3943
|
+
const checkout = this;
|
|
3944
|
+
app.get(ucpPath, async (req, res) => {
|
|
3945
|
+
const resp = await _ucpSignedResp(checkout, _headersFromExpressLike(req.headers), opts);
|
|
3946
|
+
res.status(resp.status);
|
|
3947
|
+
res.set(resp.headers);
|
|
3948
|
+
res.type(resp.mediaType);
|
|
3949
|
+
res.send(resp.body);
|
|
3950
|
+
});
|
|
3951
|
+
app.get(jwksPath, async (req, res) => {
|
|
3952
|
+
const resp = await _jwksSignedResp(_headersFromExpressLike(req.headers), opts);
|
|
3953
|
+
res.status(resp.status);
|
|
3954
|
+
res.set(resp.headers);
|
|
3955
|
+
res.type(resp.mediaType);
|
|
3956
|
+
res.send(resp.body);
|
|
3957
|
+
});
|
|
3958
|
+
const preflight = (req, res) => {
|
|
3959
|
+
const reqHeaders = _headersFromExpressLike(req.headers);
|
|
3960
|
+
res.status(204);
|
|
3961
|
+
res.set(_preflightHeaders(reqHeaders));
|
|
3962
|
+
res.send("");
|
|
3963
|
+
};
|
|
3964
|
+
app.options(ucpPath, preflight);
|
|
3965
|
+
app.options(jwksPath, preflight);
|
|
3966
|
+
};
|
|
3967
|
+
Checkout.prototype.mountUcpRoutesFastify = function(app, opts) {
|
|
3968
|
+
const ucpPath = opts.ucpPath ?? "/.well-known/ucp";
|
|
3969
|
+
const jwksPath = opts.jwksPath ?? "/.well-known/jwks.json";
|
|
3970
|
+
const checkout = this;
|
|
3971
|
+
app.get(ucpPath, async (request, reply) => {
|
|
3972
|
+
const resp = await _ucpSignedResp(checkout, _headersFromExpressLike(request.headers), opts);
|
|
3973
|
+
reply.code(resp.status);
|
|
3974
|
+
for (const [k, v] of Object.entries(resp.headers)) reply.header(k, v);
|
|
3975
|
+
reply.type(resp.mediaType);
|
|
3976
|
+
return reply.send(resp.body);
|
|
3977
|
+
});
|
|
3978
|
+
app.get(jwksPath, async (request, reply) => {
|
|
3979
|
+
const resp = await _jwksSignedResp(_headersFromExpressLike(request.headers), opts);
|
|
3980
|
+
reply.code(resp.status);
|
|
3981
|
+
for (const [k, v] of Object.entries(resp.headers)) reply.header(k, v);
|
|
3982
|
+
reply.type(resp.mediaType);
|
|
3983
|
+
return reply.send(resp.body);
|
|
3984
|
+
});
|
|
3985
|
+
const preflight = (request, reply) => {
|
|
3986
|
+
const reqHeaders = _headersFromExpressLike(request.headers);
|
|
3987
|
+
reply.code(204);
|
|
3988
|
+
for (const [k, v] of Object.entries(_preflightHeaders(reqHeaders))) reply.header(k, v);
|
|
3989
|
+
return reply.send("");
|
|
3990
|
+
};
|
|
3991
|
+
app.options(ucpPath, preflight);
|
|
3992
|
+
app.options(jwksPath, preflight);
|
|
3993
|
+
};
|
|
807
3994
|
|
|
808
3995
|
// src/identity/policy.ts
|
|
809
|
-
function
|
|
3996
|
+
function buildGateFromPolicy(policy, base) {
|
|
810
3997
|
if (!policy || !policy.enforcement) return null;
|
|
811
3998
|
return {
|
|
812
3999
|
apiKey: base.apiKey,
|
|
@@ -851,8 +4038,72 @@ function shippingStateAllowed(state, country, policy) {
|
|
|
851
4038
|
const allowed = new Set(policy.allowedShippingStates.map((s) => s.toUpperCase()));
|
|
852
4039
|
return allowed.has(state.toUpperCase());
|
|
853
4040
|
}
|
|
4041
|
+
function validateShippingAgainstPolicy(opts) {
|
|
4042
|
+
const code = opts.errorCode ?? "unsupported_jurisdiction";
|
|
4043
|
+
const action = opts.errorAction ?? "change_shipping_state";
|
|
4044
|
+
const item = opts.productName ? `'${opts.productName}'` : "this item";
|
|
4045
|
+
if (!shippingCountryAllowed(opts.country, opts.policy)) {
|
|
4046
|
+
throw new CheckoutValidationError({
|
|
4047
|
+
code,
|
|
4048
|
+
message: opts.countryMessage ?? `We can't ship ${item} to ${opts.country.toUpperCase() || "<unset>"}.`,
|
|
4049
|
+
action
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
if (!shippingStateAllowed(opts.state, opts.country, opts.policy)) {
|
|
4053
|
+
throw new CheckoutValidationError({
|
|
4054
|
+
code,
|
|
4055
|
+
message: opts.stateMessage ?? `We can't ship ${item} to ${opts.state.toUpperCase() || "<unset>"}.`,
|
|
4056
|
+
action
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
// src/identity/tokens.ts
|
|
4062
|
+
import { createHash } from "crypto";
|
|
4063
|
+
function hashOperatorToken(plaintext) {
|
|
4064
|
+
return createHash("sha256").update(plaintext, "utf8").digest("hex");
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
// src/payment/index.ts
|
|
4068
|
+
init_directive();
|
|
4069
|
+
init_networks();
|
|
4070
|
+
init_usdc();
|
|
4071
|
+
init_rails();
|
|
4072
|
+
init_wwwauthenticate();
|
|
4073
|
+
|
|
4074
|
+
// src/payment/headers.ts
|
|
4075
|
+
init_directive();
|
|
4076
|
+
init_wwwauthenticate();
|
|
4077
|
+
|
|
4078
|
+
// src/payment/amounts.ts
|
|
4079
|
+
function formatUsdCents(cents) {
|
|
4080
|
+
return (cents / 100).toFixed(2);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
// src/payment/solana.ts
|
|
4084
|
+
async function loadSolanaFeePayer(opts) {
|
|
4085
|
+
const raw = opts.privateKey;
|
|
4086
|
+
if (!raw) return void 0;
|
|
4087
|
+
const moduleName = "@solana/kit";
|
|
4088
|
+
const kit = await import(moduleName).catch(() => null);
|
|
4089
|
+
if (!kit?.createKeyPairSignerFromPrivateKeyBytes || !kit.getBase58Codec) {
|
|
4090
|
+
throw new Error(
|
|
4091
|
+
"@solana/kit not installed \u2014 `npm install @solana/kit` for loadSolanaFeePayer."
|
|
4092
|
+
);
|
|
4093
|
+
}
|
|
4094
|
+
let bytes;
|
|
4095
|
+
if (/^[0-9a-fA-F]{128}$/.test(raw)) {
|
|
4096
|
+
bytes = new Uint8Array(raw.match(/.{2}/g).map((h) => parseInt(h, 16))).slice(0, 32);
|
|
4097
|
+
} else {
|
|
4098
|
+
const decoded = new Uint8Array(kit.getBase58Codec().encode(raw));
|
|
4099
|
+
bytes = decoded.length === 64 ? decoded.slice(0, 32) : decoded;
|
|
4100
|
+
}
|
|
4101
|
+
return kit.createKeyPairSignerFromPrivateKeyBytes(bytes);
|
|
4102
|
+
}
|
|
854
4103
|
export {
|
|
855
4104
|
AGENTSCORE_UCP_CAPABILITY,
|
|
4105
|
+
Checkout,
|
|
4106
|
+
CheckoutValidationError,
|
|
856
4107
|
FIXABLE_DENIAL_REASONS,
|
|
857
4108
|
UCPSigningKey,
|
|
858
4109
|
UCPVerificationError,
|
|
@@ -860,16 +4111,25 @@ export {
|
|
|
860
4111
|
buildA2AAgentCard,
|
|
861
4112
|
buildAgentMemoryHint,
|
|
862
4113
|
buildContactSupportNextSteps,
|
|
863
|
-
|
|
4114
|
+
buildGateFromPolicy,
|
|
864
4115
|
buildJWKSResponse,
|
|
865
4116
|
buildSignerMismatchBody,
|
|
866
4117
|
buildUCPProfile,
|
|
867
4118
|
denialReasonStatus,
|
|
868
4119
|
denialReasonToBody,
|
|
869
4120
|
extractPaymentSigner,
|
|
4121
|
+
extractPaymentSignerFromAuth,
|
|
4122
|
+
extractSignerForPrecheck,
|
|
4123
|
+
formatUsdCents,
|
|
870
4124
|
generateUCPSigningKey,
|
|
4125
|
+
getIdentityStatus,
|
|
4126
|
+
hashOperatorToken,
|
|
871
4127
|
isFixableDenial,
|
|
4128
|
+
loadSolanaFeePayer,
|
|
4129
|
+
loadUCPSigningKeyFromEnv,
|
|
4130
|
+
makeMppxComposeHook,
|
|
872
4131
|
mppPaymentHandler,
|
|
4132
|
+
pricingResult,
|
|
873
4133
|
readX402PaymentHeader,
|
|
874
4134
|
runGateWithEnforcement,
|
|
875
4135
|
shippingCountryAllowed,
|
|
@@ -877,6 +4137,13 @@ export {
|
|
|
877
4137
|
signUCPProfile,
|
|
878
4138
|
stripeSptPaymentHandler,
|
|
879
4139
|
ucpA2AExtension,
|
|
4140
|
+
validateShippingAgainstPolicy,
|
|
4141
|
+
validationEnvelope,
|
|
4142
|
+
validationResponseExpress,
|
|
4143
|
+
validationResponseFastify,
|
|
4144
|
+
validationResponseHono,
|
|
4145
|
+
validationResponseNextjs,
|
|
4146
|
+
validationResponseWeb,
|
|
880
4147
|
verificationAgentInstructions,
|
|
881
4148
|
verifyUCPProfile,
|
|
882
4149
|
x402PaymentHandler
|