@devlab.group/agent-commerce 2.0.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 (105) hide show
  1. package/README.md +137 -56
  2. package/dist/ap2.d.ts +2 -2
  3. package/dist/ap2.js +4 -3
  4. package/dist/ap2.js.map +1 -1
  5. package/dist/{authorization-BpbWGGrf.d.ts → authorization-CMkIuAkO.d.ts} +1 -1
  6. package/dist/chunk-2CJNA7MS.js +8 -0
  7. package/dist/chunk-2CJNA7MS.js.map +1 -0
  8. package/dist/{chunk-CKJRITWY.js → chunk-5ZYXBCDX.js} +3 -3
  9. package/dist/{chunk-CKJRITWY.js.map → chunk-5ZYXBCDX.js.map} +1 -1
  10. package/dist/{chunk-3LUJHVKX.js → chunk-6Z2H53CG.js} +5 -4
  11. package/dist/chunk-6Z2H53CG.js.map +1 -0
  12. package/dist/{chunk-53WXXIPR.js → chunk-7ZMMXZ4G.js} +6 -10
  13. package/dist/chunk-7ZMMXZ4G.js.map +1 -0
  14. package/dist/{chunk-HOZA3IWP.js → chunk-ES3V6ECS.js} +16 -6
  15. package/dist/chunk-ES3V6ECS.js.map +1 -0
  16. package/dist/chunk-HNRDZLSN.js +1398 -0
  17. package/dist/chunk-HNRDZLSN.js.map +1 -0
  18. package/dist/{chunk-SYEGCNJZ.js → chunk-IEW7NSP2.js} +23 -5
  19. package/dist/chunk-IEW7NSP2.js.map +1 -0
  20. package/dist/chunk-KA2AX5FH.js +44 -0
  21. package/dist/chunk-KA2AX5FH.js.map +1 -0
  22. package/dist/chunk-Y4RE3XYF.js +581 -0
  23. package/dist/chunk-Y4RE3XYF.js.map +1 -0
  24. package/dist/cli/ap2-MBMQHFX4.js +678 -0
  25. package/dist/cli/ap2-MBMQHFX4.js.map +1 -0
  26. package/dist/cli/chunk-2ZZOGI5I.js +1352 -0
  27. package/dist/cli/chunk-2ZZOGI5I.js.map +1 -0
  28. package/dist/cli/chunk-3EQBCFZY.js +53 -0
  29. package/dist/cli/chunk-3EQBCFZY.js.map +1 -0
  30. package/dist/cli/chunk-3M6U7ILB.js +8 -0
  31. package/dist/cli/chunk-3M6U7ILB.js.map +1 -0
  32. package/dist/cli/chunk-3MGAJ5FX.js +70 -0
  33. package/dist/cli/chunk-3MGAJ5FX.js.map +1 -0
  34. package/dist/cli/chunk-5K4XEAZV.js +113 -0
  35. package/dist/cli/chunk-5K4XEAZV.js.map +1 -0
  36. package/dist/cli/chunk-7JNYFUT3.js +256 -0
  37. package/dist/cli/chunk-7JNYFUT3.js.map +1 -0
  38. package/dist/cli/chunk-AA3FIEAR.js +27 -0
  39. package/dist/cli/chunk-AA3FIEAR.js.map +1 -0
  40. package/dist/cli/chunk-FHCFR2GS.js +17 -0
  41. package/dist/cli/chunk-FHCFR2GS.js.map +1 -0
  42. package/dist/cli/chunk-FRMLWGSS.js +8 -0
  43. package/dist/cli/chunk-FRMLWGSS.js.map +1 -0
  44. package/dist/cli/chunk-KLX2YZDG.js +1400 -0
  45. package/dist/cli/chunk-KLX2YZDG.js.map +1 -0
  46. package/dist/cli/chunk-NSPPEF5M.js +334 -0
  47. package/dist/cli/chunk-NSPPEF5M.js.map +1 -0
  48. package/dist/cli/chunk-OSTCU6ZK.js +44 -0
  49. package/dist/cli/chunk-OSTCU6ZK.js.map +1 -0
  50. package/dist/cli/chunk-QQ3WY5L3.js +271 -0
  51. package/dist/cli/chunk-QQ3WY5L3.js.map +1 -0
  52. package/dist/cli/chunk-RWVPYAKC.js +1742 -0
  53. package/dist/cli/chunk-RWVPYAKC.js.map +1 -0
  54. package/dist/cli/chunk-SPAYKVUP.js +118 -0
  55. package/dist/cli/chunk-SPAYKVUP.js.map +1 -0
  56. package/dist/cli/chunk-SS3QFF5N.js +614 -0
  57. package/dist/cli/chunk-SS3QFF5N.js.map +1 -0
  58. package/dist/cli/chunk-TPC4EJLT.js +11 -0
  59. package/dist/cli/chunk-TPC4EJLT.js.map +1 -0
  60. package/dist/cli/config-4IBGVFSP.js +11 -0
  61. package/dist/cli/config-4IBGVFSP.js.map +1 -0
  62. package/dist/cli/index.js +822 -3392
  63. package/dist/cli/index.js.map +1 -1
  64. package/dist/cli/mcp-CNWQ2MOE.js +559 -0
  65. package/dist/cli/mcp-CNWQ2MOE.js.map +1 -0
  66. package/dist/cli/near-5AY3HWYF.js +58 -0
  67. package/dist/cli/near-5AY3HWYF.js.map +1 -0
  68. package/dist/cli/provider-4BQDH56Y.js +396 -0
  69. package/dist/cli/provider-4BQDH56Y.js.map +1 -0
  70. package/dist/cli/run-5MB2D45X.js +7286 -0
  71. package/dist/cli/run-5MB2D45X.js.map +1 -0
  72. package/dist/cli/svm-3MMYFPC7.js +33 -0
  73. package/dist/cli/svm-3MMYFPC7.js.map +1 -0
  74. package/dist/cli/x402-NKR3WQDQ.js +96 -0
  75. package/dist/cli/x402-NKR3WQDQ.js.map +1 -0
  76. package/dist/{constants-BSBRlKRR.d.ts → constants-nackLItD.d.ts} +2 -1
  77. package/dist/index.d.ts +61 -40
  78. package/dist/index.js +2642 -2205
  79. package/dist/index.js.map +1 -1
  80. package/dist/mcp.d.ts +3 -3
  81. package/dist/mcp.js +15 -20
  82. package/dist/mcp.js.map +1 -1
  83. package/dist/mpp.d.ts +12 -5
  84. package/dist/mpp.js +69 -65
  85. package/dist/mpp.js.map +1 -1
  86. package/dist/near-D6C42VPE.js +58 -0
  87. package/dist/near-D6C42VPE.js.map +1 -0
  88. package/dist/networks-olfflCI_.d.ts +38 -0
  89. package/dist/{protocol-adapter-DonxfAwa.d.ts → protocol-adapter-CfxU8Z2T.d.ts} +2 -2
  90. package/dist/{runtime-CiYbgGpi.d.ts → runtime-7vLNYvmU.d.ts} +25 -3
  91. package/dist/svm-QEUAUNPQ.js +33 -0
  92. package/dist/svm-QEUAUNPQ.js.map +1 -0
  93. package/dist/x402.d.ts +76 -64
  94. package/dist/x402.js +13 -4
  95. package/dist/x402.js.map +1 -1
  96. package/package.json +22 -2
  97. package/dist/chunk-3LUJHVKX.js.map +0 -1
  98. package/dist/chunk-53WXXIPR.js.map +0 -1
  99. package/dist/chunk-HOZA3IWP.js.map +0 -1
  100. package/dist/chunk-RKBGCPCK.js +0 -733
  101. package/dist/chunk-RKBGCPCK.js.map +0 -1
  102. package/dist/chunk-SYEGCNJZ.js.map +0 -1
  103. package/dist/chunk-TWRUNWDR.js +0 -265
  104. package/dist/chunk-TWRUNWDR.js.map +0 -1
  105. package/dist/{payment-flow-DCS882tR.d.ts → guardrails-CX_jYXvZ.d.ts} +8 -8
package/dist/cli/index.js CHANGED
@@ -1,2191 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { isDeepStrictEqual } from 'util';
3
- import { z } from 'zod';
4
- import { existsSync, mkdirSync, readFileSync, accessSync, constants, closeSync, openSync, chmodSync, promises } from 'fs';
5
- import * as path from 'path';
6
- import { dirname, resolve, basename, extname, win32, join } from 'path';
7
- import { stringify, parse, Document, YAMLError } from 'yaml';
8
- import { CommanderError, Command } from 'commander';
9
- import picocolors from 'picocolors';
10
- import { randomUUID } from 'crypto';
11
- import 'module';
12
- import Database from 'better-sqlite3';
13
- import { writeFile, stat, mkdir, rename, rm, open } from 'fs/promises';
14
- import { validate } from '@scalar/openapi-parser';
15
- import * as clack from '@clack/prompts';
16
-
17
- var __defProp = Object.defineProperty;
18
- var __getOwnPropNames = Object.getOwnPropertyNames;
19
- var __esm = (fn, res) => function __init() {
20
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
- };
22
- var __export = (target, all) => {
23
- for (var name in all)
24
- __defProp(target, name, { get: all[name], enumerable: true });
25
- };
26
-
27
- // src/config/filename.ts
28
- var DEFAULT_CONFIG_FILENAME;
29
- var init_filename = __esm({
30
- "src/config/filename.ts"() {
31
- DEFAULT_CONFIG_FILENAME = "config.yaml";
32
- }
33
- });
34
-
35
- // src/core/domain/common.ts
36
- var PROTOCOL_NAME_KEYS, PROTOCOL_NAMES, PAYMENT_METHOD_NAME_KEYS, PAYMENT_METHOD_NAMES;
37
- var init_common = __esm({
38
- "src/core/domain/common.ts"() {
39
- PROTOCOL_NAME_KEYS = {
40
- http: true,
41
- mcp: true,
42
- a2a: true,
43
- acp: true
44
- };
45
- PROTOCOL_NAMES = Object.keys(
46
- PROTOCOL_NAME_KEYS
47
- );
48
- PAYMENT_METHOD_NAME_KEYS = { x402: true, mpp: true };
49
- PAYMENT_METHOD_NAMES = Object.keys(
50
- PAYMENT_METHOD_NAME_KEYS
51
- );
52
- }
53
- });
54
-
55
- // src/core/domain/event.ts
56
- var init_event = __esm({
57
- "src/core/domain/event.ts"() {
58
- }
59
- });
60
-
61
- // src/core/domain/resource.ts
62
- var init_resource = __esm({
63
- "src/core/domain/resource.ts"() {
64
- }
65
- });
66
-
67
- // src/core/errors/codes.ts
68
- var COMMERCE_ERROR_HTTP_STATUS, RETRYABLE_ERROR_CODES;
69
- var init_codes = __esm({
70
- "src/core/errors/codes.ts"() {
71
- COMMERCE_ERROR_HTTP_STATUS = {
72
- CONFIG_INVALID: 500,
73
- RESOURCE_NOT_FOUND: 404,
74
- INPUT_INVALID: 400,
75
- PAYMENT_REQUIRED: 402,
76
- PAYMENT_INVALID: 402,
77
- PAYMENT_REPLAYED: 409,
78
- PAYMENT_PROVIDER_UNAVAILABLE: 503,
79
- PAYMENT_SETTLEMENT_FAILED: 502,
80
- // 403, not 402: a 402 tells a client to pay and retry, which cannot fix a
81
- // missing or rejected mandate, and a client that auto-pays on 402 would be
82
- // charged for a request that was never going to be delivered
83
- AUTHORIZATION_REQUIRED: 403,
84
- AUTHORIZATION_INVALID: 403,
85
- AUTHORIZATION_REPLAYED: 409,
86
- AUTHORIZATION_PROVIDER_UNAVAILABLE: 503,
87
- BACKEND_TIMEOUT: 504,
88
- BACKEND_ERROR: 502,
89
- PROTOCOL_UNSUPPORTED: 501,
90
- GATEWAY_BUSY: 503,
91
- STORAGE_ERROR: 500,
92
- INTERNAL_ERROR: 500
93
- };
94
- RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
95
- "PAYMENT_PROVIDER_UNAVAILABLE",
96
- // The verifier or replay store was unreachable, so the mandate got no
97
- // verdict. The same proof can verify once the outage clears.
98
- "AUTHORIZATION_PROVIDER_UNAVAILABLE",
99
- "BACKEND_TIMEOUT",
100
- // Load shedding is transient: the caller should back off and try again.
101
- // Clients act on this flag, and leaving it out would make a momentary
102
- // throttle look like a permanent failure.
103
- "GATEWAY_BUSY"
104
- ]);
105
- }
106
- });
107
-
108
- // src/core/errors/commerce-error.ts
109
- function isCommerceError(value) {
110
- return value instanceof CommerceError;
111
- }
112
- function toCommerceError(value, fallbackCode = "INTERNAL_ERROR", fallbackMessage = "Unexpected internal error") {
113
- if (isCommerceError(value)) return value;
114
- return new CommerceError(fallbackCode, fallbackMessage, { cause: value });
115
- }
116
- var CommerceError;
117
- var init_commerce_error = __esm({
118
- "src/core/errors/commerce-error.ts"() {
119
- init_codes();
120
- CommerceError = class _CommerceError extends Error {
121
- code;
122
- httpStatus;
123
- retryable;
124
- details;
125
- requestId;
126
- resourceId;
127
- constructor(code, message, options = {}) {
128
- super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
129
- this.name = "CommerceError";
130
- this.code = code;
131
- this.httpStatus = options.httpStatus ?? COMMERCE_ERROR_HTTP_STATUS[code];
132
- this.retryable = RETRYABLE_ERROR_CODES.has(code);
133
- if (options.details !== void 0) this.details = options.details;
134
- if (options.requestId !== void 0) this.requestId = options.requestId;
135
- if (options.resourceId !== void 0) this.resourceId = options.resourceId;
136
- Error.captureStackTrace?.(this, _CommerceError);
137
- }
138
- /** Non-secret, client-safe representation. `cause` is never included */
139
- toInfo() {
140
- return {
141
- code: this.code,
142
- message: this.message,
143
- httpStatus: this.httpStatus,
144
- retryable: this.retryable,
145
- ...this.requestId !== void 0 ? { requestId: this.requestId } : {},
146
- ...this.resourceId !== void 0 ? { resourceId: this.resourceId } : {},
147
- ...this.details !== void 0 ? { details: this.details } : {}
148
- };
149
- }
150
- };
151
- }
152
- });
153
-
154
- // src/core/errors/index.ts
155
- var init_errors = __esm({
156
- "src/core/errors/index.ts"() {
157
- init_codes();
158
- init_commerce_error();
159
- }
160
- });
161
-
162
- // src/core/is-record.ts
163
- function isRecord(value) {
164
- return typeof value === "object" && value !== null && !Array.isArray(value);
165
- }
166
- var init_is_record = __esm({
167
- "src/core/is-record.ts"() {
168
- }
169
- });
170
-
171
- // src/core/domain/wire.ts
172
- var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
173
- var init_wire = __esm({
174
- "src/core/domain/wire.ts"() {
175
- init_errors();
176
- init_is_record();
177
- PAYMENT_INPUT_FIELD = "_payment";
178
- AUTHORIZATION_INPUT_FIELD = "_authorization";
179
- RESERVED_INPUT_FIELDS = [
180
- PAYMENT_INPUT_FIELD,
181
- AUTHORIZATION_INPUT_FIELD
182
- ];
183
- }
184
- });
185
-
186
- // src/core/interfaces/logger.ts
187
- var NOOP_LOGGER;
188
- var init_logger = __esm({
189
- "src/core/interfaces/logger.ts"() {
190
- NOOP_LOGGER = {
191
- debug: () => {
192
- },
193
- info: () => {
194
- },
195
- warn: () => {
196
- },
197
- error: () => {
198
- },
199
- child: () => NOOP_LOGGER
200
- };
201
- }
202
- });
203
-
204
- // src/core/interfaces/protocol-adapter.ts
205
- var init_protocol_adapter = __esm({
206
- "src/core/interfaces/protocol-adapter.ts"() {
207
- }
208
- });
209
-
210
- // src/core/interfaces/runtime.ts
211
- var systemClock;
212
- var init_runtime = __esm({
213
- "src/core/interfaces/runtime.ts"() {
214
- systemClock = {
215
- now: () => /* @__PURE__ */ new Date(),
216
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
217
- monotonicMs: () => performance.now()
218
- };
219
- }
220
- });
221
-
222
- // src/core/public-types.ts
223
- var init_public_types = __esm({
224
- "src/core/public-types.ts"() {
225
- init_common();
226
- init_event();
227
- init_resource();
228
- init_wire();
229
- init_errors();
230
- init_logger();
231
- init_protocol_adapter();
232
- init_runtime();
233
- }
234
- });
235
-
236
- // src/core/index.ts
237
- var init_core = __esm({
238
- "src/core/index.ts"() {
239
- init_public_types();
240
- }
241
- });
242
-
243
- // src/authorization/ap2/constants.ts
244
- var AP2_SPEC_VERSION, AP2_MODES, AP2_SIGNING_ALGORITHM, AP2_KEY_TYPE, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, AP2_JWK_MEMBERS, AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS;
245
- var init_constants = __esm({
246
- "src/authorization/ap2/constants.ts"() {
247
- AP2_SPEC_VERSION = "0.2.0";
248
- AP2_MODES = ["direct"];
249
- AP2_SIGNING_ALGORITHM = "ES256";
250
- AP2_KEY_TYPE = "EC";
251
- AP2_JWK_CURVE = "P-256";
252
- AP2_JWK_COORDINATE_BYTES = 32;
253
- AP2_JWK_MEMBERS = ["kty", "crv", "x", "y", "kid", "alg", "use"];
254
- AP2_CHECKOUT_MANDATE_VCT = "mandate.checkout.1";
255
- AP2_CHECKOUT_PROFILE = "agent-commerce/ap2/checkout/v1";
256
- AP2_DEFAULT_CLOCK_SKEW_SECONDS = 60;
257
- AP2_MAX_CLOCK_SKEW_SECONDS = 300;
258
- }
259
- });
260
-
261
- // src/core/execution/backend-http.ts
262
- function extractPathParameterNames(url) {
263
- return [...url.matchAll(PATH_PARAM_PATTERN)].map((match) => match[1]);
264
- }
265
- function findUnparsedBraceToken(url) {
266
- const residue = url.replace(PATH_PARAM_PATTERN, "");
267
- const index = residue.search(/[{}]/);
268
- if (index === -1) return void 0;
269
- const token = /\{[^{}]*\}?|\}/.exec(residue.slice(index));
270
- return token?.[0] ?? residue[index];
271
- }
272
- var PATH_PARAM_PATTERN;
273
- var init_backend_http = __esm({
274
- "src/core/execution/backend-http.ts"() {
275
- init_resource();
276
- init_errors();
277
- init_logger();
278
- init_is_record();
279
- PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
280
- }
281
- });
282
-
283
- // src/core/execution/settlement-order.ts
284
- var init_settlement_order = __esm({
285
- "src/core/execution/settlement-order.ts"() {
286
- }
287
- });
288
- function typeList(raw) {
289
- if (typeof raw === "string") return [raw];
290
- if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
291
- return raw;
292
- }
293
- return void 0;
294
- }
295
- function isObjectSchemaNode(schema) {
296
- const types = typeList(schema["type"]);
297
- if (types !== void 0) return types.includes("object");
298
- return Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required");
299
- }
300
- var init_validation = __esm({
301
- "src/core/execution/validation.ts"() {
302
- init_is_record();
303
- }
304
- });
305
- var init_pipeline = __esm({
306
- "src/core/execution/pipeline.ts"() {
307
- init_wire();
308
- init_errors();
309
- init_is_record();
310
- init_backend_http();
311
- init_settlement_order();
312
- init_validation();
313
- [...RESERVED_INPUT_FIELDS, "__proto__"];
314
- }
315
- });
316
-
317
- // src/core/execution/registry.ts
318
- var init_registry = __esm({
319
- "src/core/execution/registry.ts"() {
320
- init_errors();
321
- }
322
- });
323
-
324
- // src/core/execution/store-event-sink.ts
325
- var init_store_event_sink = __esm({
326
- "src/core/execution/store-event-sink.ts"() {
327
- init_errors();
328
- }
329
- });
330
-
331
- // src/core/execution/index.ts
332
- var init_execution = __esm({
333
- "src/core/execution/index.ts"() {
334
- init_backend_http();
335
- init_pipeline();
336
- init_registry();
337
- init_store_event_sink();
338
- init_validation();
339
- }
340
- });
341
-
342
- // src/payments/mpp/constants.ts
343
- function isMppNetwork(value) {
344
- return MPP_NETWORKS.includes(value);
345
- }
346
- var MPP_SPEC_COMMIT, MPP_SPEC_DRAFTS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPPX_VERSION, MPP_PROFILE, MPP_NETWORKS, MPP_DEFAULT_NETWORK;
347
- var init_constants2 = __esm({
348
- "src/payments/mpp/constants.ts"() {
349
- MPP_SPEC_COMMIT = "806fdb8b8c92cda5c84b8660558921b6bbeef7e0";
350
- MPP_SPEC_DRAFTS = {
351
- core: "draft-httpauth-payment-01",
352
- intent: "draft-payment-intent-charge-00",
353
- method: "draft-evm-charge-00"
354
- };
355
- MPP_MIN_CHALLENGE_SECRET_LENGTH = 32;
356
- MPPX_VERSION = "0.13.1";
357
- MPP_PROFILE = {
358
- intent: "charge",
359
- method: "evm",
360
- credentialType: "authorization",
361
- assetSymbol: "USDC",
362
- assetDecimals: 6
363
- };
364
- MPP_NETWORKS = ["eip155:84532", "eip155:8453"];
365
- MPP_DEFAULT_NETWORK = "eip155:84532";
366
- `mpp/${MPP_SPEC_DRAFTS.core} intent=${MPP_PROFILE.intent} method=${MPP_PROFILE.method}`;
367
- }
368
- });
369
-
370
- // src/payments/x402/networks.ts
371
- function findNetworkProfile(id) {
372
- return BY_ID.get(id);
373
- }
374
- function requireNetworkProfile(id, path2) {
375
- const profile = BY_ID.get(id);
376
- if (!profile) {
377
- throw new CommerceError(
378
- "CONFIG_INVALID",
379
- `${path2}: "${id}" is not a supported network. Supported CAIP-2 identifiers: ${SUPPORTED_NETWORK_IDS.join(", ")}`,
380
- { details: { path: path2, network: id } }
381
- );
382
- }
383
- return profile;
384
- }
385
- function resolveDeploymentMode(profile, facilitatorMode) {
386
- if (facilitatorMode === "local") return "local";
387
- return profile.kind;
388
- }
389
- function describeDeploymentMode(mode) {
390
- return mode === "mainnet" ? LIVE_MAINNET_BANNER : mode.toUpperCase();
391
- }
392
- var PROFILES, BY_ID, SUPPORTED_NETWORK_IDS, LIVE_MAINNET_BANNER;
393
- var init_networks = __esm({
394
- "src/payments/x402/networks.ts"() {
395
- init_core();
396
- PROFILES = [
397
- {
398
- id: "eip155:84532",
399
- chainId: 84532,
400
- displayName: "Base Sepolia",
401
- kind: "testnet",
402
- canonicalAsset: {
403
- symbol: "USDC",
404
- address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
405
- name: "USDC",
406
- version: "2"
407
- }
408
- },
409
- {
410
- id: "eip155:8453",
411
- chainId: 8453,
412
- displayName: "Base",
413
- kind: "mainnet",
414
- canonicalAsset: {
415
- symbol: "USDC",
416
- address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
417
- name: "USD Coin",
418
- version: "2"
419
- }
420
- }
421
- ];
422
- BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
423
- SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
424
- LIVE_MAINNET_BANNER = "LIVE MAINNET MODE - REAL FUNDS";
425
- }
426
- });
427
-
428
- // src/protocols/acp/constants.ts
429
- var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS, ACP_SESSION_ID_INPUT_KEY;
430
- var init_constants3 = __esm({
431
- "src/protocols/acp/constants.ts"() {
432
- ACP_SPEC_VERSION = "2026-04-17";
433
- ACP_API_VERSION = "2026-04-17";
434
- ACP_WELL_KNOWN_PATH = "/.well-known/acp.json";
435
- ACP_CHECKOUT_OPERATIONS = [
436
- "createCheckoutSession",
437
- "updateCheckoutSession",
438
- "getCheckoutSession",
439
- "completeCheckoutSession",
440
- "cancelCheckoutSession"
441
- ];
442
- ACP_OPERATION_INPUT_KEYS = {
443
- createCheckoutSession: ["body"],
444
- updateCheckoutSession: ["path", "body"],
445
- getCheckoutSession: ["path"],
446
- completeCheckoutSession: ["path", "body"],
447
- cancelCheckoutSession: ["path"]
448
- };
449
- ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
450
- cancelCheckoutSession: ["body"]
451
- };
452
- ACP_SESSION_ID_INPUT_KEY = "checkout_session_id";
453
- }
454
- });
455
-
456
- // src/config/env.ts
457
- function substituteEnv(value, env, path2 = "$") {
458
- if (typeof value === "string") {
459
- return substituteString(value, env, path2);
460
- }
461
- if (Array.isArray(value)) {
462
- return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
463
- }
464
- if (isRecord(value)) {
465
- const result = {};
466
- for (const [key, entry] of Object.entries(value)) {
467
- result[key] = substituteEnv(entry, env, `${path2}.${key}`);
468
- }
469
- return result;
470
- }
471
- return value;
472
- }
473
- function substituteString(value, env, path2) {
474
- assertTemplateIsSupported(value, path2);
475
- return substituteOnce(value, env, path2);
476
- }
477
- function assertTemplateIsSupported(value, path2) {
478
- if (NESTED_PLACEHOLDER.test(value)) {
479
- throw new CommerceError(
480
- "CONFIG_INVALID",
481
- `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.`,
482
- { details: { path: path2 } }
483
- );
484
- }
485
- for (const [token] of value.matchAll(BRACE_TOKEN)) {
486
- if (SUPPORTED_TOKEN.test(token)) continue;
487
- throw new CommerceError(
488
- "CONFIG_INVALID",
489
- `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.`,
490
- { details: { path: path2, token } }
491
- );
492
- }
493
- }
494
- function substituteOnce(value, env, path2) {
495
- return value.replace(
496
- PLACEHOLDER_PATTERN,
497
- (_match, name, hasDefault, def) => {
498
- const envValue = env[name];
499
- if (hasDefault !== void 0) {
500
- if (envValue === void 0 || envValue === "") return def ?? "";
501
- return envValue;
502
- }
503
- if (envValue === void 0) {
504
- throw new CommerceError(
505
- "CONFIG_INVALID",
506
- `Unresolved environment variable "\${${name}}" referenced at config path "${path2}"`,
507
- { details: { variable: name, path: path2 } }
508
- );
509
- }
510
- return envValue;
511
- }
512
- );
513
- }
514
- var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
515
- var init_env = __esm({
516
- "src/config/env.ts"() {
517
- init_core();
518
- init_is_record();
519
- PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
520
- BRACE_TOKEN = /\$\{[^{}]*\}/g;
521
- SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
522
- NESTED_PLACEHOLDER = /\$\{[^{}]*\$\{/;
523
- }
524
- });
525
-
526
- // src/payments/x402/local-chain/accounts.ts
527
- var ANVIL_WELL_KNOWN_ACCOUNTS;
528
- var init_accounts = __esm({
529
- "src/payments/x402/local-chain/accounts.ts"() {
530
- ANVIL_WELL_KNOWN_ACCOUNTS = [
531
- {
532
- address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
533
- privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
534
- },
535
- {
536
- address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
537
- privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
538
- },
539
- {
540
- address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
541
- privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
542
- }
543
- ];
544
- ANVIL_WELL_KNOWN_ACCOUNTS[0];
545
- ANVIL_WELL_KNOWN_ACCOUNTS[1];
546
- ANVIL_WELL_KNOWN_ACCOUNTS[2];
547
- }
548
- });
549
-
550
- // src/payments/x402/dev-key-guard.ts
551
- function isWellKnownDevAddress(address) {
552
- return WELL_KNOWN_DEV_ADDRESSES.has(address.toLowerCase());
553
- }
554
- function isLikelyLocalOrPrivateHost(hostname) {
555
- const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
556
- if (h === "localhost" || h === "::1") return true;
557
- if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
558
- if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
559
- if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
560
- if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
561
- if (!h.includes(".") && !h.includes(":")) return true;
562
- return false;
563
- }
564
- var WELL_KNOWN_DEV_ADDRESSES;
565
- var init_dev_key_guard = __esm({
566
- "src/payments/x402/dev-key-guard.ts"() {
567
- init_core();
568
- init_accounts();
569
- new Set(
570
- ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.privateKey.toLowerCase())
571
- );
572
- WELL_KNOWN_DEV_ADDRESSES = new Set(
573
- ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.address.toLowerCase())
574
- );
575
- }
576
- });
577
-
578
- // src/payments/x402/guardrails.ts
579
- function invalid(message, path2) {
580
- return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
581
- }
582
- function resolveX402Deployment(input) {
583
- const at = input.configPath ?? "payments.x402";
584
- const payTo = input.payToField ?? "payTo";
585
- const profile = requireNetworkProfile(input.network, `${at}.network`);
586
- const mode = resolveDeploymentMode(profile, input.facilitator.mode);
587
- if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
588
- throw invalid(
589
- `${at}: network "${profile.id}" (${profile.displayName}) is a mainnet and cannot be served by facilitator.mode "local". A mainnet deployment must settle through a remote facilitator.`,
590
- `${at}.facilitator.mode`
591
- );
592
- }
593
- if (mode === "mainnet" && input.allowMainnet !== true) {
594
- throw invalid(
595
- `${at}: network "${profile.id}" (${profile.displayName}) settles real funds. Set ${at}.allowMainnet: true to acknowledge this explicitly - it is never the default.`,
596
- `${at}.allowMainnet`
597
- );
598
- }
599
- if (input.facilitator.mode === "remote") {
600
- assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
601
- if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
602
- throw invalid(
603
- `${at}: no credential is configured for facilitator ${describeOrigin(input.facilitator.url)} on mainnet. Set ${at}.allowUnauthenticatedFacilitator: true to accept anonymous access and the facilitator's limits, or configure facilitator.auth.`,
604
- `${at}.allowUnauthenticatedFacilitator`
605
- );
606
- }
607
- for (const [field, value] of credentialFields(input.facilitator.auth)) {
608
- if (value.trim() === "") {
609
- throw invalid(
610
- `${at}: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
611
- `${at}.facilitator.auth.${field}`
612
- );
613
- }
614
- }
615
- }
616
- if (/^0x0{40}$/i.test(input.payTo)) {
617
- throw invalid(
618
- `${at}: "${payTo}" is the zero address. Every payment settled there is destroyed.`,
619
- `${at}.${payTo}`
620
- );
621
- }
622
- if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
623
- throw invalid(
624
- `${at}: "${payTo}" (${input.payTo}) is a well-known Anvil development address and this is a ${mode} deployment. Anyone can spend what settles there. Set ${payTo} to your own merchant wallet.`,
625
- `${at}.${payTo}`
626
- );
627
- }
628
- const canonical = profile.canonicalAsset;
629
- if (mode === "mainnet" && canonical) {
630
- if (!sameAddress(input.asset, canonical.address)) {
631
- throw invalid(
632
- `${at}: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
633
- `${at}.asset`
634
- );
635
- }
636
- if (input.assetName !== void 0 && input.assetName !== canonical.name) {
637
- throw invalid(
638
- `${at}: assetName "${input.assetName}" is not the EIP-712 domain name ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.name}"). Every payment would be refused after the buyer signed.`,
639
- `${at}.assetName`
640
- );
641
- }
642
- if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
643
- throw invalid(
644
- `${at}: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
645
- `${at}.assetVersion`
646
- );
647
- }
648
- }
649
- return { profile, mode };
650
- }
651
- function assertFacilitatorUrlIsSafe(url, mode, at) {
652
- let parsed;
653
- try {
654
- parsed = new URL(url);
655
- } catch (cause) {
656
- throw new CommerceError("CONFIG_INVALID", `${at}: facilitator.url is not a valid URL`, {
657
- cause,
658
- details: { path: `${at}.facilitator.url` }
659
- });
660
- }
661
- if (parsed.protocol === "https:") return;
662
- if (parsed.protocol !== "http:") {
663
- throw invalid(
664
- `${at}: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
665
- `${at}.facilitator.url`
666
- );
667
- }
668
- if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
669
- throw invalid(
670
- `${at}: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorizations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
671
- `${at}.facilitator.url`
672
- );
673
- }
674
- function describeOrigin(url) {
675
- try {
676
- return new URL(url).origin;
677
- } catch {
678
- return "[unparseable facilitator.url]";
679
- }
680
- }
681
- function credentialFields(auth) {
682
- switch (auth.type) {
683
- case "bearer":
684
- return [["token", auth.token]];
685
- case "cdp":
686
- return [
687
- ["apiKeyId", auth.apiKeyId],
688
- ["apiKeySecret", auth.apiKeySecret]
689
- ];
690
- default:
691
- return [];
692
- }
693
- }
694
- function sameAddress(a, b) {
695
- return a.toLowerCase() === b.toLowerCase();
696
- }
697
- var init_guardrails = __esm({
698
- "src/payments/x402/guardrails.ts"() {
699
- init_core();
700
- init_dev_key_guard();
701
- init_networks();
702
- }
703
- });
704
-
705
- // src/payments/x402/payment-flow.ts
706
- var X402_PAYMENT_FLOWS, X402_DEFAULT_PAYMENT_FLOW;
707
- var init_payment_flow = __esm({
708
- "src/payments/x402/payment-flow.ts"() {
709
- X402_PAYMENT_FLOWS = ["authorization", "upfront"];
710
- X402_DEFAULT_PAYMENT_FLOW = "authorization";
711
- }
712
- });
713
-
714
- // src/protocols/mcp/constants.ts
715
- var MCP_TOOL_NAME_CHARS, MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_PATTERN;
716
- var init_constants4 = __esm({
717
- "src/protocols/mcp/constants.ts"() {
718
- MCP_TOOL_NAME_CHARS = "A-Za-z0-9._-";
719
- MCP_TOOL_NAME_MAX_LENGTH = 128;
720
- MCP_TOOL_NAME_PATTERN = new RegExp(
721
- `^[${MCP_TOOL_NAME_CHARS}]{1,${MCP_TOOL_NAME_MAX_LENGTH}}$`
722
- );
723
- }
724
- });
725
- function parseConfig(raw, env) {
726
- if (!isRecord(raw)) {
727
- throw new CommerceError(
728
- "CONFIG_INVALID",
729
- 'Configuration root must be a mapping (object) at "$"',
730
- {
731
- details: { path: "$" }
732
- }
733
- );
734
- }
735
- checkVersion(raw);
736
- const substituted = substituteEnv(raw, env);
737
- const parsed = parseWithZod(RawConfigSchema, substituted);
738
- return normalize(parsed);
739
- }
740
- function checkVersion(raw) {
741
- if (!("version" in raw)) {
742
- throw new CommerceError("CONFIG_INVALID", 'Configuration is missing required field "version"', {
743
- details: { path: "version" }
744
- });
745
- }
746
- const version = raw["version"];
747
- if (version !== SUPPORTED_CONFIG_VERSION) {
748
- const hint = typeof version === "string" ? ", written as a number without quotes" : "";
749
- throw new CommerceError(
750
- "CONFIG_INVALID",
751
- `Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
752
- { details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
753
- );
754
- }
755
- }
756
- function parseWithZod(schema, value) {
757
- const result = schema.safeParse(value);
758
- if (!result.success) {
759
- throw zodErrorToConfigError(result.error);
760
- }
761
- return result.data;
762
- }
763
- function zodErrorToConfigError(error) {
764
- const issues = error.issues.map(describeIssue);
765
- const first = issues[0];
766
- const message = first ? `Configuration invalid at "${first.path}": ${first.message}` : "Configuration invalid";
767
- return new CommerceError("CONFIG_INVALID", message, { details: { issues } });
768
- }
769
- function describeIssue(issue) {
770
- const path2 = issue.path.length > 0 ? issue.path.join(".") : "$";
771
- return { path: path2, message: issue.message, code: issue.code };
772
- }
773
- function toNumber(value, path2, bounds = {}) {
774
- if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
775
- throw new CommerceError(
776
- "CONFIG_INVALID",
777
- `Configuration value at "${path2}" must be an integer written in decimal digits (got ${value === "" ? "an empty string" : `"${value}"`})`,
778
- { details: { path: path2 } }
779
- );
780
- }
781
- const n = typeof value === "number" ? value : Number(value);
782
- if (!Number.isFinite(n) || !Number.isInteger(n)) {
783
- throw new CommerceError(
784
- "CONFIG_INVALID",
785
- `Configuration value at "${path2}" must be an integer`,
786
- {
787
- details: { path: path2 }
788
- }
789
- );
790
- }
791
- if (bounds.min !== void 0 && n < bounds.min) {
792
- throw new CommerceError(
793
- "CONFIG_INVALID",
794
- `Configuration value at "${path2}" must be >= ${bounds.min}`,
795
- {
796
- details: { path: path2 }
797
- }
798
- );
799
- }
800
- if (bounds.max !== void 0 && n > bounds.max) {
801
- throw new CommerceError(
802
- "CONFIG_INVALID",
803
- `Configuration value at "${path2}" must be <= ${bounds.max}`,
804
- {
805
- details: { path: path2 }
806
- }
807
- );
808
- }
809
- return n;
810
- }
811
- function toBoolean(value, path2) {
812
- if (typeof value === "boolean") return value;
813
- const normalized = value.trim().toLowerCase();
814
- if (normalized === "true") return true;
815
- if (normalized === "false") return false;
816
- throw new CommerceError(
817
- "CONFIG_INVALID",
818
- `Configuration value at "${path2}" must be a boolean ("true" or "false")`,
819
- { details: { path: path2 } }
820
- );
821
- }
822
- function normalize(raw) {
823
- const protocols = {
824
- http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
825
- mcp: {
826
- enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
827
- mountPath: raw.protocols.mcp.mountPath
828
- },
829
- a2a: {
830
- enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
831
- mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
832
- },
833
- acp: normalizeAcp(raw.protocols.acp)
834
- };
835
- validateMountPaths(protocols);
836
- const x402Raw = raw.payments.x402;
837
- const x402 = x402Raw !== void 0 ? {
838
- enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
839
- network: x402Raw.network,
840
- rpcUrl: x402Raw.rpcUrl,
841
- asset: x402Raw.asset,
842
- assetName: x402Raw.assetName,
843
- assetVersion: x402Raw.assetVersion,
844
- assetDecimals: toNumber(x402Raw.assetDecimals, "payments.x402.assetDecimals", {
845
- min: 0,
846
- max: 36
847
- }),
848
- payTo: x402Raw.payTo,
849
- maxTimeoutSeconds: toNumber(
850
- x402Raw.maxTimeoutSeconds,
851
- "payments.x402.maxTimeoutSeconds",
852
- {
853
- min: 1
854
- }
855
- ),
856
- paymentFlow: x402Raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
857
- facilitator: normalizeFacilitator(x402Raw.facilitator),
858
- ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
859
- ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
860
- allowUnauthenticatedFacilitator: toBoolean(
861
- x402Raw.allowUnauthenticatedFacilitator,
862
- "payments.x402.allowUnauthenticatedFacilitator"
863
- )
864
- } : {}
865
- } : void 0;
866
- if (x402) {
867
- validateAddress("payments.x402.payTo", x402.payTo);
868
- validateAddress("payments.x402.asset", x402.asset);
869
- resolveX402Deployment({
870
- network: x402.network,
871
- payTo: x402.payTo,
872
- asset: x402.asset,
873
- assetName: x402.assetName,
874
- assetVersion: x402.assetVersion,
875
- facilitator: x402.facilitator,
876
- ...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
877
- ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
878
- });
879
- }
880
- const mpp = normalizeMpp(raw.payments.mpp);
881
- const ap2 = normalizeAp2(raw.authorization?.ap2);
882
- if (ap2?.enabled) {
883
- validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
884
- }
885
- const resources = Object.entries(raw.resources).map(
886
- ([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
887
- );
888
- const resourcePaymentFlows = Object.fromEntries(
889
- Object.entries(raw.resources).flatMap(
890
- ([id, entry]) => entry.paymentFlow !== void 0 ? [[id, entry.paymentFlow]] : []
891
- )
892
- );
893
- const hasFlowOverrides = Object.keys(resourcePaymentFlows).length > 0;
894
- const x402WithFlows = x402 !== void 0 && hasFlowOverrides ? { ...x402, resourcePaymentFlows } : x402;
895
- const mppWithFlows = mpp !== void 0 && hasFlowOverrides ? { ...mpp, resourcePaymentFlows } : mpp;
896
- if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
897
- if (protocols.a2a.enabled && !resources.some((r) => r.exposedVia.includes("a2a"))) {
898
- throw new CommerceError(
899
- "CONFIG_INVALID",
900
- 'protocols.a2a.enabled is true but no resource lists "a2a" in its expose',
901
- { details: { path: "protocols.a2a.enabled" } }
902
- );
903
- }
904
- return {
905
- version: SUPPORTED_CONFIG_VERSION,
906
- merchant: {
907
- id: raw.merchant.id,
908
- name: raw.merchant.name,
909
- publicBaseUrl: raw.merchant.publicBaseUrl
910
- },
911
- // Port 0 lets the OS pick a free port, for tests and throwaway instances
912
- server: {
913
- port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
914
- host: raw.server.host,
915
- ...raw.server.adminToken !== void 0 ? { adminToken: raw.server.adminToken } : {},
916
- allowedOrigins: raw.server.allowedOrigins ?? []
917
- },
918
- storage: { receipts: { driver: "sqlite", path: raw.storage.receipts.path } },
919
- protocols,
920
- resources,
921
- payments: {
922
- ...x402WithFlows !== void 0 ? { x402: x402WithFlows } : {},
923
- ...mppWithFlows !== void 0 ? { mpp: mppWithFlows } : {}
924
- },
925
- ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
926
- };
927
- }
928
- function validateMountPaths(protocols) {
929
- const mounts = [
930
- ["mcp", protocols.mcp],
931
- ["a2a", protocols.a2a],
932
- ["acp", protocols.acp]
933
- ].filter(([, p]) => p.enabled);
934
- for (const [index, [nameA, a]] of mounts.entries()) {
935
- for (const [nameB, b] of mounts.slice(index + 1)) {
936
- const baseA = a.mountPath.replace(/\/+$/, "");
937
- const baseB = b.mountPath.replace(/\/+$/, "");
938
- if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
939
- throw new CommerceError(
940
- "CONFIG_INVALID",
941
- `protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") - each mount registers a wildcard, so overlapping prefixes cannot both be served`,
942
- { details: { path: `protocols.${nameA}.mountPath` } }
943
- );
944
- }
945
- }
946
- }
947
- }
948
- function ap2Invalid(path2, message, extra = {}) {
949
- return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
950
- }
951
- function normalizeAp2(raw) {
952
- if (raw === void 0) return void 0;
953
- if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
954
- const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
955
- if (specVersion !== AP2_SPEC_VERSION) {
956
- throw ap2Invalid(
957
- "authorization.ap2.specVersion",
958
- `authorization.ap2.specVersion "${specVersion}" is not supported - this gateway verifies against the tagged AP2 release ${AP2_SPEC_VERSION} only`
959
- );
960
- }
961
- const mode = raw.mode ?? AP2_MODES[0];
962
- if (!AP2_MODES.includes(mode)) {
963
- throw ap2Invalid(
964
- "authorization.ap2.mode",
965
- `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.`
966
- );
967
- }
968
- if (raw.replay === void 0) {
969
- throw ap2Invalid(
970
- "authorization.ap2.replay",
971
- "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"
972
- );
973
- }
974
- const mandateIssuers = normalizeIssuers(
975
- raw.trust?.mandateIssuers,
976
- "authorization.ap2.trust.mandateIssuers"
977
- );
978
- const checkoutIssuers = normalizeIssuers(
979
- raw.trust?.checkoutIssuers,
980
- "authorization.ap2.trust.checkoutIssuers"
981
- );
982
- return {
983
- enabled: true,
984
- specVersion: AP2_SPEC_VERSION,
985
- mode,
986
- trust: { mandateIssuers, checkoutIssuers },
987
- clockSkewSeconds: toNumber(
988
- raw.clockSkewSeconds ?? AP2_DEFAULT_CLOCK_SKEW_SECONDS,
989
- "authorization.ap2.clockSkewSeconds",
990
- { min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
991
- ),
992
- requireMandateAudience: toBoolean(
993
- raw.requireMandateAudience ?? false,
994
- "authorization.ap2.requireMandateAudience"
995
- ),
996
- requireMandateExpiry: toBoolean(
997
- raw.requireMandateExpiry ?? false,
998
- "authorization.ap2.requireMandateExpiry"
999
- ),
1000
- replay: { path: raw.replay.path }
1001
- };
1002
- }
1003
- function normalizeIssuers(raw, path2) {
1004
- if (raw === void 0 || raw.length === 0) {
1005
- throw ap2Invalid(
1006
- path2,
1007
- `${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`
1008
- );
1009
- }
1010
- const seen = /* @__PURE__ */ new Set();
1011
- return raw.map((entry, index) => {
1012
- const entryPath = `${path2}.${index}`;
1013
- if (seen.has(entry.issuer)) {
1014
- throw ap2Invalid(
1015
- `${entryPath}.issuer`,
1016
- `${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).`,
1017
- { issuer: entry.issuer }
1018
- );
1019
- }
1020
- seen.add(entry.issuer);
1021
- const kids = /* @__PURE__ */ new Set();
1022
- const keys = entry.keys.map((key, keyIndex) => {
1023
- const keyPath = `${entryPath}.keys.${keyIndex}`;
1024
- if (kids.has(key.kid)) {
1025
- throw ap2Invalid(
1026
- `${keyPath}.kid`,
1027
- `${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`,
1028
- { issuer: entry.issuer, kid: key.kid }
1029
- );
1030
- }
1031
- kids.add(key.kid);
1032
- return { kid: key.kid, jwk: validateJwk(key.jwk, key.kid, keyPath) };
1033
- });
1034
- return { issuer: entry.issuer, audience: entry.audience, keys };
1035
- });
1036
- }
1037
- function validateJwk(jwk, kid, path2) {
1038
- const fail = (detail) => {
1039
- throw ap2Invalid(`${path2}.jwk`, `${path2}.jwk (kid "${kid}") ${detail}`, { kid });
1040
- };
1041
- for (const member of Object.keys(jwk)) {
1042
- if (AP2_JWK_MEMBERS.includes(member)) continue;
1043
- if (member === "d" || member === "k") {
1044
- fail(
1045
- `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.`
1046
- );
1047
- }
1048
- fail(
1049
- `has unsupported member "${member}". Allowed: ${AP2_JWK_MEMBERS.join(", ")}. Members naming a URL are refused on purpose - keys are configured inline and never fetched.`
1050
- );
1051
- }
1052
- const value = (member) => {
1053
- const raw = jwk[member];
1054
- if (typeof raw !== "string" || raw.length === 0) {
1055
- fail(`must give "${member}" as a non-empty string`);
1056
- }
1057
- return raw;
1058
- };
1059
- if (value("kty") !== AP2_KEY_TYPE) {
1060
- fail(
1061
- `must have kty "${AP2_KEY_TYPE}" (got "${value("kty")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
1062
- );
1063
- }
1064
- if (value("crv") !== AP2_JWK_CURVE) {
1065
- fail(
1066
- `must be on curve "${AP2_JWK_CURVE}" (got "${value("crv")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
1067
- );
1068
- }
1069
- for (const coordinate of ["x", "y"]) {
1070
- const encoded = value(coordinate);
1071
- if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
1072
- fail(`coordinate "${coordinate}" is not base64url (no padding, no "+" or "/")`);
1073
- }
1074
- if (Buffer.from(encoded, "base64url").length !== AP2_JWK_COORDINATE_BYTES) {
1075
- fail(
1076
- `coordinate "${coordinate}" decodes to ${Buffer.from(encoded, "base64url").length} bytes; a ${AP2_JWK_CURVE} coordinate is ${AP2_JWK_COORDINATE_BYTES}`
1077
- );
1078
- }
1079
- }
1080
- if (jwk["alg"] !== void 0 && value("alg") !== AP2_SIGNING_ALGORITHM) {
1081
- fail(`declares alg "${value("alg")}"; only ${AP2_SIGNING_ALGORITHM} is accepted`);
1082
- }
1083
- if (jwk["use"] !== void 0 && value("use") !== "sig") {
1084
- fail(`declares use "${value("use")}"; a verification key must be "sig"`);
1085
- }
1086
- if (jwk["kid"] !== void 0 && value("kid") !== kid) {
1087
- fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
1088
- }
1089
- const normalized = {};
1090
- for (const member of AP2_JWK_MEMBERS) {
1091
- if (jwk[member] !== void 0) normalized[member] = value(member);
1092
- }
1093
- return normalized;
1094
- }
1095
- function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1096
- if (replayPath === ":memory:") return;
1097
- const others = [["storage.receipts.path", receiptsPath]];
1098
- if (acp.enabled) others.push(["protocols.acp.idempotency.path", acp.idempotency.path]);
1099
- for (const [otherPath, other] of others) {
1100
- if (replayPath !== other) continue;
1101
- throw ap2Invalid(
1102
- "authorization.ap2.replay.path",
1103
- `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`
1104
- );
1105
- }
1106
- }
1107
- function normalizeResourceAuthorization(id, entry, pricing, ap2) {
1108
- const required = entry.authorization?.required;
1109
- if (required === void 0) return void 0;
1110
- const path2 = `resources.${id}.authorization.required`;
1111
- const methods = [];
1112
- for (const method of required) {
1113
- if (!SUPPORTED_AUTHORIZATION_METHODS.has(method)) {
1114
- throw ap2Invalid(
1115
- path2,
1116
- `Resource "${id}" requires unsupported authorization method "${method}". Supported: ${[...SUPPORTED_AUTHORIZATION_METHODS].join(", ")}.`,
1117
- { resourceId: id, method }
1118
- );
1119
- }
1120
- if (methods.includes(method)) {
1121
- throw ap2Invalid(path2, `Resource "${id}" lists authorization method "${method}" twice`, {
1122
- resourceId: id,
1123
- method
1124
- });
1125
- }
1126
- if (method === "ap2" && (ap2 === void 0 || !ap2.enabled)) {
1127
- throw ap2Invalid(
1128
- path2,
1129
- `Resource "${id}" requires authorization method "ap2", which is not configured or not enabled under authorization.ap2`,
1130
- { resourceId: id, method }
1131
- );
1132
- }
1133
- methods.push(method);
1134
- }
1135
- if (pricing.type !== "fixed") {
1136
- throw ap2Invalid(
1137
- path2,
1138
- `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`,
1139
- { resourceId: id }
1140
- );
1141
- }
1142
- return { required: methods };
1143
- }
1144
- function acpInvalid(path2, message, extra = {}) {
1145
- return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1146
- }
1147
- function normalizeAcp(raw) {
1148
- const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
1149
- if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
1150
- return { enabled: false, mountPath };
1151
- }
1152
- if (raw.auth === void 0) {
1153
- throw acpInvalid(
1154
- "protocols.acp.auth",
1155
- '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'
1156
- );
1157
- }
1158
- if (raw.idempotency === void 0) {
1159
- throw acpInvalid(
1160
- "protocols.acp.idempotency",
1161
- "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"
1162
- );
1163
- }
1164
- const discovery = raw.discovery;
1165
- return {
1166
- enabled: true,
1167
- mountPath,
1168
- auth: raw.auth,
1169
- idempotency: {
1170
- path: raw.idempotency.path,
1171
- retentionHours: toNumber(
1172
- raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
1173
- "protocols.acp.idempotency.retentionHours",
1174
- { min: ACP_RETENTION_HOURS }
1175
- ),
1176
- merchantIdempotent: raw.idempotency.merchantIdempotent ?? false
1177
- },
1178
- checkout: { operations: normalizeAcpOperations(raw.checkout) },
1179
- // Built key by key so absent metadata stays absent rather than `undefined`
1180
- ...discovery !== void 0 ? {
1181
- discovery: {
1182
- ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
1183
- ...discovery.supportedCurrencies !== void 0 ? { supportedCurrencies: discovery.supportedCurrencies } : {},
1184
- ...discovery.supportedLocales !== void 0 ? { supportedLocales: discovery.supportedLocales } : {},
1185
- ...discovery.interventionTypes !== void 0 ? { interventionTypes: discovery.interventionTypes } : {}
1186
- }
1187
- } : {}
1188
- };
1189
- }
1190
- function normalizeAcpOperations(checkout) {
1191
- const configured = checkout?.operations ?? {};
1192
- for (const key of Object.keys(configured)) {
1193
- if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
1194
- throw acpInvalid(
1195
- `protocols.acp.checkout.operations.${key}`,
1196
- `protocols.acp.checkout.operations names unknown ACP checkout operation "${key}". Supported: ${ACP_CHECKOUT_OPERATIONS.join(", ")}.`
1197
- );
1198
- }
1199
- }
1200
- const operations = {};
1201
- const claimedBy = /* @__PURE__ */ new Map();
1202
- for (const operation of ACP_CHECKOUT_OPERATIONS) {
1203
- const path2 = `protocols.acp.checkout.operations.${operation}`;
1204
- const resourceId = configured[operation];
1205
- if (resourceId === void 0) {
1206
- throw acpInvalid(
1207
- path2,
1208
- `${path2} is required when ACP is enabled - all five checkout operations must be mapped, since ACP discovery advertises the checkout service as a whole`
1209
- );
1210
- }
1211
- const claimed = claimedBy.get(resourceId);
1212
- if (claimed !== void 0) {
1213
- throw acpInvalid(
1214
- path2,
1215
- `${path2} maps resource "${resourceId}", which is already mapped to "${claimed}" - each ACP checkout operation needs its own resource`,
1216
- { resourceId }
1217
- );
1218
- }
1219
- claimedBy.set(resourceId, operation);
1220
- operations[operation] = resourceId;
1221
- }
1222
- return operations;
1223
- }
1224
- function validateAcpCheckoutMapping(acp, resources) {
1225
- const byId = new Map(resources.map((resource) => [resource.id, resource]));
1226
- for (const operation of ACP_CHECKOUT_OPERATIONS) {
1227
- const resourceId = acp.checkout.operations[operation];
1228
- const path2 = `protocols.acp.checkout.operations.${operation}`;
1229
- const resource = byId.get(resourceId);
1230
- if (resource === void 0) {
1231
- throw acpInvalid(
1232
- path2,
1233
- `${path2} maps resource "${resourceId}", which is not defined under "resources"`,
1234
- { resourceId }
1235
- );
1236
- }
1237
- if (!resource.exposedVia.includes("acp")) {
1238
- throw acpInvalid(
1239
- path2,
1240
- `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`,
1241
- { resourceId }
1242
- );
1243
- }
1244
- if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
1245
- throw acpInvalid(
1246
- path2,
1247
- `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`,
1248
- { resourceId }
1249
- );
1250
- }
1251
- validateAcpOperationInput(path2, operation, resource);
1252
- }
1253
- }
1254
- function validateAcpOperationInput(path2, operation, resource) {
1255
- const schema = resource.inputSchema;
1256
- if (schema === void 0) return;
1257
- const keys = ACP_OPERATION_INPUT_KEYS[operation];
1258
- const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
1259
- const sent = [...keys, ...optionalKeys];
1260
- const topLevel = closedObjectProperties(schema);
1261
- if (topLevel !== void 0) {
1262
- for (const key of sent) {
1263
- if (!Object.hasOwn(topLevel, key)) {
1264
- const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
1265
- throw acpInvalid(
1266
- path2,
1267
- `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter ${when} for this operation`,
1268
- { resourceId: resource.id }
1269
- );
1270
- }
1271
- }
1272
- }
1273
- const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
1274
- const pathKeys = closedObjectProperties(declared["path"]);
1275
- if (sent.includes("path") && pathKeys !== void 0 && !Object.hasOwn(pathKeys, ACP_SESSION_ID_INPUT_KEY)) {
1276
- throw acpInvalid(
1277
- path2,
1278
- `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"`,
1279
- { resourceId: resource.id }
1280
- );
1281
- }
1282
- const bodyKeys = closedObjectProperties(declared["body"]);
1283
- if (sent.includes("body") && bodyKeys !== void 0 && Object.keys(bodyKeys).length === 0) {
1284
- throw acpInvalid(
1285
- path2,
1286
- `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 }`,
1287
- { resourceId: resource.id }
1288
- );
1289
- }
1290
- const required = schema["required"];
1291
- if (Array.isArray(required)) {
1292
- for (const entry of required) {
1293
- if (typeof entry === "string" && !keys.includes(entry)) {
1294
- throw acpInvalid(
1295
- path2,
1296
- `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(", ")})`,
1297
- { resourceId: resource.id }
1298
- );
1299
- }
1300
- }
1301
- }
1302
- }
1303
- function closedObjectProperties(schema) {
1304
- if (!isRecord(schema) || schema["additionalProperties"] !== false) return void 0;
1305
- return isRecord(schema["properties"]) ? schema["properties"] : {};
1306
- }
1307
- function normalizeFacilitator(raw) {
1308
- return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
1309
- }
1310
- function normalizeMpp(raw) {
1311
- if (raw === void 0) return void 0;
1312
- const network = raw.network ?? MPP_DEFAULT_NETWORK;
1313
- if (!isMppNetwork(network)) {
1314
- throw new CommerceError(
1315
- "CONFIG_INVALID",
1316
- `payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
1317
- { details: { path: "payments.mpp.network" } }
1318
- );
1319
- }
1320
- const mpp = {
1321
- enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
1322
- network,
1323
- rpcUrl: raw.rpcUrl,
1324
- asset: raw.asset,
1325
- assetName: raw.assetName,
1326
- assetVersion: raw.assetVersion,
1327
- recipient: raw.recipient,
1328
- realm: raw.realm,
1329
- challengeSecret: raw.challengeSecret,
1330
- ...raw.challengeTtlSeconds !== void 0 ? {
1331
- challengeTtlSeconds: toNumber(
1332
- raw.challengeTtlSeconds,
1333
- "payments.mpp.challengeTtlSeconds",
1334
- { min: 1 }
1335
- )
1336
- } : {},
1337
- paymentFlow: raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
1338
- facilitator: normalizeFacilitator(raw.facilitator),
1339
- ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
1340
- ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
1341
- allowUnauthenticatedFacilitator: toBoolean(
1342
- raw.allowUnauthenticatedFacilitator,
1343
- "payments.mpp.allowUnauthenticatedFacilitator"
1344
- )
1345
- } : {}
1346
- };
1347
- validateAddress("payments.mpp.recipient", mpp.recipient);
1348
- validateAddress("payments.mpp.asset", mpp.asset);
1349
- if (/[\r\n]/.test(mpp.realm)) {
1350
- throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
1351
- details: { path: "payments.mpp.realm" }
1352
- });
1353
- }
1354
- if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
1355
- throw new CommerceError(
1356
- "CONFIG_INVALID",
1357
- `payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
1358
- { details: { path: "payments.mpp.challengeSecret" } }
1359
- );
1360
- }
1361
- resolveX402Deployment({
1362
- network: mpp.network,
1363
- payTo: mpp.recipient,
1364
- asset: mpp.asset,
1365
- assetName: mpp.assetName,
1366
- assetVersion: mpp.assetVersion,
1367
- facilitator: mpp.facilitator,
1368
- ...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
1369
- ...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
1370
- configPath: "payments.mpp",
1371
- payToField: "recipient"
1372
- });
1373
- return mpp;
1374
- }
1375
- function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
1376
- if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1377
- const inputProperties = entry.input?.["properties"];
1378
- if (inputProperties && typeof inputProperties === "object") {
1379
- for (const reserved of RESERVED_INPUT_FIELDS) {
1380
- if (!(reserved in inputProperties)) continue;
1381
- throw new CommerceError(
1382
- "CONFIG_INVALID",
1383
- `Resource "${id}" declares an input property "${reserved}", which is reserved by the gateway`,
1384
- {
1385
- details: {
1386
- path: `resources.${id}.input.properties.${reserved}`,
1387
- resourceId: id
1388
- }
1389
- }
1390
- );
1391
- }
1392
- }
1393
- for (const protocol of entry.expose) {
1394
- if (!SUPPORTED_PROTOCOLS.has(protocol)) {
1395
- const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
1396
- throw new CommerceError(
1397
- "CONFIG_INVALID",
1398
- `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: ${PROTOCOL_NAMES.join(", ")}.`,
1399
- { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
1400
- );
1401
- }
1402
- }
1403
- if (entry.expose.includes("mcp")) {
1404
- if (!protocols.mcp.enabled) {
1405
- throw new CommerceError(
1406
- "CONFIG_INVALID",
1407
- `Resource "${id}" is exposed via "mcp" but protocols.mcp.enabled is false`,
1408
- { details: { path: `resources.${id}.expose`, resourceId: id } }
1409
- );
1410
- }
1411
- if (!MCP_TOOL_NAME_PATTERN.test(id)) {
1412
- throw new CommerceError(
1413
- "CONFIG_INVALID",
1414
- `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)`,
1415
- { details: { path: `resources.${id}`, resourceId: id } }
1416
- );
1417
- }
1418
- }
1419
- if (entry.expose.includes("a2a") && !protocols.a2a.enabled) {
1420
- throw new CommerceError(
1421
- "CONFIG_INVALID",
1422
- `Resource "${id}" is exposed via "a2a" but protocols.a2a.enabled is false`,
1423
- { details: { path: `resources.${id}.expose`, resourceId: id } }
1424
- );
1425
- }
1426
- if (entry.expose.includes("acp") && !protocols.acp.enabled) {
1427
- throw new CommerceError(
1428
- "CONFIG_INVALID",
1429
- `Resource "${id}" is exposed via "acp" but protocols.acp.enabled is false`,
1430
- { details: { path: `resources.${id}.expose`, resourceId: id } }
1431
- );
1432
- }
1433
- if (entry.expose.includes("http") && !protocols.http.enabled) {
1434
- throw new CommerceError(
1435
- "CONFIG_INVALID",
1436
- `Resource "${id}" is exposed via "http" but protocols.http.enabled is false`,
1437
- { details: { path: `resources.${id}.expose`, resourceId: id } }
1438
- );
1439
- }
1440
- validateBackendUrl(id, entry.backend.url);
1441
- validateBackendHeaders(id, entry.backend.headers);
1442
- const pathScope = validateInputBindings(id, entry.backend, entry.input);
1443
- validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
1444
- if (entry.pricing.type === "dynamic") {
1445
- throw new CommerceError(
1446
- "CONFIG_INVALID",
1447
- `Resource "${id}" uses pricing.type "dynamic", which is not supported in this release`,
1448
- { details: { path: `resources.${id}.pricing.type`, resourceId: id } }
1449
- );
1450
- }
1451
- if (entry.pricing.type === "fixed") {
1452
- validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
1453
- }
1454
- const paymentMethods = entry.payments ?? [];
1455
- if (entry.pricing.type === "fixed") {
1456
- if (paymentMethods.length === 0) {
1457
- throw new CommerceError(
1458
- "CONFIG_INVALID",
1459
- `Resource "${id}" has fixed pricing but declares no "payments" (a paid resource must name at least one payment method)`,
1460
- { details: { path: `resources.${id}.payments`, resourceId: id } }
1461
- );
1462
- }
1463
- for (const method of paymentMethods) {
1464
- if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
1465
- throw new CommerceError(
1466
- "CONFIG_INVALID",
1467
- `Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
1468
- { details: { path: `resources.${id}.payments`, resourceId: id, method } }
1469
- );
1470
- }
1471
- }
1472
- const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
1473
- if (!paymentMethods.some((method) => enabledRails.includes(method))) {
1474
- throw new CommerceError(
1475
- "CONFIG_INVALID",
1476
- `Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
1477
- { details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
1478
- );
1479
- }
1480
- if (mpp?.enabled && paymentMethods.includes("mpp")) {
1481
- if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
1482
- throw new CommerceError(
1483
- "CONFIG_INVALID",
1484
- `Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
1485
- { details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
1486
- );
1487
- }
1488
- validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
1489
- }
1490
- }
1491
- if (entry.paymentFlow !== void 0 && (entry.pricing.type !== "fixed" || !paymentMethods.some((method) => method === "x402" || method === "mpp"))) {
1492
- throw new CommerceError(
1493
- "CONFIG_INVALID",
1494
- `Resource "${id}" sets paymentFlow, which requires fixed pricing and "x402" or "mpp" in payments`,
1495
- { details: { path: `resources.${id}.paymentFlow`, resourceId: id } }
1496
- );
1497
- }
1498
- const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
1499
- const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
1500
- return {
1501
- id,
1502
- name: entry.name,
1503
- ...entry.description !== void 0 ? { description: entry.description } : {},
1504
- inputSchema: entry.input !== void 0 ? defaultClosedObjectSchema(entry.input) : EMPTY_CLOSED_OBJECT_SCHEMA,
1505
- ...entry.output !== void 0 ? { outputSchema: entry.output } : {},
1506
- handler: {
1507
- type: "http",
1508
- method: entry.backend.method,
1509
- url: entry.backend.url,
1510
- ...entry.backend.headers !== void 0 ? { headers: entry.backend.headers } : {},
1511
- ...entry.backend.timeoutMs !== void 0 ? {
1512
- timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
1513
- min: 1
1514
- })
1515
- } : {},
1516
- ...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
1517
- },
1518
- pricing,
1519
- exposedVia: entry.expose,
1520
- paymentMethods,
1521
- ...authorization !== void 0 ? { authorization } : {}
1522
- };
1523
- }
1524
- function defaultClosedObjectSchema(schema) {
1525
- const isObjectSchema = isObjectSchemaNode(schema);
1526
- const result = { ...schema };
1527
- if (isObjectSchema) {
1528
- const properties = schema["properties"];
1529
- if (isRecord(properties)) {
1530
- result["properties"] = Object.fromEntries(
1531
- Object.entries(properties).map(([key, sub]) => [
1532
- key,
1533
- isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
1534
- ])
1535
- );
1536
- }
1537
- if (!Object.hasOwn(schema, "additionalProperties")) {
1538
- result["additionalProperties"] = false;
1539
- }
1540
- }
1541
- const items = schema["items"];
1542
- if (isRecord(items)) {
1543
- result["items"] = defaultClosedObjectSchema(items);
1544
- }
1545
- const additional = schema["additionalProperties"];
1546
- if (isRecord(additional)) {
1547
- result["additionalProperties"] = defaultClosedObjectSchema(additional);
1548
- }
1549
- return result;
1550
- }
1551
- function excludesObjectType(schema) {
1552
- const type = schema["type"];
1553
- if (type === void 0 || type === "object") return false;
1554
- if (Array.isArray(type)) return !type.includes("object");
1555
- return true;
1556
- }
1557
- function validateResourceSchemaKeywords(id, path2, schema) {
1558
- for (const keyword of UNSUPPORTED_SCHEMA_KEYWORDS) {
1559
- if (Object.hasOwn(schema, keyword)) {
1560
- console.warn(
1561
- `[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.`
1562
- );
1563
- }
1564
- }
1565
- if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
1566
- throw new CommerceError(
1567
- "CONFIG_INVALID",
1568
- `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".`,
1569
- { details: { path: `resources.${id}.${path2}`, resourceId: id } }
1570
- );
1571
- }
1572
- const requiredRaw = schema["required"];
1573
- const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
1574
- if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
1575
- const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
1576
- const undeclared = requiredRaw.filter(
1577
- (name) => typeof name === "string" && !Object.hasOwn(declared, name)
1578
- );
1579
- if (undeclared.length > 0) {
1580
- throw new CommerceError(
1581
- "CONFIG_INVALID",
1582
- `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".`,
1583
- { details: { path: `resources.${id}.${path2}`, resourceId: id, undeclared } }
1584
- );
1585
- }
1586
- }
1587
- const properties = schema["properties"];
1588
- if (isRecord(properties)) {
1589
- for (const [key, sub] of Object.entries(properties)) {
1590
- if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1591
- }
1592
- }
1593
- const additional = schema["additionalProperties"];
1594
- if (isRecord(additional)) {
1595
- validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
1596
- }
1597
- const items = schema["items"];
1598
- if (isRecord(items)) {
1599
- validateResourceSchemaKeywords(id, `${path2}.items`, items);
1600
- } else if (Array.isArray(items)) {
1601
- console.warn(
1602
- `[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.`
1603
- );
1604
- }
1605
- }
1606
- function validatePricingAmount(id, amount, decimals) {
1607
- const path2 = `resources.${id}.pricing.amount`;
1608
- if (!PRICING_AMOUNT_PATTERN.test(amount)) {
1609
- throw new CommerceError(
1610
- "CONFIG_INVALID",
1611
- `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")`,
1612
- { details: { path: path2, resourceId: id } }
1613
- );
1614
- }
1615
- if (ZERO_AMOUNT_PATTERN.test(amount)) {
1616
- throw new CommerceError(
1617
- "CONFIG_INVALID",
1618
- `Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
1619
- { details: { path: path2, resourceId: id } }
1620
- );
1621
- }
1622
- if (decimals !== void 0) {
1623
- const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
1624
- if (fractionalDigits > decimals) {
1625
- throw new CommerceError(
1626
- "CONFIG_INVALID",
1627
- `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
1628
- { details: { path: path2, resourceId: id } }
1629
- );
1630
- }
1631
- }
1632
- }
1633
- function validateInputBindings(id, backend, input) {
1634
- const legacy = { schema: input, where: "its input schema" };
1635
- const bindings = backend.inputBindings;
1636
- const templated = extractPathParameterNames(backend.url).length > 0;
1637
- if (bindings === void 0) return legacy;
1638
- const path2 = `resources.${id}.backend.inputBindings`;
1639
- const fail = (message, details = {}) => {
1640
- throw new CommerceError("CONFIG_INVALID", `Resource "${id}" ${message}`, {
1641
- details: { path: path2, resourceId: id, ...details }
1642
- });
1643
- };
1644
- const entries = Object.entries(bindings).filter(
1645
- (entry) => entry[1] !== void 0
1646
- );
1647
- if (entries.length === 0) {
1648
- fail("has an empty backend.inputBindings - remove the block to use the default mapping");
1649
- }
1650
- if (backend.method === "GET" || backend.method === "DELETE") {
1651
- if (bindings.body !== void 0) {
1652
- fail(
1653
- `binds a request body on a ${backend.method}, which sends none - the value would be silently dropped`
1654
- );
1655
- }
1656
- }
1657
- if (templated && bindings.path === void 0) {
1658
- fail(
1659
- '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'
1660
- );
1661
- }
1662
- const seen = /* @__PURE__ */ new Map();
1663
- const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
1664
- const required = new Set(
1665
- Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1666
- );
1667
- for (const [location, property] of entries) {
1668
- if (RESERVED_INPUT_FIELDS.includes(property)) {
1669
- fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
1670
- }
1671
- const other = seen.get(property);
1672
- if (other !== void 0) {
1673
- fail(`binds both "${other}" and "${location}" to the input property "${property}"`, {
1674
- location,
1675
- property
1676
- });
1677
- }
1678
- seen.set(property, location);
1679
- if (!Object.hasOwn(properties, property)) {
1680
- fail(
1681
- `binds "${location}" to input property "${property}", which the input schema does not declare - the schema is closed, so a caller could never supply it`,
1682
- { location, property }
1683
- );
1684
- }
1685
- const declared = properties[property];
1686
- if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
1687
- fail(
1688
- `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
1689
- { location, property }
1690
- );
1691
- }
1692
- }
1693
- if (templated && bindings.path !== void 0 && !required.has(bindings.path)) {
1694
- fail(
1695
- `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`,
1696
- { property: bindings.path }
1697
- );
1698
- }
1699
- if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
1700
- const group = properties[bindings.path];
1701
- return {
1702
- schema: isRecord(group) ? group : void 0,
1703
- where: `input.properties.${bindings.path}`
1704
- };
1705
- }
1706
- function pickDefined(value) {
1707
- return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1708
- }
1709
- function validatePathParametersDeclared(id, url, input, where) {
1710
- const stray = findUnparsedBraceToken(url);
1711
- if (stray !== void 0) {
1712
- throw new CommerceError(
1713
- "CONFIG_INVALID",
1714
- `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`,
1715
- { details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
1716
- );
1717
- }
1718
- const params = extractPathParameterNames(url);
1719
- if (params.length === 0) return;
1720
- const firstBrace = url.indexOf("{");
1721
- if (firstBrace !== -1) {
1722
- const prefix = url.slice(0, firstBrace);
1723
- let origin;
1724
- try {
1725
- const parsedPrefix = new URL(prefix);
1726
- origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
1727
- } catch {
1728
- }
1729
- if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
1730
- throw new CommerceError(
1731
- "CONFIG_INVALID",
1732
- `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`,
1733
- { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1734
- );
1735
- }
1736
- }
1737
- const propertiesRaw = input?.["properties"];
1738
- const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
1739
- const requiredRaw = input?.["required"];
1740
- const required = new Set(
1741
- Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
1742
- );
1743
- for (const param of params) {
1744
- const path2 = `resources.${id}.backend.url`;
1745
- if (!Object.hasOwn(properties, param)) {
1746
- throw new CommerceError(
1747
- "CONFIG_INVALID",
1748
- `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`,
1749
- { details: { path: path2, resourceId: id, param } }
1750
- );
1751
- }
1752
- if (!required.has(param)) {
1753
- throw new CommerceError(
1754
- "CONFIG_INVALID",
1755
- `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`,
1756
- { details: { path: path2, resourceId: id, param } }
1757
- );
1758
- }
1759
- }
1760
- }
1761
- function validateBackendUrl(id, url) {
1762
- let parsed;
1763
- try {
1764
- parsed = new URL(url);
1765
- } catch {
1766
- throw new CommerceError(
1767
- "CONFIG_INVALID",
1768
- `Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
1769
- { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1770
- );
1771
- }
1772
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1773
- throw new CommerceError(
1774
- "CONFIG_INVALID",
1775
- `Resource "${id}" has a backend.url with scheme "${parsed.protocol}": must be http:// or https://`,
1776
- { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1777
- );
1778
- }
1779
- }
1780
- function validateBackendHeaders(id, headers) {
1781
- for (const [name, value] of Object.entries(headers ?? {})) {
1782
- try {
1783
- new Headers([[name, value]]);
1784
- } catch {
1785
- throw new CommerceError(
1786
- "CONFIG_INVALID",
1787
- `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)`,
1788
- { details: { path: `resources.${id}.backend.headers.${name}`, resourceId: id } }
1789
- );
1790
- }
1791
- }
1792
- }
1793
- function validateAddress(path2, value) {
1794
- if (!ADDRESS_PATTERN.test(value)) {
1795
- throw new CommerceError(
1796
- "CONFIG_INVALID",
1797
- `Configuration value at "${path2}" is not a plausible address (expected "0x" followed by 40 hex characters)`,
1798
- { details: { path: path2 } }
1799
- );
1800
- }
1801
- if (ZERO_ADDRESS_PATTERN.test(value)) {
1802
- throw new CommerceError(
1803
- "CONFIG_INVALID",
1804
- `Configuration value at "${path2}" must not be the zero address`,
1805
- {
1806
- details: { path: path2 }
1807
- }
1808
- );
1809
- }
1810
- }
1811
- var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, MountPathSchema, AcpAuthSchema, AcpIdempotencySchema, AcpCheckoutSchema, AcpDiscoverySchema, ProtocolsSchema, DEFAULT_A2A_MOUNT_PATH, DEFAULT_ACP_MOUNT_PATH, ACP_RETENTION_HOURS, BackendMethodSchema, BackendInputBindingsSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, MppSchema, PaymentsSchema, Ap2KeySchema, Ap2IssuerSchema, Ap2Schema, AuthorizationSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, SUPPORTED_AUTHORIZATION_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
1812
- var init_schema = __esm({
1813
- "src/config/schema.ts"() {
1814
- init_constants();
1815
- init_core();
1816
- init_execution();
1817
- init_is_record();
1818
- init_constants2();
1819
- init_guardrails();
1820
- init_payment_flow();
1821
- init_constants3();
1822
- init_constants4();
1823
- init_env();
1824
- SUPPORTED_CONFIG_VERSION = 1;
1825
- ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
1826
- ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
1827
- NumberOrString = z.union([z.number(), z.string()]);
1828
- BooleanOrString = z.union([z.boolean(), z.string()]);
1829
- MerchantSchema = z.object({
1830
- id: z.string().min(1),
1831
- name: z.string().min(1),
1832
- publicBaseUrl: z.string().min(1)
1833
- }).strict();
1834
- ServerSchema = z.object({
1835
- port: NumberOrString,
1836
- host: z.string().min(1),
1837
- // Shared secret gating /api/receipts and /api/events. Without it both
1838
- // routes 404.
1839
- adminToken: z.string().min(1).optional(),
1840
- // Browser origins allowed to call the gateway; none by default. A request
1841
- // with any other `Origin` gets 403 on every route. Entries match the
1842
- // `Origin` header exactly (ignoring case), so `"*"` and a trailing slash
1843
- // would match nothing: a silent lockout that an operator might "fix" by
1844
- // disabling the check. Both are refused with a pointer.
1845
- allowedOrigins: z.array(
1846
- z.string().min(1).refine((origin) => origin !== "*", {
1847
- 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.`
1848
- }).refine((origin) => !origin.endsWith("/"), {
1849
- 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".'
1850
- })
1851
- ).optional()
1852
- }).strict();
1853
- StorageSchema = z.object({
1854
- receipts: z.object({
1855
- driver: z.literal("sqlite"),
1856
- path: z.string().min(1)
1857
- }).strict()
1858
- }).strict();
1859
- RESERVED_GATEWAY_PATHS = [
1860
- "/health",
1861
- "/ready",
1862
- "/.well-known/agent-commerce",
1863
- // Fixed by the A2A specification and served by the adapter
1864
- "/.well-known/agent-card.json",
1865
- // Fixed by the ACP specification
1866
- ACP_WELL_KNOWN_PATH,
1867
- "/api/resources",
1868
- "/api/resources/:id/invoke",
1869
- "/api/receipts",
1870
- "/api/events"
1871
- ];
1872
- MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1873
- message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
1874
- }).refine((value) => !/[:*?\s]/.test(value), {
1875
- message: 'must not contain ":", "*", "?" or whitespace - the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
1876
- }).refine(
1877
- (value) => {
1878
- const base = value.replace(/\/+$/, "");
1879
- return !RESERVED_GATEWAY_PATHS.some(
1880
- (reserved) => reserved === base || reserved.startsWith(`${base}/`)
1881
- );
1882
- },
1883
- {
1884
- message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
1885
- }
1886
- );
1887
- AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
1888
- AcpIdempotencySchema = z.object({
1889
- path: z.string().min(1),
1890
- retentionHours: NumberOrString.optional(),
1891
- merchantIdempotent: z.boolean().optional()
1892
- }).strict();
1893
- AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
1894
- AcpDiscoverySchema = z.object({
1895
- documentationUrl: z.string().min(1).optional(),
1896
- supportedCurrencies: z.array(z.string().min(1)).optional(),
1897
- supportedLocales: z.array(z.string().min(1)).optional(),
1898
- interventionTypes: z.array(z.string().min(1)).optional()
1899
- }).strict();
1900
- ProtocolsSchema = z.object({
1901
- http: z.object({ enabled: BooleanOrString }).strict(),
1902
- mcp: z.object({
1903
- enabled: BooleanOrString,
1904
- mountPath: MountPathSchema
1905
- }).strict(),
1906
- // Optional; A2A is off unless configured
1907
- a2a: z.object({
1908
- enabled: BooleanOrString,
1909
- mountPath: MountPathSchema.optional()
1910
- }).strict().optional(),
1911
- // Optional and off by default. The business pass requires the sub-blocks
1912
- // only when ACP is enabled, so a disabled placeholder stays valid and an
1913
- // enabled block gets a message naming what it lacks.
1914
- acp: z.object({
1915
- enabled: BooleanOrString,
1916
- mountPath: MountPathSchema.optional(),
1917
- auth: AcpAuthSchema.optional(),
1918
- idempotency: AcpIdempotencySchema.optional(),
1919
- checkout: AcpCheckoutSchema.optional(),
1920
- discovery: AcpDiscoverySchema.optional()
1921
- }).strict().optional()
1922
- }).strict();
1923
- DEFAULT_A2A_MOUNT_PATH = "/a2a";
1924
- DEFAULT_ACP_MOUNT_PATH = "/acp";
1925
- ACP_RETENTION_HOURS = 24;
1926
- BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1927
- BackendInputBindingsSchema = z.object({
1928
- path: z.string().min(1).optional(),
1929
- query: z.string().min(1).optional(),
1930
- body: z.string().min(1).optional()
1931
- }).strict();
1932
- BackendHandlerSchema = z.object({
1933
- type: z.literal("http"),
1934
- method: BackendMethodSchema,
1935
- url: z.string().min(1),
1936
- headers: z.record(z.string(), z.string()).optional(),
1937
- timeoutMs: NumberOrString.optional(),
1938
- inputBindings: BackendInputBindingsSchema.optional()
1939
- }).strict();
1940
- PricingSchema = z.discriminatedUnion("type", [
1941
- z.object({ type: z.literal("free") }).strict(),
1942
- z.object({
1943
- type: z.literal("fixed"),
1944
- amount: z.string().min(1),
1945
- currency: z.string().min(1)
1946
- }).strict(),
1947
- z.object({
1948
- type: z.literal("dynamic"),
1949
- resolver: z.string().min(1)
1950
- }).strict()
1951
- ]);
1952
- JsonSchemaValueSchema = z.record(z.string(), z.unknown());
1953
- ResourceEntrySchema = z.object({
1954
- name: z.string().min(1),
1955
- description: z.string().optional(),
1956
- input: JsonSchemaValueSchema.optional(),
1957
- output: JsonSchemaValueSchema.optional(),
1958
- backend: BackendHandlerSchema,
1959
- pricing: PricingSchema,
1960
- expose: z.array(z.string().min(1)).min(1),
1961
- payments: z.array(z.string().min(1)).optional(),
1962
- paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
1963
- authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
1964
- }).strict();
1965
- ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
1966
- FacilitatorAuthSchema = z.discriminatedUnion("type", [
1967
- z.object({ type: z.literal("none") }).strict(),
1968
- z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
1969
- // Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
1970
- // which signs a fresh JWT per request.
1971
- z.object({
1972
- type: z.literal("cdp"),
1973
- apiKeyId: z.string().min(1),
1974
- apiKeySecret: z.string().min(1)
1975
- }).strict()
1976
- ]);
1977
- FacilitatorSchema = z.discriminatedUnion("mode", [
1978
- z.object({
1979
- mode: z.literal("local"),
1980
- signerPrivateKey: z.string().min(1)
1981
- }).strict(),
1982
- z.object({
1983
- mode: z.literal("remote"),
1984
- url: z.string().min(1),
1985
- // Absent auth sends no credential and normalizes to `{ type: 'none' }`,
1986
- // which on mainnet requires `allowUnauthenticatedFacilitator`
1987
- auth: FacilitatorAuthSchema.optional()
1988
- }).strict()
1989
- ]);
1990
- X402Schema = z.object({
1991
- enabled: BooleanOrString,
1992
- network: z.string().min(1),
1993
- rpcUrl: z.string().min(1),
1994
- asset: z.string().min(1),
1995
- assetName: z.string().min(1),
1996
- assetVersion: z.string().min(1),
1997
- assetDecimals: NumberOrString,
1998
- payTo: z.string().min(1),
1999
- maxTimeoutSeconds: NumberOrString,
2000
- // `authorization` (the x402 default) settles after the backend call
2001
- paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
2002
- facilitator: FacilitatorSchema,
2003
- // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
2004
- allowMainnet: BooleanOrString.optional(),
2005
- // Accepts credential-free facilitator access on mainnet. Never defaulted
2006
- allowUnauthenticatedFacilitator: BooleanOrString.optional()
2007
- }).strict();
2008
- MppSchema = z.object({
2009
- enabled: BooleanOrString,
2010
- network: z.string().min(1).optional(),
2011
- rpcUrl: z.string().min(1),
2012
- asset: z.string().min(1),
2013
- assetName: z.string().min(1),
2014
- assetVersion: z.string().min(1),
2015
- recipient: z.string().min(1),
2016
- realm: z.string().min(1),
2017
- challengeSecret: z.string().min(1),
2018
- challengeTtlSeconds: NumberOrString.optional(),
2019
- // Same values and default as `payments.x402.paymentFlow`
2020
- paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
2021
- facilitator: FacilitatorSchema,
2022
- // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
2023
- allowMainnet: BooleanOrString.optional(),
2024
- // Accepts credential-free facilitator access on mainnet. Never defaulted
2025
- allowUnauthenticatedFacilitator: BooleanOrString.optional()
2026
- }).strict();
2027
- PaymentsSchema = z.object({
2028
- x402: X402Schema.optional(),
2029
- mpp: MppSchema.optional()
2030
- }).strict();
2031
- Ap2KeySchema = z.object({
2032
- kid: z.string().min(1),
2033
- jwk: z.record(z.string(), z.unknown())
2034
- }).strict();
2035
- Ap2IssuerSchema = z.object({
2036
- issuer: z.string().min(1),
2037
- // Required, not defaulted: without it a mandate minted for another
2038
- // merchant would verify here
2039
- audience: z.string().min(1),
2040
- keys: z.array(Ap2KeySchema).min(1)
2041
- }).strict();
2042
- Ap2Schema = z.object({
2043
- enabled: BooleanOrString,
2044
- specVersion: z.string().min(1).optional(),
2045
- mode: z.string().min(1).optional(),
2046
- trust: z.object({
2047
- mandateIssuers: z.array(Ap2IssuerSchema).optional(),
2048
- checkoutIssuers: z.array(Ap2IssuerSchema).optional()
2049
- }).strict().optional(),
2050
- clockSkewSeconds: NumberOrString.optional(),
2051
- requireMandateAudience: BooleanOrString.optional(),
2052
- requireMandateExpiry: BooleanOrString.optional(),
2053
- replay: z.object({ path: z.string().min(1) }).strict().optional()
2054
- }).strict();
2055
- AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
2056
- RawConfigSchema = z.object({
2057
- version: z.literal(SUPPORTED_CONFIG_VERSION),
2058
- merchant: MerchantSchema,
2059
- server: ServerSchema,
2060
- storage: StorageSchema,
2061
- protocols: ProtocolsSchema,
2062
- resources: ResourcesMapSchema,
2063
- payments: PaymentsSchema,
2064
- authorization: AuthorizationSchema.optional()
2065
- }).strict();
2066
- SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
2067
- SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
2068
- SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
2069
- EMPTY_CLOSED_OBJECT_SCHEMA = {
2070
- type: "object",
2071
- properties: {},
2072
- additionalProperties: false
2073
- };
2074
- UNSUPPORTED_SCHEMA_KEYWORDS = [
2075
- "pattern",
2076
- "minLength",
2077
- "maxLength",
2078
- "format",
2079
- "minimum",
2080
- "maximum",
2081
- "exclusiveMinimum",
2082
- "exclusiveMaximum",
2083
- "multipleOf",
2084
- "oneOf",
2085
- "anyOf",
2086
- "allOf",
2087
- "not",
2088
- "$ref",
2089
- "const",
2090
- "patternProperties",
2091
- "minItems",
2092
- "maxItems",
2093
- "uniqueItems"
2094
- ];
2095
- PRICING_AMOUNT_PATTERN = /^\d+(?:\.\d+)?$/;
2096
- ZERO_AMOUNT_PATTERN = /^0(?:\.0+)?$/;
2097
- }
2098
- });
2099
- async function loadConfig(options = {}) {
2100
- const env = options.env ?? process.env;
2101
- const cwd = options.cwd ?? process.cwd();
2102
- const configPath = resolveConfigPath(options.path, env, cwd);
2103
- let text2;
2104
- try {
2105
- text2 = await promises.readFile(configPath, "utf8");
2106
- } catch (error) {
2107
- throw new CommerceError("CONFIG_INVALID", `Could not read configuration file "${configPath}"`, {
2108
- details: { path: configPath },
2109
- cause: error
2110
- });
2111
- }
2112
- let raw;
2113
- try {
2114
- raw = parse(text2);
2115
- } catch (error) {
2116
- const { reason, details } = describeYamlError(error);
2117
- throw new CommerceError(
2118
- "CONFIG_INVALID",
2119
- `Configuration file "${configPath}" is not valid YAML: ${reason}`,
2120
- {
2121
- details: { path: configPath, ...details },
2122
- // Kept for programmatic callers. Never print it: its `.message` quotes
2123
- // the source line (see describeYamlError).
2124
- cause: error
2125
- }
2126
- );
2127
- }
2128
- return parseConfig(raw, env);
2129
- }
2130
- function describeYamlError(error) {
2131
- if (!(error instanceof YAMLError)) {
2132
- return { reason: "unknown YAML parse error", details: {} };
2133
- }
2134
- const start = error.linePos?.[0];
2135
- const where = start === void 0 ? "" : ` at line ${String(start.line)}, column ${String(start.col)}`;
2136
- const explanation = YAML_ERROR_EXPLANATIONS[error.code];
2137
- const what = explanation === void 0 ? error.code : `${error.code} (${explanation})`;
2138
- return {
2139
- reason: `${what}${where}`,
2140
- details: {
2141
- yamlErrorCode: error.code,
2142
- ...start === void 0 ? {} : { line: start.line, column: start.col }
2143
- }
2144
- };
2145
- }
2146
- function resolveConfigPath(explicitPath, env, cwd) {
2147
- if (explicitPath) return path.resolve(cwd, explicitPath);
2148
- const fromEnv = env[CONFIG_PATH_ENV_VAR];
2149
- if (fromEnv) return path.resolve(cwd, fromEnv);
2150
- return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
2151
- }
2152
- var CONFIG_PATH_ENV_VAR, YAML_ERROR_EXPLANATIONS;
2153
- var init_load = __esm({
2154
- "src/config/load.ts"() {
2155
- init_core();
2156
- init_filename();
2157
- init_schema();
2158
- CONFIG_PATH_ENV_VAR = "AGENT_COMMERCE_CONFIG";
2159
- YAML_ERROR_EXPLANATIONS = {
2160
- DUPLICATE_KEY: "map keys must be unique",
2161
- BAD_INDENT: "bad indentation",
2162
- TAB_AS_INDENT: "tabs cannot be used for indentation",
2163
- MISSING_CHAR: "a required character is missing",
2164
- BLOCK_AS_IMPLICIT_KEY: "a block value cannot be used as a key (missing quotes, or a stray colon)",
2165
- MULTILINE_IMPLICIT_KEY: "a key spans multiple lines (missing quotes, or a stray colon)",
2166
- UNEXPECTED_TOKEN: "unexpected token"
2167
- };
2168
- }
2169
- });
2170
-
2171
- // src/config/index.ts
2172
- var config_exports = {};
2173
- __export(config_exports, {
2174
- loadConfig: () => loadConfig,
2175
- parseConfig: () => parseConfig,
2176
- substituteEnv: () => substituteEnv
2177
- });
2178
- var init_config = __esm({
2179
- "src/config/index.ts"() {
2180
- init_env();
2181
- init_load();
2182
- init_schema();
2183
- }
2184
- });
2185
-
2186
- // src/cli/program.ts
2187
- init_filename();
2188
- init_core();
2
+ import { createSqliteReceiptStore, A2A_SPEC_VERSION, A2A_PROTOCOL_VERSION, A2A_PROTOCOL_BINDING, A2A_AGENT_CARD_PATH, A2A_UNSUPPORTED, ACP_UNSUPPORTED } from './chunk-SS3QFF5N.js';
3
+ import { DEFAULT_CONFIG_FILENAME, extractPathParameterNames, ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, findUnparsedBraceToken } from './chunk-2ZZOGI5I.js';
4
+ import { MPP_PROFILE, MPP_SPEC_DRAFTS, MPPX_VERSION, MPP_SPEC_COMMIT, MPP_DEFAULT_PAYMENT_FLOW } from './chunk-AA3FIEAR.js';
5
+ import { ANVIL_WELL_KNOWN_ACCOUNTS, describeDeploymentMode, X402_DEFAULT_PAYMENT_FLOW, findX402Network, isEvmAddress, LOCAL_CHAIN_ID, resolveDeploymentMode, sameAddress } from './chunk-7JNYFUT3.js';
6
+ import { AP2_UNSUPPORTED } from './chunk-3EQBCFZY.js';
7
+ import './chunk-3MGAJ5FX.js';
8
+ import { AP2_SPEC_VERSION, AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE } from './chunk-FHCFR2GS.js';
9
+ import { MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_CHARS } from './chunk-TPC4EJLT.js';
10
+ import { PACKAGE_VERSION } from './chunk-FRMLWGSS.js';
11
+ import { PROTOCOL_NAMES, isCommerceError, CommerceError } from './chunk-QQ3WY5L3.js';
12
+ import { isRecord } from './chunk-3M6U7ILB.js';
13
+ import { CommanderError, Command } from 'commander';
14
+ import { existsSync, mkdirSync, readFileSync, accessSync, constants } from 'fs';
15
+ import { dirname, resolve, join, basename, extname, win32 } from 'path';
16
+ import { z } from 'zod';
17
+ import { stringify, parse, Document } from 'yaml';
18
+ import picocolors from 'picocolors';
19
+ import { writeFile, stat, mkdir, rename, rm, open } from 'fs/promises';
20
+ import { isDeepStrictEqual } from 'util';
21
+ import { validate } from '@scalar/openapi-parser';
22
+ import * as clack from '@clack/prompts';
2189
23
 
2190
24
  // src/cli/commands/demo.ts
2191
25
  function defaultDemoSteps() {
@@ -2196,659 +30,55 @@ function defaultDemoSteps() {
2196
30
  args: ["compose", "up", "--build", "--detach", "--wait"]
2197
31
  },
2198
32
  { name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
2199
- ];
2200
- }
2201
- async function runDemo(io, deps = {}) {
2202
- const steps = deps.steps ?? defaultDemoSteps();
2203
- const run = deps.run ?? execCommand;
2204
- io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
2205
- for (const step of steps) {
2206
- io.stdout("");
2207
- io.stdout(`==> ${step.name}`);
2208
- io.stdout(` $ ${step.command} ${step.args.join(" ")}`);
2209
- const result = await run(step.command, step.args);
2210
- if (result.code !== 0) {
2211
- io.stderr(`FAIL "${step.name}" exited with code ${result.code}.`);
2212
- if (result.stderrTail !== void 0 && result.stderrTail.length > 0) {
2213
- io.stderr(result.stderrTail);
2214
- }
2215
- io.stderr("");
2216
- io.stderr("Nothing further was run. To retry manually:");
2217
- io.stderr(` ${step.command} ${step.args.join(" ")}`);
2218
- return 1;
2219
- }
2220
- io.stdout(`OK ${step.name}`);
2221
- }
2222
- io.stdout("");
2223
- io.stdout("PASS Demo is up. Try: agent-commerce doctor --config config-demo.yaml");
2224
- return 0;
2225
- }
2226
- async function execCommand(command, args) {
2227
- const { spawn } = await import('child_process');
2228
- return new Promise((resolvePromise) => {
2229
- let stderr = "";
2230
- const child = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
2231
- child.stderr?.on("data", (chunk) => {
2232
- stderr += chunk.toString("utf8");
2233
- });
2234
- child.on("error", (err) => {
2235
- resolvePromise({ code: 1, stderrTail: err.message });
2236
- });
2237
- child.on("close", (code) => {
2238
- const tail = stderr.trim().split("\n").slice(-10).join("\n");
2239
- resolvePromise({ code: code ?? 1, ...tail.length > 0 ? { stderrTail: tail } : {} });
2240
- });
2241
- });
2242
- }
2243
-
2244
- // src/cli/commands/doctor.ts
2245
- init_constants();
2246
-
2247
- // src/authorization/ap2/descriptor.ts
2248
- init_constants();
2249
- var AP2_UNSUPPORTED = [
2250
- // Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
2251
- "autonomous mode",
2252
- "open checkout mandates (mandate.checkout.open.1)",
2253
- "payment mandates (mandate.payment.1)",
2254
- "spending constraint evaluation",
2255
- "cnf-bound agent keys",
2256
- // Delegation models other than a trusted agent provider
2257
- "user credential delegation (OpenID4VP)",
2258
- "delegation chains and key-binding JWTs",
2259
- // Key handling. Every key is written into config by an operator
2260
- "JWKS and any key discovery by URL (jku, x5u)",
2261
- "issuer metadata fetching",
2262
- "key rotation without a config change",
2263
- // Algorithms
2264
- "signature algorithms other than ES256",
2265
- "digest algorithms other than sha-256",
2266
- // Roles this gateway does not play
2267
- "mandate issuance",
2268
- "checkout receipts",
2269
- "AP2 over the ACP checkout adapter"
2270
- ];
2271
-
2272
- // src/cli/commands/doctor.ts
2273
- init_core();
2274
- init_execution();
2275
- init_constants2();
2276
- init_networks();
2277
-
2278
- // src/protocols/a2a/constants.ts
2279
- var A2A_SPEC_VERSION = "1.0.1";
2280
- var A2A_PROTOCOL_VERSION = "1.0";
2281
- var A2A_PROTOCOL_BINDING = "JSONRPC";
2282
- var A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
2283
- var A2A_PUSH_CONFIG_METHODS = [
2284
- "CreateTaskPushNotificationConfig",
2285
- "GetTaskPushNotificationConfig",
2286
- "ListTaskPushNotificationConfigs",
2287
- "DeleteTaskPushNotificationConfig"
2288
- ];
2289
- var A2A_UNSUPPORTED_METHODS = [
2290
- "SendStreamingMessage",
2291
- "GetTask",
2292
- "ListTasks",
2293
- "CancelTask",
2294
- "SubscribeToTask",
2295
- ...A2A_PUSH_CONFIG_METHODS,
2296
- "GetExtendedAgentCard"
2297
- ];
2298
-
2299
- // src/protocols/a2a/descriptor.ts
2300
- var A2A_UNSUPPORTED = [
2301
- // Methods, as the protocol names them; the transport refuses the same list
2302
- ...A2A_UNSUPPORTED_METHODS,
2303
- // Transports other than the one binding served
2304
- "HTTP+JSON/REST binding",
2305
- "gRPC binding",
2306
- // Behaviors
2307
- "SSE",
2308
- "long-running task persistence",
2309
- "task resumption other than an x402 extension payment",
2310
- "push notifications",
2311
- "multi-turn conversational continuation",
2312
- "authenticated extended agent cards",
2313
- "A2A authentication schemes",
2314
- "artifact types beyond Agent Commerce outcome data",
2315
- // Pending x402 payments live in process memory
2316
- "x402 extension payments across gateway restarts or instances"
2317
- ];
2318
-
2319
- // src/cli/commands/doctor.ts
2320
- init_constants3();
2321
-
2322
- // src/protocols/acp/descriptor.ts
2323
- init_constants3();
2324
- [
2325
- "rest transport",
2326
- "well-known discovery",
2327
- "bearer authentication",
2328
- "API-Version enforcement",
2329
- // Forwarded as received: the gateway verifies neither signature header
2330
- "request header forwarding to the merchant",
2331
- "checkout service",
2332
- ...ACP_CHECKOUT_OPERATIONS
2333
- ];
2334
- var ACP_UNSUPPORTED = [
2335
- // Services this seller does not implement, named as ACP names them. The
2336
- // discovery document advertises `checkout` alone, and these are the reason.
2337
- "carts service",
2338
- "feed service",
2339
- "standalone orders service",
2340
- "delegate_payment",
2341
- "delegate_authentication",
2342
- // Bindings and delivery directions
2343
- "ACP MCP transport binding",
2344
- "webhooks",
2345
- "outbound merchant-to-agent delivery",
2346
- "ACP client role",
2347
- // Request authentication beyond the bearer token
2348
- "Signature header verification",
2349
- "Timestamp replay-window verification",
2350
- // Extensions, versioning and payment handlers
2351
- "discount extension",
2352
- "general ACP extension framework",
2353
- "multiple ACP API versions",
2354
- "seller-backed payment handler integration"
2355
- ];
2356
-
2357
- // src/storage/receipts/store.ts
2358
- init_core();
2359
- var PACKAGE_VERSION = "2.0.0" ;
2360
-
2361
- // src/storage/sqlite.ts
2362
- init_core();
2363
- function openSqliteDatabase(options) {
2364
- const { path: path2, label, logger } = options;
2365
- const isFileBacked = path2 !== ":memory:";
2366
- if (isFileBacked) {
2367
- mkdirSync(dirname(path2), { recursive: true, mode: 448 });
2368
- }
2369
- if (isFileBacked && existsSync(path2)) {
2370
- try {
2371
- accessSync(path2, constants.W_OK);
2372
- } catch {
2373
- throw new CommerceError(
2374
- "STORAGE_ERROR",
2375
- `${label} "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one - recreate it (docker compose down -v) or fix its ownership.`,
2376
- { details: { path: path2 } }
2377
- );
2378
- }
2379
- }
2380
- if (isFileBacked) {
2381
- try {
2382
- closeSync(openSync(path2, "a", 384));
2383
- } catch {
2384
- }
2385
- }
2386
- const db = new Database(path2);
2387
- db.pragma("journal_mode = WAL");
2388
- db.pragma("foreign_keys = ON");
2389
- if (isFileBacked) restrictDatabasePermissions(path2, label, logger);
2390
- return db;
2391
- }
2392
- function restrictDatabasePermissions(path2, label, logger) {
2393
- for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
2394
- try {
2395
- chmodSync(file, 384);
2396
- } catch (error) {
2397
- const code = error.code;
2398
- if (code === "ENOENT") continue;
2399
- logger.warn(
2400
- { file, label, error: code ?? "unknown" },
2401
- "could not restrict database permissions to owner-only"
2402
- );
2403
- }
2404
- }
2405
- }
2406
-
2407
- // src/storage/receipts/redact.ts
2408
- var SECRET_KEY_PATTERN = /private[-_ ]?key|secret|password|mnemonic|seed[-_ ]?phrase|api[-_ ]?key|authoriz(e|ation)|^auth$|auth[-_]?header|payment[-_ ]?proof|^proof$|x-payment|signature|signed[-_ ]?message|token|bearer|credential|cookie|session|jwt/i;
2409
- var REDACTED = "[REDACTED]";
2410
- function isSecretKey(key) {
2411
- return SECRET_KEY_PATTERN.test(key);
2412
- }
2413
- function redact(value) {
2414
- return redactValue(value);
33
+ ];
2415
34
  }
2416
- function redactValue(value) {
2417
- if (Array.isArray(value)) {
2418
- return value.map(redactValue);
2419
- }
2420
- if (value !== null && typeof value === "object") {
2421
- const out = {};
2422
- for (const [key, inner] of Object.entries(value)) {
2423
- out[key] = isSecretKey(key) ? REDACTED : redactValue(inner);
35
+ async function runDemo(io, deps = {}) {
36
+ const steps = deps.steps ?? defaultDemoSteps();
37
+ const run = deps.run ?? execCommand;
38
+ io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
39
+ for (const step of steps) {
40
+ io.stdout("");
41
+ io.stdout(`==> ${step.name}`);
42
+ io.stdout(` $ ${step.command} ${step.args.join(" ")}`);
43
+ const result = await run(step.command, step.args);
44
+ if (result.code !== 0) {
45
+ io.stderr(`FAIL "${step.name}" exited with code ${result.code}.`);
46
+ if (result.stderrTail !== void 0 && result.stderrTail.length > 0) {
47
+ io.stderr(result.stderrTail);
48
+ }
49
+ io.stderr("");
50
+ io.stderr("Nothing further was run. To retry manually:");
51
+ io.stderr(` ${step.command} ${step.args.join(" ")}`);
52
+ return 1;
2424
53
  }
2425
- return out;
54
+ io.stdout(`OK ${step.name}`);
2426
55
  }
2427
- return value;
2428
- }
2429
-
2430
- // src/storage/receipts/rows.ts
2431
- function receiptToRow(receipt) {
2432
- return {
2433
- id: receipt.id,
2434
- request_id: receipt.requestId,
2435
- resource_id: receipt.resourceId,
2436
- payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
2437
- delivered_at: receipt.deliveredAt,
2438
- backend_status: receipt.backendStatus,
2439
- duration_ms: receipt.durationMs ?? null,
2440
- protocol: receipt.protocol ?? null,
2441
- metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
2442
- authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
2443
- };
2444
- }
2445
- function rowToReceipt(row) {
2446
- const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
2447
- const metadata = row.metadata_json !== null ? JSON.parse(row.metadata_json) : void 0;
2448
- const authorization = row.authorization_json !== null ? JSON.parse(row.authorization_json) : void 0;
2449
- return {
2450
- id: row.id,
2451
- requestId: row.request_id,
2452
- resourceId: row.resource_id,
2453
- deliveredAt: row.delivered_at,
2454
- backendStatus: row.backend_status,
2455
- ...payment !== void 0 ? { payment } : {},
2456
- ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
2457
- ...row.protocol !== null ? { protocol: row.protocol } : {},
2458
- ...metadata !== void 0 ? { metadata } : {},
2459
- ...authorization !== void 0 ? { authorization } : {}
2460
- };
2461
- }
2462
- function eventToRow(event) {
2463
- return {
2464
- id: event.id,
2465
- type: event.type,
2466
- request_id: event.requestId,
2467
- resource_id: event.resourceId ?? null,
2468
- at: event.at,
2469
- adapter: event.adapter ?? null,
2470
- payment_provider: event.paymentProvider ?? null,
2471
- duration_ms: event.durationMs ?? null,
2472
- status: event.status ?? null,
2473
- data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
2474
- };
2475
- }
2476
- function rowToEvent(row) {
2477
- const data = row.data_json !== null ? JSON.parse(row.data_json) : void 0;
2478
- return {
2479
- id: row.id,
2480
- type: row.type,
2481
- requestId: row.request_id,
2482
- at: row.at,
2483
- ...row.resource_id !== null ? { resourceId: row.resource_id } : {},
2484
- ...row.adapter !== null ? { adapter: row.adapter } : {},
2485
- ...row.payment_provider !== null ? { paymentProvider: row.payment_provider } : {},
2486
- ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
2487
- ...row.status !== null ? { status: row.status } : {},
2488
- ...data !== void 0 ? { data } : {}
2489
- };
2490
- }
2491
- function rowToPaymentAttempt(row) {
2492
- return {
2493
- id: row.id,
2494
- requestId: row.request_id,
2495
- resourceId: row.resource_id,
2496
- provider: row.provider,
2497
- replayKey: row.replay_key,
2498
- status: row.status,
2499
- amount: row.amount,
2500
- currency: row.currency,
2501
- ...row.payer !== null ? { payer: row.payer } : {},
2502
- ...row.payee !== null ? { payee: row.payee } : {},
2503
- ...row.external_reference !== null ? { externalReference: row.external_reference } : {},
2504
- ...row.rejection_reason !== null ? { rejectionReason: row.rejection_reason } : {},
2505
- createdAt: row.created_at,
2506
- updatedAt: row.updated_at
2507
- };
56
+ io.stdout("");
57
+ io.stdout("PASS Demo is up. Try: agent-commerce doctor --config config-demo.yaml");
58
+ return 0;
2508
59
  }
2509
-
2510
- // src/storage/receipts/schema.ts
2511
- var MIGRATIONS = [
2512
- {
2513
- version: 1,
2514
- up(db) {
2515
- db.exec(`
2516
- CREATE TABLE IF NOT EXISTS receipts (
2517
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
2518
- id TEXT NOT NULL UNIQUE,
2519
- request_id TEXT NOT NULL,
2520
- resource_id TEXT NOT NULL,
2521
- payment_json TEXT,
2522
- delivered_at TEXT NOT NULL,
2523
- backend_status INTEGER NOT NULL,
2524
- duration_ms INTEGER,
2525
- protocol TEXT,
2526
- metadata_json TEXT
2527
- );
2528
- CREATE INDEX IF NOT EXISTS idx_receipts_request_id ON receipts(request_id);
2529
- CREATE INDEX IF NOT EXISTS idx_receipts_delivered_at ON receipts(delivered_at);
2530
-
2531
- CREATE TABLE IF NOT EXISTS events (
2532
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
2533
- id TEXT NOT NULL UNIQUE,
2534
- type TEXT NOT NULL,
2535
- request_id TEXT NOT NULL,
2536
- resource_id TEXT,
2537
- at TEXT NOT NULL,
2538
- adapter TEXT,
2539
- payment_provider TEXT,
2540
- duration_ms INTEGER,
2541
- status TEXT,
2542
- data_json TEXT
2543
- );
2544
- CREATE INDEX IF NOT EXISTS idx_events_request_id ON events(request_id);
2545
- CREATE INDEX IF NOT EXISTS idx_events_at ON events(at);
2546
-
2547
- CREATE TABLE IF NOT EXISTS payment_attempts (
2548
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
2549
- id TEXT NOT NULL UNIQUE,
2550
- request_id TEXT NOT NULL,
2551
- resource_id TEXT NOT NULL,
2552
- provider TEXT NOT NULL,
2553
- replay_key TEXT NOT NULL UNIQUE,
2554
- status TEXT NOT NULL,
2555
- amount TEXT NOT NULL,
2556
- currency TEXT NOT NULL,
2557
- payer TEXT,
2558
- payee TEXT,
2559
- external_reference TEXT,
2560
- rejection_reason TEXT,
2561
- created_at TEXT NOT NULL,
2562
- updated_at TEXT NOT NULL
2563
- );
2564
- CREATE INDEX IF NOT EXISTS idx_payment_attempts_request_id ON payment_attempts(request_id);
2565
- CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
2566
- `);
2567
- }
2568
- },
2569
- {
2570
- version: 2,
2571
- up(db) {
2572
- db.exec(`ALTER TABLE receipts ADD COLUMN authorization_json TEXT;`);
2573
- }
2574
- }
2575
- ];
2576
- var SCHEMA_VERSION = MIGRATIONS.at(-1)?.version ?? 0;
2577
- function migrate(db) {
2578
- const current = db.pragma("user_version", { simple: true });
2579
- for (const migration of MIGRATIONS) {
2580
- if (migration.version <= current) continue;
2581
- const apply = db.transaction(() => {
2582
- migration.up(db);
2583
- db.pragma(`user_version = ${migration.version}`);
60
+ async function execCommand(command, args) {
61
+ const { spawn } = await import('child_process');
62
+ return new Promise((resolvePromise) => {
63
+ let stderr = "";
64
+ const child = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
65
+ child.stderr?.on("data", (chunk) => {
66
+ stderr += chunk.toString("utf8");
2584
67
  });
2585
- apply();
2586
- }
2587
- }
2588
-
2589
- // src/storage/receipts/store.ts
2590
- var DEFAULT_LIST_LIMIT = 50;
2591
- var MAX_LIST_LIMIT = 500;
2592
- function clampListLimit(limit) {
2593
- const base = limit !== void 0 && Number.isFinite(limit) ? limit : DEFAULT_LIST_LIMIT;
2594
- return Math.min(Math.max(Math.trunc(base), 1), MAX_LIST_LIMIT);
2595
- }
2596
- var defaultIds = {
2597
- next: (prefix) => prefix !== void 0 ? `${prefix}_${randomUUID()}` : randomUUID()
2598
- };
2599
- function isUniqueConstraintOn(err, column) {
2600
- return err instanceof Error && "code" in err && err.code === "SQLITE_CONSTRAINT_UNIQUE" && err.message.includes(column);
2601
- }
2602
- function createSqliteReceiptStore(options) {
2603
- const logger = options.logger ?? NOOP_LOGGER;
2604
- const clock = options.clock ?? systemClock;
2605
- const ids = options.ids ?? defaultIds;
2606
- const path2 = options.path;
2607
- const isFileBacked = path2 !== ":memory:";
2608
- const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
2609
- migrate(db);
2610
- const insertReceiptStmt = db.prepare(
2611
- `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json)
2612
- VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json)`
2613
- );
2614
- const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
2615
- const listReceiptsStmt = db.prepare(
2616
- "SELECT * FROM receipts ORDER BY delivered_at DESC, seq DESC LIMIT ?"
2617
- );
2618
- const listReceiptsByRequestStmt = db.prepare(
2619
- "SELECT * FROM receipts WHERE request_id = ? ORDER BY delivered_at DESC, seq DESC LIMIT ?"
2620
- );
2621
- const countReceiptsStmt = db.prepare(
2622
- "SELECT COUNT(*) AS count FROM receipts"
2623
- );
2624
- const countUndeliveredReceiptsStmt = db.prepare(
2625
- "SELECT COUNT(*) AS count FROM receipts WHERE backend_status NOT BETWEEN 200 AND 299"
2626
- );
2627
- const insertEventStmt = db.prepare(
2628
- `INSERT INTO events (id, type, request_id, resource_id, at, adapter, payment_provider, duration_ms, status, data_json)
2629
- VALUES (@id, @type, @request_id, @resource_id, @at, @adapter, @payment_provider, @duration_ms, @status, @data_json)`
2630
- );
2631
- const listEventsStmt = db.prepare(
2632
- "SELECT * FROM events ORDER BY at DESC, seq DESC LIMIT ?"
2633
- );
2634
- const listEventsByRequestStmt = db.prepare(
2635
- "SELECT * FROM events WHERE request_id = ? ORDER BY at DESC, seq DESC LIMIT ?"
2636
- );
2637
- const insertPaymentAttemptStmt = db.prepare(
2638
- `INSERT INTO payment_attempts (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at)
2639
- VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
2640
- RETURNING *`
2641
- );
2642
- const updatePaymentAttemptStmt = db.prepare(
2643
- `UPDATE payment_attempts SET status = @status,
2644
- external_reference = COALESCE(@external_reference, external_reference),
2645
- rejection_reason = COALESCE(@rejection_reason, rejection_reason),
2646
- updated_at = @updated_at
2647
- WHERE replay_key = @replay_key`
2648
- );
2649
- const listPaymentAttemptsStmt = db.prepare(
2650
- "SELECT * FROM payment_attempts ORDER BY created_at DESC, seq DESC LIMIT ?"
2651
- );
2652
- const listPaymentAttemptsByRequestStmt = db.prepare(
2653
- "SELECT * FROM payment_attempts WHERE request_id = ? ORDER BY created_at DESC, seq DESC LIMIT ?"
2654
- );
2655
- const paymentAttemptStatusStmt = db.prepare(
2656
- "SELECT status FROM payment_attempts WHERE replay_key = ?"
2657
- );
2658
- let closed = false;
2659
- const descriptor = {
2660
- name: "sqlite-receipt-store",
2661
- kind: "storage",
2662
- implementationVersion: PACKAGE_VERSION,
2663
- supportedSpec: `sqlite-schema-v${SCHEMA_VERSION}`,
2664
- capabilities: ["receipts", "events", "payment-attempts"],
2665
- status: "stable"
2666
- };
2667
- const store = {
2668
- async init() {
2669
- const version = db.pragma("user_version", { simple: true });
2670
- if (version !== SCHEMA_VERSION) {
2671
- throw new CommerceError(
2672
- "STORAGE_ERROR",
2673
- `Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION})`
2674
- );
2675
- }
2676
- },
2677
- async appendEvent(event) {
2678
- try {
2679
- insertEventStmt.run(eventToRow(event));
2680
- } catch (err) {
2681
- logger.warn(
2682
- {
2683
- err: err instanceof Error ? err.message : String(err),
2684
- eventId: event.id,
2685
- requestId: event.requestId
2686
- },
2687
- "receipt-store: failed to persist commerce event"
2688
- );
2689
- }
2690
- },
2691
- async reservePaymentAttempt(reservation) {
2692
- const now = clock.nowIso();
2693
- const id = ids.next("attempt");
2694
- let row;
2695
- try {
2696
- row = insertPaymentAttemptStmt.get({
2697
- id,
2698
- request_id: reservation.requestId,
2699
- resource_id: reservation.resourceId,
2700
- provider: reservation.provider,
2701
- replay_key: reservation.replayKey,
2702
- status: "reserved",
2703
- amount: reservation.amount,
2704
- currency: reservation.currency,
2705
- payer: reservation.payer ?? null,
2706
- payee: reservation.payee ?? null,
2707
- external_reference: null,
2708
- rejection_reason: null,
2709
- created_at: now,
2710
- updated_at: now
2711
- });
2712
- } catch (err) {
2713
- if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
2714
- const existing = paymentAttemptStatusStmt.get(reservation.replayKey);
2715
- throw new CommerceError(
2716
- "PAYMENT_REPLAYED",
2717
- `Payment authorization has already been used (replayKey=${reservation.replayKey})`,
2718
- {
2719
- requestId: reservation.requestId,
2720
- resourceId: reservation.resourceId,
2721
- ...existing !== void 0 ? { details: { attemptStatus: existing.status } } : {}
2722
- }
2723
- );
2724
- }
2725
- throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
2726
- }
2727
- return rowToPaymentAttempt(row);
2728
- },
2729
- async updatePaymentAttempt(update) {
2730
- try {
2731
- const result = updatePaymentAttemptStmt.run({
2732
- replay_key: update.replayKey,
2733
- status: update.status,
2734
- external_reference: update.externalReference ?? null,
2735
- rejection_reason: update.rejectionReason ?? null,
2736
- updated_at: clock.nowIso()
2737
- });
2738
- if (result.changes === 0) {
2739
- throw new CommerceError(
2740
- "STORAGE_ERROR",
2741
- `No payment attempt found for replayKey=${update.replayKey}`
2742
- );
2743
- }
2744
- } catch (err) {
2745
- throw toCommerceError(err, "STORAGE_ERROR", "Failed to update payment attempt");
2746
- }
2747
- },
2748
- async saveReceipt(receipt) {
2749
- try {
2750
- insertReceiptStmt.run(receiptToRow(receipt));
2751
- } catch (err) {
2752
- throw toCommerceError(err, "STORAGE_ERROR", "Failed to save receipt");
2753
- }
2754
- },
2755
- async getReceipt(id) {
2756
- const row = getReceiptStmt.get(id);
2757
- return row !== void 0 ? rowToReceipt(row) : void 0;
2758
- },
2759
- async listReceipts(listOptions = {}) {
2760
- const limit = clampListLimit(listOptions.limit);
2761
- const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
2762
- return rows.map(rowToReceipt);
2763
- },
2764
- // Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
2765
- async countReceipts() {
2766
- return countReceiptsStmt.get()?.count ?? 0;
2767
- },
2768
- // Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
2769
- async countUndeliveredReceipts() {
2770
- return countUndeliveredReceiptsStmt.get()?.count ?? 0;
2771
- },
2772
- async listEvents(listOptions = {}) {
2773
- const limit = clampListLimit(listOptions.limit);
2774
- const rows = listOptions.requestId !== void 0 ? listEventsByRequestStmt.all(listOptions.requestId, limit) : listEventsStmt.all(limit);
2775
- return rows.map(rowToEvent);
2776
- },
2777
- async listPaymentAttempts(listOptions = {}) {
2778
- const limit = clampListLimit(listOptions.limit);
2779
- const rows = listOptions.requestId !== void 0 ? listPaymentAttemptsByRequestStmt.all(listOptions.requestId, limit) : listPaymentAttemptsStmt.all(limit);
2780
- return rows.map(rowToPaymentAttempt);
2781
- },
2782
- descriptor,
2783
- async health() {
2784
- const startedAt = clock.monotonicMs();
2785
- if (closed) {
2786
- return { status: "fail", detail: "store-unavailable", checkedAt: clock.nowIso() };
2787
- }
2788
- if (isFileBacked) {
2789
- try {
2790
- accessSync(path2, constants.W_OK);
2791
- } catch (err) {
2792
- logger.warn(
2793
- { err: err instanceof Error ? err.message : String(err) },
2794
- "receipt-store: health check failed (store not writable)"
2795
- );
2796
- return {
2797
- status: "fail",
2798
- detail: "store-unwritable",
2799
- checkedAt: clock.nowIso(),
2800
- durationMs: clock.monotonicMs() - startedAt
2801
- };
2802
- }
2803
- }
2804
- try {
2805
- const version = db.pragma("user_version", { simple: true });
2806
- if (version !== SCHEMA_VERSION) {
2807
- logger.warn(
2808
- { found: version, expected: SCHEMA_VERSION },
2809
- "receipt-store: health check failed (schema version mismatch)"
2810
- );
2811
- return {
2812
- status: "fail",
2813
- detail: "store-schema-mismatch",
2814
- checkedAt: clock.nowIso(),
2815
- durationMs: clock.monotonicMs() - startedAt
2816
- };
2817
- }
2818
- return {
2819
- status: "pass",
2820
- detail: `sqlite schema v${SCHEMA_VERSION} writable`,
2821
- checkedAt: clock.nowIso(),
2822
- durationMs: clock.monotonicMs() - startedAt
2823
- };
2824
- } catch (err) {
2825
- logger.warn(
2826
- { err: err instanceof Error ? err.message : String(err) },
2827
- "receipt-store: health check failed (store unavailable)"
2828
- );
2829
- return {
2830
- status: "fail",
2831
- detail: "store-unavailable",
2832
- checkedAt: clock.nowIso(),
2833
- durationMs: clock.monotonicMs() - startedAt
2834
- };
2835
- }
2836
- },
2837
- async close() {
2838
- if (!closed) {
2839
- closed = true;
2840
- db.close();
2841
- }
2842
- }
2843
- };
2844
- return store;
68
+ child.on("error", (err) => {
69
+ resolvePromise({ code: 1, stderrTail: err.message });
70
+ });
71
+ child.on("close", (code) => {
72
+ const tail = stderr.trim().split("\n").slice(-10).join("\n");
73
+ resolvePromise({ code: code ?? 1, ...tail.length > 0 ? { stderrTail: tail } : {} });
74
+ });
75
+ });
2845
76
  }
2846
77
 
2847
78
  // src/cli/lib/config-client.ts
2848
- init_core();
2849
79
  async function importConfig() {
2850
80
  try {
2851
- return await Promise.resolve().then(() => (init_config(), config_exports));
81
+ return await import('./config-4IBGVFSP.js');
2852
82
  } catch (err) {
2853
83
  throw new CommerceError(
2854
84
  "INTERNAL_ERROR",
@@ -2871,141 +101,17 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
2871
101
  try {
2872
102
  body = await res.json();
2873
103
  } catch {
2874
- body = void 0;
2875
- }
2876
- return { ok: res.ok, status: res.status, ...body !== void 0 ? { body } : {} };
2877
- } catch (err) {
2878
- return {
2879
- ok: false,
2880
- status: 0,
2881
- error: err instanceof Error ? err.message : "unknown fetch error"
2882
- };
2883
- }
2884
- }
2885
-
2886
- // src/cli/lib/init-config.ts
2887
- init_accounts();
2888
- var LOCAL_DEV_MERCHANT_ADDRESS = ANVIL_WELL_KNOWN_ACCOUNTS[1].address;
2889
- var LOCAL_DEV_FACILITATOR_PRIVATE_KEY = ANVIL_WELL_KNOWN_ACCOUNTS[0].privateKey;
2890
- var PLACEHOLDER_ASSET_ADDRESS = "0x000000000000000000000000000000000000dEaD";
2891
- function defaultInitAnswers() {
2892
- return {
2893
- backendBaseUrl: "http://localhost:3000",
2894
- resources: ["weather", "report"],
2895
- protocols: ["http", "mcp"],
2896
- x402Enabled: true,
2897
- merchantPayTo: LOCAL_DEV_MERCHANT_ADDRESS
2898
- };
2899
- }
2900
- function buildResourcesMap(answers) {
2901
- const resources = {};
2902
- const expose = [...answers.protocols];
2903
- if (answers.resources.includes("weather")) {
2904
- resources.weather = {
2905
- name: "Get weather",
2906
- description: "Deterministic demo weather lookup (free resource).",
2907
- input: {
2908
- type: "object",
2909
- properties: { city: { type: "string" } },
2910
- required: ["city"],
2911
- additionalProperties: false
2912
- },
2913
- backend: {
2914
- type: "http",
2915
- method: "GET",
2916
- url: `${answers.backendBaseUrl}/api/weather/{city}`
2917
- },
2918
- pricing: { type: "free" },
2919
- expose
2920
- };
2921
- }
2922
- if (answers.resources.includes("report")) {
2923
- const paid = answers.x402Enabled;
2924
- resources.report = {
2925
- name: "Get market report",
2926
- description: "Deterministic demo premium market report.",
2927
- // No caller-supplied fields. Config loads a missing `input` as this same
2928
- // closed schema; writing it out shows that in the generated file
2929
- input: {
2930
- type: "object",
2931
- properties: {},
2932
- additionalProperties: false
2933
- },
2934
- backend: {
2935
- type: "http",
2936
- method: "GET",
2937
- url: `${answers.backendBaseUrl}/api/report`
2938
- },
2939
- pricing: paid ? { type: "fixed", amount: "0.01", currency: "USDC" } : { type: "free" },
2940
- expose,
2941
- ...paid ? { payments: ["x402"] } : {}
2942
- };
2943
- }
2944
- return resources;
2945
- }
2946
- function buildInitConfigObject(answers) {
2947
- return {
2948
- version: 1,
2949
- merchant: {
2950
- id: "demo-merchant",
2951
- name: "Demo Merchant",
2952
- publicBaseUrl: "http://localhost:8080"
2953
- },
2954
- server: { port: 8080, host: "127.0.0.1" },
2955
- storage: { receipts: { driver: "sqlite", path: "./data/receipts.db" } },
2956
- protocols: {
2957
- http: { enabled: answers.protocols.includes("http") },
2958
- mcp: { enabled: answers.protocols.includes("mcp"), mountPath: "/mcp" }
2959
- },
2960
- resources: buildResourcesMap(answers),
2961
- payments: answers.x402Enabled ? {
2962
- x402: {
2963
- enabled: true,
2964
- network: "eip155:84532",
2965
- rpcUrl: "http://127.0.0.1:8545",
2966
- asset: PLACEHOLDER_ASSET_ADDRESS,
2967
- assetName: "MockUSDC",
2968
- assetVersion: "2",
2969
- assetDecimals: 6,
2970
- payTo: answers.merchantPayTo,
2971
- maxTimeoutSeconds: 60,
2972
- facilitator: { mode: "local", signerPrivateKey: LOCAL_DEV_FACILITATOR_PRIVATE_KEY }
2973
- }
2974
- } : {}
2975
- };
2976
- }
2977
- var HEADER = `# Generated by \`agent-commerce init\`.
2978
- #
2979
- # With x402 enabled, the facilitator signerPrivateKey is Anvil's public dev
2980
- # account #0 and the default payTo is account #1.
2981
- # LOCAL DEVELOPMENT ONLY - DO NOT FUND: never use them outside a local chain.
2982
- # payments.x402.asset is a placeholder, not a real contract. Before running
2983
- # against the local chain, run \`npm run chain:deploy\` and set it to the
2984
- # MockUSDC address in \`.deploy/local.json\`. Before pointing this at a funded
2985
- # network, set \`payments.x402.payTo\` to your own settlement address.
2986
- #
2987
- # server.host is loopback (127.0.0.1): binding every interface should be a
2988
- # deliberate edit.
2989
- #
2990
- # server.adminToken and server.allowedOrigins are left unset, so no secret
2991
- # lands in a file you might commit. Without a token the operator routes
2992
- # (/api/receipts, /api/events) return 404. They serve the merchant's commerce
2993
- # ledger: payer addresses, amounts and settlement hashes. To enable them, add
2994
- # these keys to the server block below:
2995
- # server:
2996
- # adminToken: \${ADMIN_TOKEN}
2997
- # allowedOrigins:
2998
- # - http://localhost:5173
2999
- # See SECURITY.md before exposing this gateway beyond localhost.
3000
- `;
3001
- function renderInitConfigYaml(answers) {
3002
- const body = stringify(buildInitConfigObject(answers), { indent: 2 });
3003
- return `${HEADER}
3004
- ${body}`;
104
+ body = void 0;
105
+ }
106
+ return { ok: res.ok, status: res.status, ...body !== void 0 ? { body } : {} };
107
+ } catch (err) {
108
+ return {
109
+ ok: false,
110
+ status: 0,
111
+ error: err instanceof Error ? err.message : "unknown fetch error"
112
+ };
113
+ }
3005
114
  }
3006
-
3007
- // src/payments/x402/local-chain/manifest.ts
3008
- init_core();
3009
115
  var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
3010
116
  var nonEmpty = z.string().min(1, "must be a non-empty string");
3011
117
  var keyedAccount = z.object({ address: z.string(), privateKey: z.string(), note: z.string() });
@@ -3023,18 +129,18 @@ var MANIFEST_SCHEMA = z.object({
3023
129
  buyerInitialBalance: nonEmpty
3024
130
  });
3025
131
  function readLocalChainManifest(cwd = process.cwd()) {
3026
- const path2 = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
3027
- if (!existsSync(path2)) {
132
+ const path = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
133
+ if (!existsSync(path)) {
3028
134
  throw new CommerceError(
3029
135
  "CONFIG_INVALID",
3030
- `Local chain manifest not found at "${path2}". Run "npm run chain:start" then "npm run chain:deploy" first.`
136
+ `Local chain manifest not found at "${path}". Run "npm run chain:start" then "npm run chain:deploy" first.`
3031
137
  );
3032
138
  }
3033
139
  let raw;
3034
140
  try {
3035
- raw = readFileSync(path2, "utf8");
141
+ raw = readFileSync(path, "utf8");
3036
142
  } catch (cause) {
3037
- throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${path2}".`, {
143
+ throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${path}".`, {
3038
144
  cause
3039
145
  });
3040
146
  }
@@ -3044,7 +150,7 @@ function readLocalChainManifest(cwd = process.cwd()) {
3044
150
  } catch (cause) {
3045
151
  throw new CommerceError(
3046
152
  "CONFIG_INVALID",
3047
- `Local chain manifest at "${path2}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
153
+ `Local chain manifest at "${path}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
3048
154
  { cause }
3049
155
  );
3050
156
  }
@@ -3054,7 +160,7 @@ function readLocalChainManifest(cwd = process.cwd()) {
3054
160
  const where = issue?.path.length ? `${issue.path.join(".")}: ` : "";
3055
161
  throw new CommerceError(
3056
162
  "CONFIG_INVALID",
3057
- `Local chain manifest at "${path2}" is invalid: ${where}${issue?.message ?? "unexpected shape"}. Re-run "npm run chain:deploy".`
163
+ `Local chain manifest at "${path}" is invalid: ${where}${issue?.message ?? "unexpected shape"}. Re-run "npm run chain:deploy".`
3058
164
  );
3059
165
  }
3060
166
  return result.data;
@@ -3089,11 +195,19 @@ function fillEnvFromLocalChainManifest(baseEnv, cwd = process.cwd()) {
3089
195
  }
3090
196
  return { env, filled, manifestFound: true };
3091
197
  }
3092
-
3093
- // src/cli/lib/mask.ts
3094
- function maskMiddle(value, keep = 6) {
3095
- if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
3096
- return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
198
+ function hasChainDeployScript(cwd = process.cwd()) {
199
+ try {
200
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
201
+ return pkg.name === "@devlab.group/agent-commerce" && typeof pkg.scripts?.["chain:deploy"] === "string";
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+ function unresolvedVariableHint(variable, manifestFound, chainDeploy) {
207
+ if (chainDeploy && !manifestFound && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable)) {
208
+ return `"npm run chain:deploy" writes ${LOCAL_CHAIN_MANIFEST_PATH}, which validate and doctor read to fill local X402_* and MERCHANT_WALLET placeholders`;
209
+ }
210
+ return `set ${variable} in the environment, or in a file passed with --env-file`;
3097
211
  }
3098
212
 
3099
213
  // package.json
@@ -3115,8 +229,11 @@ var package_default = {
3115
229
  "@coinbase/x402": "2.1.0",
3116
230
  "@modelcontextprotocol/server": "2.3.0",
3117
231
  "@sd-jwt/core": "0.20.1",
232
+ "@solana/kit": "5.5.1",
3118
233
  "@x402/core": "2.25.0",
3119
234
  "@x402/evm": "2.25.0",
235
+ "@x402/near": "2.25.0",
236
+ "@x402/svm": "2.25.0",
3120
237
  canonicalize: "5.0.0",
3121
238
  jose: "6.2.12",
3122
239
  mppx: "0.13.1",
@@ -3130,6 +247,9 @@ var PINS = [
3130
247
  ["ajv-formats", "protocols/acp"],
3131
248
  ["@x402/core", "payments/x402"],
3132
249
  ["@x402/evm", "payments/x402"],
250
+ ["@x402/svm", "payments/x402"],
251
+ ["@solana/kit", "payments/x402"],
252
+ ["@x402/near", "payments/x402"],
3133
253
  ["@coinbase/x402", "payments/x402"],
3134
254
  ["viem", "payments/x402"],
3135
255
  ["mppx", "payments/mpp"],
@@ -3153,278 +273,99 @@ function readVersionReport() {
3153
273
  });
3154
274
  return { cliVersion: PACKAGE_VERSION, pinned };
3155
275
  }
3156
-
3157
- // src/cli/commands/doctor.ts
3158
- var CHECK_TIMEOUT_MS = 1500;
3159
- function deriveGatewayUrl(explicit, config) {
3160
- if (explicit !== void 0) return explicit.replace(/\/$/, "");
3161
- if (config !== void 0) {
3162
- const raw = config.server.host;
3163
- const host = raw === "0.0.0.0" ? "127.0.0.1" : raw === "::" ? "::1" : raw;
3164
- const authority = host.includes(":") ? `[${host}]` : host;
3165
- return `http://${authority}:${config.server.port}`;
3166
- }
3167
- return "http://localhost:8080";
3168
- }
3169
- function substitutePathParams(url) {
3170
- let filled = url;
3171
- for (const name of extractPathParameterNames(url)) {
3172
- filled = filled.replaceAll(`{${name}}`, "demo-check");
3173
- }
3174
- return filled;
3175
- }
3176
- function describePaymentFlow(rail) {
3177
- const overrides = Object.keys(rail.resourcePaymentFlows ?? {}).length;
3178
- const overrideCount = overrides > 0 ? ` (${overrides} resource ${overrides === 1 ? "override" : "overrides"})` : "";
3179
- return `paymentFlow=${rail.paymentFlow ?? "authorization"}${overrideCount}`;
3180
- }
3181
- function extractWellKnownX402(body, rail = "x402") {
3182
- const payments = body?.["payments"];
3183
- if (typeof payments !== "object" || payments === null) return void 0;
3184
- const block = payments[rail];
3185
- if (typeof block !== "object" || block === null) return void 0;
3186
- const rec = block;
3187
- if (rec["enabled"] === false) return "disabled";
3188
- if (rec["enabled"] !== true) return void 0;
3189
- const { asset, network } = rec;
3190
- const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
3191
- if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
3192
- return void 0;
3193
- }
3194
- return { asset, network, payTo };
3195
- }
3196
- function sameAddress2(a, b) {
3197
- return a.toLowerCase() === b.toLowerCase();
3198
- }
3199
- function findX402Mismatch(configured, live, payToField = "payTo") {
3200
- const diffs = [];
3201
- if (!sameAddress2(configured.asset, live.asset)) {
3202
- diffs.push(
3203
- `asset: gateway is using ${live.asset} but local config resolves to ${configured.asset}`
3204
- );
3205
- }
3206
- if (configured.network !== live.network) {
3207
- diffs.push(
3208
- `network: gateway is using "${live.network}" but local config resolves to "${configured.network}"`
3209
- );
3210
- }
3211
- if (!sameAddress2(configured.payTo, live.payTo)) {
3212
- diffs.push(
3213
- `${payToField}: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
3214
- );
3215
- }
3216
- if (diffs.length === 0) return void 0;
3217
- return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
3218
- }
3219
- function describeSettlement(network, facilitator) {
3220
- const profile = findNetworkProfile(network);
3221
- const mode = profile ? resolveDeploymentMode(profile, facilitator.mode) : void 0;
3222
- const where = profile === void 0 || mode === void 0 ? `unknown network ${network}` : mode === "local" ? `LOCAL dev chain (${network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${network})`;
3223
- const via = facilitator.mode === "local" ? "local (in-process)" : `remote (auth=${facilitator.auth.type})`;
3224
- return { mode, where, via };
3225
- }
3226
- function mainnetSafetyCheck(name, payToField, facilitator) {
3227
- const unauthenticated = facilitator.mode === "remote" && facilitator.auth.type === "none";
3228
- return {
3229
- name,
3230
- status: unauthenticated ? "WARN" : "INFO",
3231
- detail: unauthenticated ? `${describeDeploymentMode("mainnet")}: no credential is sent to the remote facilitator (accepted via allowUnauthenticatedFacilitator); its anonymous-access limits apply. Config load also requires allowMainnet, HTTPS, a non-development ${payToField} and the canonical asset. Payments fail closed.` : `${describeDeploymentMode("mainnet")}. Enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development ${payToField}, and the canonical asset for this network. Payments fail closed.`
3232
- };
3233
- }
3234
- function describeIssuers(label, issuers) {
3235
- const described = issuers.map(
3236
- (entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
3237
- ).join(", ");
3238
- return `${label} issuers: ${described}`;
3239
- }
3240
- function isWritableFile(path2) {
276
+ function isWritableFile(path) {
3241
277
  try {
3242
- accessSync(path2, constants.W_OK);
278
+ accessSync(path, constants.W_OK);
3243
279
  return true;
3244
280
  } catch {
3245
281
  return false;
3246
282
  }
3247
283
  }
3248
- function storeFileCheck(name, path2, risk, suffix = "") {
3249
- if (path2 === ":memory:") {
284
+ function storeFileCheck(name, path, risk, suffix = "") {
285
+ if (path === ":memory:") {
3250
286
  return {
3251
287
  name,
3252
288
  status: "WARN",
3253
289
  detail: `in-memory store: ${risk.inMemory}; use a file path in production${suffix}`
3254
290
  };
3255
291
  }
3256
- if (existsSync(path2)) {
3257
- return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"${suffix}` } : {
292
+ if (existsSync(path)) {
293
+ return isWritableFile(path) ? { name, status: "PASS", detail: `writable at "${path}"${suffix}` } : {
3258
294
  name,
3259
295
  status: "FAIL",
3260
- detail: `"${path2}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
296
+ detail: `"${path}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
3261
297
  };
3262
298
  }
3263
- return { name, status: "WARN", detail: `${missingStoreDetail(path2)}${suffix}` };
3264
- }
3265
- function missingStoreDetail(path2) {
3266
- const directory = dirname(path2);
3267
- return directory !== "." && !existsSync(directory) ? `no store at "${path2}" and its directory does not exist. Both are created when the gateway starts, provided the path is writable` : `no store at "${path2}" yet. It is created the first time the gateway starts`;
299
+ return { name, status: "WARN", detail: `${missingStoreDetail(path)}${suffix}` };
3268
300
  }
3269
- function acpMappingCheck(config, operations) {
3270
- const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
3271
- const problems = [];
3272
- for (const [operation, resourceId] of Object.entries(operations)) {
3273
- const resource = byId.get(resourceId);
3274
- if (resource === void 0) {
3275
- problems.push(`${operation} -> "${resourceId}" does not exist`);
3276
- continue;
3277
- }
3278
- if (!resource.exposedVia.includes("acp")) {
3279
- problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
3280
- }
3281
- if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
3282
- problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
3283
- }
3284
- }
3285
- if (problems.length > 0) {
3286
- return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
3287
- }
3288
- return {
3289
- name: "ACP checkout mapping",
3290
- status: "PASS",
3291
- detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
3292
- };
301
+ function missingStoreDetail(path) {
302
+ const directory = dirname(path);
303
+ return directory !== "." && !existsSync(directory) ? `no store at "${path}" and its directory does not exist. Both are created when the gateway starts, provided the path is writable` : `no store at "${path}" yet. It is created the first time the gateway starts`;
3293
304
  }
3294
- async function runDoctor(options, deps = {}) {
3295
- const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
3296
- const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
3297
- const createStore = deps.createStore ?? ((path2) => createSqliteReceiptStore({ path: path2 }));
3298
- const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
305
+ async function storageChecks(config, createStore) {
3299
306
  const checks = [];
3300
- let config;
3301
- const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
3302
- const filledSuffix = filled.length > 0 ? ` (using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")})` : "";
3303
- try {
3304
- config = await loadConfig2({
3305
- ...options.configPath !== void 0 ? { path: options.configPath } : {},
3306
- env
3307
- });
3308
- checks.push({
3309
- name: "Config",
3310
- status: "PASS",
3311
- detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
3312
- });
3313
- } catch (err) {
3314
- const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
3315
- const hint = !manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable) ? `. Run "npm run chain:deploy": it writes ${LOCAL_CHAIN_MANIFEST_PATH}, which fills local X402_*/MERCHANT_WALLET placeholders automatically` : "";
3316
- checks.push({
3317
- name: "Config",
3318
- status: "FAIL",
3319
- detail: `${err instanceof Error ? err.message : String(err)}${hint}`
3320
- });
3321
- }
3322
- const gatewayUrl = deriveGatewayUrl(options.gatewayUrl, config);
3323
- const health = await fetchJson(fetchImpl, `${gatewayUrl}/health`, CHECK_TIMEOUT_MS);
3324
- let gatewayUp = false;
3325
- if (!health.ok) {
3326
- checks.push({
3327
- name: "Gateway",
3328
- status: "FAIL",
3329
- detail: `unreachable at ${gatewayUrl} (${health.error ?? `HTTP ${health.status}`})`
3330
- });
3331
- } else {
3332
- gatewayUp = true;
3333
- const ready = await fetchJson(fetchImpl, `${gatewayUrl}/ready`, CHECK_TIMEOUT_MS);
3334
- checks.push(
3335
- ready.ok ? { name: "Gateway", status: "PASS", detail: `healthy and ready at ${gatewayUrl}` } : {
3336
- name: "Gateway",
3337
- status: "WARN",
3338
- detail: `healthy but not ready at ${gatewayUrl} (${ready.error ?? `HTTP ${ready.status}`})`
3339
- }
3340
- );
3341
- }
3342
- if (config === void 0) {
3343
- checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
3344
- } else if (config.resources.length === 0) {
3345
- checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
3346
- } else {
3347
- const resources = config.resources;
3348
- const urls = [...new Set(resources.map((r) => substitutePathParams(r.handler.url)))];
3349
- const results = await Promise.all(
3350
- urls.map((url) => fetchJson(fetchImpl, url, CHECK_TIMEOUT_MS))
3351
- );
3352
- const reachable = results.filter((r) => r.status !== 0).length;
3353
- const status = reachable === urls.length ? "PASS" : reachable === 0 ? "FAIL" : "WARN";
3354
- checks.push({
3355
- name: "Backend",
3356
- status,
3357
- detail: `${reachable}/${urls.length} backend host(s) reachable`
3358
- });
3359
- }
3360
- const wellKnown = gatewayUp ? await fetchJson(
3361
- fetchImpl,
3362
- `${gatewayUrl}/.well-known/agent-commerce`,
3363
- CHECK_TIMEOUT_MS
3364
- ) : void 0;
3365
- if (config === void 0) {
3366
- checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
3367
- } else if (wellKnown === void 0 || !wellKnown.ok) {
3368
- checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
3369
- } else {
3370
- const mcpMountPath = config.protocols.mcp.enabled ? config.protocols.mcp.mountPath : void 0;
3371
- const a2aMountPath = config.protocols.a2a.enabled ? config.protocols.a2a.mountPath : void 0;
3372
- checks.push({
3373
- name: "Protocols",
3374
- status: "PASS",
3375
- detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"} acp=${config.protocols.acp.enabled ? `on (${config.protocols.acp.mountPath})` : "off"}`
3376
- });
3377
- }
3378
307
  if (config === void 0) {
3379
- checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
3380
- } else if (!config.protocols.a2a.enabled) {
3381
- checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
3382
- } else {
3383
- checks.push({
3384
- name: "A2A",
3385
- status: "PASS",
3386
- detail: `experimental \xB7 spec ${A2A_SPEC_VERSION} \xB7 protocol ${A2A_PROTOCOL_VERSION} \xB7 binding ${A2A_PROTOCOL_BINDING} \xB7 mount ${config.protocols.a2a.mountPath} \xB7 card ${A2A_AGENT_CARD_PATH}`
3387
- });
308
+ checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
309
+ } else if (
310
+ // Diagnose without creating: createSqliteReceiptStore creates the file,
311
+ // so opening a wrong path (a typo, or a container path read from the
312
+ // host) would leave an empty database and report it healthy, hiding the
313
+ // real one. `:memory:` never exists on disk, so it takes the open branch
314
+ // below.
315
+ config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
316
+ ) {
3388
317
  checks.push({
3389
- name: "A2A unsupported",
3390
- status: "INFO",
3391
- detail: A2A_UNSUPPORTED.join(", ")
318
+ name: "Storage",
319
+ status: "WARN",
320
+ detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
3392
321
  });
3393
- }
3394
- if (config === void 0) {
3395
- checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
3396
- } else if (!config.protocols.acp.enabled) {
3397
- checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
3398
322
  } else {
3399
- const acp = config.protocols.acp;
3400
- checks.push({
3401
- name: "ACP",
3402
- status: "PASS",
3403
- detail: `experimental \xB7 spec ${ACP_SPEC_VERSION} \xB7 API-Version ${ACP_API_VERSION} \xB7 service checkout \xB7 mount ${acp.mountPath} \xB7 discovery ${ACP_WELL_KNOWN_PATH}`
3404
- });
3405
- checks.push({
3406
- name: "ACP auth",
3407
- status: "PASS",
3408
- detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
3409
- });
3410
- checks.push(
3411
- storeFileCheck(
3412
- "ACP idempotency",
3413
- acp.idempotency.path,
3414
- {
3415
- inMemory: "replay protection is lost on every restart",
3416
- unwritable: "the adapter cannot claim idempotency keys"
3417
- },
3418
- ` (retention ${acp.idempotency.retentionHours}h)`
3419
- )
3420
- );
3421
- checks.push(acpMappingCheck(config, acp.checkout.operations));
3422
- checks.push({
3423
- name: "ACP unsupported",
3424
- status: "INFO",
3425
- detail: ACP_UNSUPPORTED.join(", ")
3426
- });
323
+ let store;
324
+ try {
325
+ store = createStore(config.storage.receipts.path);
326
+ await store.init();
327
+ const storeHealth = await store.health();
328
+ let receiptCount;
329
+ try {
330
+ receiptCount = await store.countReceipts();
331
+ } catch {
332
+ receiptCount = void 0;
333
+ }
334
+ let undeliveredCount;
335
+ try {
336
+ undeliveredCount = await store.countUndeliveredReceipts();
337
+ } catch {
338
+ undeliveredCount = void 0;
339
+ }
340
+ const status = storeHealth.status === "pass" ? "PASS" : storeHealth.status === "warn" ? "WARN" : "FAIL";
341
+ const receiptsSuffix = receiptCount !== void 0 ? `; receipts=${receiptCount}${undeliveredCount !== void 0 && undeliveredCount > 0 ? ` (${undeliveredCount} undelivered)` : ""}` : "";
342
+ checks.push({
343
+ name: "Storage",
344
+ status,
345
+ detail: `${storeHealth.detail ?? "sqlite"}${receiptsSuffix}`
346
+ });
347
+ } catch (err) {
348
+ checks.push({
349
+ name: "Storage",
350
+ status: "FAIL",
351
+ detail: err instanceof Error ? err.message : String(err)
352
+ });
353
+ } finally {
354
+ await store?.close();
355
+ }
3427
356
  }
357
+ return checks;
358
+ }
359
+
360
+ // src/cli/commands/doctor/ap2.ts
361
+ function describeIssuers(label, issuers) {
362
+ const described = issuers.map(
363
+ (entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
364
+ ).join(", ");
365
+ return `${label} issuers: ${described}`;
366
+ }
367
+ function ap2Checks(config) {
368
+ const checks = [];
3428
369
  const ap2 = config?.authorization?.ap2;
3429
370
  if (config === void 0) {
3430
371
  checks.push({ name: "AP2", status: "WARN", detail: "skipped: config invalid" });
@@ -3461,19 +402,331 @@ async function runDoctor(options, deps = {}) {
3461
402
  detail: AP2_UNSUPPORTED.join(", ")
3462
403
  });
3463
404
  }
405
+ return checks;
406
+ }
407
+ var LOCAL_DEV_MERCHANT_ADDRESS = ANVIL_WELL_KNOWN_ACCOUNTS[1].address;
408
+ var LOCAL_DEV_FACILITATOR_PRIVATE_KEY = ANVIL_WELL_KNOWN_ACCOUNTS[0].privateKey;
409
+ var PLACEHOLDER_ASSET_ADDRESS = "0x000000000000000000000000000000000000dEaD";
410
+ function defaultInitAnswers() {
411
+ return {
412
+ backendBaseUrl: "http://localhost:3000",
413
+ resources: ["weather", "report"],
414
+ protocols: ["http", "mcp"],
415
+ x402Enabled: true,
416
+ merchantPayTo: LOCAL_DEV_MERCHANT_ADDRESS
417
+ };
418
+ }
419
+ function buildResourcesMap(answers) {
420
+ const resources = {};
421
+ const expose = [...answers.protocols];
422
+ if (answers.resources.includes("weather")) {
423
+ resources.weather = {
424
+ name: "Get weather",
425
+ description: "Deterministic demo weather lookup (free resource).",
426
+ input: {
427
+ type: "object",
428
+ properties: { city: { type: "string" } },
429
+ required: ["city"],
430
+ additionalProperties: false
431
+ },
432
+ backend: {
433
+ type: "http",
434
+ method: "GET",
435
+ url: `${answers.backendBaseUrl}/api/weather/{city}`
436
+ },
437
+ pricing: { type: "free" },
438
+ expose
439
+ };
440
+ }
441
+ if (answers.resources.includes("report")) {
442
+ const paid = answers.x402Enabled;
443
+ resources.report = {
444
+ name: "Get market report",
445
+ description: "Deterministic demo premium market report.",
446
+ // No caller-supplied fields. Config loads a missing `input` as this same
447
+ // closed schema; writing it out shows that in the generated file
448
+ input: {
449
+ type: "object",
450
+ properties: {},
451
+ additionalProperties: false
452
+ },
453
+ backend: {
454
+ type: "http",
455
+ method: "GET",
456
+ url: `${answers.backendBaseUrl}/api/report`
457
+ },
458
+ pricing: paid ? { type: "fixed", amount: "0.01", currency: "USDC" } : { type: "free" },
459
+ expose,
460
+ ...paid ? { payments: ["x402"] } : {}
461
+ };
462
+ }
463
+ return resources;
464
+ }
465
+ function buildInitConfigObject(answers) {
466
+ return {
467
+ version: 1,
468
+ merchant: {
469
+ id: "demo-merchant",
470
+ name: "Demo Merchant",
471
+ publicBaseUrl: "http://localhost:8080"
472
+ },
473
+ server: { port: 8080, host: "127.0.0.1" },
474
+ storage: { receipts: { driver: "sqlite", path: "./data/receipts.db" } },
475
+ protocols: {
476
+ http: { enabled: answers.protocols.includes("http") },
477
+ mcp: { enabled: answers.protocols.includes("mcp"), mountPath: "/mcp" }
478
+ },
479
+ resources: buildResourcesMap(answers),
480
+ payments: answers.x402Enabled ? {
481
+ x402: {
482
+ enabled: true,
483
+ network: `eip155:${LOCAL_CHAIN_ID}`,
484
+ rpcUrl: "http://127.0.0.1:8545",
485
+ asset: PLACEHOLDER_ASSET_ADDRESS,
486
+ assetName: "MockUSDC",
487
+ assetVersion: "2",
488
+ assetDecimals: 6,
489
+ payTo: answers.merchantPayTo,
490
+ maxTimeoutSeconds: 60,
491
+ facilitator: { mode: "local", signerPrivateKey: LOCAL_DEV_FACILITATOR_PRIVATE_KEY }
492
+ }
493
+ } : {}
494
+ };
495
+ }
496
+ var HEADER = `# Generated by \`agent-commerce init\`.
497
+ #
498
+ # With x402 enabled, the facilitator signerPrivateKey is Anvil's public dev
499
+ # account #0 and the default payTo is account #1.
500
+ # LOCAL DEVELOPMENT ONLY - DO NOT FUND: never use them outside a local chain.
501
+ # payments.x402.asset is a placeholder, not a real contract. Before running
502
+ # against the local chain, run \`npm run chain:deploy\` and set it to the
503
+ # MockUSDC address in \`.deploy/local.json\`. Before pointing this at a funded
504
+ # network, set \`payments.x402.payTo\` to your own settlement address.
505
+ #
506
+ # server.host is loopback (127.0.0.1): binding every interface should be a
507
+ # deliberate edit.
508
+ #
509
+ # server.adminToken and server.allowedOrigins are left unset, so no secret
510
+ # lands in a file you might commit. Without a token the operator routes
511
+ # (/api/receipts, /api/events) return 404. They serve the merchant's commerce
512
+ # ledger: payer addresses, amounts and settlement hashes. To enable them, add
513
+ # these keys to the server block below:
514
+ # server:
515
+ # adminToken: \${ADMIN_TOKEN}
516
+ # allowedOrigins:
517
+ # - http://localhost:5173
518
+ # See SECURITY.md before exposing this gateway beyond localhost.
519
+ `;
520
+ function renderInitConfigYaml(answers) {
521
+ const body = stringify(buildInitConfigObject(answers), { indent: 2 });
522
+ return `${HEADER}
523
+ ${body}`;
524
+ }
525
+
526
+ // src/cli/lib/mask.ts
527
+ function maskMiddle(value, keep = 6) {
528
+ if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
529
+ return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
530
+ }
531
+
532
+ // src/cli/commands/doctor/payments.ts
533
+ function describePaymentFlow(rail, defaultFlow) {
534
+ const overrides = Object.keys(rail.resourcePaymentFlows ?? {}).length;
535
+ const overrideCount = overrides > 0 ? ` (${overrides} resource ${overrides === 1 ? "override" : "overrides"})` : "";
536
+ return `paymentFlow=${rail.paymentFlow ?? defaultFlow}${overrideCount}`;
537
+ }
538
+ function readOffer(rec, rail) {
539
+ const { asset, network, assetDecimals, assetTransferMethod, status } = rec;
540
+ const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
541
+ if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
542
+ return void 0;
543
+ }
544
+ return {
545
+ network,
546
+ asset,
547
+ payTo,
548
+ ...typeof assetDecimals === "number" ? { assetDecimals } : {},
549
+ ...typeof assetTransferMethod === "string" ? { assetTransferMethod } : {},
550
+ ...typeof status === "string" ? { status } : {}
551
+ };
552
+ }
553
+ function readOffers(value, rail) {
554
+ if (!Array.isArray(value)) return void 0;
555
+ const read = value.map((entry) => isRecord(entry) ? readOffer(entry, rail) : void 0);
556
+ return read.some((offer) => offer === void 0) ? void 0 : read;
557
+ }
558
+ function extractLiveRail(body, rail = "x402") {
559
+ const block = isRecord(body?.["payments"]) ? body["payments"][rail] : void 0;
560
+ if (!isRecord(block)) return void 0;
561
+ if (block["enabled"] === false) return "disabled";
562
+ if (block["enabled"] !== true) return void 0;
563
+ const first = readOffer(block, rail);
564
+ if (first === void 0) return void 0;
565
+ let offers = [first];
566
+ if (block["options"] !== void 0) {
567
+ const listed = readOffers(block["options"], rail);
568
+ if (listed === void 0) return void 0;
569
+ offers = listed;
570
+ }
571
+ let resourceOffers;
572
+ const ownLists = block["resourceOptions"];
573
+ if (ownLists !== void 0) {
574
+ if (!isRecord(ownLists)) return void 0;
575
+ resourceOffers = {};
576
+ for (const [id, list] of Object.entries(ownLists)) {
577
+ const read = readOffers(list, rail);
578
+ if (read === void 0) return void 0;
579
+ resourceOffers[id] = read;
580
+ }
581
+ }
582
+ const { paymentFlow, resourcePaymentFlows } = block;
583
+ return {
584
+ offers,
585
+ ...resourceOffers !== void 0 ? { resourceOffers } : {},
586
+ ...typeof paymentFlow === "string" ? { paymentFlow } : {},
587
+ ...isRecord(resourcePaymentFlows) ? {
588
+ resourcePaymentFlows: Object.fromEntries(
589
+ Object.entries(resourcePaymentFlows).map(([id, flow]) => [id, String(flow)])
590
+ )
591
+ } : {}
592
+ };
593
+ }
594
+ function describeOverrides(flows) {
595
+ const entries = Object.entries(flows ?? {}).sort(([a], [b]) => a.localeCompare(b));
596
+ return entries.length > 0 ? entries.map(([id, flow]) => `${id}=${flow}`).join(", ") : "none";
597
+ }
598
+ function findRailMismatch(configured, live, payToField = "payTo", customNetworks) {
599
+ const diffs = [];
600
+ const differ = (field, liveValue, localValue) => diffs.push(
601
+ `${field}: gateway is using ${liveValue} but local config resolves to ${localValue}`
602
+ );
603
+ const compareOffers = (label, localOffers, liveOffers) => {
604
+ if (liveOffers.length !== localOffers.length) {
605
+ differ(label, `${liveOffers.length} offer(s)`, localOffers.length);
606
+ return;
607
+ }
608
+ localOffers.forEach((local, index) => {
609
+ const remote = liveOffers[index];
610
+ const prefix = label !== "options" ? `${label}[${index}] ` : localOffers.length > 1 ? `option ${index} ` : "";
611
+ const profile = findX402Network(local.network, customNetworks);
612
+ const same = (a, b) => profile ? sameAddress(profile, a, b) : a === b;
613
+ if (local.network !== remote.network) {
614
+ differ(`${prefix}network`, `"${remote.network}"`, `"${local.network}"`);
615
+ }
616
+ if (!same(local.asset, remote.asset)) differ(`${prefix}asset`, remote.asset, local.asset);
617
+ if (!same(local.payTo, remote.payTo)) {
618
+ differ(`${prefix}${payToField}`, remote.payTo, local.payTo);
619
+ }
620
+ if (local.assetDecimals !== void 0 && remote.assetDecimals !== void 0 && local.assetDecimals !== remote.assetDecimals) {
621
+ differ(`${prefix}assetDecimals`, remote.assetDecimals, local.assetDecimals);
622
+ }
623
+ if (local.assetTransferMethod !== remote.assetTransferMethod) {
624
+ differ(
625
+ `${prefix}assetTransferMethod`,
626
+ remote.assetTransferMethod ?? "eip3009",
627
+ local.assetTransferMethod ?? "eip3009"
628
+ );
629
+ }
630
+ });
631
+ };
632
+ compareOffers("options", configured.offers, live.offers);
633
+ const localLists = configured.resourceOffers ?? {};
634
+ const liveLists = live.resourceOffers ?? {};
635
+ for (const id of [.../* @__PURE__ */ new Set([...Object.keys(localLists), ...Object.keys(liveLists)])].sort()) {
636
+ compareOffers(`resources.${id}.x402.accepts`, localLists[id] ?? [], liveLists[id] ?? []);
637
+ }
638
+ if (live.paymentFlow !== void 0) {
639
+ if (live.paymentFlow !== configured.paymentFlow) {
640
+ differ("paymentFlow", live.paymentFlow, configured.paymentFlow);
641
+ }
642
+ const liveOverrides = describeOverrides(live.resourcePaymentFlows);
643
+ const localOverrides = describeOverrides(configured.resourcePaymentFlows);
644
+ if (liveOverrides !== localOverrides)
645
+ differ("resourcePaymentFlows", liveOverrides, localOverrides);
646
+ }
647
+ return diffs.length === 0 ? void 0 : diffs.join("; ");
648
+ }
649
+ function describeSettlement(network, facilitator, customNetworks) {
650
+ const profile = findX402Network(network, customNetworks);
651
+ const mode = profile ? resolveDeploymentMode(profile) : void 0;
652
+ const where = profile === void 0 || mode === void 0 ? `unknown network ${network}` : mode === "local" ? `LOCAL dev chain (${network})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${network})`;
653
+ const via = facilitator.mode === "local" ? "local (in-process)" : `remote (auth=${facilitator.auth.type})`;
654
+ return { mode, where, via };
655
+ }
656
+ function mainnetSafetyCheck(name, payToField, facilitator, networks) {
657
+ const unauthenticated = facilitator.mode === "remote" && facilitator.auth.type === "none";
658
+ const where = `${describeDeploymentMode("mainnet")} on ${networks.join(", ")}`;
659
+ return {
660
+ name,
661
+ status: unauthenticated ? "WARN" : "INFO",
662
+ detail: unauthenticated ? `${where}: no credential is sent to the remote facilitator (accepted via allowUnauthenticatedFacilitator); its anonymous-access limits apply. Config load also requires allowMainnet, HTTPS, a non-development ${payToField} and the canonical asset. Payments fail closed.` : `${where}. Enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development ${payToField}, and the canonical asset of each network. Payments fail closed.`
663
+ };
664
+ }
665
+ function railOffer(option) {
666
+ const { network, asset, payTo, assetDecimals, assetTransferMethod } = option;
667
+ return {
668
+ network,
669
+ asset,
670
+ payTo,
671
+ assetDecimals,
672
+ ...assetTransferMethod !== void 0 ? { assetTransferMethod } : {}
673
+ };
674
+ }
675
+ function offerHealthCheck(live, named) {
676
+ const all = [...live.offers, ...Object.values(live.resourceOffers ?? {}).flat()];
677
+ const offers = [
678
+ ...new Map(
679
+ all.map((offer) => [`${offer.network} ${offer.asset} ${offer.payTo}`, offer])
680
+ ).values()
681
+ ];
682
+ const reported = offers.filter((offer) => offer.status !== void 0);
683
+ if (reported.length === 0) return void 0;
684
+ const failing = reported.filter((offer) => offer.status === "fail");
685
+ const degraded = reported.filter((offer) => offer.status === "warn");
686
+ const describe2 = (list) => list.map(
687
+ (offer) => `${named(offer.network)} ${maskMiddle(offer.asset)} to ${maskMiddle(offer.payTo)}`
688
+ ).join("; ");
689
+ if (failing.length === 0 && degraded.length === 0) {
690
+ return {
691
+ name: "Payments (offers)",
692
+ status: "PASS",
693
+ detail: `${reported.length} offer(s) pass their health checks`
694
+ };
695
+ }
696
+ const problems = [
697
+ ...failing.length > 0 ? [`failing: ${describe2(failing)}`] : [],
698
+ ...degraded.length > 0 ? [`degraded: ${describe2(degraded)}`] : []
699
+ ];
700
+ return {
701
+ name: "Payments (offers)",
702
+ status: failing.length > 0 ? "FAIL" : "WARN",
703
+ detail: `${problems.join("; ")}. The gateway logs each reason at debug level`
704
+ };
705
+ }
706
+ function paymentChecks(config, wellKnown, gatewayUrl, chainDeploy = false) {
707
+ const checks = [];
708
+ const stale = `; the gateway may be running against an older deployment - restart it${chainDeploy ? " or re-run chain:deploy" : ""}`;
3464
709
  const x402 = config?.payments.x402;
3465
710
  if (x402 === void 0 || !x402.enabled) {
3466
711
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
3467
712
  } else {
3468
- const { mode, where, via } = describeSettlement(x402.network, x402.facilitator);
3469
- const flow = describePaymentFlow(x402);
3470
- const summary = `x402 v2 (scheme=exact) enabled - ${where}, destination=${maskMiddle(x402.payTo)}, facilitator=${via}, ${flow}`;
3471
- const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
3472
- if (sameAddress2(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
713
+ const options = x402.options ?? [x402];
714
+ const resourceOptions = x402.resourceOptions ?? {};
715
+ const everyOption = [...options, ...Object.values(resourceOptions).flat()];
716
+ const { mode, where, via } = describeSettlement(x402.network, x402.facilitator, x402.networks);
717
+ const flow = describePaymentFlow(x402, X402_DEFAULT_PAYMENT_FLOW);
718
+ const named = (network) => `${findX402Network(network, x402.networks)?.displayName ?? "unknown network"} (${network})`;
719
+ const terms = options.length === 1 ? `${where}, destination=${maskMiddle(x402.payTo)}` : `${mode !== void 0 ? describeDeploymentMode(mode) : "unknown mode"}, ${options.length} options: ` + options.map((option) => `${named(option.network)} to ${maskMiddle(option.payTo)}`).join("; ");
720
+ const ownCount = Object.keys(resourceOptions).length;
721
+ const own = ownCount > 0 ? `, ${ownCount} resource${ownCount === 1 ? "" : "s"} with ${ownCount === 1 ? "its" : "their"} own offers` : "";
722
+ const summary = `x402 v2 (scheme=exact) enabled - ${terms}${own}, facilitator=${via}, ${flow}`;
723
+ const live = wellKnown?.ok ? extractLiveRail(wellKnown.body) : void 0;
724
+ const placeholder = PLACEHOLDER_ASSET_ADDRESS.toLowerCase();
725
+ if (everyOption.some((option) => option.asset.toLowerCase() === placeholder)) {
3473
726
  checks.push({
3474
727
  name: "Payments",
3475
728
  status: "WARN",
3476
- detail: `${summary}, but the asset is still the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)}. No token contract exists there, so every paid call will fail. Set payments.x402.asset (\${X402_ASSET} is filled from .deploy/local.json, which "npm run chain:deploy" writes)`
729
+ detail: `${summary}, but at least one offer uses the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)}. Set that offer's asset to its token contract address${chainDeploy ? ' (${X402_ASSET} is filled from .deploy/local.json, which "npm run chain:deploy" writes)' : ""}`
3477
730
  });
3478
731
  } else if (live === "disabled") {
3479
732
  checks.push({
@@ -3488,13 +741,29 @@ async function runDoctor(options, deps = {}) {
3488
741
  detail: `${summary} (gateway's live settlement config could not be verified)`
3489
742
  });
3490
743
  } else {
3491
- const mismatch = findX402Mismatch(x402, live);
744
+ const configured = {
745
+ offers: options.map(railOffer),
746
+ ...ownCount > 0 ? {
747
+ resourceOffers: Object.fromEntries(
748
+ Object.entries(resourceOptions).map(([id, list]) => [id, list.map(railOffer)])
749
+ )
750
+ } : {},
751
+ paymentFlow: x402.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
752
+ ...x402.resourcePaymentFlows !== void 0 ? { resourcePaymentFlows: x402.resourcePaymentFlows } : {}
753
+ };
754
+ const mismatch = findRailMismatch(configured, live, "payTo", x402.networks);
3492
755
  checks.push(
3493
- mismatch === void 0 ? { name: "Payments", status: "PASS", detail: summary } : { name: "Payments", status: "FAIL", detail: mismatch }
756
+ mismatch === void 0 ? { name: "Payments", status: "PASS", detail: summary } : { name: "Payments", status: "FAIL", detail: `${mismatch}${stale}` }
3494
757
  );
758
+ const offerHealth = offerHealthCheck(live, named);
759
+ if (offerHealth !== void 0) checks.push(offerHealth);
3495
760
  }
3496
761
  if (mode === "mainnet") {
3497
- checks.push(mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator));
762
+ checks.push(
763
+ mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator, [
764
+ ...new Set(everyOption.map((option) => named(option.network)))
765
+ ])
766
+ );
3498
767
  }
3499
768
  }
3500
769
  const mpp = config?.payments.mpp;
@@ -3502,99 +771,141 @@ async function runDoctor(options, deps = {}) {
3502
771
  checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
3503
772
  } else {
3504
773
  const { mode, where, via } = describeSettlement(mpp.network, mpp.facilitator);
3505
- const summary = `MPP ${MPP_PROFILE.intent}/${MPP_PROFILE.method}/${MPP_PROFILE.credentialType} enabled - ${where}, asset=${MPP_PROFILE.assetSymbol} ${maskMiddle(mpp.asset)}, recipient=${maskMiddle(mpp.recipient)}, facilitator=${via}, ${describePaymentFlow(mpp)}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
3506
- const configured = { asset: mpp.asset, network: mpp.network, payTo: mpp.recipient };
3507
- const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body, "mpp") : void 0;
3508
- if (live === "disabled") {
3509
- checks.push({
3510
- name: "Payments (MPP)",
3511
- status: "FAIL",
3512
- detail: `Local config enables MPP, but ${gatewayUrl} reports it disabled`
3513
- });
3514
- } else if (live === void 0) {
3515
- checks.push({
3516
- name: "Payments (MPP)",
3517
- status: "INFO",
3518
- detail: `${summary} (live MPP settlement config unavailable for comparison)`
3519
- });
3520
- } else {
3521
- const mismatch = findX402Mismatch(configured, live, "recipient");
3522
- checks.push(
3523
- mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
3524
- );
3525
- }
3526
- if (mode === "mainnet") {
3527
- checks.push(mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator));
3528
- }
3529
- }
3530
- if (config === void 0) {
3531
- checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
3532
- } else if (
3533
- // Diagnose without creating: createSqliteReceiptStore creates the file,
3534
- // so opening a wrong path (a typo, or a container path read from the
3535
- // host) would leave an empty database and report it healthy, hiding the
3536
- // real one. `:memory:` never exists on disk, so it takes the open branch
3537
- // below.
3538
- config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
3539
- ) {
3540
- checks.push({
3541
- name: "Storage",
3542
- status: "WARN",
3543
- detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
3544
- });
3545
- } else {
3546
- let store;
3547
- try {
3548
- store = createStore(config.storage.receipts.path);
3549
- await store.init();
3550
- const storeHealth = await store.health();
3551
- let receiptCount;
3552
- try {
3553
- receiptCount = await store.countReceipts();
3554
- } catch {
3555
- receiptCount = void 0;
3556
- }
3557
- let undeliveredCount;
3558
- try {
3559
- undeliveredCount = await store.countUndeliveredReceipts();
3560
- } catch {
3561
- undeliveredCount = void 0;
3562
- }
3563
- const status = storeHealth.status === "pass" ? "PASS" : storeHealth.status === "warn" ? "WARN" : "FAIL";
3564
- const receiptsSuffix = receiptCount !== void 0 ? `; receipts=${receiptCount}${undeliveredCount !== void 0 && undeliveredCount > 0 ? ` (${undeliveredCount} undelivered)` : ""}` : "";
774
+ const summary = `MPP ${MPP_PROFILE.intent}/${MPP_PROFILE.method}/${MPP_PROFILE.credentialType} enabled - ${where}, asset=${MPP_PROFILE.assetSymbol} ${maskMiddle(mpp.asset)}, recipient=${maskMiddle(mpp.recipient)}, facilitator=${via}, ${describePaymentFlow(mpp, MPP_DEFAULT_PAYMENT_FLOW)}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
775
+ const configured = {
776
+ offers: [
777
+ {
778
+ network: mpp.network,
779
+ asset: mpp.asset,
780
+ payTo: mpp.recipient,
781
+ assetDecimals: MPP_PROFILE.assetDecimals
782
+ }
783
+ ],
784
+ paymentFlow: mpp.paymentFlow ?? MPP_DEFAULT_PAYMENT_FLOW,
785
+ ...mpp.resourcePaymentFlows !== void 0 ? { resourcePaymentFlows: mpp.resourcePaymentFlows } : {}
786
+ };
787
+ const live = wellKnown?.ok ? extractLiveRail(wellKnown.body, "mpp") : void 0;
788
+ if (live === "disabled") {
3565
789
  checks.push({
3566
- name: "Storage",
3567
- status,
3568
- detail: `${storeHealth.detail ?? "sqlite"}${receiptsSuffix}`
790
+ name: "Payments (MPP)",
791
+ status: "FAIL",
792
+ detail: `Local config enables MPP, but ${gatewayUrl} reports it disabled`
3569
793
  });
3570
- } catch (err) {
794
+ } else if (live === void 0) {
3571
795
  checks.push({
3572
- name: "Storage",
3573
- status: "FAIL",
3574
- detail: err instanceof Error ? err.message : String(err)
796
+ name: "Payments (MPP)",
797
+ status: "INFO",
798
+ detail: `${summary} (live MPP settlement config unavailable for comparison)`
3575
799
  });
3576
- } finally {
3577
- await store?.close();
800
+ } else {
801
+ const mismatch = findRailMismatch(configured, live, "recipient");
802
+ checks.push(
803
+ mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: `${mismatch}${stale}` }
804
+ );
805
+ }
806
+ if (mode === "mainnet") {
807
+ checks.push(
808
+ mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator, [
809
+ `${findX402Network(mpp.network)?.displayName ?? "unknown network"} (${mpp.network})`
810
+ ])
811
+ );
3578
812
  }
3579
813
  }
3580
- if (wellKnown?.ok) {
814
+ return checks;
815
+ }
816
+
817
+ // src/cli/commands/doctor/protocols.ts
818
+ function acpMappingCheck(config, operations) {
819
+ const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
820
+ const problems = [];
821
+ for (const [operation, resourceId] of Object.entries(operations)) {
822
+ const resource = byId.get(resourceId);
823
+ if (resource === void 0) {
824
+ problems.push(`${operation} -> "${resourceId}" does not exist`);
825
+ continue;
826
+ }
827
+ if (!resource.exposedVia.includes("acp")) {
828
+ problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
829
+ }
830
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
831
+ problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
832
+ }
833
+ }
834
+ if (problems.length > 0) {
835
+ return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
836
+ }
837
+ return {
838
+ name: "ACP checkout mapping",
839
+ status: "PASS",
840
+ detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
841
+ };
842
+ }
843
+ function protocolChecks(config, wellKnown) {
844
+ const checks = [];
845
+ if (config === void 0) {
846
+ checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
847
+ } else if (wellKnown === void 0 || !wellKnown.ok) {
848
+ checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
849
+ } else {
850
+ const mcpMountPath = config.protocols.mcp.enabled ? config.protocols.mcp.mountPath : void 0;
851
+ const a2aMountPath = config.protocols.a2a.enabled ? config.protocols.a2a.mountPath : void 0;
3581
852
  checks.push({
3582
- name: "Protocol versions",
853
+ name: "Protocols",
3583
854
  status: "PASS",
3584
- detail: "reported by gateway /.well-known/agent-commerce"
855
+ detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"} acp=${config.protocols.acp.enabled ? `on (${config.protocols.acp.mountPath})` : "off"}`
3585
856
  });
857
+ }
858
+ if (config === void 0) {
859
+ checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
860
+ } else if (!config.protocols.a2a.enabled) {
861
+ checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
3586
862
  } else {
3587
- const local = readVersionReport();
3588
863
  checks.push({
3589
- name: "Protocol versions",
864
+ name: "A2A",
865
+ status: "PASS",
866
+ detail: `experimental \xB7 spec ${A2A_SPEC_VERSION} \xB7 protocol ${A2A_PROTOCOL_VERSION} \xB7 binding ${A2A_PROTOCOL_BINDING} \xB7 mount ${config.protocols.a2a.mountPath} \xB7 card ${A2A_AGENT_CARD_PATH}`
867
+ });
868
+ checks.push({
869
+ name: "A2A unsupported",
3590
870
  status: "INFO",
3591
- detail: `gateway unreachable; local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
871
+ detail: A2A_UNSUPPORTED.join(", ")
3592
872
  });
3593
873
  }
3594
- const scored = checks.filter((c) => c.status !== "INFO");
3595
- const passed = scored.filter((c) => c.status === "PASS").length;
3596
- const exitCode = checks.some((c) => c.status === "FAIL") ? 1 : 0;
3597
- return { checks, score: { passed, total: scored.length }, exitCode };
874
+ if (config === void 0) {
875
+ checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
876
+ } else if (!config.protocols.acp.enabled) {
877
+ checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
878
+ } else {
879
+ const acp = config.protocols.acp;
880
+ checks.push({
881
+ name: "ACP",
882
+ status: "PASS",
883
+ detail: `experimental \xB7 spec ${ACP_SPEC_VERSION} \xB7 API-Version ${ACP_API_VERSION} \xB7 service checkout \xB7 mount ${acp.mountPath} \xB7 discovery ${ACP_WELL_KNOWN_PATH}`
884
+ });
885
+ checks.push({
886
+ name: "ACP auth",
887
+ status: "PASS",
888
+ detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
889
+ });
890
+ checks.push(
891
+ storeFileCheck(
892
+ "ACP idempotency",
893
+ acp.idempotency.path,
894
+ {
895
+ inMemory: "replay protection is lost on every restart",
896
+ unwritable: "the adapter cannot claim idempotency keys"
897
+ },
898
+ ` (retention ${acp.idempotency.retentionHours}h)`
899
+ )
900
+ );
901
+ checks.push(acpMappingCheck(config, acp.checkout.operations));
902
+ checks.push({
903
+ name: "ACP unsupported",
904
+ status: "INFO",
905
+ detail: ACP_UNSUPPORTED.join(", ")
906
+ });
907
+ }
908
+ return checks;
3598
909
  }
3599
910
  function statusColor(status, text2) {
3600
911
  switch (status) {
@@ -3621,17 +932,122 @@ function printDoctorReport(report, io, json) {
3621
932
  io.stdout(`Score: ${report.score.passed}/${report.score.total} checks passed`);
3622
933
  }
3623
934
 
3624
- // src/cli/commands/import-openapi.ts
3625
- init_core();
3626
-
3627
- // src/openapi/discover.ts
3628
- init_errors();
3629
- init_execution();
3630
- init_is_record();
3631
- init_constants4();
935
+ // src/cli/commands/doctor/index.ts
936
+ var CHECK_TIMEOUT_MS = 1500;
937
+ function deriveGatewayUrl(explicit, config) {
938
+ if (explicit !== void 0) return explicit.replace(/\/$/, "");
939
+ if (config !== void 0) {
940
+ const raw = config.server.host;
941
+ const host = raw === "0.0.0.0" ? "127.0.0.1" : raw === "::" ? "::1" : raw;
942
+ const authority = host.includes(":") ? `[${host}]` : host;
943
+ return `http://${authority}:${config.server.port}`;
944
+ }
945
+ return "http://localhost:8080";
946
+ }
947
+ function substitutePathParams(url) {
948
+ let filled = url;
949
+ for (const name of extractPathParameterNames(url)) {
950
+ filled = filled.replaceAll(`{${name}}`, "demo-check");
951
+ }
952
+ return filled;
953
+ }
954
+ async function runDoctor(options, deps = {}) {
955
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
956
+ const loadConfig = deps.loadConfig ?? loadConfigDynamic;
957
+ const createStore = deps.createStore ?? ((path) => createSqliteReceiptStore({ path }));
958
+ const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
959
+ const chainDeploy = (deps.hasChainDeployScript ?? hasChainDeployScript)();
960
+ const checks = [];
961
+ let config;
962
+ const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
963
+ const filledSuffix = filled.length > 0 ? ` (using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")})` : "";
964
+ try {
965
+ config = await loadConfig({
966
+ ...options.configPath !== void 0 ? { path: options.configPath } : {},
967
+ env
968
+ });
969
+ checks.push({
970
+ name: "Config",
971
+ status: "PASS",
972
+ detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
973
+ });
974
+ } catch (err) {
975
+ const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
976
+ const hint = typeof variable === "string" ? `. Hint: ${unresolvedVariableHint(variable, manifestFound, chainDeploy)}` : "";
977
+ checks.push({
978
+ name: "Config",
979
+ status: "FAIL",
980
+ detail: `${err instanceof Error ? err.message : String(err)}${hint}`
981
+ });
982
+ }
983
+ const gatewayUrl = deriveGatewayUrl(options.gatewayUrl, config);
984
+ const health = await fetchJson(fetchImpl, `${gatewayUrl}/health`, CHECK_TIMEOUT_MS);
985
+ let gatewayUp = false;
986
+ if (!health.ok) {
987
+ checks.push({
988
+ name: "Gateway",
989
+ status: "FAIL",
990
+ detail: `unreachable at ${gatewayUrl} (${health.error ?? `HTTP ${health.status}`})`
991
+ });
992
+ } else {
993
+ gatewayUp = true;
994
+ const ready = await fetchJson(fetchImpl, `${gatewayUrl}/ready`, CHECK_TIMEOUT_MS);
995
+ checks.push(
996
+ ready.ok ? { name: "Gateway", status: "PASS", detail: `healthy and ready at ${gatewayUrl}` } : {
997
+ name: "Gateway",
998
+ status: "WARN",
999
+ detail: `healthy but not ready at ${gatewayUrl} (${ready.error ?? `HTTP ${ready.status}`})`
1000
+ }
1001
+ );
1002
+ }
1003
+ if (config === void 0) {
1004
+ checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
1005
+ } else if (config.resources.length === 0) {
1006
+ checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
1007
+ } else {
1008
+ const resources = config.resources;
1009
+ const urls = [...new Set(resources.map((r) => substitutePathParams(r.handler.url)))];
1010
+ const results = await Promise.all(
1011
+ urls.map((url) => fetchJson(fetchImpl, url, CHECK_TIMEOUT_MS))
1012
+ );
1013
+ const reachable = results.filter((r) => r.status !== 0).length;
1014
+ const status = reachable === urls.length ? "PASS" : reachable === 0 ? "FAIL" : "WARN";
1015
+ checks.push({
1016
+ name: "Backend",
1017
+ status,
1018
+ detail: `${reachable}/${urls.length} backend host(s) reachable`
1019
+ });
1020
+ }
1021
+ const wellKnown = gatewayUp ? await fetchJson(
1022
+ fetchImpl,
1023
+ `${gatewayUrl}/.well-known/agent-commerce`,
1024
+ CHECK_TIMEOUT_MS
1025
+ ) : void 0;
1026
+ checks.push(...protocolChecks(config, wellKnown));
1027
+ checks.push(...ap2Checks(config));
1028
+ checks.push(...paymentChecks(config, wellKnown, gatewayUrl, chainDeploy));
1029
+ checks.push(...await storageChecks(config, createStore));
1030
+ if (wellKnown?.ok) {
1031
+ checks.push({
1032
+ name: "Protocol versions",
1033
+ status: "PASS",
1034
+ detail: "reported by gateway /.well-known/agent-commerce"
1035
+ });
1036
+ } else {
1037
+ const local = readVersionReport();
1038
+ checks.push({
1039
+ name: "Protocol versions",
1040
+ status: "INFO",
1041
+ detail: `gateway unreachable; local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
1042
+ });
1043
+ }
1044
+ const scored = checks.filter((c) => c.status !== "INFO");
1045
+ const passed = scored.filter((c) => c.status === "PASS").length;
1046
+ const exitCode = checks.some((c) => c.status === "FAIL") ? 1 : 0;
1047
+ return { checks, score: { passed, total: scored.length }, exitCode };
1048
+ }
3632
1049
 
3633
1050
  // src/openapi/refs.ts
3634
- init_errors();
3635
1051
  var MAX_REF_DEPTH = 100;
3636
1052
  function isRefNode(value) {
3637
1053
  return typeof value === "object" && value !== null && typeof value.$ref === "string";
@@ -3731,14 +1147,14 @@ function discoverOperations(loaded, options = {}) {
3731
1147
  }
3732
1148
  const rootServers = document["servers"];
3733
1149
  const rootSecurity = document["security"];
3734
- for (const [path2, rawPathItem] of Object.entries(paths)) {
3735
- if (path2.startsWith("x-")) continue;
3736
- if (!path2.startsWith("/")) {
1150
+ for (const [path, rawPathItem] of Object.entries(paths)) {
1151
+ if (path.startsWith("x-")) continue;
1152
+ if (!path.startsWith("/")) {
3737
1153
  diagnostics.push({
3738
1154
  severity: "warning",
3739
1155
  code: "invalid-path-key",
3740
- operation: path2,
3741
- message: `Skipped "${path2}": a Paths key must start with "/"`
1156
+ operation: path,
1157
+ message: `Skipped "${path}": a Paths key must start with "/"`
3742
1158
  });
3743
1159
  continue;
3744
1160
  }
@@ -3748,8 +1164,8 @@ function discoverOperations(loaded, options = {}) {
3748
1164
  diagnostics.push({
3749
1165
  severity: "warning",
3750
1166
  code: "unsupported-method",
3751
- operation: `${name.toUpperCase()} ${path2}`,
3752
- message: `Skipped ${name.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
1167
+ operation: `${name.toUpperCase()} ${path}`,
1168
+ message: `Skipped ${name.toUpperCase()} ${path}: only GET, POST, PUT, PATCH and DELETE are supported`
3753
1169
  });
3754
1170
  };
3755
1171
  for (const [key, rawOperation] of Object.entries(pathItem)) {
@@ -3766,9 +1182,9 @@ function discoverOperations(loaded, options = {}) {
3766
1182
  }
3767
1183
  const operation = dereference(document, rawOperation).value;
3768
1184
  if (!isRecord(operation)) continue;
3769
- const where = `${method} ${path2}`;
1185
+ const where = `${method} ${path}`;
3770
1186
  const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : void 0;
3771
- const resourceId = toResourceId(operationId, method, path2);
1187
+ const resourceId = toResourceId(operationId, method, path);
3772
1188
  const previous = claimed.get(resourceId);
3773
1189
  if (previous !== void 0) {
3774
1190
  throw new CommerceError(
@@ -3786,7 +1202,7 @@ function discoverOperations(loaded, options = {}) {
3786
1202
  diagnostics
3787
1203
  );
3788
1204
  if (server === void 0) continue;
3789
- const backendUrl = `${server.replace(/\/+$/, "")}${path2}`;
1205
+ const backendUrl = `${server.replace(/\/+$/, "")}${path}`;
3790
1206
  const stray = findUnparsedBraceToken(backendUrl);
3791
1207
  if (stray !== void 0) {
3792
1208
  diagnostics.push({
@@ -3803,7 +1219,7 @@ function discoverOperations(loaded, options = {}) {
3803
1219
  operations.push({
3804
1220
  resourceId,
3805
1221
  method,
3806
- path: path2,
1222
+ path,
3807
1223
  backendUrl,
3808
1224
  ...operationId !== void 0 ? { operationId } : {},
3809
1225
  tags: toArray(operation["tags"]).filter((tag) => typeof tag === "string"),
@@ -3823,10 +1239,10 @@ function discoverOperations(loaded, options = {}) {
3823
1239
  }
3824
1240
  return { operations, diagnostics };
3825
1241
  }
3826
- function toResourceId(operationId, method, path2) {
1242
+ function toResourceId(operationId, method, path) {
3827
1243
  const fromOperationId = operationId === void 0 ? "" : normalizeId(operationId);
3828
1244
  if (fromOperationId !== "") return fromOperationId;
3829
- return normalizeId(`${method.toLowerCase()}_${path2}`);
1245
+ return normalizeId(`${method.toLowerCase()}_${path}`);
3830
1246
  }
3831
1247
  function normalizeId(value) {
3832
1248
  return value.replace(ID_DISALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MCP_TOOL_NAME_MAX_LENGTH);
@@ -3930,17 +1346,6 @@ function assertAbsoluteHttpUrl(value, label) {
3930
1346
  function toArray(value) {
3931
1347
  return Array.isArray(value) ? value : [];
3932
1348
  }
3933
-
3934
- // src/openapi/draft.ts
3935
- init_is_record();
3936
-
3937
- // src/openapi/request.ts
3938
- init_execution();
3939
- init_is_record();
3940
-
3941
- // src/openapi/schema.ts
3942
- init_errors();
3943
- init_is_record();
3944
1349
  var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
3945
1350
  "object",
3946
1351
  "string",
@@ -3963,7 +1368,7 @@ var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
3963
1368
  "definitions"
3964
1369
  ]);
3965
1370
  function isPrimitiveSchema(schema) {
3966
- const types = typeList2(schema["type"]);
1371
+ const types = typeList(schema["type"]);
3967
1372
  if (types === void 0) return false;
3968
1373
  return types.every((type) => type !== "object" && type !== "array" && SUPPORTED_TYPES.has(type));
3969
1374
  }
@@ -4047,7 +1452,7 @@ function isCarriedKeyword(keyword) {
4047
1452
  return keyword === "type" || keyword === "properties" || keyword === "required" || keyword === "additionalProperties" || keyword === "enum" || keyword === "items" || keyword === "nullable" || keyword === "$ref" || METADATA_KEYWORDS.includes(keyword);
4048
1453
  }
4049
1454
  function resolveTypes(schema) {
4050
- const declared = typeList2(schema["type"]);
1455
+ const declared = typeList(schema["type"]);
4051
1456
  if (declared === void 0) return void 0;
4052
1457
  const known = declared.filter((type) => SUPPORTED_TYPES.has(type));
4053
1458
  if (known.length === 0) {
@@ -4056,7 +1461,7 @@ function resolveTypes(schema) {
4056
1461
  if (schema["nullable"] === true && !known.includes("null")) known.push("null");
4057
1462
  return known;
4058
1463
  }
4059
- function typeList2(raw) {
1464
+ function typeList(raw) {
4060
1465
  if (typeof raw === "string") return [raw];
4061
1466
  if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
4062
1467
  return raw;
@@ -4074,7 +1479,7 @@ function mergeAllOf(document, schema, stack, dropped) {
4074
1479
  const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
4075
1480
  for (const branch of parts) {
4076
1481
  const converted = convertNode(document, branch, stack, dropped);
4077
- const types = typeList2(converted["type"]);
1482
+ const types = typeList(converted["type"]);
4078
1483
  if (types !== void 0 && !types.includes("object")) {
4079
1484
  throw new UnsupportedSchema("allOf mixes object and non-object schemas");
4080
1485
  }
@@ -4507,13 +1912,10 @@ function renderResourcesYaml(result) {
4507
1912
  });
4508
1913
  return doc.toString({ lineWidth: 0 });
4509
1914
  }
4510
-
4511
- // src/openapi/load.ts
4512
- init_errors();
4513
1915
  var MAX_SOURCE_BYTES = 10 * 1024 * 1024;
4514
1916
  var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
4515
1917
  var SUPPORTED_VERSIONS = /* @__PURE__ */ new Set(["3.0", "3.1", "3.2"]);
4516
- function invalid2(message, details, cause) {
1918
+ function invalid(message, details, cause) {
4517
1919
  return new CommerceError("CONFIG_INVALID", message, {
4518
1920
  details,
4519
1921
  ...cause !== void 0 ? { cause } : {}
@@ -4534,10 +1936,10 @@ async function readCapped(sourcePath, details) {
4534
1936
  await handle.close();
4535
1937
  }
4536
1938
  } catch (error) {
4537
- throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
1939
+ throw invalid(`OpenAPI document "${sourcePath}" could not be read`, details, error);
4538
1940
  }
4539
1941
  if (length > MAX_SOURCE_BYTES) {
4540
- throw invalid2(`OpenAPI document "${sourcePath}" is over the ${MAX_SOURCE_BYTES}-byte limit`, {
1942
+ throw invalid(`OpenAPI document "${sourcePath}" is over the ${MAX_SOURCE_BYTES}-byte limit`, {
4541
1943
  ...details,
4542
1944
  maxBytes: MAX_SOURCE_BYTES
4543
1945
  });
@@ -4550,40 +1952,40 @@ async function loadOpenApiDocument(sourcePath) {
4550
1952
  try {
4551
1953
  stats = await stat(sourcePath);
4552
1954
  } catch (error) {
4553
- throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
1955
+ throw invalid(`OpenAPI document "${sourcePath}" could not be read`, details, error);
4554
1956
  }
4555
1957
  if (stats.isDirectory()) {
4556
- throw invalid2(`"${sourcePath}" is a directory, not an OpenAPI document`, details);
1958
+ throw invalid(`"${sourcePath}" is a directory, not an OpenAPI document`, details);
4557
1959
  }
4558
1960
  if (stats.size > MAX_SOURCE_BYTES) {
4559
- throw invalid2(
1961
+ throw invalid(
4560
1962
  `OpenAPI document "${sourcePath}" is ${stats.size} bytes, over the ${MAX_SOURCE_BYTES}-byte limit`,
4561
1963
  { ...details, size: stats.size, maxBytes: MAX_SOURCE_BYTES }
4562
1964
  );
4563
1965
  }
4564
1966
  const extension = extname(sourcePath).toLowerCase();
4565
1967
  if (!SUPPORTED_EXTENSIONS.has(extension)) {
4566
- throw invalid2(
1968
+ throw invalid(
4567
1969
  `OpenAPI document "${sourcePath}" has an unsupported extension "${extension || "(none)"}". Supported: .yaml, .yml, .json`,
4568
1970
  details
4569
1971
  );
4570
1972
  }
4571
1973
  const source = await readCapped(sourcePath, details);
4572
1974
  if (source.trim() === "") {
4573
- throw invalid2(`OpenAPI document "${sourcePath}" is empty`, details);
1975
+ throw invalid(`OpenAPI document "${sourcePath}" is empty`, details);
4574
1976
  }
4575
1977
  let parsed;
4576
1978
  try {
4577
1979
  parsed = extension === ".json" ? JSON.parse(source) : parse(source);
4578
1980
  } catch (error) {
4579
- throw invalid2(
1981
+ throw invalid(
4580
1982
  `OpenAPI document "${sourcePath}" is not valid ${extension === ".json" ? "JSON" : "YAML"}: ${error instanceof Error ? error.message : String(error)}`,
4581
1983
  details,
4582
1984
  error
4583
1985
  );
4584
1986
  }
4585
1987
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4586
- throw invalid2(`OpenAPI document "${sourcePath}" is not an object`, details);
1988
+ throw invalid(`OpenAPI document "${sourcePath}" is not an object`, details);
4587
1989
  }
4588
1990
  const document = parsed;
4589
1991
  const version = readVersion(document, sourcePath);
@@ -4591,7 +1993,7 @@ async function loadOpenApiDocument(sourcePath) {
4591
1993
  const result = await validate(document);
4592
1994
  if (!result.valid) {
4593
1995
  const first = result.errors?.[0];
4594
- throw invalid2(
1996
+ throw invalid(
4595
1997
  `OpenAPI document "${sourcePath}" is not a valid OpenAPI ${version} description: ${first ? `${first.message}${first.path ? ` (at ${first.path})` : ""}` : "unknown validation error"}`,
4596
1998
  { ...details, errors: result.errors?.slice(0, 10) ?? [] }
4597
1999
  );
@@ -4601,18 +2003,18 @@ async function loadOpenApiDocument(sourcePath) {
4601
2003
  function readVersion(document, sourcePath) {
4602
2004
  const details = { sourcePath };
4603
2005
  if (typeof document["swagger"] === "string") {
4604
- throw invalid2(
2006
+ throw invalid(
4605
2007
  `"${sourcePath}" is a Swagger ${document["swagger"]} document. Only OpenAPI 3.0, 3.1 and 3.2 are supported - convert it first`,
4606
2008
  details
4607
2009
  );
4608
2010
  }
4609
2011
  const declared = document["openapi"];
4610
2012
  if (typeof declared !== "string") {
4611
- throw invalid2(`"${sourcePath}" has no "openapi" version field`, details);
2013
+ throw invalid(`"${sourcePath}" has no "openapi" version field`, details);
4612
2014
  }
4613
2015
  const featureVersion = /^(\d+\.\d+)(?:\.|$)/.exec(declared)?.[1];
4614
2016
  if (featureVersion === void 0 || !SUPPORTED_VERSIONS.has(featureVersion)) {
4615
- throw invalid2(
2017
+ throw invalid(
4616
2018
  `OpenAPI version "${declared}" is not supported. Supported: 3.0.x, 3.1.x, 3.2.x`,
4617
2019
  { ...details, version: declared }
4618
2020
  );
@@ -4634,13 +2036,13 @@ function rejectUnsupportedReferences(document, sourcePath) {
4634
2036
  for (const [key, value] of Object.entries(node)) {
4635
2037
  if (key === "$ref" && typeof value === "string") {
4636
2038
  if (!value.startsWith("#")) {
4637
- throw invalid2(
2039
+ throw invalid(
4638
2040
  `OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
4639
2041
  { sourcePath, ref: value }
4640
2042
  );
4641
2043
  }
4642
2044
  if (!isPercentDecodable(value)) {
4643
- throw invalid2(
2045
+ throw invalid(
4644
2046
  `OpenAPI document "${sourcePath}" contains a reference "${value}" with a malformed percent-escape`,
4645
2047
  { sourcePath, ref: value }
4646
2048
  );
@@ -4811,22 +2213,16 @@ function describe(error) {
4811
2213
  return error instanceof Error ? error.message : String(error);
4812
2214
  }
4813
2215
 
4814
- // src/cli/commands/init.ts
4815
- init_filename();
4816
-
4817
2216
  // src/cli/commands/validate.ts
4818
- init_core();
4819
- function formatConfigError(err, manifestFound = false) {
2217
+ function formatConfigError(err, manifestFound = false, chainDeploy = hasChainDeployScript()) {
4820
2218
  if (isCommerceError(err)) {
4821
2219
  const lines = [`FAIL ${err.code}: ${err.message}`];
4822
2220
  if (err.details !== void 0) {
4823
2221
  lines.push(` details: ${JSON.stringify(err.details)}`);
4824
2222
  }
4825
2223
  const variable = err.details?.["variable"];
4826
- if (!manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable)) {
4827
- lines.push(
4828
- ` hint: "npm run chain:deploy" writes ${LOCAL_CHAIN_MANIFEST_PATH}, which validate/doctor read automatically to fill local X402_*/MERCHANT_WALLET placeholders.`
4829
- );
2224
+ if (typeof variable === "string") {
2225
+ lines.push(` hint: ${unresolvedVariableHint(variable, manifestFound, chainDeploy)}`);
4830
2226
  }
4831
2227
  return lines.join("\n");
4832
2228
  }
@@ -4836,14 +2232,14 @@ function formatConfigError(err, manifestFound = false) {
4836
2232
  return `FAIL ${String(err)}`;
4837
2233
  }
4838
2234
  async function runValidate(options, io, deps = {}) {
4839
- const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
2235
+ const loadConfig = deps.loadConfig ?? loadConfigDynamic;
4840
2236
  const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
4841
2237
  const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
4842
2238
  if (filled.length > 0) {
4843
2239
  io.stdout(`using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")}`);
4844
2240
  }
4845
2241
  try {
4846
- const config = await loadConfig2({
2242
+ const config = await loadConfig({
4847
2243
  ...options.configPath !== void 0 ? { path: options.configPath } : {},
4848
2244
  env
4849
2245
  });
@@ -4860,7 +2256,9 @@ async function runValidate(options, io, deps = {}) {
4860
2256
  io.stdout(` authorization: ap2=${onOff(config.authorization?.ap2.enabled)}`);
4861
2257
  return 0;
4862
2258
  } catch (err) {
4863
- io.stderr(formatConfigError(err, manifestFound));
2259
+ io.stderr(
2260
+ formatConfigError(err, manifestFound, (deps.hasChainDeployScript ?? hasChainDeployScript)())
2261
+ );
4864
2262
  return 1;
4865
2263
  }
4866
2264
  }
@@ -4928,7 +2326,7 @@ async function collectAnswersInteractive() {
4928
2326
  validate: (value) => {
4929
2327
  const trimmed = (value ?? "").trim();
4930
2328
  if (trimmed.length === 0) return "Required";
4931
- return /^0x[0-9a-fA-F]{40}$/.test(trimmed) ? void 0 : "Must be a 0x-prefixed 20-byte EVM address (42 characters)";
2329
+ return isEvmAddress(trimmed) ? void 0 : "Must be a 0x-prefixed 20-byte EVM address (42 characters)";
4932
2330
  }
4933
2331
  });
4934
2332
  if (clack.isCancel(answer)) {
@@ -4942,11 +2340,11 @@ async function collectAnswersInteractive() {
4942
2340
  }
4943
2341
  async function runInit(options, io, deps = {}) {
4944
2342
  const outputPath = options.outputPath ?? DEFAULT_CONFIG_FILENAME;
4945
- const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
2343
+ const loadConfig = deps.loadConfig ?? loadConfigDynamic;
4946
2344
  const parseConfigInMemory = deps.parseConfig ?? parseConfigDynamic;
4947
2345
  const fileExists = deps.fileExists ?? existsSync;
4948
- const writeFileImpl = deps.writeFile ?? (async (path2, content) => {
4949
- await writeFile(path2, content, "utf8");
2346
+ const writeFileImpl = deps.writeFile ?? (async (path, content) => {
2347
+ await writeFile(path, content, "utf8");
4950
2348
  });
4951
2349
  if (fileExists(outputPath) && options.force !== true) {
4952
2350
  io.stderr(`FAIL ${outputPath} already exists. Re-run with --force to overwrite.`);
@@ -4971,7 +2369,7 @@ async function runInit(options, io, deps = {}) {
4971
2369
  io.stdout(`PASS Wrote ${outputPath}`);
4972
2370
  io.stdout(`PASS ${outputPath} is valid: ${config.resources.length} resource(s)`);
4973
2371
  try {
4974
- await loadConfig2({ path: outputPath });
2372
+ await loadConfig({ path: outputPath });
4975
2373
  } catch (err) {
4976
2374
  io.stderr(formatConfigError(err));
4977
2375
  io.stderr(
@@ -4979,11 +2377,17 @@ async function runInit(options, io, deps = {}) {
4979
2377
  );
4980
2378
  return 1;
4981
2379
  }
2380
+ const peers = [
2381
+ ...config.payments.x402?.enabled === true ? ["x402"] : [],
2382
+ ...config.protocols.mcp.enabled ? ["MCP"] : []
2383
+ ];
2384
+ const startNote = peers.length > 0 ? `needs the ${peers.join(" and ")} peers` : "needs no optional peers";
4982
2385
  io.stdout("");
4983
2386
  io.stdout("Next steps:");
4984
2387
  io.stdout(` agent-commerce validate --config ${outputPath}`);
4985
- io.stdout(" docker compose up # start the local demo stack (from a repo clone)");
4986
- io.stdout(` agent-commerce doctor --config ${outputPath}`);
2388
+ io.stdout(` agent-commerce start --config ${outputPath} # ${startNote}`);
2389
+ io.stdout(` agent-commerce doctor --config ${outputPath} # from a second terminal`);
2390
+ io.stdout(" docker compose up # or the whole local demo stack, from a repo clone");
4987
2391
  return 0;
4988
2392
  }
4989
2393
 
@@ -5015,23 +2419,45 @@ var processIo = {
5015
2419
  };
5016
2420
 
5017
2421
  // src/cli/program.ts
2422
+ var ENV_FILE_HELP = "load environment variables from this file; variables already set win";
5018
2423
  function buildProgram(io = processIo) {
5019
2424
  const program = new Command();
5020
- program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, import, validate, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
2425
+ program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, import, validate, start, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
5021
2426
  writeOut: (str) => io.stdout(str.replace(/\n+$/, "")),
5022
2427
  writeErr: (str) => io.stderr(str.replace(/\n+$/, ""))
2428
+ }).hook("preAction", (_program, command) => {
2429
+ const { envFile } = command.opts();
2430
+ if (envFile === void 0) return;
2431
+ try {
2432
+ process.loadEnvFile(envFile);
2433
+ } catch (error) {
2434
+ command.error(
2435
+ `error: cannot read --env-file "${envFile}": ${error instanceof Error ? error.message : String(error)}`
2436
+ );
2437
+ }
5023
2438
  });
5024
2439
  program.command("version").description("Print the CLI version and pinned protocol/SDK versions.").action(() => {
5025
2440
  process.exitCode = runVersion(io);
5026
2441
  });
5027
- program.command("validate").description(`Validate ${DEFAULT_CONFIG_FILENAME} and exit non-zero if it is invalid.`).option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).action(async (opts) => {
2442
+ program.command("validate").description(`Validate ${DEFAULT_CONFIG_FILENAME} and exit non-zero if it is invalid.`).option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option("--env-file <path>", ENV_FILE_HELP).action(async (opts) => {
5028
2443
  const code = await runValidate(
5029
2444
  opts.config !== void 0 ? { configPath: opts.config } : {},
5030
2445
  io
5031
2446
  );
5032
2447
  process.exitCode = code;
5033
2448
  });
5034
- program.command("doctor").description("Diagnose config, gateway, backend, protocol and payment health.").option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option(
2449
+ program.command("start").description(
2450
+ `Start the gateway from ${DEFAULT_CONFIG_FILENAME} and serve until SIGTERM or SIGINT.`
2451
+ ).option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option("--env-file <path>", ENV_FILE_HELP).action(async (opts) => {
2452
+ try {
2453
+ const { runGateway } = await import('./run-5MB2D45X.js');
2454
+ await runGateway(opts.config !== void 0 ? { configPath: opts.config } : {});
2455
+ } catch (error) {
2456
+ io.stderr(formatConfigError(error, false, false));
2457
+ process.exit(1);
2458
+ }
2459
+ });
2460
+ program.command("doctor").description("Diagnose config, gateway, backend, protocol and payment health.").option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option("--env-file <path>", ENV_FILE_HELP).option(
5035
2461
  "--gateway <url>",
5036
2462
  "gateway base URL (default: derived from config, else http://localhost:8080)"
5037
2463
  ).option("--json", "emit machine-readable JSON instead of a formatted report", false).action(async (opts) => {
@@ -5086,14 +2512,18 @@ function collect(value, previous) {
5086
2512
  }
5087
2513
 
5088
2514
  // src/cli/index.ts
2515
+ var serving = false;
5089
2516
  for (const stream of [process.stdout, process.stderr]) {
5090
2517
  stream.on("error", (err) => {
5091
- if (err.code === "EPIPE") process.exit(0);
5092
- throw err;
2518
+ if (err.code !== "EPIPE") throw err;
2519
+ if (!serving) process.exit(0);
5093
2520
  });
5094
2521
  }
5095
2522
  async function main() {
5096
2523
  const program = buildProgram();
2524
+ program.hook("preAction", (_program, command) => {
2525
+ serving = command.name() === "start";
2526
+ });
5097
2527
  try {
5098
2528
  await program.parseAsync(process.argv);
5099
2529
  } catch (err) {