@devlab.group/agent-commerce 2.1.0 → 2.2.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.
Files changed (73) hide show
  1. package/README.md +35 -5
  2. package/dist/ap2.js +3 -3
  3. package/dist/{chunk-SY6GUYSQ.js → chunk-7ZMMXZ4G.js} +3 -3
  4. package/dist/{chunk-SY6GUYSQ.js.map → chunk-7ZMMXZ4G.js.map} +1 -1
  5. package/dist/{chunk-UHMYRV3J.js → chunk-HNRDZLSN.js} +7 -5
  6. package/dist/chunk-HNRDZLSN.js.map +1 -0
  7. package/dist/{chunk-LZS5BL3W.js → chunk-IEW7NSP2.js} +23 -5
  8. package/dist/chunk-IEW7NSP2.js.map +1 -0
  9. package/dist/{chunk-IFGRCXME.js → chunk-Y4RE3XYF.js} +3 -3
  10. package/dist/{chunk-IFGRCXME.js.map → chunk-Y4RE3XYF.js.map} +1 -1
  11. package/dist/cli/ap2-MBMQHFX4.js +678 -0
  12. package/dist/cli/ap2-MBMQHFX4.js.map +1 -0
  13. package/dist/cli/chunk-2ZZOGI5I.js +1352 -0
  14. package/dist/cli/chunk-2ZZOGI5I.js.map +1 -0
  15. package/dist/cli/chunk-3EQBCFZY.js +53 -0
  16. package/dist/cli/chunk-3EQBCFZY.js.map +1 -0
  17. package/dist/cli/chunk-3M6U7ILB.js +8 -0
  18. package/dist/cli/chunk-3M6U7ILB.js.map +1 -0
  19. package/dist/cli/chunk-3MGAJ5FX.js +70 -0
  20. package/dist/cli/chunk-3MGAJ5FX.js.map +1 -0
  21. package/dist/cli/chunk-5K4XEAZV.js +113 -0
  22. package/dist/cli/chunk-5K4XEAZV.js.map +1 -0
  23. package/dist/cli/chunk-7JNYFUT3.js +256 -0
  24. package/dist/cli/chunk-7JNYFUT3.js.map +1 -0
  25. package/dist/cli/chunk-AA3FIEAR.js +27 -0
  26. package/dist/cli/chunk-AA3FIEAR.js.map +1 -0
  27. package/dist/cli/chunk-FHCFR2GS.js +17 -0
  28. package/dist/cli/chunk-FHCFR2GS.js.map +1 -0
  29. package/dist/cli/chunk-FRMLWGSS.js +8 -0
  30. package/dist/cli/chunk-FRMLWGSS.js.map +1 -0
  31. package/dist/cli/chunk-KLX2YZDG.js +1400 -0
  32. package/dist/cli/chunk-KLX2YZDG.js.map +1 -0
  33. package/dist/cli/chunk-NSPPEF5M.js +334 -0
  34. package/dist/cli/chunk-NSPPEF5M.js.map +1 -0
  35. package/dist/cli/chunk-OSTCU6ZK.js +44 -0
  36. package/dist/cli/chunk-OSTCU6ZK.js.map +1 -0
  37. package/dist/cli/chunk-QQ3WY5L3.js +271 -0
  38. package/dist/cli/chunk-QQ3WY5L3.js.map +1 -0
  39. package/dist/cli/chunk-RWVPYAKC.js +1742 -0
  40. package/dist/cli/chunk-RWVPYAKC.js.map +1 -0
  41. package/dist/cli/chunk-SPAYKVUP.js +118 -0
  42. package/dist/cli/chunk-SPAYKVUP.js.map +1 -0
  43. package/dist/cli/chunk-SS3QFF5N.js +614 -0
  44. package/dist/cli/chunk-SS3QFF5N.js.map +1 -0
  45. package/dist/cli/chunk-TPC4EJLT.js +11 -0
  46. package/dist/cli/chunk-TPC4EJLT.js.map +1 -0
  47. package/dist/cli/config-4IBGVFSP.js +11 -0
  48. package/dist/cli/config-4IBGVFSP.js.map +1 -0
  49. package/dist/cli/index.js +150 -3627
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/mcp-CNWQ2MOE.js +559 -0
  52. package/dist/cli/mcp-CNWQ2MOE.js.map +1 -0
  53. package/dist/cli/near-5AY3HWYF.js +58 -0
  54. package/dist/cli/near-5AY3HWYF.js.map +1 -0
  55. package/dist/cli/provider-4BQDH56Y.js +396 -0
  56. package/dist/cli/provider-4BQDH56Y.js.map +1 -0
  57. package/dist/cli/run-5MB2D45X.js +7286 -0
  58. package/dist/cli/run-5MB2D45X.js.map +1 -0
  59. package/dist/cli/svm-3MMYFPC7.js +33 -0
  60. package/dist/cli/svm-3MMYFPC7.js.map +1 -0
  61. package/dist/cli/x402-NKR3WQDQ.js +96 -0
  62. package/dist/cli/x402-NKR3WQDQ.js.map +1 -0
  63. package/dist/index.d.ts +2 -0
  64. package/dist/index.js +48 -5
  65. package/dist/index.js.map +1 -1
  66. package/dist/mcp.js +6 -16
  67. package/dist/mcp.js.map +1 -1
  68. package/dist/mpp.js +3 -3
  69. package/dist/x402.d.ts +5 -0
  70. package/dist/x402.js +4 -4
  71. package/package.json +2 -2
  72. package/dist/chunk-LZS5BL3W.js.map +0 -1
  73. package/dist/chunk-UHMYRV3J.js.map +0 -1
@@ -0,0 +1,1742 @@
1
+ import { ACP_WELL_KNOWN_PATH, DEFAULT_CONFIG_FILENAME, ACP_CHECKOUT_OPERATIONS, isObjectSchemaNode, extractPathParameterNames, findUnparsedBraceToken, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS, ACP_SESSION_ID_INPUT_KEY } from './chunk-2ZZOGI5I.js';
2
+ import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_DEFAULT_PAYMENT_FLOW, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-AA3FIEAR.js';
3
+ import { resolveX402Deployment, resolveX402Option } from './chunk-NSPPEF5M.js';
4
+ import { X402_PAYMENT_FLOWS, validateCustomNetworks, findX402Network, X402_DEFAULT_PAYMENT_FLOW, isEvmAddress, isEvmZeroAddress } from './chunk-7JNYFUT3.js';
5
+ import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-FHCFR2GS.js';
6
+ import { MCP_TOOL_NAME_PATTERN } from './chunk-TPC4EJLT.js';
7
+ import { PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, CommerceError, RESERVED_INPUT_FIELDS } from './chunk-QQ3WY5L3.js';
8
+ import { isRecord } from './chunk-3M6U7ILB.js';
9
+ import { promises } from 'fs';
10
+ import * as path from 'path';
11
+ import { parse, YAMLError } from 'yaml';
12
+ import { z } from 'zod';
13
+
14
+ // src/config/env.ts
15
+ var PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
16
+ var BRACE_TOKEN = /\$\{[^{}]*\}/g;
17
+ var SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
18
+ var NESTED_PLACEHOLDER = /\$\{[^{}]*\$\{/;
19
+ function substituteEnv(value, env, path2 = "$") {
20
+ if (typeof value === "string") {
21
+ return substituteString(value, env, path2);
22
+ }
23
+ if (Array.isArray(value)) {
24
+ return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
25
+ }
26
+ if (isRecord(value)) {
27
+ const result = {};
28
+ for (const [key, entry] of Object.entries(value)) {
29
+ result[key] = substituteEnv(entry, env, `${path2}.${key}`);
30
+ }
31
+ return result;
32
+ }
33
+ return value;
34
+ }
35
+ function substituteString(value, env, path2) {
36
+ assertTemplateIsSupported(value, path2);
37
+ return substituteOnce(value, env, path2);
38
+ }
39
+ function assertTemplateIsSupported(value, path2) {
40
+ if (NESTED_PLACEHOLDER.test(value)) {
41
+ throw new CommerceError(
42
+ "CONFIG_INVALID",
43
+ `Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported: the inner one is not resolved and its closing brace ends up in the value. Use a plain default, or set the variable.`,
44
+ { details: { path: path2 } }
45
+ );
46
+ }
47
+ for (const [token] of value.matchAll(BRACE_TOKEN)) {
48
+ if (SUPPORTED_TOKEN.test(token)) continue;
49
+ throw new CommerceError(
50
+ "CONFIG_INVALID",
51
+ `Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are recognized; shell forms such as "\${VAR-default}", "\${VAR:=default}" and "\${VAR:?message}" would load as literal text.`,
52
+ { details: { path: path2, token } }
53
+ );
54
+ }
55
+ }
56
+ function substituteOnce(value, env, path2) {
57
+ return value.replace(
58
+ PLACEHOLDER_PATTERN,
59
+ (_match, name, hasDefault, def) => {
60
+ const envValue = env[name];
61
+ if (hasDefault !== void 0) {
62
+ if (envValue === void 0 || envValue === "") return def ?? "";
63
+ return envValue;
64
+ }
65
+ if (envValue === void 0) {
66
+ throw new CommerceError(
67
+ "CONFIG_INVALID",
68
+ `Unresolved environment variable "\${${name}}" referenced at config path "${path2}"`,
69
+ { details: { variable: name, path: path2 } }
70
+ );
71
+ }
72
+ return envValue;
73
+ }
74
+ );
75
+ }
76
+
77
+ // src/config/schema/values.ts
78
+ function toNumber(value, path2, bounds = {}) {
79
+ if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
80
+ throw new CommerceError(
81
+ "CONFIG_INVALID",
82
+ `Configuration value at "${path2}" must be an integer written in decimal digits (got ${value === "" ? "an empty string" : `"${value}"`})`,
83
+ { details: { path: path2 } }
84
+ );
85
+ }
86
+ const n = typeof value === "number" ? value : Number(value);
87
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
88
+ throw new CommerceError(
89
+ "CONFIG_INVALID",
90
+ `Configuration value at "${path2}" must be an integer`,
91
+ {
92
+ details: { path: path2 }
93
+ }
94
+ );
95
+ }
96
+ if (bounds.min !== void 0 && n < bounds.min) {
97
+ throw new CommerceError(
98
+ "CONFIG_INVALID",
99
+ `Configuration value at "${path2}" must be >= ${bounds.min}`,
100
+ {
101
+ details: { path: path2 }
102
+ }
103
+ );
104
+ }
105
+ if (bounds.max !== void 0 && n > bounds.max) {
106
+ throw new CommerceError(
107
+ "CONFIG_INVALID",
108
+ `Configuration value at "${path2}" must be <= ${bounds.max}`,
109
+ {
110
+ details: { path: path2 }
111
+ }
112
+ );
113
+ }
114
+ return n;
115
+ }
116
+ function toBoolean(value, path2) {
117
+ if (typeof value === "boolean") return value;
118
+ const normalized = value.trim().toLowerCase();
119
+ if (normalized === "true") return true;
120
+ if (normalized === "false") return false;
121
+ throw new CommerceError(
122
+ "CONFIG_INVALID",
123
+ `Configuration value at "${path2}" must be a boolean ("true" or "false")`,
124
+ { details: { path: path2 } }
125
+ );
126
+ }
127
+ function validateAddress(path2, value) {
128
+ if (!isEvmAddress(value)) {
129
+ throw new CommerceError(
130
+ "CONFIG_INVALID",
131
+ `Configuration value at "${path2}" is not a plausible address (expected "0x" followed by 40 hex characters)`,
132
+ { details: { path: path2 } }
133
+ );
134
+ }
135
+ if (isEvmZeroAddress(value)) {
136
+ throw new CommerceError(
137
+ "CONFIG_INVALID",
138
+ `Configuration value at "${path2}" must not be the zero address`,
139
+ {
140
+ details: { path: path2 }
141
+ }
142
+ );
143
+ }
144
+ }
145
+
146
+ // src/config/schema/acp.ts
147
+ var DEFAULT_ACP_MOUNT_PATH = "/acp";
148
+ var ACP_RETENTION_HOURS = 24;
149
+ function acpInvalid(path2, message, extra = {}) {
150
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
151
+ }
152
+ function normalizeAcp(raw) {
153
+ const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
154
+ if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
155
+ return { enabled: false, mountPath };
156
+ }
157
+ if (raw.auth === void 0) {
158
+ throw acpInvalid(
159
+ "protocols.acp.auth",
160
+ 'protocols.acp.auth is required when ACP is enabled - every ACP checkout endpoint is authenticated, and "bearer" is the only scheme supported in this release'
161
+ );
162
+ }
163
+ if (raw.idempotency === void 0) {
164
+ throw acpInvalid(
165
+ "protocols.acp.idempotency",
166
+ "protocols.acp.idempotency is required when ACP is enabled - it names the SQLite file holding checkout idempotency records, and ACP makes the Idempotency-Key mandatory on every checkout POST"
167
+ );
168
+ }
169
+ const discovery = raw.discovery;
170
+ return {
171
+ enabled: true,
172
+ mountPath,
173
+ auth: raw.auth,
174
+ idempotency: {
175
+ path: raw.idempotency.path,
176
+ retentionHours: toNumber(
177
+ raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
178
+ "protocols.acp.idempotency.retentionHours",
179
+ { min: ACP_RETENTION_HOURS }
180
+ ),
181
+ merchantIdempotent: raw.idempotency.merchantIdempotent ?? false
182
+ },
183
+ checkout: { operations: normalizeAcpOperations(raw.checkout) },
184
+ // Built key by key so absent metadata stays absent rather than `undefined`
185
+ ...discovery !== void 0 ? {
186
+ discovery: {
187
+ ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
188
+ ...discovery.supportedCurrencies !== void 0 ? { supportedCurrencies: discovery.supportedCurrencies } : {},
189
+ ...discovery.supportedLocales !== void 0 ? { supportedLocales: discovery.supportedLocales } : {},
190
+ ...discovery.interventionTypes !== void 0 ? { interventionTypes: discovery.interventionTypes } : {}
191
+ }
192
+ } : {}
193
+ };
194
+ }
195
+ function normalizeAcpOperations(checkout) {
196
+ const configured = checkout?.operations ?? {};
197
+ for (const key of Object.keys(configured)) {
198
+ if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
199
+ throw acpInvalid(
200
+ `protocols.acp.checkout.operations.${key}`,
201
+ `protocols.acp.checkout.operations names unknown ACP checkout operation "${key}". Supported: ${ACP_CHECKOUT_OPERATIONS.join(", ")}.`
202
+ );
203
+ }
204
+ }
205
+ const operations = {};
206
+ const claimedBy = /* @__PURE__ */ new Map();
207
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
208
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
209
+ const resourceId = configured[operation];
210
+ if (resourceId === void 0) {
211
+ throw acpInvalid(
212
+ path2,
213
+ `${path2} is required when ACP is enabled - all five checkout operations must be mapped, since ACP discovery advertises the checkout service as a whole`
214
+ );
215
+ }
216
+ const claimed = claimedBy.get(resourceId);
217
+ if (claimed !== void 0) {
218
+ throw acpInvalid(
219
+ path2,
220
+ `${path2} maps resource "${resourceId}", which is already mapped to "${claimed}" - each ACP checkout operation needs its own resource`,
221
+ { resourceId }
222
+ );
223
+ }
224
+ claimedBy.set(resourceId, operation);
225
+ operations[operation] = resourceId;
226
+ }
227
+ return operations;
228
+ }
229
+ function validateAcpCheckoutMapping(acp, resources) {
230
+ const byId = new Map(resources.map((resource) => [resource.id, resource]));
231
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
232
+ const resourceId = acp.checkout.operations[operation];
233
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
234
+ const resource = byId.get(resourceId);
235
+ if (resource === void 0) {
236
+ throw acpInvalid(
237
+ path2,
238
+ `${path2} maps resource "${resourceId}", which is not defined under "resources"`,
239
+ { resourceId }
240
+ );
241
+ }
242
+ if (!resource.exposedVia.includes("acp")) {
243
+ throw acpInvalid(
244
+ path2,
245
+ `Resource "${resourceId}" implements ACP operation "${operation}" but does not list "acp" in its expose - the mapping says which resource serves the operation, expose says ACP may invoke it, and both are required`,
246
+ { resourceId }
247
+ );
248
+ }
249
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
250
+ throw acpInvalid(
251
+ path2,
252
+ `Resource "${resourceId}" implements ACP operation "${operation}" and must use pricing.type "free" with no "payments" - ACP checkout carries the merchant's own purchase payment, so the gateway does not also charge for the invocation`,
253
+ { resourceId }
254
+ );
255
+ }
256
+ validateAcpOperationInput(path2, operation, resource);
257
+ }
258
+ }
259
+ function validateAcpOperationInput(path2, operation, resource) {
260
+ const schema = resource.inputSchema;
261
+ if (schema === void 0) return;
262
+ const keys = ACP_OPERATION_INPUT_KEYS[operation];
263
+ const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
264
+ const sent = [...keys, ...optionalKeys];
265
+ const topLevel = closedObjectProperties(schema);
266
+ if (topLevel !== void 0) {
267
+ for (const key of sent) {
268
+ if (!Object.hasOwn(topLevel, key)) {
269
+ const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
270
+ throw acpInvalid(
271
+ path2,
272
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter ${when} for this operation`,
273
+ { resourceId: resource.id }
274
+ );
275
+ }
276
+ }
277
+ }
278
+ const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
279
+ const pathKeys = closedObjectProperties(declared["path"]);
280
+ if (sent.includes("path") && pathKeys !== void 0 && !Object.hasOwn(pathKeys, ACP_SESSION_ID_INPUT_KEY)) {
281
+ throw acpInvalid(
282
+ path2,
283
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its "path" schema sets additionalProperties: false without declaring "${ACP_SESSION_ID_INPUT_KEY}", which the adapter always sends inside "path"`,
284
+ { resourceId: resource.id }
285
+ );
286
+ }
287
+ const bodyKeys = closedObjectProperties(declared["body"]);
288
+ if (sent.includes("body") && bodyKeys !== void 0 && Object.keys(bodyKeys).length === 0) {
289
+ throw acpInvalid(
290
+ path2,
291
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its "body" schema accepts no keys, so it refuses every ACP document. Config load closes an object schema that omits additionalProperties: declare body as { type: object, additionalProperties: true }`,
292
+ { resourceId: resource.id }
293
+ );
294
+ }
295
+ const required = schema["required"];
296
+ if (Array.isArray(required)) {
297
+ for (const entry of required) {
298
+ if (typeof entry === "string" && !keys.includes(entry)) {
299
+ throw acpInvalid(
300
+ path2,
301
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation does not always supply (it always sends: ${keys.join(", ")})`,
302
+ { resourceId: resource.id }
303
+ );
304
+ }
305
+ }
306
+ }
307
+ }
308
+ function closedObjectProperties(schema) {
309
+ if (!isRecord(schema) || schema["additionalProperties"] !== false) return void 0;
310
+ return isRecord(schema["properties"]) ? schema["properties"] : {};
311
+ }
312
+
313
+ // src/config/schema/ap2.ts
314
+ var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
315
+ function ap2Invalid(path2, message, extra = {}) {
316
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
317
+ }
318
+ function normalizeAp2(raw) {
319
+ if (raw === void 0) return void 0;
320
+ if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
321
+ const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
322
+ if (specVersion !== AP2_SPEC_VERSION) {
323
+ throw ap2Invalid(
324
+ "authorization.ap2.specVersion",
325
+ `authorization.ap2.specVersion "${specVersion}" is not supported - this gateway verifies against the tagged AP2 release ${AP2_SPEC_VERSION} only`
326
+ );
327
+ }
328
+ const mode = raw.mode ?? AP2_MODES[0];
329
+ if (!AP2_MODES.includes(mode)) {
330
+ throw ap2Invalid(
331
+ "authorization.ap2.mode",
332
+ `authorization.ap2.mode "${mode}" is not supported. Supported: ${AP2_MODES.join(", ")}. Autonomous mode needs open mandates, agent key binding and constraint evaluation, none of which this release implements.`
333
+ );
334
+ }
335
+ if (raw.replay === void 0) {
336
+ throw ap2Invalid(
337
+ "authorization.ap2.replay",
338
+ "authorization.ap2.replay is required when AP2 is enabled - it names the SQLite file recording which mandates have been spent, and without it a verified mandate could authorize a second settlement"
339
+ );
340
+ }
341
+ const mandateIssuers = normalizeIssuers(
342
+ raw.trust?.mandateIssuers,
343
+ "authorization.ap2.trust.mandateIssuers"
344
+ );
345
+ const checkoutIssuers = normalizeIssuers(
346
+ raw.trust?.checkoutIssuers,
347
+ "authorization.ap2.trust.checkoutIssuers"
348
+ );
349
+ return {
350
+ enabled: true,
351
+ specVersion: AP2_SPEC_VERSION,
352
+ mode,
353
+ trust: { mandateIssuers, checkoutIssuers },
354
+ clockSkewSeconds: toNumber(
355
+ raw.clockSkewSeconds ?? AP2_DEFAULT_CLOCK_SKEW_SECONDS,
356
+ "authorization.ap2.clockSkewSeconds",
357
+ { min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
358
+ ),
359
+ requireMandateAudience: toBoolean(
360
+ raw.requireMandateAudience ?? false,
361
+ "authorization.ap2.requireMandateAudience"
362
+ ),
363
+ requireMandateExpiry: toBoolean(
364
+ raw.requireMandateExpiry ?? false,
365
+ "authorization.ap2.requireMandateExpiry"
366
+ ),
367
+ replay: { path: raw.replay.path }
368
+ };
369
+ }
370
+ function normalizeIssuers(raw, path2) {
371
+ if (raw === void 0 || raw.length === 0) {
372
+ throw ap2Invalid(
373
+ path2,
374
+ `${path2} is required when AP2 is enabled and must name at least one issuer - trust is operator-configured and static, so an empty list means no mandate can ever verify`
375
+ );
376
+ }
377
+ const seen = /* @__PURE__ */ new Set();
378
+ return raw.map((entry, index) => {
379
+ const entryPath = `${path2}.${index}`;
380
+ if (seen.has(entry.issuer)) {
381
+ throw ap2Invalid(
382
+ `${entryPath}.issuer`,
383
+ `${entryPath}.issuer "${entry.issuer}" is listed twice - key lookup resolves by issuer, so a second entry for the same one is silently unreachable. Merge the keys into one entry instead (which is also how a rotation overlaps an old and a new key).`,
384
+ { issuer: entry.issuer }
385
+ );
386
+ }
387
+ seen.add(entry.issuer);
388
+ const kids = /* @__PURE__ */ new Set();
389
+ const keys = entry.keys.map((key, keyIndex) => {
390
+ const keyPath = `${entryPath}.keys.${keyIndex}`;
391
+ if (kids.has(key.kid)) {
392
+ throw ap2Invalid(
393
+ `${keyPath}.kid`,
394
+ `${keyPath}.kid "${key.kid}" is listed twice for issuer "${entry.issuer}" - a kid selects exactly one key, so which of the two verifies a signature would be undefined`,
395
+ { issuer: entry.issuer, kid: key.kid }
396
+ );
397
+ }
398
+ kids.add(key.kid);
399
+ return { kid: key.kid, jwk: validateJwk(key.jwk, key.kid, keyPath) };
400
+ });
401
+ return { issuer: entry.issuer, audience: entry.audience, keys };
402
+ });
403
+ }
404
+ function validateJwk(jwk, kid, path2) {
405
+ const fail = (detail) => {
406
+ throw ap2Invalid(`${path2}.jwk`, `${path2}.jwk (kid "${kid}") ${detail}`, { kid });
407
+ };
408
+ for (const member of Object.keys(jwk)) {
409
+ if (AP2_JWK_MEMBERS.includes(member)) continue;
410
+ if (member === "d" || member === "k") {
411
+ fail(
412
+ `carries private key material ("${member}"). The gateway verifies signatures and never produces them; publish only the public half. Treat the pasted key as compromised and rotate it.`
413
+ );
414
+ }
415
+ fail(
416
+ `has unsupported member "${member}". Allowed: ${AP2_JWK_MEMBERS.join(", ")}. Members naming a URL are refused on purpose - keys are configured inline and never fetched.`
417
+ );
418
+ }
419
+ const value = (member) => {
420
+ const raw = jwk[member];
421
+ if (typeof raw !== "string" || raw.length === 0) {
422
+ fail(`must give "${member}" as a non-empty string`);
423
+ }
424
+ return raw;
425
+ };
426
+ if (value("kty") !== AP2_KEY_TYPE) {
427
+ fail(
428
+ `must have kty "${AP2_KEY_TYPE}" (got "${value("kty")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
429
+ );
430
+ }
431
+ if (value("crv") !== AP2_JWK_CURVE) {
432
+ fail(
433
+ `must be on curve "${AP2_JWK_CURVE}" (got "${value("crv")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
434
+ );
435
+ }
436
+ for (const coordinate of ["x", "y"]) {
437
+ const encoded = value(coordinate);
438
+ if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
439
+ fail(`coordinate "${coordinate}" is not base64url (no padding, no "+" or "/")`);
440
+ }
441
+ if (Buffer.from(encoded, "base64url").length !== AP2_JWK_COORDINATE_BYTES) {
442
+ fail(
443
+ `coordinate "${coordinate}" decodes to ${Buffer.from(encoded, "base64url").length} bytes; a ${AP2_JWK_CURVE} coordinate is ${AP2_JWK_COORDINATE_BYTES}`
444
+ );
445
+ }
446
+ }
447
+ if (jwk["alg"] !== void 0 && value("alg") !== AP2_SIGNING_ALGORITHM) {
448
+ fail(`declares alg "${value("alg")}"; only ${AP2_SIGNING_ALGORITHM} is accepted`);
449
+ }
450
+ if (jwk["use"] !== void 0 && value("use") !== "sig") {
451
+ fail(`declares use "${value("use")}"; a verification key must be "sig"`);
452
+ }
453
+ if (jwk["kid"] !== void 0 && value("kid") !== kid) {
454
+ fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
455
+ }
456
+ const normalized = {};
457
+ for (const member of AP2_JWK_MEMBERS) {
458
+ if (jwk[member] !== void 0) normalized[member] = value(member);
459
+ }
460
+ return normalized;
461
+ }
462
+ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
463
+ if (replayPath === ":memory:") return;
464
+ const others = [["storage.receipts.path", receiptsPath]];
465
+ if (acp.enabled) others.push(["protocols.acp.idempotency.path", acp.idempotency.path]);
466
+ for (const [otherPath, other] of others) {
467
+ if (replayPath !== other) continue;
468
+ throw ap2Invalid(
469
+ "authorization.ap2.replay.path",
470
+ `authorization.ap2.replay.path is the same file as ${otherPath} ("${replayPath}") - the AP2 replay store keeps its own schema and must not share a database with another store`
471
+ );
472
+ }
473
+ }
474
+ function normalizeResourceAuthorization(id, entry, pricing, ap2) {
475
+ const required = entry.authorization?.required;
476
+ if (required === void 0) return void 0;
477
+ const path2 = `resources.${id}.authorization.required`;
478
+ const methods = [];
479
+ for (const method of required) {
480
+ if (!SUPPORTED_AUTHORIZATION_METHODS.has(method)) {
481
+ throw ap2Invalid(
482
+ path2,
483
+ `Resource "${id}" requires unsupported authorization method "${method}". Supported: ${[...SUPPORTED_AUTHORIZATION_METHODS].join(", ")}.`,
484
+ { resourceId: id, method }
485
+ );
486
+ }
487
+ if (methods.includes(method)) {
488
+ throw ap2Invalid(path2, `Resource "${id}" lists authorization method "${method}" twice`, {
489
+ resourceId: id,
490
+ method
491
+ });
492
+ }
493
+ if (method === "ap2" && (ap2 === void 0 || !ap2.enabled)) {
494
+ throw ap2Invalid(
495
+ path2,
496
+ `Resource "${id}" requires authorization method "ap2", which is not configured or not enabled under authorization.ap2`,
497
+ { resourceId: id, method }
498
+ );
499
+ }
500
+ methods.push(method);
501
+ }
502
+ if (pricing.type !== "fixed") {
503
+ throw ap2Invalid(
504
+ path2,
505
+ `Resource "${id}" requires authorization but its pricing is "${pricing.type}" - authorization proves a purchase was approved and never replaces payment, so it applies to fixed-price paid resources only in this release`,
506
+ { resourceId: id }
507
+ );
508
+ }
509
+ return { required: methods };
510
+ }
511
+
512
+ // src/config/schema/x402.ts
513
+ function x402Invalid(message, path2) {
514
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
515
+ }
516
+ var DIRECT_OPTION_FIELDS = [
517
+ "network",
518
+ "payTo",
519
+ "asset",
520
+ "assetName",
521
+ "assetVersion",
522
+ "assetDecimals",
523
+ "assetTransferMethod"
524
+ ];
525
+ function x402OptionInput(raw, path2) {
526
+ return {
527
+ ...raw.scheme !== void 0 ? { scheme: raw.scheme } : {},
528
+ network: raw.network,
529
+ payTo: raw.payTo,
530
+ ...raw.asset !== void 0 ? { asset: raw.asset } : {},
531
+ ...raw.assetName !== void 0 ? { assetName: raw.assetName } : {},
532
+ ...raw.assetVersion !== void 0 ? { assetVersion: raw.assetVersion } : {},
533
+ ...raw.assetTransferMethod !== void 0 ? { assetTransferMethod: raw.assetTransferMethod } : {},
534
+ ...raw.assetDecimals !== void 0 ? {
535
+ assetDecimals: toNumber(raw.assetDecimals, `${path2}.assetDecimals`, { min: 0, max: 36 })
536
+ } : {}
537
+ };
538
+ }
539
+ function normalizeX402(raw, resources) {
540
+ const at = "payments.x402";
541
+ const direct = DIRECT_OPTION_FIELDS.filter((field) => raw[field] !== void 0);
542
+ if (raw.options !== void 0 && direct.length > 0) {
543
+ throw x402Invalid(
544
+ `${at}: give the option either under ${at}.options or directly (${direct.join(", ")}), not both`,
545
+ `${at}.options`
546
+ );
547
+ }
548
+ if (raw.options?.length === 0) {
549
+ throw x402Invalid(`${at}.options is empty; list at least one option`, `${at}.options`);
550
+ }
551
+ if (raw.options === void 0) {
552
+ for (const field of ["network", "payTo"]) {
553
+ if (raw[field] === void 0) {
554
+ throw x402Invalid(
555
+ `${at}.${field} is required, unless the options are listed under ${at}.options`,
556
+ `${at}.${field}`
557
+ );
558
+ }
559
+ }
560
+ }
561
+ const settings = raw.networks ?? {};
562
+ const declared = {};
563
+ for (const [id, entry] of Object.entries(settings)) {
564
+ if (entry.kind === void 0 && entry.displayName === void 0) continue;
565
+ if (entry.kind === void 0 || entry.displayName === void 0) {
566
+ const path2 = `${at}.networks.${id}`;
567
+ throw x402Invalid(`${path2}: declaring a network needs both kind and displayName`, path2);
568
+ }
569
+ declared[id] = { kind: entry.kind, displayName: entry.displayName };
570
+ }
571
+ const networks = Object.keys(declared).length > 0 ? declared : void 0;
572
+ if (networks !== void 0) validateCustomNetworks(networks, `${at}.networks`);
573
+ for (const id of Object.keys(settings)) {
574
+ if (!findX402Network(id, networks)) {
575
+ const path2 = `${at}.networks.${id}`;
576
+ throw x402Invalid(
577
+ `${path2}: "${id}" is not a known network; declare it with kind and displayName`,
578
+ path2
579
+ );
580
+ }
581
+ }
582
+ const facilitator = normalizeFacilitator(raw.facilitator);
583
+ const allowMainnet = raw.allowMainnet !== void 0 ? toBoolean(raw.allowMainnet, `${at}.allowMainnet`) : void 0;
584
+ const allowUnauthenticatedFacilitator = raw.allowUnauthenticatedFacilitator !== void 0 ? toBoolean(raw.allowUnauthenticatedFacilitator, `${at}.allowUnauthenticatedFacilitator`) : void 0;
585
+ const rpcOwners = /* @__PURE__ */ new Map();
586
+ let firstMode;
587
+ const resolveList = (entries) => {
588
+ const resolved = entries.map(
589
+ ([path2, entry]) => resolveX402Option(x402OptionInput(entry, path2), path2, networks)
590
+ );
591
+ const seen = /* @__PURE__ */ new Set();
592
+ const options2 = resolved.map((option, i) => {
593
+ const path2 = entries[i]?.[0];
594
+ const network = option.network.id;
595
+ const rpcUrl = settings[network]?.rpcUrl ?? raw.rpcUrl;
596
+ if (rpcUrl === void 0) {
597
+ throw x402Invalid(
598
+ `${path2}.network: no RPC endpoint for ${network}; set ${at}.rpcUrl or ${at}.networks.${network}.rpcUrl`,
599
+ `${at}.rpcUrl`
600
+ );
601
+ }
602
+ const endpoint = new URL(rpcUrl).href;
603
+ const owner = rpcOwners.get(endpoint);
604
+ if (owner !== void 0 && owner !== network) {
605
+ throw x402Invalid(
606
+ `${at}: ${owner} and ${network} would use the same RPC endpoint; give each network its own ${at}.networks.<id>.rpcUrl`,
607
+ `${at}.networks`
608
+ );
609
+ }
610
+ rpcOwners.set(endpoint, network);
611
+ const comparable = (value) => option.network.family === "evm" ? value.toLowerCase() : value;
612
+ const key = `${network}|${comparable(option.asset)}|${comparable(option.payTo)}`;
613
+ if (seen.has(key)) {
614
+ throw x402Invalid(
615
+ `${path2}: repeats an earlier option, the same asset to the same payTo on ${network}`,
616
+ path2
617
+ );
618
+ }
619
+ seen.add(key);
620
+ const { mode } = resolveX402Deployment({
621
+ network,
622
+ payTo: option.payTo,
623
+ asset: option.asset,
624
+ ...option.assetName !== void 0 ? { assetName: option.assetName } : {},
625
+ ...option.assetVersion !== void 0 ? { assetVersion: option.assetVersion } : {},
626
+ facilitator,
627
+ ...allowMainnet !== void 0 ? { allowMainnet } : {},
628
+ ...allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator } : {},
629
+ ...networks !== void 0 ? { customNetworks: networks } : {}
630
+ });
631
+ if (firstMode !== void 0 && mode !== firstMode) {
632
+ throw x402Invalid(
633
+ `${path2}.network: ${network} is a ${mode} network, but the first option is on a ${firstMode} one; every option must be on the same kind of network`,
634
+ `${path2}.network`
635
+ );
636
+ }
637
+ firstMode = mode;
638
+ return {
639
+ network,
640
+ rpcUrl,
641
+ asset: option.asset,
642
+ ...option.assetName !== void 0 ? { assetName: option.assetName } : {},
643
+ ...option.assetVersion !== void 0 ? { assetVersion: option.assetVersion } : {},
644
+ assetDecimals: option.assetDecimals,
645
+ payTo: option.payTo,
646
+ ...option.assetTransferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {}
647
+ };
648
+ });
649
+ return {
650
+ options: options2,
651
+ defaultAssetSymbols: [
652
+ ...new Set(resolved.flatMap((option) => option.defaultAssetSymbol ?? []))
653
+ ]
654
+ };
655
+ };
656
+ const rail = resolveList(
657
+ raw.options !== void 0 ? raw.options.map((option, i) => [`${at}.options[${i}]`, option]) : [[at, raw]]
658
+ );
659
+ const resourceTerms = {};
660
+ for (const [id, entry] of Object.entries(resources)) {
661
+ const accepts = entry.x402?.accepts;
662
+ if (accepts === void 0) continue;
663
+ const path2 = `resources.${id}.x402.accepts`;
664
+ if (accepts.length === 0) {
665
+ throw x402Invalid(`${path2} is empty; list at least one option`, path2);
666
+ }
667
+ resourceTerms[id] = resolveList(accepts.map((option, i) => [`${path2}[${i}]`, option]));
668
+ }
669
+ const options = rail.options;
670
+ const first = options[0];
671
+ const resourceOptions = Object.fromEntries(
672
+ Object.entries(resourceTerms).map(([id, terms]) => [id, terms.options])
673
+ );
674
+ const config = {
675
+ enabled: toBoolean(raw.enabled, `${at}.enabled`),
676
+ ...first,
677
+ maxTimeoutSeconds: toNumber(raw.maxTimeoutSeconds, `${at}.maxTimeoutSeconds`, { min: 1 }),
678
+ paymentFlow: raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
679
+ facilitator,
680
+ ...allowMainnet !== void 0 ? { allowMainnet } : {},
681
+ ...allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator } : {},
682
+ ...networks !== void 0 ? { networks } : {},
683
+ options,
684
+ ...Object.keys(resourceOptions).length > 0 ? { resourceOptions } : {}
685
+ };
686
+ return { config, ...rail, resources: resourceTerms };
687
+ }
688
+ function normalizeFacilitator(raw) {
689
+ return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
690
+ }
691
+
692
+ // src/config/schema/mpp.ts
693
+ function normalizeMpp(raw) {
694
+ if (raw === void 0) return void 0;
695
+ const network = raw.network ?? MPP_DEFAULT_NETWORK;
696
+ if (!isMppNetwork(network)) {
697
+ throw new CommerceError(
698
+ "CONFIG_INVALID",
699
+ `payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
700
+ { details: { path: "payments.mpp.network" } }
701
+ );
702
+ }
703
+ const mpp = {
704
+ enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
705
+ network,
706
+ rpcUrl: raw.rpcUrl,
707
+ asset: raw.asset,
708
+ assetName: raw.assetName,
709
+ assetVersion: raw.assetVersion,
710
+ recipient: raw.recipient,
711
+ realm: raw.realm,
712
+ challengeSecret: raw.challengeSecret,
713
+ ...raw.challengeTtlSeconds !== void 0 ? {
714
+ challengeTtlSeconds: toNumber(
715
+ raw.challengeTtlSeconds,
716
+ "payments.mpp.challengeTtlSeconds",
717
+ { min: 1 }
718
+ )
719
+ } : {},
720
+ paymentFlow: raw.paymentFlow ?? MPP_DEFAULT_PAYMENT_FLOW,
721
+ facilitator: normalizeFacilitator(raw.facilitator),
722
+ ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
723
+ ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
724
+ allowUnauthenticatedFacilitator: toBoolean(
725
+ raw.allowUnauthenticatedFacilitator,
726
+ "payments.mpp.allowUnauthenticatedFacilitator"
727
+ )
728
+ } : {}
729
+ };
730
+ validateAddress("payments.mpp.recipient", mpp.recipient);
731
+ validateAddress("payments.mpp.asset", mpp.asset);
732
+ if (/[\r\n]/.test(mpp.realm)) {
733
+ throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
734
+ details: { path: "payments.mpp.realm" }
735
+ });
736
+ }
737
+ if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
738
+ throw new CommerceError(
739
+ "CONFIG_INVALID",
740
+ `payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
741
+ { details: { path: "payments.mpp.challengeSecret" } }
742
+ );
743
+ }
744
+ resolveX402Deployment({
745
+ network: mpp.network,
746
+ payTo: mpp.recipient,
747
+ asset: mpp.asset,
748
+ assetName: mpp.assetName,
749
+ assetVersion: mpp.assetVersion,
750
+ facilitator: mpp.facilitator,
751
+ ...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
752
+ ...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
753
+ configPath: "payments.mpp",
754
+ payToField: "recipient"
755
+ });
756
+ return mpp;
757
+ }
758
+ var SUPPORTED_CONFIG_VERSION = 1;
759
+ var NumberOrString = z.union([z.number(), z.string()]);
760
+ var BooleanOrString = z.union([z.boolean(), z.string()]);
761
+ var MediaType = z.string().regex(/^[A-Za-z0-9][\w!#$&^.+-]*\/[A-Za-z0-9][\w!#$&^.+-]*(?:[ \t]*;.*)?$/, {
762
+ message: "must be a media type such as application/json"
763
+ });
764
+ var RpcUrl = z.string().min(1).refine(isHttpUrl, { message: "must be an http:// or https:// URL" });
765
+ function isHttpUrl(value) {
766
+ try {
767
+ const { protocol } = new URL(value);
768
+ return protocol === "http:" || protocol === "https:";
769
+ } catch {
770
+ return false;
771
+ }
772
+ }
773
+ var MerchantSchema = z.object({
774
+ id: z.string().min(1),
775
+ name: z.string().min(1),
776
+ publicBaseUrl: z.string().min(1)
777
+ }).strict();
778
+ var ServerSchema = z.object({
779
+ port: NumberOrString,
780
+ host: z.string().min(1),
781
+ // Shared secret gating /api/receipts and /api/events. Without it both
782
+ // routes 404.
783
+ adminToken: z.string().min(1).optional(),
784
+ // Browser origins allowed to call the gateway; none by default. A request
785
+ // with any other `Origin` gets 403 on every route. Entries match the
786
+ // `Origin` header exactly (ignoring case), so `"*"` and a trailing slash
787
+ // would match nothing: a silent lockout that an operator might "fix" by
788
+ // disabling the check. Both are refused with a pointer.
789
+ allowedOrigins: z.array(
790
+ z.string().min(1).refine((origin) => origin !== "*", {
791
+ message: `wildcard "*" is not supported - allowedOrigins entries are matched literally against the browser's Origin header, so "*" would match nothing. List each scheme://host[:port] explicitly.`
792
+ }).refine((origin) => !origin.endsWith("/"), {
793
+ message: 'must not end with "/" - a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
794
+ })
795
+ ).optional()
796
+ }).strict();
797
+ var StorageSchema = z.object({
798
+ receipts: z.object({
799
+ driver: z.literal("sqlite"),
800
+ path: z.string().min(1)
801
+ }).strict()
802
+ }).strict();
803
+ var RESERVED_GATEWAY_PATHS = [
804
+ "/health",
805
+ "/ready",
806
+ "/.well-known/agent-commerce",
807
+ // Fixed by the A2A specification and served by the adapter
808
+ "/.well-known/agent-card.json",
809
+ // Fixed by the ACP specification
810
+ ACP_WELL_KNOWN_PATH,
811
+ "/api/resources",
812
+ "/api/resources/:id/invoke",
813
+ "/api/receipts",
814
+ "/api/events"
815
+ ];
816
+ var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
817
+ message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
818
+ }).refine((value) => !/[:*?\s]/.test(value), {
819
+ message: 'must not contain ":", "*", "?" or whitespace - the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
820
+ }).refine(
821
+ (value) => {
822
+ const base = value.replace(/\/+$/, "");
823
+ return !RESERVED_GATEWAY_PATHS.some(
824
+ (reserved) => reserved === base || reserved.startsWith(`${base}/`)
825
+ );
826
+ },
827
+ {
828
+ message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
829
+ }
830
+ );
831
+ var AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
832
+ var AcpIdempotencySchema = z.object({
833
+ path: z.string().min(1),
834
+ retentionHours: NumberOrString.optional(),
835
+ merchantIdempotent: z.boolean().optional()
836
+ }).strict();
837
+ var AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
838
+ var AcpDiscoverySchema = z.object({
839
+ documentationUrl: z.string().min(1).optional(),
840
+ supportedCurrencies: z.array(z.string().min(1)).optional(),
841
+ supportedLocales: z.array(z.string().min(1)).optional(),
842
+ interventionTypes: z.array(z.string().min(1)).optional()
843
+ }).strict();
844
+ var ProtocolsSchema = z.object({
845
+ http: z.object({ enabled: BooleanOrString }).strict(),
846
+ mcp: z.object({
847
+ enabled: BooleanOrString,
848
+ mountPath: MountPathSchema
849
+ }).strict(),
850
+ // Optional; A2A is off unless configured
851
+ a2a: z.object({
852
+ enabled: BooleanOrString,
853
+ mountPath: MountPathSchema.optional()
854
+ }).strict().optional(),
855
+ // Optional and off by default. The business pass requires the sub-blocks
856
+ // only when ACP is enabled, so a disabled placeholder stays valid and an
857
+ // enabled block gets a message naming what it lacks.
858
+ acp: z.object({
859
+ enabled: BooleanOrString,
860
+ mountPath: MountPathSchema.optional(),
861
+ auth: AcpAuthSchema.optional(),
862
+ idempotency: AcpIdempotencySchema.optional(),
863
+ checkout: AcpCheckoutSchema.optional(),
864
+ discovery: AcpDiscoverySchema.optional()
865
+ }).strict().optional()
866
+ }).strict();
867
+ var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
868
+ var BackendInputBindingsSchema = z.object({
869
+ path: z.string().min(1).optional(),
870
+ query: z.string().min(1).optional(),
871
+ body: z.string().min(1).optional()
872
+ }).strict();
873
+ var BackendHandlerSchema = z.object({
874
+ type: z.literal("http"),
875
+ method: BackendMethodSchema,
876
+ url: z.string().min(1),
877
+ headers: z.record(z.string(), z.string()).optional(),
878
+ timeoutMs: NumberOrString.optional(),
879
+ inputBindings: BackendInputBindingsSchema.optional()
880
+ }).strict();
881
+ var PricingSchema = z.discriminatedUnion("type", [
882
+ z.object({ type: z.literal("free") }).strict(),
883
+ z.object({
884
+ type: z.literal("fixed"),
885
+ amount: z.string().min(1),
886
+ currency: z.string().min(1)
887
+ }).strict(),
888
+ z.object({
889
+ type: z.literal("dynamic"),
890
+ resolver: z.string().min(1)
891
+ }).strict()
892
+ ]);
893
+ var JsonSchemaValueSchema = z.record(z.string(), z.unknown());
894
+ var X402OptionSchema = z.object({
895
+ scheme: z.string().min(1).optional(),
896
+ network: z.string().min(1),
897
+ payTo: z.string().min(1),
898
+ asset: z.string().min(1).optional(),
899
+ assetName: z.string().min(1).optional(),
900
+ assetVersion: z.string().min(1).optional(),
901
+ assetDecimals: NumberOrString.optional(),
902
+ assetTransferMethod: z.string().min(1).optional()
903
+ }).strict();
904
+ var ResourceEntrySchema = z.object({
905
+ name: z.string().min(1),
906
+ description: z.string().optional(),
907
+ input: JsonSchemaValueSchema.optional(),
908
+ output: JsonSchemaValueSchema.optional(),
909
+ // What the backend returns; x402 challenges declare it, `application/json` by default
910
+ mimeType: MediaType.optional(),
911
+ backend: BackendHandlerSchema,
912
+ pricing: PricingSchema,
913
+ expose: z.array(z.string().min(1)).min(1),
914
+ payments: z.array(z.string().min(1)).optional(),
915
+ paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
916
+ // This resource's own x402 offers, in place of `payments.x402` options
917
+ x402: z.object({ accepts: z.array(X402OptionSchema) }).strict().optional(),
918
+ authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
919
+ }).strict();
920
+ var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
921
+ var FacilitatorAuthSchema = z.discriminatedUnion("type", [
922
+ z.object({ type: z.literal("none") }).strict(),
923
+ z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
924
+ // Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
925
+ // which signs a fresh JWT per request.
926
+ z.object({
927
+ type: z.literal("cdp"),
928
+ apiKeyId: z.string().min(1),
929
+ apiKeySecret: z.string().min(1)
930
+ }).strict()
931
+ ]);
932
+ var FacilitatorSchema = z.discriminatedUnion("mode", [
933
+ z.object({
934
+ mode: z.literal("local"),
935
+ signerPrivateKey: z.string().min(1)
936
+ }).strict(),
937
+ z.object({
938
+ mode: z.literal("remote"),
939
+ url: z.string().min(1),
940
+ // Absent auth sends no credential and normalizes to `{ type: 'none' }`,
941
+ // which on mainnet requires `allowUnauthenticatedFacilitator`
942
+ auth: FacilitatorAuthSchema.optional()
943
+ }).strict()
944
+ ]);
945
+ var X402NetworkSettingsSchema = z.object({
946
+ kind: z.enum(["testnet", "mainnet"]).optional(),
947
+ displayName: z.string().min(1).optional(),
948
+ rpcUrl: RpcUrl.optional()
949
+ }).strict();
950
+ var X402Schema = z.object({
951
+ enabled: BooleanOrString,
952
+ // Accept direct fields or an `options` array, never both
953
+ network: z.string().min(1).optional(),
954
+ payTo: z.string().min(1).optional(),
955
+ asset: z.string().min(1).optional(),
956
+ assetName: z.string().min(1).optional(),
957
+ assetVersion: z.string().min(1).optional(),
958
+ assetDecimals: NumberOrString.optional(),
959
+ assetTransferMethod: z.string().min(1).optional(),
960
+ options: z.array(X402OptionSchema).optional(),
961
+ networks: z.record(z.string().min(1), X402NetworkSettingsSchema).optional(),
962
+ // The RPC for every network without its own `networks.<id>.rpcUrl`
963
+ rpcUrl: RpcUrl.optional(),
964
+ maxTimeoutSeconds: NumberOrString,
965
+ // `authorization` (the x402 default) settles after the backend call
966
+ paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
967
+ facilitator: FacilitatorSchema,
968
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
969
+ allowMainnet: BooleanOrString.optional(),
970
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
971
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
972
+ }).strict();
973
+ var MppSchema = z.object({
974
+ enabled: BooleanOrString,
975
+ network: z.string().min(1).optional(),
976
+ rpcUrl: RpcUrl,
977
+ asset: z.string().min(1),
978
+ assetName: z.string().min(1),
979
+ assetVersion: z.string().min(1),
980
+ recipient: z.string().min(1),
981
+ realm: z.string().min(1),
982
+ challengeSecret: z.string().min(1),
983
+ challengeTtlSeconds: NumberOrString.optional(),
984
+ // Same values as `payments.x402.paymentFlow`; the default is `upfront`
985
+ paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
986
+ facilitator: FacilitatorSchema,
987
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
988
+ allowMainnet: BooleanOrString.optional(),
989
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
990
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
991
+ }).strict();
992
+ var PaymentsSchema = z.object({
993
+ x402: X402Schema.optional(),
994
+ mpp: MppSchema.optional()
995
+ }).strict();
996
+ var Ap2KeySchema = z.object({
997
+ kid: z.string().min(1),
998
+ jwk: z.record(z.string(), z.unknown())
999
+ }).strict();
1000
+ var Ap2IssuerSchema = z.object({
1001
+ issuer: z.string().min(1),
1002
+ // Required, not defaulted: without it a mandate minted for another
1003
+ // merchant would verify here
1004
+ audience: z.string().min(1),
1005
+ keys: z.array(Ap2KeySchema).min(1)
1006
+ }).strict();
1007
+ var Ap2Schema = z.object({
1008
+ enabled: BooleanOrString,
1009
+ specVersion: z.string().min(1).optional(),
1010
+ mode: z.string().min(1).optional(),
1011
+ trust: z.object({
1012
+ mandateIssuers: z.array(Ap2IssuerSchema).optional(),
1013
+ checkoutIssuers: z.array(Ap2IssuerSchema).optional()
1014
+ }).strict().optional(),
1015
+ clockSkewSeconds: NumberOrString.optional(),
1016
+ requireMandateAudience: BooleanOrString.optional(),
1017
+ requireMandateExpiry: BooleanOrString.optional(),
1018
+ replay: z.object({ path: z.string().min(1) }).strict().optional()
1019
+ }).strict();
1020
+ var AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
1021
+ var RawConfigSchema = z.object({
1022
+ version: z.literal(SUPPORTED_CONFIG_VERSION),
1023
+ merchant: MerchantSchema,
1024
+ server: ServerSchema,
1025
+ storage: StorageSchema,
1026
+ protocols: ProtocolsSchema,
1027
+ resources: ResourcesMapSchema,
1028
+ payments: PaymentsSchema,
1029
+ authorization: AuthorizationSchema.optional()
1030
+ }).strict();
1031
+
1032
+ // src/config/schema/backend.ts
1033
+ function validateInputBindings(id, backend, input) {
1034
+ const legacy = { schema: input, where: "its input schema" };
1035
+ const bindings = backend.inputBindings;
1036
+ const templated = extractPathParameterNames(backend.url).length > 0;
1037
+ if (bindings === void 0) return legacy;
1038
+ const path2 = `resources.${id}.backend.inputBindings`;
1039
+ const fail = (message, details = {}) => {
1040
+ throw new CommerceError("CONFIG_INVALID", `Resource "${id}" ${message}`, {
1041
+ details: { path: path2, resourceId: id, ...details }
1042
+ });
1043
+ };
1044
+ const entries = Object.entries(bindings).filter(
1045
+ (entry) => entry[1] !== void 0
1046
+ );
1047
+ if (entries.length === 0) {
1048
+ fail("has an empty backend.inputBindings - remove the block to use the default mapping");
1049
+ }
1050
+ if (backend.method === "GET" || backend.method === "DELETE") {
1051
+ if (bindings.body !== void 0) {
1052
+ fail(
1053
+ `binds a request body on a ${backend.method}, which sends none - the value would be silently dropped`
1054
+ );
1055
+ }
1056
+ }
1057
+ if (templated && bindings.path === void 0) {
1058
+ fail(
1059
+ 'has backend.url path parameters but no "path" binding - in explicit binding mode nothing else supplies them, so every call would fail to reach the backend'
1060
+ );
1061
+ }
1062
+ const seen = /* @__PURE__ */ new Map();
1063
+ const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
1064
+ const required = new Set(
1065
+ Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1066
+ );
1067
+ for (const [location, property] of entries) {
1068
+ if (RESERVED_INPUT_FIELDS.includes(property)) {
1069
+ fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
1070
+ }
1071
+ const other = seen.get(property);
1072
+ if (other !== void 0) {
1073
+ fail(`binds both "${other}" and "${location}" to the input property "${property}"`, {
1074
+ location,
1075
+ property
1076
+ });
1077
+ }
1078
+ seen.set(property, location);
1079
+ if (!Object.hasOwn(properties, property)) {
1080
+ fail(
1081
+ `binds "${location}" to input property "${property}", which the input schema does not declare - the schema is closed, so a caller could never supply it`,
1082
+ { location, property }
1083
+ );
1084
+ }
1085
+ const declared = properties[property];
1086
+ if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
1087
+ fail(
1088
+ `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
1089
+ { location, property }
1090
+ );
1091
+ }
1092
+ }
1093
+ if (templated && bindings.path !== void 0 && !required.has(bindings.path)) {
1094
+ fail(
1095
+ `binds path parameters to input property "${bindings.path}" without listing it in the input schema's "required" - a caller that omits it cannot supply any path parameter, so the request could never be built`,
1096
+ { property: bindings.path }
1097
+ );
1098
+ }
1099
+ if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
1100
+ const group = properties[bindings.path];
1101
+ return {
1102
+ schema: isRecord(group) ? group : void 0,
1103
+ where: `input.properties.${bindings.path}`
1104
+ };
1105
+ }
1106
+ function validatePathParametersDeclared(id, url, input, where) {
1107
+ const stray = findUnparsedBraceToken(url);
1108
+ if (stray !== void 0) {
1109
+ throw new CommerceError(
1110
+ "CONFIG_INVALID",
1111
+ `Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would reach the backend as literal text, so a paid resource would take payment for a request the backend cannot serve`,
1112
+ { details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
1113
+ );
1114
+ }
1115
+ const params = extractPathParameterNames(url);
1116
+ if (params.length === 0) return;
1117
+ const firstBrace = url.indexOf("{");
1118
+ if (firstBrace !== -1) {
1119
+ const prefix = url.slice(0, firstBrace);
1120
+ let origin;
1121
+ try {
1122
+ const parsedPrefix = new URL(prefix);
1123
+ origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
1124
+ } catch {
1125
+ }
1126
+ if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
1127
+ throw new CommerceError(
1128
+ "CONFIG_INVALID",
1129
+ `Resource "${id}" has a backend.url parameter before the end of the host. A caller-supplied value would choose which host the gateway calls, which is request forgery with the gateway's own network position. Parameters are supported inside the path and query only`,
1130
+ { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1131
+ );
1132
+ }
1133
+ }
1134
+ const propertiesRaw = input?.["properties"];
1135
+ const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
1136
+ const requiredRaw = input?.["required"];
1137
+ const required = new Set(
1138
+ Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
1139
+ );
1140
+ for (const param of params) {
1141
+ const path2 = `resources.${id}.backend.url`;
1142
+ if (!Object.hasOwn(properties, param)) {
1143
+ throw new CommerceError(
1144
+ "CONFIG_INVALID",
1145
+ `Resource "${id}" has backend.url path parameter "{${param}}" which is not declared in ${where} - the caller has no way to supply it, so every call would be refused`,
1146
+ { details: { path: path2, resourceId: id, param } }
1147
+ );
1148
+ }
1149
+ if (!required.has(param)) {
1150
+ throw new CommerceError(
1151
+ "CONFIG_INVALID",
1152
+ `Resource "${id}" has backend.url path parameter "{${param}}" declared in ${where} but not listed in its "required" - a call that omits it would be refused`,
1153
+ { details: { path: path2, resourceId: id, param } }
1154
+ );
1155
+ }
1156
+ }
1157
+ }
1158
+ function validateBackendUrl(id, url) {
1159
+ let parsed;
1160
+ try {
1161
+ parsed = new URL(url);
1162
+ } catch {
1163
+ throw new CommerceError(
1164
+ "CONFIG_INVALID",
1165
+ `Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
1166
+ { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1167
+ );
1168
+ }
1169
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1170
+ throw new CommerceError(
1171
+ "CONFIG_INVALID",
1172
+ `Resource "${id}" has a backend.url with scheme "${parsed.protocol}": must be http:// or https://`,
1173
+ { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1174
+ );
1175
+ }
1176
+ }
1177
+ function validateBackendHeaders(id, headers) {
1178
+ for (const [name, value] of Object.entries(headers ?? {})) {
1179
+ try {
1180
+ new Headers([[name, value]]);
1181
+ } catch {
1182
+ throw new CommerceError(
1183
+ "CONFIG_INVALID",
1184
+ `Resource "${id}" has backend header "${name}" with an illegal name or value (a name must be an HTTP token; a value must not contain NUL, a character above U+00FF, or a line break except at either end, which fetch trims)`,
1185
+ { details: { path: `resources.${id}.backend.headers.${name}`, resourceId: id } }
1186
+ );
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ // src/config/schema/resource-schema.ts
1192
+ function defaultClosedObjectSchema(schema) {
1193
+ const isObjectSchema = isObjectSchemaNode(schema);
1194
+ const result = { ...schema };
1195
+ if (isObjectSchema) {
1196
+ const properties = schema["properties"];
1197
+ if (isRecord(properties)) {
1198
+ result["properties"] = Object.fromEntries(
1199
+ Object.entries(properties).map(([key, sub]) => [
1200
+ key,
1201
+ isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
1202
+ ])
1203
+ );
1204
+ }
1205
+ if (!Object.hasOwn(schema, "additionalProperties")) {
1206
+ result["additionalProperties"] = false;
1207
+ }
1208
+ }
1209
+ const items = schema["items"];
1210
+ if (isRecord(items)) {
1211
+ result["items"] = defaultClosedObjectSchema(items);
1212
+ }
1213
+ const additional = schema["additionalProperties"];
1214
+ if (isRecord(additional)) {
1215
+ result["additionalProperties"] = defaultClosedObjectSchema(additional);
1216
+ }
1217
+ return result;
1218
+ }
1219
+ var EMPTY_CLOSED_OBJECT_SCHEMA = {
1220
+ type: "object",
1221
+ properties: {},
1222
+ additionalProperties: false
1223
+ };
1224
+ var UNSUPPORTED_SCHEMA_KEYWORDS = [
1225
+ "pattern",
1226
+ "minLength",
1227
+ "maxLength",
1228
+ "format",
1229
+ "minimum",
1230
+ "maximum",
1231
+ "exclusiveMinimum",
1232
+ "exclusiveMaximum",
1233
+ "multipleOf",
1234
+ "oneOf",
1235
+ "anyOf",
1236
+ "allOf",
1237
+ "not",
1238
+ "$ref",
1239
+ "const",
1240
+ "patternProperties",
1241
+ "minItems",
1242
+ "maxItems",
1243
+ "uniqueItems"
1244
+ ];
1245
+ function excludesObjectType(schema) {
1246
+ const type = schema["type"];
1247
+ if (type === void 0 || type === "object") return false;
1248
+ if (Array.isArray(type)) return !type.includes("object");
1249
+ return true;
1250
+ }
1251
+ function validateResourceSchemaKeywords(id, path2, schema) {
1252
+ for (const keyword of UNSUPPORTED_SCHEMA_KEYWORDS) {
1253
+ if (Object.hasOwn(schema, keyword)) {
1254
+ console.warn(
1255
+ `[agent-commerce] resource "${id}" ${path2} uses JSON Schema keyword "${keyword}", which this gateway does not enforce (see src/core/execution/validation.ts for the supported subset). Remove it or treat it as documentation only.`
1256
+ );
1257
+ }
1258
+ }
1259
+ if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
1260
+ throw new CommerceError(
1261
+ "CONFIG_INVALID",
1262
+ `Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object" - the validator will never route a value through either check under that type, so this schema claims to constrain input it does not actually enforce. Remove "properties"/"required", or include "object" in "type".`,
1263
+ { details: { path: `resources.${id}.${path2}`, resourceId: id } }
1264
+ );
1265
+ }
1266
+ const requiredRaw = schema["required"];
1267
+ const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
1268
+ if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
1269
+ const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
1270
+ const undeclared = requiredRaw.filter(
1271
+ (name) => typeof name === "string" && !Object.hasOwn(declared, name)
1272
+ );
1273
+ if (undeclared.length > 0) {
1274
+ throw new CommerceError(
1275
+ "CONFIG_INVALID",
1276
+ `Resource "${id}" ${path2} lists ${undeclared.map((n) => `"${n}"`).join(", ")} in "required" but does not declare ${undeclared.length === 1 ? "it" : "them"} in "properties". The schema is closed (additionalProperties: false), so ${undeclared.length === 1 ? "that name is" : "those names are"} rejected as an unknown key and no input can ever satisfy this schema. Declare ${undeclared.length === 1 ? "it" : "them"} in "properties".`,
1277
+ { details: { path: `resources.${id}.${path2}`, resourceId: id, undeclared } }
1278
+ );
1279
+ }
1280
+ }
1281
+ const properties = schema["properties"];
1282
+ if (isRecord(properties)) {
1283
+ for (const [key, sub] of Object.entries(properties)) {
1284
+ if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1285
+ }
1286
+ }
1287
+ const additional = schema["additionalProperties"];
1288
+ if (isRecord(additional)) {
1289
+ validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
1290
+ }
1291
+ const items = schema["items"];
1292
+ if (isRecord(items)) {
1293
+ validateResourceSchemaKeywords(id, `${path2}.items`, items);
1294
+ } else if (Array.isArray(items)) {
1295
+ console.warn(
1296
+ `[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce - only a single schema applied to every array element is supported (see src/core/execution/validation.ts). Each position's schema is unenforced; treat it as documentation only.`
1297
+ );
1298
+ }
1299
+ }
1300
+
1301
+ // src/config/schema/resources.ts
1302
+ var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1303
+ var SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
1304
+ function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
1305
+ if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1306
+ const inputProperties = entry.input?.["properties"];
1307
+ if (inputProperties && typeof inputProperties === "object") {
1308
+ for (const reserved of RESERVED_INPUT_FIELDS) {
1309
+ if (!(reserved in inputProperties)) continue;
1310
+ throw new CommerceError(
1311
+ "CONFIG_INVALID",
1312
+ `Resource "${id}" declares an input property "${reserved}", which is reserved by the gateway`,
1313
+ {
1314
+ details: {
1315
+ path: `resources.${id}.input.properties.${reserved}`,
1316
+ resourceId: id
1317
+ }
1318
+ }
1319
+ );
1320
+ }
1321
+ }
1322
+ for (const protocol of entry.expose) {
1323
+ if (!SUPPORTED_PROTOCOLS.has(protocol)) {
1324
+ const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
1325
+ throw new CommerceError(
1326
+ "CONFIG_INVALID",
1327
+ `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: ${PROTOCOL_NAMES.join(", ")}.`,
1328
+ { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
1329
+ );
1330
+ }
1331
+ }
1332
+ if (entry.expose.includes("mcp")) {
1333
+ if (!protocols.mcp.enabled) {
1334
+ throw new CommerceError(
1335
+ "CONFIG_INVALID",
1336
+ `Resource "${id}" is exposed via "mcp" but protocols.mcp.enabled is false`,
1337
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1338
+ );
1339
+ }
1340
+ if (!MCP_TOOL_NAME_PATTERN.test(id)) {
1341
+ throw new CommerceError(
1342
+ "CONFIG_INVALID",
1343
+ `Resource "${id}" is exposed via "mcp" but its id is not a legal MCP tool name (allowed: A-Z, a-z, 0-9, "_", "-", ".", 1-128 chars)`,
1344
+ { details: { path: `resources.${id}`, resourceId: id } }
1345
+ );
1346
+ }
1347
+ }
1348
+ if (entry.expose.includes("a2a") && !protocols.a2a.enabled) {
1349
+ throw new CommerceError(
1350
+ "CONFIG_INVALID",
1351
+ `Resource "${id}" is exposed via "a2a" but protocols.a2a.enabled is false`,
1352
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1353
+ );
1354
+ }
1355
+ if (entry.expose.includes("acp") && !protocols.acp.enabled) {
1356
+ throw new CommerceError(
1357
+ "CONFIG_INVALID",
1358
+ `Resource "${id}" is exposed via "acp" but protocols.acp.enabled is false`,
1359
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1360
+ );
1361
+ }
1362
+ if (entry.expose.includes("http") && !protocols.http.enabled) {
1363
+ throw new CommerceError(
1364
+ "CONFIG_INVALID",
1365
+ `Resource "${id}" is exposed via "http" but protocols.http.enabled is false`,
1366
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1367
+ );
1368
+ }
1369
+ validateBackendUrl(id, entry.backend.url);
1370
+ validateBackendHeaders(id, entry.backend.headers);
1371
+ const pathScope = validateInputBindings(id, entry.backend, entry.input);
1372
+ validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
1373
+ if (entry.pricing.type === "dynamic") {
1374
+ throw new CommerceError(
1375
+ "CONFIG_INVALID",
1376
+ `Resource "${id}" uses pricing.type "dynamic", which is not supported in this release`,
1377
+ { details: { path: `resources.${id}.pricing.type`, resourceId: id } }
1378
+ );
1379
+ }
1380
+ const paymentMethods = entry.payments ?? [];
1381
+ if (entry.x402 !== void 0) {
1382
+ const at = `resources.${id}.x402`;
1383
+ if (x402 === void 0) {
1384
+ throw new CommerceError(
1385
+ "CONFIG_INVALID",
1386
+ `Resource "${id}" sets x402.accepts, but payments.x402 is not configured`,
1387
+ { details: { path: at, resourceId: id } }
1388
+ );
1389
+ }
1390
+ if (entry.pricing.type !== "fixed" || !paymentMethods.includes("x402")) {
1391
+ throw new CommerceError(
1392
+ "CONFIG_INVALID",
1393
+ `Resource "${id}" sets x402.accepts, which requires fixed pricing and "x402" in payments`,
1394
+ { details: { path: at, resourceId: id } }
1395
+ );
1396
+ }
1397
+ }
1398
+ const offers = x402 !== void 0 ? x402.resources[id] ?? x402 : void 0;
1399
+ if (entry.pricing.type === "fixed") {
1400
+ const decimals = paymentMethods.includes("x402") ? offers?.options.map((option) => option.assetDecimals) : void 0;
1401
+ validatePricingAmount(id, entry.pricing.amount, decimals && Math.min(...decimals));
1402
+ }
1403
+ if (entry.pricing.type === "fixed") {
1404
+ if (paymentMethods.length === 0) {
1405
+ throw new CommerceError(
1406
+ "CONFIG_INVALID",
1407
+ `Resource "${id}" has fixed pricing but declares no "payments" (a paid resource must name at least one payment method)`,
1408
+ { details: { path: `resources.${id}.payments`, resourceId: id } }
1409
+ );
1410
+ }
1411
+ for (const method of paymentMethods) {
1412
+ if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
1413
+ throw new CommerceError(
1414
+ "CONFIG_INVALID",
1415
+ `Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
1416
+ { details: { path: `resources.${id}.payments`, resourceId: id, method } }
1417
+ );
1418
+ }
1419
+ }
1420
+ const enabledRails = [
1421
+ ...x402?.config.enabled ? ["x402"] : [],
1422
+ ...mpp?.enabled ? ["mpp"] : []
1423
+ ];
1424
+ if (!paymentMethods.some((method) => enabledRails.includes(method))) {
1425
+ throw new CommerceError(
1426
+ "CONFIG_INVALID",
1427
+ `Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
1428
+ { details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
1429
+ );
1430
+ }
1431
+ const symbols = offers?.defaultAssetSymbols ?? [];
1432
+ const currency = entry.pricing.currency;
1433
+ if (x402?.config.enabled && symbols.length > 0 && paymentMethods.includes("x402") && currency !== "USD" && !(symbols.length === 1 && currency === symbols[0])) {
1434
+ const tokens = symbols.join(" and ");
1435
+ throw new CommerceError(
1436
+ "CONFIG_INVALID",
1437
+ `Resource "${id}" is priced in ${currency}, but x402 settles in ${tokens}, the default asset of its network. Price it in ${symbols.length === 1 ? `${tokens} or ` : ""}USD.`,
1438
+ { details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
1439
+ );
1440
+ }
1441
+ if (mpp?.enabled && paymentMethods.includes("mpp")) {
1442
+ if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
1443
+ throw new CommerceError(
1444
+ "CONFIG_INVALID",
1445
+ `Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
1446
+ { details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
1447
+ );
1448
+ }
1449
+ validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
1450
+ }
1451
+ }
1452
+ if (entry.paymentFlow !== void 0 && (entry.pricing.type !== "fixed" || !paymentMethods.some((method) => method === "x402" || method === "mpp"))) {
1453
+ throw new CommerceError(
1454
+ "CONFIG_INVALID",
1455
+ `Resource "${id}" sets paymentFlow, which requires fixed pricing and "x402" or "mpp" in payments`,
1456
+ { details: { path: `resources.${id}.paymentFlow`, resourceId: id } }
1457
+ );
1458
+ }
1459
+ if (entry.mimeType !== void 0 && (entry.pricing.type !== "fixed" || !paymentMethods.includes("x402"))) {
1460
+ throw new CommerceError(
1461
+ "CONFIG_INVALID",
1462
+ `Resource "${id}" sets mimeType, which requires fixed pricing and "x402" in payments`,
1463
+ { details: { path: `resources.${id}.mimeType`, resourceId: id } }
1464
+ );
1465
+ }
1466
+ const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
1467
+ const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
1468
+ return {
1469
+ id,
1470
+ name: entry.name,
1471
+ ...entry.description !== void 0 ? { description: entry.description } : {},
1472
+ inputSchema: entry.input !== void 0 ? defaultClosedObjectSchema(entry.input) : EMPTY_CLOSED_OBJECT_SCHEMA,
1473
+ ...entry.output !== void 0 ? { outputSchema: entry.output } : {},
1474
+ handler: {
1475
+ type: "http",
1476
+ method: entry.backend.method,
1477
+ url: entry.backend.url,
1478
+ ...entry.backend.headers !== void 0 ? { headers: entry.backend.headers } : {},
1479
+ ...entry.backend.timeoutMs !== void 0 ? {
1480
+ timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
1481
+ min: 1
1482
+ })
1483
+ } : {},
1484
+ ...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
1485
+ },
1486
+ pricing,
1487
+ exposedVia: entry.expose,
1488
+ paymentMethods,
1489
+ ...authorization !== void 0 ? { authorization } : {}
1490
+ };
1491
+ }
1492
+ var PRICING_AMOUNT_PATTERN = /^\d+(?:\.\d+)?$/;
1493
+ var ZERO_AMOUNT_PATTERN = /^0(?:\.0+)?$/;
1494
+ function validatePricingAmount(id, amount, decimals) {
1495
+ const path2 = `resources.${id}.pricing.amount`;
1496
+ if (!PRICING_AMOUNT_PATTERN.test(amount)) {
1497
+ throw new CommerceError(
1498
+ "CONFIG_INVALID",
1499
+ `Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent - e.g. "0.01")`,
1500
+ { details: { path: path2, resourceId: id } }
1501
+ );
1502
+ }
1503
+ if (ZERO_AMOUNT_PATTERN.test(amount)) {
1504
+ throw new CommerceError(
1505
+ "CONFIG_INVALID",
1506
+ `Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
1507
+ { details: { path: path2, resourceId: id } }
1508
+ );
1509
+ }
1510
+ if (decimals !== void 0) {
1511
+ const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
1512
+ if (fractionalDigits > decimals) {
1513
+ throw new CommerceError(
1514
+ "CONFIG_INVALID",
1515
+ `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
1516
+ { details: { path: path2, resourceId: id } }
1517
+ );
1518
+ }
1519
+ }
1520
+ }
1521
+ function pickDefined(value) {
1522
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1523
+ }
1524
+
1525
+ // src/config/schema/normalize.ts
1526
+ var DEFAULT_A2A_MOUNT_PATH = "/a2a";
1527
+ function normalize(raw) {
1528
+ const protocols = {
1529
+ http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
1530
+ mcp: {
1531
+ enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
1532
+ mountPath: raw.protocols.mcp.mountPath
1533
+ },
1534
+ a2a: {
1535
+ enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
1536
+ mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
1537
+ },
1538
+ acp: normalizeAcp(raw.protocols.acp)
1539
+ };
1540
+ validateMountPaths(protocols);
1541
+ const x402Settings = raw.payments.x402 !== void 0 ? normalizeX402(raw.payments.x402, raw.resources) : void 0;
1542
+ const x402 = x402Settings?.config;
1543
+ const mpp = normalizeMpp(raw.payments.mpp);
1544
+ const ap2 = normalizeAp2(raw.authorization?.ap2);
1545
+ if (ap2?.enabled) {
1546
+ validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
1547
+ }
1548
+ const resources = Object.entries(raw.resources).map(
1549
+ ([id, entry]) => normalizeResource(id, entry, protocols, x402Settings, mpp, ap2)
1550
+ );
1551
+ const flowOverrides = (rail) => {
1552
+ const flows = Object.fromEntries(
1553
+ resources.flatMap((resource) => {
1554
+ const flow = raw.resources[resource.id]?.paymentFlow;
1555
+ return flow !== void 0 && resource.paymentMethods.includes(rail) ? [[resource.id, flow]] : [];
1556
+ })
1557
+ );
1558
+ return Object.keys(flows).length > 0 ? { resourcePaymentFlows: flows } : {};
1559
+ };
1560
+ const mimeTypes = Object.fromEntries(
1561
+ resources.flatMap((resource) => {
1562
+ const mimeType = raw.resources[resource.id]?.mimeType;
1563
+ return mimeType !== void 0 ? [[resource.id, mimeType]] : [];
1564
+ })
1565
+ );
1566
+ const x402WithFlows = x402 !== void 0 ? {
1567
+ ...x402,
1568
+ ...flowOverrides("x402"),
1569
+ ...Object.keys(mimeTypes).length > 0 ? { resourceMimeTypes: mimeTypes } : {}
1570
+ } : x402;
1571
+ const mppWithFlows = mpp !== void 0 ? { ...mpp, ...flowOverrides("mpp") } : mpp;
1572
+ if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
1573
+ if (protocols.a2a.enabled && !resources.some((r) => r.exposedVia.includes("a2a"))) {
1574
+ throw new CommerceError(
1575
+ "CONFIG_INVALID",
1576
+ 'protocols.a2a.enabled is true but no resource lists "a2a" in its expose',
1577
+ { details: { path: "protocols.a2a.enabled" } }
1578
+ );
1579
+ }
1580
+ return {
1581
+ version: SUPPORTED_CONFIG_VERSION,
1582
+ merchant: {
1583
+ id: raw.merchant.id,
1584
+ name: raw.merchant.name,
1585
+ publicBaseUrl: raw.merchant.publicBaseUrl
1586
+ },
1587
+ // Port 0 lets the OS pick a free port, for tests and throwaway instances
1588
+ server: {
1589
+ port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
1590
+ host: raw.server.host,
1591
+ ...raw.server.adminToken !== void 0 ? { adminToken: raw.server.adminToken } : {},
1592
+ allowedOrigins: raw.server.allowedOrigins ?? []
1593
+ },
1594
+ storage: { receipts: { driver: "sqlite", path: raw.storage.receipts.path } },
1595
+ protocols,
1596
+ resources,
1597
+ payments: {
1598
+ ...x402WithFlows !== void 0 ? { x402: x402WithFlows } : {},
1599
+ ...mppWithFlows !== void 0 ? { mpp: mppWithFlows } : {}
1600
+ },
1601
+ ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
1602
+ };
1603
+ }
1604
+ function validateMountPaths(protocols) {
1605
+ const mounts = [
1606
+ ["mcp", protocols.mcp],
1607
+ ["a2a", protocols.a2a],
1608
+ ["acp", protocols.acp]
1609
+ ].filter(([, p]) => p.enabled);
1610
+ for (const [index, [nameA, a]] of mounts.entries()) {
1611
+ for (const [nameB, b] of mounts.slice(index + 1)) {
1612
+ const baseA = a.mountPath.replace(/\/+$/, "");
1613
+ const baseB = b.mountPath.replace(/\/+$/, "");
1614
+ if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
1615
+ throw new CommerceError(
1616
+ "CONFIG_INVALID",
1617
+ `protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") - each mount registers a wildcard, so overlapping prefixes cannot both be served`,
1618
+ { details: { path: `protocols.${nameA}.mountPath` } }
1619
+ );
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+
1625
+ // src/config/schema/index.ts
1626
+ function parseConfig(raw, env) {
1627
+ if (!isRecord(raw)) {
1628
+ throw new CommerceError(
1629
+ "CONFIG_INVALID",
1630
+ 'Configuration root must be a mapping (object) at "$"',
1631
+ {
1632
+ details: { path: "$" }
1633
+ }
1634
+ );
1635
+ }
1636
+ checkVersion(raw);
1637
+ const substituted = substituteEnv(raw, env);
1638
+ const parsed = parseWithZod(RawConfigSchema, substituted);
1639
+ return normalize(parsed);
1640
+ }
1641
+ function checkVersion(raw) {
1642
+ if (!("version" in raw)) {
1643
+ throw new CommerceError("CONFIG_INVALID", 'Configuration is missing required field "version"', {
1644
+ details: { path: "version" }
1645
+ });
1646
+ }
1647
+ const version = raw["version"];
1648
+ if (version !== SUPPORTED_CONFIG_VERSION) {
1649
+ const hint = typeof version === "string" ? ", written as a number without quotes" : "";
1650
+ throw new CommerceError(
1651
+ "CONFIG_INVALID",
1652
+ `Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
1653
+ { details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
1654
+ );
1655
+ }
1656
+ }
1657
+ function parseWithZod(schema, value) {
1658
+ const result = schema.safeParse(value);
1659
+ if (!result.success) {
1660
+ throw zodErrorToConfigError(result.error);
1661
+ }
1662
+ return result.data;
1663
+ }
1664
+ function zodErrorToConfigError(error) {
1665
+ const issues = error.issues.map(describeIssue);
1666
+ const first = issues[0];
1667
+ const message = first ? `Configuration invalid at "${first.path}": ${first.message}` : "Configuration invalid";
1668
+ return new CommerceError("CONFIG_INVALID", message, { details: { issues } });
1669
+ }
1670
+ function describeIssue(issue) {
1671
+ const path2 = issue.path.length > 0 ? issue.path.join(".") : "$";
1672
+ return { path: path2, message: issue.message, code: issue.code };
1673
+ }
1674
+
1675
+ // src/config/load.ts
1676
+ var CONFIG_PATH_ENV_VAR = "AGENT_COMMERCE_CONFIG";
1677
+ async function loadConfig(options = {}) {
1678
+ const env = options.env ?? process.env;
1679
+ const cwd = options.cwd ?? process.cwd();
1680
+ const configPath = resolveConfigPath(options.path, env, cwd);
1681
+ let text;
1682
+ try {
1683
+ text = await promises.readFile(configPath, "utf8");
1684
+ } catch (error) {
1685
+ throw new CommerceError("CONFIG_INVALID", `Could not read configuration file "${configPath}"`, {
1686
+ details: { path: configPath },
1687
+ cause: error
1688
+ });
1689
+ }
1690
+ let raw;
1691
+ try {
1692
+ raw = parse(text);
1693
+ } catch (error) {
1694
+ const { reason, details } = describeYamlError(error);
1695
+ throw new CommerceError(
1696
+ "CONFIG_INVALID",
1697
+ `Configuration file "${configPath}" is not valid YAML: ${reason}`,
1698
+ {
1699
+ details: { path: configPath, ...details },
1700
+ // Kept for programmatic callers. Never print it: its `.message` quotes
1701
+ // the source line (see describeYamlError).
1702
+ cause: error
1703
+ }
1704
+ );
1705
+ }
1706
+ return parseConfig(raw, env);
1707
+ }
1708
+ var YAML_ERROR_EXPLANATIONS = {
1709
+ DUPLICATE_KEY: "map keys must be unique",
1710
+ BAD_INDENT: "bad indentation",
1711
+ TAB_AS_INDENT: "tabs cannot be used for indentation",
1712
+ MISSING_CHAR: "a required character is missing",
1713
+ BLOCK_AS_IMPLICIT_KEY: "a block value cannot be used as a key (missing quotes, or a stray colon)",
1714
+ MULTILINE_IMPLICIT_KEY: "a key spans multiple lines (missing quotes, or a stray colon)",
1715
+ UNEXPECTED_TOKEN: "unexpected token"
1716
+ };
1717
+ function describeYamlError(error) {
1718
+ if (!(error instanceof YAMLError)) {
1719
+ return { reason: "unknown YAML parse error", details: {} };
1720
+ }
1721
+ const start = error.linePos?.[0];
1722
+ const where = start === void 0 ? "" : ` at line ${String(start.line)}, column ${String(start.col)}`;
1723
+ const explanation = YAML_ERROR_EXPLANATIONS[error.code];
1724
+ const what = explanation === void 0 ? error.code : `${error.code} (${explanation})`;
1725
+ return {
1726
+ reason: `${what}${where}`,
1727
+ details: {
1728
+ yamlErrorCode: error.code,
1729
+ ...start === void 0 ? {} : { line: start.line, column: start.col }
1730
+ }
1731
+ };
1732
+ }
1733
+ function resolveConfigPath(explicitPath, env, cwd) {
1734
+ if (explicitPath) return path.resolve(cwd, explicitPath);
1735
+ const fromEnv = env[CONFIG_PATH_ENV_VAR];
1736
+ if (fromEnv) return path.resolve(cwd, fromEnv);
1737
+ return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
1738
+ }
1739
+
1740
+ export { loadConfig, parseConfig, substituteEnv };
1741
+ //# sourceMappingURL=chunk-RWVPYAKC.js.map
1742
+ //# sourceMappingURL=chunk-RWVPYAKC.js.map