@devlab.group/agent-commerce 1.4.1 → 1.6.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 (44) hide show
  1. package/README.md +168 -183
  2. package/dist/ap2.d.ts +21 -24
  3. package/dist/ap2.js +68 -82
  4. package/dist/ap2.js.map +1 -1
  5. package/dist/authorization-BpbWGGrf.d.ts +115 -0
  6. package/dist/{chunk-NWIQLJNU.js → chunk-74BSEXB5.js} +37 -34
  7. package/dist/chunk-74BSEXB5.js.map +1 -0
  8. package/dist/chunk-AFI2CEZS.js +10 -0
  9. package/dist/chunk-AFI2CEZS.js.map +1 -0
  10. package/dist/chunk-ATTWSFQP.js +680 -0
  11. package/dist/chunk-ATTWSFQP.js.map +1 -0
  12. package/dist/{chunk-ZEGFLRGV.js → chunk-IVJC5YLE.js} +63 -26
  13. package/dist/chunk-IVJC5YLE.js.map +1 -0
  14. package/dist/{chunk-GF6KHHKL.js → chunk-RAJGPGJG.js} +5 -8
  15. package/dist/chunk-RAJGPGJG.js.map +1 -0
  16. package/dist/chunk-V2V2E7HX.js +27 -0
  17. package/dist/chunk-V2V2E7HX.js.map +1 -0
  18. package/dist/cli/index.js +812 -573
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/constants-Bkq04FRZ.d.ts +48 -0
  21. package/dist/guardrails-71XwtXlT.d.ts +31 -0
  22. package/dist/index.d.ts +245 -265
  23. package/dist/index.js +1222 -626
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp.d.ts +6 -59
  26. package/dist/mcp.js +64 -53
  27. package/dist/mcp.js.map +1 -1
  28. package/dist/mpp.d.ts +50 -0
  29. package/dist/mpp.js +353 -0
  30. package/dist/mpp.js.map +1 -0
  31. package/dist/protocol-adapter-DTkrXTev.d.ts +238 -0
  32. package/dist/runtime-CiYbgGpi.d.ts +303 -0
  33. package/dist/{types-CJtwLu1c.d.ts → types-O6lg3Q5V.d.ts} +17 -23
  34. package/dist/x402.d.ts +43 -75
  35. package/dist/x402.js +13 -693
  36. package/dist/x402.js.map +1 -1
  37. package/package.json +13 -2
  38. package/dist/authorization-CPDcpJVa.d.ts +0 -123
  39. package/dist/chunk-GF6KHHKL.js.map +0 -1
  40. package/dist/chunk-NWIQLJNU.js.map +0 -1
  41. package/dist/chunk-ZEGFLRGV.js.map +0 -1
  42. package/dist/guardrails-Dmwi63KB.d.ts +0 -38
  43. package/dist/protocol-adapter-Bebmggf_.d.ts +0 -273
  44. package/dist/runtime-CKq3d_3K.d.ts +0 -340
package/dist/cli/index.js CHANGED
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { ZodError, z } from 'zod';
2
+ import { isDeepStrictEqual } from 'util';
3
+ import { z } from 'zod';
3
4
  import { existsSync, mkdirSync, readFileSync, accessSync, constants, closeSync, openSync, chmodSync, promises } from 'fs';
4
5
  import * as path from 'path';
5
- import { resolve, dirname, basename as basename$1, extname, join } from 'path';
6
+ import { dirname, resolve, basename, extname, win32, join } from 'path';
6
7
  import { stringify, parse, Document, YAMLError } from 'yaml';
7
8
  import { CommanderError, Command } from 'commander';
8
9
  import picocolors from 'picocolors';
9
10
  import { randomUUID } from 'crypto';
10
- import { createRequire } from 'module';
11
+ import 'module';
11
12
  import Database from 'better-sqlite3';
12
- import { writeFile, stat, readFile, rename, rm } from 'fs/promises';
13
+ import { writeFile, stat, rename, rm, open } from 'fs/promises';
13
14
  import { validate } from '@scalar/openapi-parser';
14
15
  import * as clack from '@clack/prompts';
15
16
 
@@ -32,10 +33,22 @@ var init_filename = __esm({
32
33
  });
33
34
 
34
35
  // src/core/domain/common.ts
35
- var PROTOCOL_NAMES;
36
+ var PROTOCOL_NAME_KEYS, PROTOCOL_NAMES, PAYMENT_METHOD_NAME_KEYS, PAYMENT_METHOD_NAMES;
36
37
  var init_common = __esm({
37
38
  "src/core/domain/common.ts"() {
38
- PROTOCOL_NAMES = ["http", "mcp", "a2a", "acp"];
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
+ );
39
52
  }
40
53
  });
41
54
 
@@ -64,10 +77,9 @@ var init_codes = __esm({
64
77
  PAYMENT_REPLAYED: 409,
65
78
  PAYMENT_PROVIDER_UNAVAILABLE: 503,
66
79
  PAYMENT_SETTLEMENT_FAILED: 502,
67
- // 403, not 402: the buyer's money is not the problem. A 402 tells a client
68
- // "pay and retry", which cannot fix a missing or rejected mandate, and a
69
- // client that auto-pays on 402 would be charged for a request that was
70
- // never going to be delivered.
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
71
83
  AUTHORIZATION_REQUIRED: 403,
72
84
  AUTHORIZATION_INVALID: 403,
73
85
  AUTHORIZATION_REPLAYED: 409,
@@ -81,17 +93,13 @@ var init_codes = __esm({
81
93
  };
82
94
  RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
83
95
  "PAYMENT_PROVIDER_UNAVAILABLE",
84
- // Our verifier or our replay store was unreachable, so no verdict on the
85
- // mandate was ever reached. That is an outage on our side, and the same
86
- // proof will verify once it clears.
96
+ // The verifier or replay store was unreachable, so the mandate got no
97
+ // verdict. The same proof can verify once the outage clears.
87
98
  "AUTHORIZATION_PROVIDER_UNAVAILABLE",
88
99
  "BACKEND_TIMEOUT",
89
- // Load shedding is transient by definition: the caller should back off and
90
- // try again. Before this code existed the MCP adapter's queue-full path threw
91
- // PROTOCOL_UNSUPPORTED — HTTP 501, retryable:false — so an agent framework
92
- // keyed on the machine-readable flag (which exists precisely so clients need
93
- // not parse prose) would read a momentary throttle as a permanent capability
94
- // gap and stop calling the tool.
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.
95
103
  "GATEWAY_BUSY"
96
104
  ]);
97
105
  }
@@ -127,7 +135,7 @@ var init_commerce_error = __esm({
127
135
  if (options.resourceId !== void 0) this.resourceId = options.resourceId;
128
136
  Error.captureStackTrace?.(this, _CommerceError);
129
137
  }
130
- /** Non-secret, client-safe representation. */
138
+ /** Non-secret, client-safe representation. `cause` is never included */
131
139
  toInfo() {
132
140
  return {
133
141
  code: this.code,
@@ -151,11 +159,21 @@ var init_errors = __esm({
151
159
  }
152
160
  });
153
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
+
154
171
  // src/core/domain/wire.ts
155
172
  var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
156
173
  var init_wire = __esm({
157
174
  "src/core/domain/wire.ts"() {
158
175
  init_errors();
176
+ init_is_record();
159
177
  PAYMENT_INPUT_FIELD = "_payment";
160
178
  AUTHORIZATION_INPUT_FIELD = "_authorization";
161
179
  RESERVED_INPUT_FIELDS = [
@@ -257,17 +275,10 @@ var init_backend_http = __esm({
257
275
  init_resource();
258
276
  init_errors();
259
277
  init_logger();
278
+ init_is_record();
260
279
  PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
261
280
  }
262
281
  });
263
-
264
- // src/core/execution/event-bus.ts
265
- var init_event_bus = __esm({
266
- "src/core/execution/event-bus.ts"() {
267
- }
268
- });
269
-
270
- // src/core/execution/validation.ts
271
282
  function typeList(raw) {
272
283
  if (typeof raw === "string") return [raw];
273
284
  if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
@@ -282,15 +293,17 @@ function isObjectSchemaNode(schema) {
282
293
  }
283
294
  var init_validation = __esm({
284
295
  "src/core/execution/validation.ts"() {
296
+ init_is_record();
285
297
  }
286
298
  });
287
299
  var init_pipeline = __esm({
288
300
  "src/core/execution/pipeline.ts"() {
289
301
  init_wire();
290
302
  init_errors();
303
+ init_is_record();
291
304
  init_backend_http();
292
305
  init_validation();
293
- /* @__PURE__ */ new Set([...RESERVED_INPUT_FIELDS, "__proto__"]);
306
+ [...RESERVED_INPUT_FIELDS, "__proto__"];
294
307
  }
295
308
  });
296
309
 
@@ -301,17 +314,52 @@ var init_registry = __esm({
301
314
  }
302
315
  });
303
316
 
317
+ // src/core/execution/store-event-sink.ts
318
+ var init_store_event_sink = __esm({
319
+ "src/core/execution/store-event-sink.ts"() {
320
+ init_errors();
321
+ }
322
+ });
323
+
304
324
  // src/core/execution/index.ts
305
325
  var init_execution = __esm({
306
326
  "src/core/execution/index.ts"() {
307
327
  init_backend_http();
308
- init_event_bus();
309
328
  init_pipeline();
310
329
  init_registry();
330
+ init_store_event_sink();
311
331
  init_validation();
312
332
  }
313
333
  });
314
334
 
335
+ // src/payments/mpp/constants.ts
336
+ function isMppNetwork(value) {
337
+ return MPP_NETWORKS.includes(value);
338
+ }
339
+ var MPP_SPEC_COMMIT, MPP_SPEC_DRAFTS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPPX_VERSION, MPP_PROFILE, MPP_NETWORKS, MPP_DEFAULT_NETWORK;
340
+ var init_constants2 = __esm({
341
+ "src/payments/mpp/constants.ts"() {
342
+ MPP_SPEC_COMMIT = "806fdb8b8c92cda5c84b8660558921b6bbeef7e0";
343
+ MPP_SPEC_DRAFTS = {
344
+ core: "draft-httpauth-payment-00",
345
+ intent: "draft-payment-intent-charge-00",
346
+ method: "draft-evm-charge-00"
347
+ };
348
+ MPP_MIN_CHALLENGE_SECRET_LENGTH = 32;
349
+ MPPX_VERSION = "0.10.1";
350
+ MPP_PROFILE = {
351
+ intent: "charge",
352
+ method: "evm",
353
+ credentialType: "authorization",
354
+ assetSymbol: "USDC",
355
+ assetDecimals: 6
356
+ };
357
+ MPP_NETWORKS = ["eip155:84532", "eip155:8453"];
358
+ MPP_DEFAULT_NETWORK = "eip155:84532";
359
+ `mpp/${MPP_SPEC_DRAFTS.core} intent=${MPP_PROFILE.intent} method=${MPP_PROFILE.method}`;
360
+ }
361
+ });
362
+
315
363
  // src/payments/x402/networks.ts
316
364
  function findNetworkProfile(id) {
317
365
  return BY_ID.get(id);
@@ -366,13 +414,13 @@ var init_networks = __esm({
366
414
  ];
367
415
  BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
368
416
  SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
369
- LIVE_MAINNET_BANNER = "LIVE MAINNET MODE \u2014 REAL FUNDS";
417
+ LIVE_MAINNET_BANNER = "LIVE MAINNET MODE - REAL FUNDS";
370
418
  }
371
419
  });
372
420
 
373
421
  // src/protocols/acp/constants.ts
374
- var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
375
- var init_constants2 = __esm({
422
+ var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS;
423
+ var init_constants3 = __esm({
376
424
  "src/protocols/acp/constants.ts"() {
377
425
  ACP_SPEC_VERSION = "2026-04-17";
378
426
  ACP_API_VERSION = "2026-04-17";
@@ -391,6 +439,9 @@ var init_constants2 = __esm({
391
439
  completeCheckoutSession: ["path", "body"],
392
440
  cancelCheckoutSession: ["path"]
393
441
  };
442
+ ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
443
+ cancelCheckoutSession: ["body"]
444
+ };
394
445
  }
395
446
  });
396
447
 
@@ -402,7 +453,7 @@ function substituteEnv(value, env, path2 = "$") {
402
453
  if (Array.isArray(value)) {
403
454
  return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
404
455
  }
405
- if (isPlainObject(value)) {
456
+ if (isRecord(value)) {
406
457
  const result = {};
407
458
  for (const [key, entry] of Object.entries(value)) {
408
459
  result[key] = substituteEnv(entry, env, `${path2}.${key}`);
@@ -419,16 +470,15 @@ function assertTemplateIsSupported(value, path2) {
419
470
  if (NESTED_PLACEHOLDER.test(value)) {
420
471
  throw new CommerceError(
421
472
  "CONFIG_INVALID",
422
- `Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported \u2014 the inner one is not resolved and its closing brace ends up in the value. Use a plain default, or set the variable.`,
473
+ `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.`,
423
474
  { details: { path: path2 } }
424
475
  );
425
476
  }
426
- BRACE_TOKEN.lastIndex = 0;
427
477
  for (const [token] of value.matchAll(BRACE_TOKEN)) {
428
478
  if (SUPPORTED_TOKEN.test(token)) continue;
429
479
  throw new CommerceError(
430
480
  "CONFIG_INVALID",
431
- `Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are recognised; shell forms such as "\${VAR-default}", "\${VAR:=default}" and "\${VAR:?message}" would load as literal text.`,
481
+ `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.`,
432
482
  { details: { path: path2, token } }
433
483
  );
434
484
  }
@@ -453,13 +503,11 @@ function substituteOnce(value, env, path2) {
453
503
  }
454
504
  );
455
505
  }
456
- function isPlainObject(value) {
457
- return typeof value === "object" && value !== null && !Array.isArray(value);
458
- }
459
506
  var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
460
507
  var init_env = __esm({
461
508
  "src/config/env.ts"() {
462
509
  init_core();
510
+ init_is_record();
463
511
  PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
464
512
  BRACE_TOKEN = /\$\{[^{}]*\}/g;
465
513
  SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
@@ -497,7 +545,7 @@ function isWellKnownDevAddress(address) {
497
545
  }
498
546
  function isLikelyLocalOrPrivateHost(hostname) {
499
547
  const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
500
- if (h === "localhost" || h === "127.0.0.1" || h === "::1") return true;
548
+ if (h === "localhost" || h === "::1") return true;
501
549
  if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
502
550
  if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
503
551
  if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
@@ -524,93 +572,95 @@ function invalid(message, path2) {
524
572
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
525
573
  }
526
574
  function resolveX402Deployment(input) {
527
- const profile = requireNetworkProfile(input.network, "payments.x402.network");
575
+ const at = input.configPath ?? "payments.x402";
576
+ const payTo = input.payToField ?? "payTo";
577
+ const profile = requireNetworkProfile(input.network, `${at}.network`);
528
578
  const mode = resolveDeploymentMode(profile, input.facilitator.mode);
529
579
  if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
530
580
  throw invalid(
531
- `payments.x402: 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.`,
532
- "payments.x402.facilitator.mode"
581
+ `${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.`,
582
+ `${at}.facilitator.mode`
533
583
  );
534
584
  }
535
585
  if (mode === "mainnet" && input.allowMainnet !== true) {
536
586
  throw invalid(
537
- `payments.x402: network "${profile.id}" (${profile.displayName}) settles real funds. Set payments.x402.allowMainnet: true to acknowledge this explicitly \u2014 it is never the default.`,
538
- "payments.x402.allowMainnet"
587
+ `${at}: network "${profile.id}" (${profile.displayName}) settles real funds. Set ${at}.allowMainnet: true to acknowledge this explicitly - it is never the default.`,
588
+ `${at}.allowMainnet`
539
589
  );
540
590
  }
541
591
  if (input.facilitator.mode === "remote") {
542
- assertFacilitatorUrlIsSafe(input.facilitator.url, mode);
592
+ assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
543
593
  if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
544
594
  throw invalid(
545
- `payments.x402: facilitator ${describeOrigin(input.facilitator.url)} takes no credential, and this is a mainnet deployment. It will see every payment authorisation you handle, with no account, terms or support behind it. Set payments.x402.allowUnauthenticatedFacilitator: true to accept that, or configure facilitator.auth.`,
546
- "payments.x402.allowUnauthenticatedFacilitator"
595
+ `${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.`,
596
+ `${at}.allowUnauthenticatedFacilitator`
547
597
  );
548
598
  }
549
599
  for (const [field, value] of credentialFields(input.facilitator.auth)) {
550
600
  if (value.trim() === "") {
551
601
  throw invalid(
552
- `payments.x402: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
553
- `payments.x402.facilitator.auth.${field}`
602
+ `${at}: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
603
+ `${at}.facilitator.auth.${field}`
554
604
  );
555
605
  }
556
606
  }
557
607
  }
558
608
  if (/^0x0{40}$/i.test(input.payTo)) {
559
609
  throw invalid(
560
- 'payments.x402: "payTo" is the zero address. Every payment settled there is destroyed.',
561
- "payments.x402.payTo"
610
+ `${at}: "${payTo}" is the zero address. Every payment settled there is destroyed.`,
611
+ `${at}.${payTo}`
562
612
  );
563
613
  }
564
614
  if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
565
615
  throw invalid(
566
- `payments.x402: "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.`,
567
- "payments.x402.payTo"
616
+ `${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.`,
617
+ `${at}.${payTo}`
568
618
  );
569
619
  }
570
620
  const canonical = profile.canonicalAsset;
571
621
  if (mode === "mainnet" && canonical) {
572
622
  if (!sameAddress(input.asset, canonical.address)) {
573
623
  throw invalid(
574
- `payments.x402: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
575
- "payments.x402.asset"
624
+ `${at}: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
625
+ `${at}.asset`
576
626
  );
577
627
  }
578
628
  if (input.assetName !== void 0 && input.assetName !== canonical.name) {
579
629
  throw invalid(
580
- `payments.x402: 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.`,
581
- "payments.x402.assetName"
630
+ `${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.`,
631
+ `${at}.assetName`
582
632
  );
583
633
  }
584
634
  if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
585
635
  throw invalid(
586
- `payments.x402: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
587
- "payments.x402.assetVersion"
636
+ `${at}: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
637
+ `${at}.assetVersion`
588
638
  );
589
639
  }
590
640
  }
591
641
  return { profile, mode };
592
642
  }
593
- function assertFacilitatorUrlIsSafe(url, mode) {
643
+ function assertFacilitatorUrlIsSafe(url, mode, at) {
594
644
  let parsed;
595
645
  try {
596
646
  parsed = new URL(url);
597
647
  } catch (cause) {
598
- throw new CommerceError("CONFIG_INVALID", `payments.x402: facilitator.url is not a valid URL`, {
648
+ throw new CommerceError("CONFIG_INVALID", `${at}: facilitator.url is not a valid URL`, {
599
649
  cause,
600
- details: { path: "payments.x402.facilitator.url" }
650
+ details: { path: `${at}.facilitator.url` }
601
651
  });
602
652
  }
603
653
  if (parsed.protocol === "https:") return;
604
654
  if (parsed.protocol !== "http:") {
605
655
  throw invalid(
606
- `payments.x402: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
607
- "payments.x402.facilitator.url"
656
+ `${at}: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
657
+ `${at}.facilitator.url`
608
658
  );
609
659
  }
610
660
  if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
611
661
  throw invalid(
612
- `payments.x402: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorisations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
613
- "payments.x402.facilitator.url"
662
+ `${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.`,
663
+ `${at}.facilitator.url`
614
664
  );
615
665
  }
616
666
  function describeOrigin(url) {
@@ -643,8 +693,20 @@ var init_guardrails = __esm({
643
693
  init_networks();
644
694
  }
645
695
  });
696
+
697
+ // src/protocols/mcp/constants.ts
698
+ var MCP_TOOL_NAME_CHARS, MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_PATTERN;
699
+ var init_constants4 = __esm({
700
+ "src/protocols/mcp/constants.ts"() {
701
+ MCP_TOOL_NAME_CHARS = "A-Za-z0-9._-";
702
+ MCP_TOOL_NAME_MAX_LENGTH = 128;
703
+ MCP_TOOL_NAME_PATTERN = new RegExp(
704
+ `^[${MCP_TOOL_NAME_CHARS}]{1,${MCP_TOOL_NAME_MAX_LENGTH}}$`
705
+ );
706
+ }
707
+ });
646
708
  function parseConfig(raw, env) {
647
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
709
+ if (!isRecord(raw)) {
648
710
  throw new CommerceError(
649
711
  "CONFIG_INVALID",
650
712
  'Configuration root must be a mapping (object) at "$"',
@@ -656,7 +718,7 @@ function parseConfig(raw, env) {
656
718
  checkVersion(raw);
657
719
  const substituted = substituteEnv(raw, env);
658
720
  const parsed = parseWithZod(RawConfigSchema, substituted);
659
- return normalise(parsed);
721
+ return normalize(parsed);
660
722
  }
661
723
  function checkVersion(raw) {
662
724
  if (!("version" in raw)) {
@@ -664,12 +726,12 @@ function checkVersion(raw) {
664
726
  details: { path: "version" }
665
727
  });
666
728
  }
667
- const rawVersion = raw["version"];
668
- const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
729
+ const version = raw["version"];
669
730
  if (version !== SUPPORTED_CONFIG_VERSION) {
731
+ const hint = typeof version === "string" ? ", written as a number without quotes" : "";
670
732
  throw new CommerceError(
671
733
  "CONFIG_INVALID",
672
- `Unsupported config version "${String(rawVersion)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}`,
734
+ `Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
673
735
  { details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
674
736
  );
675
737
  }
@@ -740,7 +802,7 @@ function toBoolean(value, path2) {
740
802
  { details: { path: path2 } }
741
803
  );
742
804
  }
743
- function normalise(raw) {
805
+ function normalize(raw) {
744
806
  const protocols = {
745
807
  http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
746
808
  mcp: {
@@ -751,16 +813,11 @@ function normalise(raw) {
751
813
  enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
752
814
  mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
753
815
  },
754
- acp: normaliseAcp(raw.protocols.acp)
816
+ acp: normalizeAcp(raw.protocols.acp)
755
817
  };
756
818
  validateMountPaths(protocols);
757
819
  const x402Raw = raw.payments.x402;
758
- const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
759
- mode: "remote",
760
- url: x402Raw.facilitator.url,
761
- auth: x402Raw.facilitator.auth ?? { type: "none" }
762
- };
763
- const x402 = x402Raw && facilitator ? {
820
+ const x402 = x402Raw !== void 0 ? {
764
821
  enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
765
822
  network: x402Raw.network,
766
823
  rpcUrl: x402Raw.rpcUrl,
@@ -779,7 +836,7 @@ function normalise(raw) {
779
836
  min: 1
780
837
  }
781
838
  ),
782
- facilitator,
839
+ facilitator: normalizeFacilitator(x402Raw.facilitator),
783
840
  ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
784
841
  ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
785
842
  allowUnauthenticatedFacilitator: toBoolean(
@@ -802,12 +859,13 @@ function normalise(raw) {
802
859
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
803
860
  });
804
861
  }
805
- const ap2 = normaliseAp2(raw.authorization?.ap2);
862
+ const mpp = normalizeMpp(raw.payments.mpp);
863
+ const ap2 = normalizeAp2(raw.authorization?.ap2);
806
864
  if (ap2?.enabled) {
807
865
  validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
808
866
  }
809
867
  const resources = Object.entries(raw.resources).map(
810
- ([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
868
+ ([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
811
869
  );
812
870
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
813
871
  return {
@@ -817,8 +875,7 @@ function normalise(raw) {
817
875
  name: raw.merchant.name,
818
876
  publicBaseUrl: raw.merchant.publicBaseUrl
819
877
  },
820
- // min 0, not 1: port 0 is the standard "let the OS pick a free port"
821
- // convention, useful for tests and ephemeral dev/demo instances.
878
+ // Port 0 lets the OS pick a free port, for tests and throwaway instances
822
879
  server: {
823
880
  port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
824
881
  host: raw.server.host,
@@ -829,7 +886,8 @@ function normalise(raw) {
829
886
  protocols,
830
887
  resources,
831
888
  payments: {
832
- ...x402 !== void 0 ? { x402 } : {}
889
+ ...x402 !== void 0 ? { x402 } : {},
890
+ ...mpp !== void 0 ? { mpp } : {}
833
891
  },
834
892
  ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
835
893
  };
@@ -857,7 +915,7 @@ function validateMountPaths(protocols) {
857
915
  function ap2Invalid(path2, message, extra = {}) {
858
916
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
859
917
  }
860
- function normaliseAp2(raw) {
918
+ function normalizeAp2(raw) {
861
919
  if (raw === void 0) return void 0;
862
920
  if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
863
921
  const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
@@ -877,14 +935,14 @@ function normaliseAp2(raw) {
877
935
  if (raw.replay === void 0) {
878
936
  throw ap2Invalid(
879
937
  "authorization.ap2.replay",
880
- "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 authorise a second settlement"
938
+ "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"
881
939
  );
882
940
  }
883
- const mandateIssuers = normaliseIssuers(
941
+ const mandateIssuers = normalizeIssuers(
884
942
  raw.trust?.mandateIssuers,
885
943
  "authorization.ap2.trust.mandateIssuers"
886
944
  );
887
- const checkoutIssuers = normaliseIssuers(
945
+ const checkoutIssuers = normalizeIssuers(
888
946
  raw.trust?.checkoutIssuers,
889
947
  "authorization.ap2.trust.checkoutIssuers"
890
948
  );
@@ -901,7 +959,7 @@ function normaliseAp2(raw) {
901
959
  replay: { path: raw.replay.path }
902
960
  };
903
961
  }
904
- function normaliseIssuers(raw, path2) {
962
+ function normalizeIssuers(raw, path2) {
905
963
  if (raw === void 0 || raw.length === 0) {
906
964
  throw ap2Invalid(
907
965
  path2,
@@ -987,11 +1045,11 @@ function validateJwk(jwk, kid, path2) {
987
1045
  if (jwk["kid"] !== void 0 && value("kid") !== kid) {
988
1046
  fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
989
1047
  }
990
- const normalised = {};
1048
+ const normalized = {};
991
1049
  for (const member of AP2_JWK_MEMBERS) {
992
- if (jwk[member] !== void 0) normalised[member] = value(member);
1050
+ if (jwk[member] !== void 0) normalized[member] = value(member);
993
1051
  }
994
- return normalised;
1052
+ return normalized;
995
1053
  }
996
1054
  function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
997
1055
  if (replayPath === ":memory:") return;
@@ -1005,7 +1063,7 @@ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1005
1063
  );
1006
1064
  }
1007
1065
  }
1008
- function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1066
+ function normalizeResourceAuthorization(id, entry, pricing, ap2) {
1009
1067
  const required = entry.authorization?.required;
1010
1068
  if (required === void 0) return void 0;
1011
1069
  const path2 = `resources.${id}.authorization.required`;
@@ -1045,7 +1103,7 @@ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1045
1103
  function acpInvalid(path2, message, extra = {}) {
1046
1104
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1047
1105
  }
1048
- function normaliseAcp(raw) {
1106
+ function normalizeAcp(raw) {
1049
1107
  const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
1050
1108
  if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
1051
1109
  return { enabled: false, mountPath };
@@ -1075,9 +1133,8 @@ function normaliseAcp(raw) {
1075
1133
  { min: ACP_RETENTION_HOURS }
1076
1134
  )
1077
1135
  },
1078
- checkout: { operations: normaliseAcpOperations(raw.checkout) },
1079
- // Built key by key rather than passed through: absent metadata must stay
1080
- // absent from the discovery document, not appear there as `undefined`.
1136
+ checkout: { operations: normalizeAcpOperations(raw.checkout) },
1137
+ // Built key by key so absent metadata stays absent rather than `undefined`
1081
1138
  ...discovery !== void 0 ? {
1082
1139
  discovery: {
1083
1140
  ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
@@ -1088,7 +1145,7 @@ function normaliseAcp(raw) {
1088
1145
  } : {}
1089
1146
  };
1090
1147
  }
1091
- function normaliseAcpOperations(checkout) {
1148
+ function normalizeAcpOperations(checkout) {
1092
1149
  const configured = checkout?.operations ?? {};
1093
1150
  for (const key of Object.keys(configured)) {
1094
1151
  if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
@@ -1156,13 +1213,15 @@ function validateAcpOperationInput(path2, operation, resource) {
1156
1213
  const schema = resource.inputSchema;
1157
1214
  if (schema === void 0) return;
1158
1215
  const keys = ACP_OPERATION_INPUT_KEYS[operation];
1216
+ const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
1159
1217
  const properties = schema["properties"];
1160
- if (schema["additionalProperties"] === false && isPlainObject2(properties)) {
1161
- for (const key of keys) {
1218
+ if (schema["additionalProperties"] === false && isRecord(properties)) {
1219
+ for (const key of [...keys, ...optionalKeys]) {
1162
1220
  if (!Object.hasOwn(properties, key)) {
1221
+ const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
1163
1222
  throw acpInvalid(
1164
1223
  path2,
1165
- `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter always sends for this operation`,
1224
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter ${when} for this operation`,
1166
1225
  { resourceId: resource.id }
1167
1226
  );
1168
1227
  }
@@ -1174,14 +1233,81 @@ function validateAcpOperationInput(path2, operation, resource) {
1174
1233
  if (typeof entry === "string" && !keys.includes(entry)) {
1175
1234
  throw acpInvalid(
1176
1235
  path2,
1177
- `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation never supplies (it sends: ${keys.join(", ")})`,
1236
+ `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(", ")})`,
1178
1237
  { resourceId: resource.id }
1179
1238
  );
1180
1239
  }
1181
1240
  }
1182
1241
  }
1183
1242
  }
1184
- function normaliseResource(id, entry, protocols, x402, ap2) {
1243
+ function normalizeFacilitator(raw) {
1244
+ return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
1245
+ }
1246
+ function normalizeMpp(raw) {
1247
+ if (raw === void 0) return void 0;
1248
+ const network = raw.network ?? MPP_DEFAULT_NETWORK;
1249
+ if (!isMppNetwork(network)) {
1250
+ throw new CommerceError(
1251
+ "CONFIG_INVALID",
1252
+ `payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
1253
+ { details: { path: "payments.mpp.network" } }
1254
+ );
1255
+ }
1256
+ const mpp = {
1257
+ enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
1258
+ network,
1259
+ rpcUrl: raw.rpcUrl,
1260
+ asset: raw.asset,
1261
+ assetName: raw.assetName,
1262
+ assetVersion: raw.assetVersion,
1263
+ recipient: raw.recipient,
1264
+ realm: raw.realm,
1265
+ challengeSecret: raw.challengeSecret,
1266
+ ...raw.challengeTtlSeconds !== void 0 ? {
1267
+ challengeTtlSeconds: toNumber(
1268
+ raw.challengeTtlSeconds,
1269
+ "payments.mpp.challengeTtlSeconds",
1270
+ { min: 1 }
1271
+ )
1272
+ } : {},
1273
+ facilitator: normalizeFacilitator(raw.facilitator),
1274
+ ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
1275
+ ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
1276
+ allowUnauthenticatedFacilitator: toBoolean(
1277
+ raw.allowUnauthenticatedFacilitator,
1278
+ "payments.mpp.allowUnauthenticatedFacilitator"
1279
+ )
1280
+ } : {}
1281
+ };
1282
+ validateAddress("payments.mpp.recipient", mpp.recipient);
1283
+ validateAddress("payments.mpp.asset", mpp.asset);
1284
+ if (/[\r\n]/.test(mpp.realm)) {
1285
+ throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
1286
+ details: { path: "payments.mpp.realm" }
1287
+ });
1288
+ }
1289
+ if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
1290
+ throw new CommerceError(
1291
+ "CONFIG_INVALID",
1292
+ `payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
1293
+ { details: { path: "payments.mpp.challengeSecret" } }
1294
+ );
1295
+ }
1296
+ resolveX402Deployment({
1297
+ network: mpp.network,
1298
+ payTo: mpp.recipient,
1299
+ asset: mpp.asset,
1300
+ assetName: mpp.assetName,
1301
+ assetVersion: mpp.assetVersion,
1302
+ facilitator: mpp.facilitator,
1303
+ ...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
1304
+ ...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
1305
+ configPath: "payments.mpp",
1306
+ payToField: "recipient"
1307
+ });
1308
+ return mpp;
1309
+ }
1310
+ function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
1185
1311
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1186
1312
  const inputProperties = entry.input?.["properties"];
1187
1313
  if (inputProperties && typeof inputProperties === "object") {
@@ -1247,6 +1373,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
1247
1373
  );
1248
1374
  }
1249
1375
  validateBackendUrl(id, entry.backend.url);
1376
+ validateBackendHeaders(id, entry.backend.headers);
1250
1377
  const pathScope = validateInputBindings(id, entry.backend, entry.input);
1251
1378
  validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
1252
1379
  if (entry.pricing.type === "dynamic") {
@@ -1257,7 +1384,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
1257
1384
  );
1258
1385
  }
1259
1386
  if (entry.pricing.type === "fixed") {
1260
- validatePricingAmount(id, entry.pricing.amount, x402);
1387
+ validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
1261
1388
  }
1262
1389
  const paymentMethods = entry.payments ?? [];
1263
1390
  if (entry.pricing.type === "fixed") {
@@ -1272,21 +1399,32 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
1272
1399
  if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
1273
1400
  throw new CommerceError(
1274
1401
  "CONFIG_INVALID",
1275
- `Resource "${id}" names unsupported payment method "${method}". Supported: x402.`,
1402
+ `Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
1276
1403
  { details: { path: `resources.${id}.payments`, resourceId: id, method } }
1277
1404
  );
1278
1405
  }
1279
- if (method === "x402" && (!x402 || !x402.enabled)) {
1406
+ }
1407
+ const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
1408
+ if (!paymentMethods.some((method) => enabledRails.includes(method))) {
1409
+ throw new CommerceError(
1410
+ "CONFIG_INVALID",
1411
+ `Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
1412
+ { details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
1413
+ );
1414
+ }
1415
+ if (mpp?.enabled && paymentMethods.includes("mpp")) {
1416
+ if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
1280
1417
  throw new CommerceError(
1281
1418
  "CONFIG_INVALID",
1282
- `Resource "${id}" names payment method "x402" which is not configured or not enabled under payments.x402`,
1283
- { details: { path: `resources.${id}.payments`, resourceId: id, method } }
1419
+ `Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
1420
+ { details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
1284
1421
  );
1285
1422
  }
1423
+ validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
1286
1424
  }
1287
1425
  }
1288
1426
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
1289
- const authorization = normaliseResourceAuthorization(id, entry, pricing, ap2);
1427
+ const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
1290
1428
  return {
1291
1429
  id,
1292
1430
  name: entry.name,
@@ -1316,11 +1454,11 @@ function defaultClosedObjectSchema(schema) {
1316
1454
  const result = { ...schema };
1317
1455
  if (isObjectSchema) {
1318
1456
  const properties = schema["properties"];
1319
- if (isPlainObject2(properties)) {
1457
+ if (isRecord(properties)) {
1320
1458
  result["properties"] = Object.fromEntries(
1321
1459
  Object.entries(properties).map(([key, sub]) => [
1322
1460
  key,
1323
- isPlainObject2(sub) ? defaultClosedObjectSchema(sub) : sub
1461
+ isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
1324
1462
  ])
1325
1463
  );
1326
1464
  }
@@ -1329,18 +1467,15 @@ function defaultClosedObjectSchema(schema) {
1329
1467
  }
1330
1468
  }
1331
1469
  const items = schema["items"];
1332
- if (isPlainObject2(items)) {
1470
+ if (isRecord(items)) {
1333
1471
  result["items"] = defaultClosedObjectSchema(items);
1334
1472
  }
1335
1473
  const additional = schema["additionalProperties"];
1336
- if (isPlainObject2(additional)) {
1474
+ if (isRecord(additional)) {
1337
1475
  result["additionalProperties"] = defaultClosedObjectSchema(additional);
1338
1476
  }
1339
1477
  return result;
1340
1478
  }
1341
- function isPlainObject2(value) {
1342
- return typeof value === "object" && value !== null && !Array.isArray(value);
1343
- }
1344
1479
  function excludesObjectType(schema) {
1345
1480
  const type = schema["type"];
1346
1481
  if (type === void 0 || type === "object") return false;
@@ -1365,7 +1500,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1365
1500
  const requiredRaw = schema["required"];
1366
1501
  const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
1367
1502
  if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
1368
- const declared = isPlainObject2(schema["properties"]) ? schema["properties"] : {};
1503
+ const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
1369
1504
  const undeclared = requiredRaw.filter(
1370
1505
  (name) => typeof name === "string" && !Object.hasOwn(declared, name)
1371
1506
  );
@@ -1378,17 +1513,17 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1378
1513
  }
1379
1514
  }
1380
1515
  const properties = schema["properties"];
1381
- if (isPlainObject2(properties)) {
1516
+ if (isRecord(properties)) {
1382
1517
  for (const [key, sub] of Object.entries(properties)) {
1383
- if (isPlainObject2(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1518
+ if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1384
1519
  }
1385
1520
  }
1386
1521
  const additional = schema["additionalProperties"];
1387
- if (isPlainObject2(additional)) {
1522
+ if (isRecord(additional)) {
1388
1523
  validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
1389
1524
  }
1390
1525
  const items = schema["items"];
1391
- if (isPlainObject2(items)) {
1526
+ if (isRecord(items)) {
1392
1527
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
1393
1528
  } else if (Array.isArray(items)) {
1394
1529
  console.warn(
@@ -1396,7 +1531,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1396
1531
  );
1397
1532
  }
1398
1533
  }
1399
- function validatePricingAmount(id, amount, x402) {
1534
+ function validatePricingAmount(id, amount, decimals) {
1400
1535
  const path2 = `resources.${id}.pricing.amount`;
1401
1536
  if (!PRICING_AMOUNT_PATTERN.test(amount)) {
1402
1537
  throw new CommerceError(
@@ -1412,12 +1547,12 @@ function validatePricingAmount(id, amount, x402) {
1412
1547
  { details: { path: path2, resourceId: id } }
1413
1548
  );
1414
1549
  }
1415
- if (x402 !== void 0) {
1550
+ if (decimals !== void 0) {
1416
1551
  const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
1417
- if (fractionalDigits > x402.assetDecimals) {
1552
+ if (fractionalDigits > decimals) {
1418
1553
  throw new CommerceError(
1419
1554
  "CONFIG_INVALID",
1420
- `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${x402.assetDecimals} decimals)`,
1555
+ `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
1421
1556
  { details: { path: path2, resourceId: id } }
1422
1557
  );
1423
1558
  }
@@ -1453,7 +1588,7 @@ function validateInputBindings(id, backend, input) {
1453
1588
  );
1454
1589
  }
1455
1590
  const seen = /* @__PURE__ */ new Map();
1456
- const properties = isPlainObject2(input?.["properties"]) ? input["properties"] : {};
1591
+ const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
1457
1592
  const required = new Set(
1458
1593
  Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1459
1594
  );
@@ -1476,7 +1611,7 @@ function validateInputBindings(id, backend, input) {
1476
1611
  );
1477
1612
  }
1478
1613
  const declared = properties[property];
1479
- if (location !== "body" && isPlainObject2(declared) && !isObjectSchemaNode(declared)) {
1614
+ if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
1480
1615
  fail(
1481
1616
  `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
1482
1617
  { location, property }
@@ -1492,7 +1627,7 @@ function validateInputBindings(id, backend, input) {
1492
1627
  if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
1493
1628
  const group = properties[bindings.path];
1494
1629
  return {
1495
- schema: isPlainObject2(group) ? group : void 0,
1630
+ schema: isRecord(group) ? group : void 0,
1496
1631
  where: `input.properties.${bindings.path}`
1497
1632
  };
1498
1633
  }
@@ -1504,7 +1639,7 @@ function validatePathParametersDeclared(id, url, input, where) {
1504
1639
  if (stray !== void 0) {
1505
1640
  throw new CommerceError(
1506
1641
  "CONFIG_INVALID",
1507
- `Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would be sent to the backend literally, so a paid resource would settle payment and then never reach the backend`,
1642
+ `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`,
1508
1643
  { details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
1509
1644
  );
1510
1645
  }
@@ -1522,13 +1657,13 @@ function validatePathParametersDeclared(id, url, input, where) {
1522
1657
  if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
1523
1658
  throw new CommerceError(
1524
1659
  "CONFIG_INVALID",
1525
- `Resource "${id}" has a backend.url parameter before the end of the host ("${url}"). 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`,
1660
+ `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`,
1526
1661
  { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1527
1662
  );
1528
1663
  }
1529
1664
  }
1530
1665
  const propertiesRaw = input?.["properties"];
1531
- const properties = isPlainObject2(propertiesRaw) ? propertiesRaw : {};
1666
+ const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
1532
1667
  const requiredRaw = input?.["required"];
1533
1668
  const required = new Set(
1534
1669
  Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
@@ -1538,14 +1673,14 @@ function validatePathParametersDeclared(id, url, input, where) {
1538
1673
  if (!Object.hasOwn(properties, param)) {
1539
1674
  throw new CommerceError(
1540
1675
  "CONFIG_INVALID",
1541
- `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 settle payment (if priced) and then fail to reach the backend`,
1676
+ `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`,
1542
1677
  { details: { path: path2, resourceId: id, param } }
1543
1678
  );
1544
1679
  }
1545
1680
  if (!required.has(param)) {
1546
1681
  throw new CommerceError(
1547
1682
  "CONFIG_INVALID",
1548
- `Resource "${id}" has backend.url path parameter "{${param}}" declared in ${where} but not listed in its "required" - a caller that omits it hits the same unservable-request problem as an undeclared parameter`,
1683
+ `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`,
1549
1684
  { details: { path: path2, resourceId: id, param } }
1550
1685
  );
1551
1686
  }
@@ -1558,7 +1693,7 @@ function validateBackendUrl(id, url) {
1558
1693
  } catch {
1559
1694
  throw new CommerceError(
1560
1695
  "CONFIG_INVALID",
1561
- `Resource "${id}" has an invalid backend.url "${url}": must be an absolute http:// or https:// URL`,
1696
+ `Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
1562
1697
  { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1563
1698
  );
1564
1699
  }
@@ -1570,6 +1705,19 @@ function validateBackendUrl(id, url) {
1570
1705
  );
1571
1706
  }
1572
1707
  }
1708
+ function validateBackendHeaders(id, headers) {
1709
+ for (const [name, value] of Object.entries(headers ?? {})) {
1710
+ try {
1711
+ new Headers([[name, value]]);
1712
+ } catch {
1713
+ throw new CommerceError(
1714
+ "CONFIG_INVALID",
1715
+ `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)`,
1716
+ { details: { path: `resources.${id}.backend.headers.${name}`, resourceId: id } }
1717
+ );
1718
+ }
1719
+ }
1720
+ }
1573
1721
  function validateAddress(path2, value) {
1574
1722
  if (!ADDRESS_PATTERN.test(value)) {
1575
1723
  throw new CommerceError(
@@ -1588,19 +1736,21 @@ function validateAddress(path2, value) {
1588
1736
  );
1589
1737
  }
1590
1738
  }
1591
- var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_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, 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;
1739
+ 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;
1592
1740
  var init_schema = __esm({
1593
1741
  "src/config/schema.ts"() {
1594
1742
  init_constants();
1595
- init_execution();
1596
1743
  init_core();
1597
- init_guardrails();
1744
+ init_execution();
1745
+ init_is_record();
1598
1746
  init_constants2();
1747
+ init_guardrails();
1748
+ init_constants3();
1749
+ init_constants4();
1599
1750
  init_env();
1600
1751
  SUPPORTED_CONFIG_VERSION = 1;
1601
1752
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
1602
1753
  ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
1603
- MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
1604
1754
  NumberOrString = z.union([z.number(), z.string()]);
1605
1755
  BooleanOrString = z.union([z.boolean(), z.string()]);
1606
1756
  MerchantSchema = z.object({
@@ -1611,14 +1761,14 @@ var init_schema = __esm({
1611
1761
  ServerSchema = z.object({
1612
1762
  port: NumberOrString,
1613
1763
  host: z.string().min(1),
1614
- /** Shared secret gating /api/receipts, /api/events, /api/events/stream. No token -> those routes 404. */
1764
+ // Shared secret gating /api/receipts and /api/events. Without it both
1765
+ // routes 404.
1615
1766
  adminToken: z.string().min(1).optional(),
1616
- /** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
1617
- // Entries are matched literally against the browser's
1618
- // `Origin` header, so `"*"` and a trailing slash match nothing at all -
1619
- // fail-closed (a lockout, not a bypass), but silently, and a lockout with
1620
- // no explanation is the kind of thing an operator "fixes" by disabling the
1621
- // check. Reject those two shapes with a pointer instead.
1767
+ // Browser origins allowed to call the gateway; none by default. A request
1768
+ // with any other `Origin` gets 403 on every route. Entries match the
1769
+ // `Origin` header exactly (ignoring case), so `"*"` and a trailing slash
1770
+ // would match nothing: a silent lockout that an operator might "fix" by
1771
+ // disabling the check. Both are refused with a pointer.
1622
1772
  allowedOrigins: z.array(
1623
1773
  z.string().min(1).refine((origin) => origin !== "*", {
1624
1774
  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.`
@@ -1637,16 +1787,14 @@ var init_schema = __esm({
1637
1787
  "/health",
1638
1788
  "/ready",
1639
1789
  "/.well-known/agent-commerce",
1640
- // Fixed by the A2A specification, so it is the adapter's to serve and never
1641
- // a configurable mount's to claim.
1790
+ // Fixed by the A2A specification and served by the adapter
1642
1791
  "/.well-known/agent-card.json",
1643
- // Likewise fixed by the ACP specification.
1792
+ // Fixed by the ACP specification
1644
1793
  ACP_WELL_KNOWN_PATH,
1645
1794
  "/api/resources",
1646
1795
  "/api/resources/:id/invoke",
1647
1796
  "/api/receipts",
1648
- "/api/events",
1649
- "/api/events/stream"
1797
+ "/api/events"
1650
1798
  ];
1651
1799
  MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1652
1800
  message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
@@ -1678,16 +1826,14 @@ var init_schema = __esm({
1678
1826
  enabled: BooleanOrString,
1679
1827
  mountPath: MountPathSchema
1680
1828
  }).strict(),
1681
- // Optional block: A2A is off unless an operator asks for it, and an
1682
- // existing config predating the adapter stays valid.
1829
+ // Optional; A2A is off unless configured
1683
1830
  a2a: z.object({
1684
1831
  enabled: BooleanOrString,
1685
1832
  mountPath: MountPathSchema.optional()
1686
1833
  }).strict().optional(),
1687
- // Likewise optional and off by default. The sub-blocks are optional here
1688
- // and required by the business pass only when ACP is enabled, so a
1689
- // disabled placeholder block stays writable and an enabled one gets a
1690
- // message naming the piece it is missing.
1834
+ // Optional and off by default. The business pass requires the sub-blocks
1835
+ // only when ACP is enabled, so a disabled placeholder stays valid and an
1836
+ // enabled block gets a message naming what it lacks.
1691
1837
  acp: z.object({
1692
1838
  enabled: BooleanOrString,
1693
1839
  mountPath: MountPathSchema.optional(),
@@ -1743,7 +1889,7 @@ var init_schema = __esm({
1743
1889
  z.object({ type: z.literal("none") }).strict(),
1744
1890
  z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
1745
1891
  // Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
1746
- // which signs a fresh JWT per request; a static header cannot express it.
1892
+ // which signs a fresh JWT per request.
1747
1893
  z.object({
1748
1894
  type: z.literal("cdp"),
1749
1895
  apiKeyId: z.string().min(1),
@@ -1758,10 +1904,8 @@ var init_schema = __esm({
1758
1904
  z.object({
1759
1905
  mode: z.literal("remote"),
1760
1906
  url: z.string().min(1),
1761
- // Absent means "this facilitator takes no credential" - an explicit
1762
- // statement, normalised to `{ type: 'none' }` below. On a mainnet that
1763
- // combination is refused outright, so the default can never quietly
1764
- // become an unauthenticated production facilitator.
1907
+ // Absent auth sends no credential and normalizes to `{ type: 'none' }`,
1908
+ // which on mainnet requires `allowUnauthenticatedFacilitator`
1765
1909
  auth: FacilitatorAuthSchema.optional()
1766
1910
  }).strict()
1767
1911
  ]);
@@ -1776,13 +1920,31 @@ var init_schema = __esm({
1776
1920
  payTo: z.string().min(1),
1777
1921
  maxTimeoutSeconds: NumberOrString,
1778
1922
  facilitator: FacilitatorSchema,
1779
- /** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
1923
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1924
+ allowMainnet: BooleanOrString.optional(),
1925
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
1926
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
1927
+ }).strict();
1928
+ MppSchema = z.object({
1929
+ enabled: BooleanOrString,
1930
+ network: z.string().min(1).optional(),
1931
+ rpcUrl: z.string().min(1),
1932
+ asset: z.string().min(1),
1933
+ assetName: z.string().min(1),
1934
+ assetVersion: z.string().min(1),
1935
+ recipient: z.string().min(1),
1936
+ realm: z.string().min(1),
1937
+ challengeSecret: z.string().min(1),
1938
+ challengeTtlSeconds: NumberOrString.optional(),
1939
+ facilitator: FacilitatorSchema,
1940
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1780
1941
  allowMainnet: BooleanOrString.optional(),
1781
- /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1942
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
1782
1943
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
1783
1944
  }).strict();
1784
1945
  PaymentsSchema = z.object({
1785
- x402: X402Schema.optional()
1946
+ x402: X402Schema.optional(),
1947
+ mpp: MppSchema.optional()
1786
1948
  }).strict();
1787
1949
  Ap2KeySchema = z.object({
1788
1950
  kid: z.string().min(1),
@@ -1790,11 +1952,8 @@ var init_schema = __esm({
1790
1952
  }).strict();
1791
1953
  Ap2IssuerSchema = z.object({
1792
1954
  issuer: z.string().min(1),
1793
- /**
1794
- * Required, not defaulted. Without it, a mandate minted for another
1795
- * merchant would verify here, and there is no value worth guessing for
1796
- * something that decides that.
1797
- */
1955
+ // Required, not defaulted: without it a mandate minted for another
1956
+ // merchant would verify here
1798
1957
  audience: z.string().min(1),
1799
1958
  keys: z.array(Ap2KeySchema).min(1)
1800
1959
  }).strict();
@@ -1821,7 +1980,7 @@ var init_schema = __esm({
1821
1980
  authorization: AuthorizationSchema.optional()
1822
1981
  }).strict();
1823
1982
  SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1824
- SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1983
+ SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
1825
1984
  SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
1826
1985
  EMPTY_CLOSED_OBJECT_SCHEMA = {
1827
1986
  type: "object",
@@ -1876,9 +2035,8 @@ async function loadConfig(options = {}) {
1876
2035
  `Configuration file "${configPath}" is not valid YAML: ${reason}`,
1877
2036
  {
1878
2037
  details: { path: configPath, ...details },
1879
- // Retained for programmatic callers. Nothing in the CLI prints a
1880
- // `cause`, and nothing should start: this is the object whose
1881
- // `.message` carries the source excerpt (see below).
2038
+ // Kept for programmatic callers. Never print it: its `.message` quotes
2039
+ // the source line (see describeYamlError).
1882
2040
  cause: error
1883
2041
  }
1884
2042
  );
@@ -1949,9 +2107,9 @@ init_core();
1949
2107
  function defaultDemoSteps() {
1950
2108
  return [
1951
2109
  {
1952
- name: "Start local chain + gateway + merchant (docker compose)",
2110
+ name: "Start the local stack: chain, merchant, gateway and dashboard (docker compose)",
1953
2111
  command: "docker",
1954
- args: ["compose", "up", "--build", "-d"]
2112
+ args: ["compose", "up", "--build", "--detach", "--wait"]
1955
2113
  },
1956
2114
  { name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
1957
2115
  ];
@@ -1959,7 +2117,7 @@ function defaultDemoSteps() {
1959
2117
  async function runDemo(io, deps = {}) {
1960
2118
  const steps = deps.steps ?? defaultDemoSteps();
1961
2119
  const run = deps.run ?? execCommand;
1962
- io.stdout("agent-commerce demo \u2014 running the documented quickstart steps:");
2120
+ io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
1963
2121
  for (const step of steps) {
1964
2122
  io.stdout("");
1965
2123
  io.stdout(`==> ${step.name}`);
@@ -1975,13 +2133,6 @@ async function runDemo(io, deps = {}) {
1975
2133
  io.stderr(` ${step.command} ${step.args.join(" ")}`);
1976
2134
  return 1;
1977
2135
  }
1978
- if (step.healthCheck !== void 0) {
1979
- const healthy = await step.healthCheck();
1980
- if (!healthy) {
1981
- io.stderr(`FAIL "${step.name}" completed but its health check did not pass.`);
1982
- return 1;
1983
- }
1984
- }
1985
2136
  io.stdout(`OK ${step.name}`);
1986
2137
  }
1987
2138
  io.stdout("");
@@ -2012,8 +2163,7 @@ init_constants();
2012
2163
  // src/authorization/ap2/descriptor.ts
2013
2164
  init_constants();
2014
2165
  var AP2_UNSUPPORTED = [
2015
- // Mandate kinds. Open mandates carry spending constraints nothing here
2016
- // evaluates, so accepting one would tell a buyer their limits were checked.
2166
+ // Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
2017
2167
  "autonomous mode",
2018
2168
  "open checkout mandates (mandate.checkout.open.1)",
2019
2169
  "intent mandates",
@@ -2029,13 +2179,13 @@ var AP2_UNSUPPORTED = [
2029
2179
  "digest algorithms other than sha-256",
2030
2180
  // Roles this gateway does not play
2031
2181
  "mandate issuance",
2032
- "merchant checkout JWT issuance",
2033
2182
  "AP2 over the ACP checkout adapter"
2034
2183
  ];
2035
2184
 
2036
2185
  // src/cli/commands/doctor.ts
2037
- init_execution();
2038
2186
  init_core();
2187
+ init_execution();
2188
+ init_constants2();
2039
2189
  init_networks();
2040
2190
 
2041
2191
  // src/protocols/a2a/constants.ts
@@ -2058,13 +2208,12 @@ var A2A_UNSUPPORTED_METHODS = [
2058
2208
 
2059
2209
  // src/protocols/a2a/descriptor.ts
2060
2210
  var A2A_UNSUPPORTED = [
2061
- // Methods, named as the protocol names them. Same list the transport
2062
- // rejects by name, so the descriptor cannot promise less than it refuses.
2211
+ // Methods, as the protocol names them; the transport refuses the same list
2063
2212
  ...A2A_UNSUPPORTED_METHODS,
2064
- // Transports other than the one binding served.
2213
+ // Transports other than the one binding served
2065
2214
  "HTTP+JSON/REST binding",
2066
2215
  "gRPC binding",
2067
- // Behaviours.
2216
+ // Behaviors
2068
2217
  "SSE",
2069
2218
  "long-running task persistence",
2070
2219
  "task resumption",
@@ -2076,10 +2225,10 @@ var A2A_UNSUPPORTED = [
2076
2225
  ];
2077
2226
 
2078
2227
  // src/cli/commands/doctor.ts
2079
- init_constants2();
2228
+ init_constants3();
2080
2229
 
2081
2230
  // src/protocols/acp/descriptor.ts
2082
- init_constants2();
2231
+ init_constants3();
2083
2232
  [
2084
2233
  "rest transport",
2085
2234
  "well-known discovery",
@@ -2096,15 +2245,15 @@ var ACP_UNSUPPORTED = [
2096
2245
  "standalone orders service",
2097
2246
  "delegate_payment",
2098
2247
  "delegate_authentication",
2099
- // Bindings and delivery directions.
2248
+ // Bindings and delivery directions
2100
2249
  "ACP MCP transport binding",
2101
2250
  "webhooks",
2102
2251
  "outbound merchant-to-agent delivery",
2103
2252
  "ACP client role",
2104
- // Request authentication beyond the bearer token.
2253
+ // Request authentication beyond the bearer token
2105
2254
  "Signature header verification",
2106
2255
  "Timestamp replay-window verification",
2107
- // Extensions and versioning.
2256
+ // Extensions, versioning and payment handlers
2108
2257
  "discount extension",
2109
2258
  "general ACP extension framework",
2110
2259
  "multiple ACP API versions",
@@ -2113,7 +2262,7 @@ var ACP_UNSUPPORTED = [
2113
2262
 
2114
2263
  // src/storage/receipts/store.ts
2115
2264
  init_core();
2116
- var PACKAGE_VERSION = "1.4.1" ;
2265
+ var PACKAGE_VERSION = "1.6.0" ;
2117
2266
 
2118
2267
  // src/storage/sqlite.ts
2119
2268
  init_core();
@@ -2121,10 +2270,7 @@ function openSqliteDatabase(options) {
2121
2270
  const { path: path2, label, logger } = options;
2122
2271
  const isFileBacked = path2 !== ":memory:";
2123
2272
  if (isFileBacked) {
2124
- const dir = dirname(path2);
2125
- if (dir !== "" && dir !== "." && !existsSync(dir)) {
2126
- mkdirSync(dir, { recursive: true, mode: 448 });
2127
- }
2273
+ mkdirSync(dirname(path2), { recursive: true, mode: 448 });
2128
2274
  }
2129
2275
  if (isFileBacked && existsSync(path2)) {
2130
2276
  try {
@@ -2196,8 +2342,8 @@ function receiptToRow(receipt) {
2196
2342
  payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
2197
2343
  delivered_at: receipt.deliveredAt,
2198
2344
  backend_status: receipt.backendStatus,
2199
- duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
2200
- protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
2345
+ duration_ms: receipt.durationMs ?? null,
2346
+ protocol: receipt.protocol ?? null,
2201
2347
  metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
2202
2348
  authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
2203
2349
  };
@@ -2224,12 +2370,12 @@ function eventToRow(event) {
2224
2370
  id: event.id,
2225
2371
  type: event.type,
2226
2372
  request_id: event.requestId,
2227
- resource_id: event.resourceId !== void 0 ? event.resourceId : null,
2373
+ resource_id: event.resourceId ?? null,
2228
2374
  at: event.at,
2229
- adapter: event.adapter !== void 0 ? event.adapter : null,
2230
- payment_provider: event.paymentProvider !== void 0 ? event.paymentProvider : null,
2231
- duration_ms: event.durationMs !== void 0 ? event.durationMs : null,
2232
- status: event.status !== void 0 ? event.status : null,
2375
+ adapter: event.adapter ?? null,
2376
+ payment_provider: event.paymentProvider ?? null,
2377
+ duration_ms: event.durationMs ?? null,
2378
+ status: event.status ?? null,
2233
2379
  data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
2234
2380
  };
2235
2381
  }
@@ -2333,7 +2479,7 @@ var MIGRATIONS = [
2333
2479
  }
2334
2480
  }
2335
2481
  ];
2336
- var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
2482
+ var SCHEMA_VERSION = MIGRATIONS.at(-1)?.version ?? 0;
2337
2483
  function migrate(db) {
2338
2484
  const current = db.pragma("user_version", { simple: true });
2339
2485
  for (const migration of MIGRATIONS) {
@@ -2396,10 +2542,8 @@ function createSqliteReceiptStore(options) {
2396
2542
  );
2397
2543
  const insertPaymentAttemptStmt = db.prepare(
2398
2544
  `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)
2399
- VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)`
2400
- );
2401
- const getPaymentAttemptByIdStmt = db.prepare(
2402
- "SELECT * FROM payment_attempts WHERE id = ?"
2545
+ VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
2546
+ RETURNING *`
2403
2547
  );
2404
2548
  const updatePaymentAttemptStmt = db.prepare(
2405
2549
  `UPDATE payment_attempts SET status = @status,
@@ -2450,8 +2594,9 @@ function createSqliteReceiptStore(options) {
2450
2594
  async reservePaymentAttempt(reservation) {
2451
2595
  const now = clock.nowIso();
2452
2596
  const id = ids.next("attempt");
2597
+ let row;
2453
2598
  try {
2454
- insertPaymentAttemptStmt.run({
2599
+ row = insertPaymentAttemptStmt.get({
2455
2600
  id,
2456
2601
  request_id: reservation.requestId,
2457
2602
  resource_id: reservation.resourceId,
@@ -2471,19 +2616,12 @@ function createSqliteReceiptStore(options) {
2471
2616
  if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
2472
2617
  throw new CommerceError(
2473
2618
  "PAYMENT_REPLAYED",
2474
- `Payment authorisation has already been used (replayKey=${reservation.replayKey})`,
2619
+ `Payment authorization has already been used (replayKey=${reservation.replayKey})`,
2475
2620
  { requestId: reservation.requestId, resourceId: reservation.resourceId }
2476
2621
  );
2477
2622
  }
2478
2623
  throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
2479
2624
  }
2480
- const row = getPaymentAttemptByIdStmt.get(id);
2481
- if (row === void 0) {
2482
- throw new CommerceError(
2483
- "STORAGE_ERROR",
2484
- "Payment attempt vanished immediately after insert"
2485
- );
2486
- }
2487
2625
  return rowToPaymentAttempt(row);
2488
2626
  },
2489
2627
  async updatePaymentAttempt(update) {
@@ -2521,11 +2659,11 @@ function createSqliteReceiptStore(options) {
2521
2659
  const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
2522
2660
  return rows.map(rowToReceipt);
2523
2661
  },
2524
- /** Exact total, ignoring the listReceipts clamp — see the interface doc for why this exists. */
2662
+ // Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
2525
2663
  async countReceipts() {
2526
2664
  return countReceiptsStmt.get()?.count ?? 0;
2527
2665
  },
2528
- /** Exact count of non-2xx receipts — see the interface doc for why this exists. */
2666
+ // Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
2529
2667
  async countUndeliveredReceipts() {
2530
2668
  return countUndeliveredReceiptsStmt.get()?.count ?? 0;
2531
2669
  },
@@ -2605,35 +2743,28 @@ function createSqliteReceiptStore(options) {
2605
2743
  }
2606
2744
 
2607
2745
  // src/cli/lib/config-client.ts
2608
- async function loadConfigDynamic(options = {}) {
2609
- let mod;
2746
+ init_core();
2747
+ async function importConfig() {
2610
2748
  try {
2611
- mod = await Promise.resolve().then(() => (init_config(), config_exports));
2749
+ return await Promise.resolve().then(() => (init_config(), config_exports));
2612
2750
  } catch (err) {
2613
- throw new Error(
2751
+ throw new CommerceError(
2752
+ "INTERNAL_ERROR",
2614
2753
  `the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
2615
2754
  );
2616
2755
  }
2617
- return mod.loadConfig(options);
2756
+ }
2757
+ async function loadConfigDynamic(options = {}) {
2758
+ return (await importConfig()).loadConfig(options);
2618
2759
  }
2619
2760
  async function parseConfigDynamic(raw, env = process.env) {
2620
- let mod;
2621
- try {
2622
- mod = await Promise.resolve().then(() => (init_config(), config_exports));
2623
- } catch (err) {
2624
- throw new Error(
2625
- `the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
2626
- );
2627
- }
2628
- return mod.parseConfig(raw, env);
2761
+ return (await importConfig()).parseConfig(raw, env);
2629
2762
  }
2630
2763
 
2631
2764
  // src/cli/lib/http.ts
2632
2765
  async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
2633
- const controller = new AbortController();
2634
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2635
2766
  try {
2636
- const res = await fetchImpl(url, { signal: controller.signal });
2767
+ const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
2637
2768
  let body;
2638
2769
  try {
2639
2770
  body = await res.json();
@@ -2647,8 +2778,6 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
2647
2778
  status: 0,
2648
2779
  error: err instanceof Error ? err.message : "unknown fetch error"
2649
2780
  };
2650
- } finally {
2651
- clearTimeout(timer);
2652
2781
  }
2653
2782
  }
2654
2783
 
@@ -2693,9 +2822,8 @@ function buildResourcesMap(answers) {
2693
2822
  resources.report = {
2694
2823
  name: "Get market report",
2695
2824
  description: "Deterministic demo premium market report.",
2696
- // No caller-supplied fields for this resource: an explicit empty,
2697
- // closed schema (rather than omitting `input`) so a missing schema can
2698
- // never be read as "anything goes".
2825
+ // No caller-supplied fields. Config loads a missing `input` as this same
2826
+ // closed schema; writing it out shows that in the generated file
2699
2827
  input: {
2700
2828
  type: "object",
2701
2829
  properties: {},
@@ -2746,25 +2874,22 @@ function buildInitConfigObject(answers) {
2746
2874
  }
2747
2875
  var HEADER = `# Generated by \`agent-commerce init\`.
2748
2876
  #
2749
- # Any address/key below labelled "LOCAL DEVELOPMENT ONLY" or "placeholder" is
2750
- # either one of Anvil's well-known, public dev accounts (never fund them,
2751
- # never use them outside a local chain) or a
2752
- # not-yet-real address. Before running against the local chain, run
2753
- # \`npm run chain:deploy\` and replace \`payments.x402.asset\` with the deployed
2754
- # MockUSDC address from \`.deploy/local.json\`. Replace \`payments.x402.payTo\`
2755
- # with your real merchant-controlled settlement address before ever pointing
2756
- # this at a funded network.
2877
+ # With x402 enabled, the facilitator signerPrivateKey is Anvil's public dev
2878
+ # account #0 and the default payTo is account #1.
2879
+ # LOCAL DEVELOPMENT ONLY - DO NOT FUND: never use them outside a local chain.
2880
+ # payments.x402.asset is a placeholder, not a real contract. Before running
2881
+ # against the local chain, run \`npm run chain:deploy\` and set it to the
2882
+ # MockUSDC address in \`.deploy/local.json\`. Before pointing this at a funded
2883
+ # network, set \`payments.x402.payTo\` to your own settlement address.
2757
2884
  #
2758
- # server.host is loopback (127.0.0.1) on purpose: binding every interface
2759
- # should be a deliberate edit, not something you inherit from a generator.
2885
+ # server.host is loopback (127.0.0.1): binding every interface should be a
2886
+ # deliberate edit.
2760
2887
  #
2761
- # server.adminToken / server.allowedOrigins are left unset here \u2014 a generated
2762
- # secret in a file you might commit is its own problem, so this is left as a
2763
- # placeholder for you to fill in rather than a real token. Without a token,
2764
- # the operator routes (/api/receipts, /api/events, /api/events/stream) \u2014
2765
- # the merchant's commerce ledger: payer addresses, amounts, settlement
2766
- # hashes \u2014 return 404 rather than serving openly. To enable them, uncomment
2767
- # and set:
2888
+ # server.adminToken and server.allowedOrigins are left unset, so no secret
2889
+ # lands in a file you might commit. Without a token the operator routes
2890
+ # (/api/receipts, /api/events) return 404. They serve the merchant's commerce
2891
+ # ledger: payer addresses, amounts and settlement hashes. To enable them, add
2892
+ # these keys to the server block below:
2768
2893
  # server:
2769
2894
  # adminToken: \${ADMIN_TOKEN}
2770
2895
  # allowedOrigins:
@@ -2776,18 +2901,30 @@ function renderInitConfigYaml(answers) {
2776
2901
  return `${HEADER}
2777
2902
  ${body}`;
2778
2903
  }
2904
+
2905
+ // src/payments/x402/local-chain/manifest.ts
2906
+ init_core();
2779
2907
  var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
2780
- var REQUIRED_TOP_LEVEL_STRING_FIELDS = [
2781
- "rpcUrl",
2782
- "asset",
2783
- "assetName",
2784
- "assetVersion",
2785
- "buyerInitialBalance"
2786
- ];
2908
+ var nonEmpty = z.string().min(1, "must be a non-empty string");
2909
+ var keyedAccount = z.object({ address: z.string(), privateKey: z.string(), note: z.string() });
2910
+ var MANIFEST_SCHEMA = z.object({
2911
+ chainId: z.number(),
2912
+ rpcUrl: nonEmpty,
2913
+ hostRpcUrl: nonEmpty.optional(),
2914
+ asset: nonEmpty,
2915
+ assetName: nonEmpty,
2916
+ assetVersion: nonEmpty,
2917
+ assetDecimals: z.number(),
2918
+ merchant: z.object({ address: z.string(), privateKeyLabel: z.string() }),
2919
+ buyer: keyedAccount,
2920
+ facilitator: keyedAccount,
2921
+ buyerInitialBalance: nonEmpty
2922
+ });
2787
2923
  function readLocalChainManifest(cwd = process.cwd()) {
2788
2924
  const path2 = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
2789
2925
  if (!existsSync(path2)) {
2790
- throw new Error(
2926
+ throw new CommerceError(
2927
+ "CONFIG_INVALID",
2791
2928
  `Local chain manifest not found at "${path2}". Run "npm run chain:start" then "npm run chain:deploy" first.`
2792
2929
  );
2793
2930
  }
@@ -2795,56 +2932,30 @@ function readLocalChainManifest(cwd = process.cwd()) {
2795
2932
  try {
2796
2933
  raw = readFileSync(path2, "utf8");
2797
2934
  } catch (cause) {
2798
- throw new Error(`Could not read local chain manifest at "${path2}".`, { cause });
2935
+ throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${path2}".`, {
2936
+ cause
2937
+ });
2799
2938
  }
2800
2939
  let parsed;
2801
2940
  try {
2802
2941
  parsed = JSON.parse(raw);
2803
2942
  } catch (cause) {
2804
- throw new Error(
2943
+ throw new CommerceError(
2944
+ "CONFIG_INVALID",
2805
2945
  `Local chain manifest at "${path2}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
2806
2946
  { cause }
2807
2947
  );
2808
2948
  }
2809
- return validateManifestShape(parsed, path2);
2810
- }
2811
- function validateManifestShape(value, path2) {
2812
- const fail = (detail) => {
2813
- throw new Error(
2814
- `Local chain manifest at "${path2}" is invalid: ${detail}. Re-run "npm run chain:deploy".`
2949
+ const result = MANIFEST_SCHEMA.safeParse(parsed);
2950
+ if (!result.success) {
2951
+ const issue = result.error.issues[0];
2952
+ const where = issue?.path.length ? `${issue.path.join(".")}: ` : "";
2953
+ throw new CommerceError(
2954
+ "CONFIG_INVALID",
2955
+ `Local chain manifest at "${path2}" is invalid: ${where}${issue?.message ?? "unexpected shape"}. Re-run "npm run chain:deploy".`
2815
2956
  );
2816
- };
2817
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2818
- return fail("expected a JSON object");
2819
- }
2820
- const record = value;
2821
- if (typeof record.chainId !== "number") {
2822
- return fail('"chainId" must be a number');
2823
2957
  }
2824
- if (typeof record.assetDecimals !== "number") {
2825
- return fail('"assetDecimals" must be a number');
2826
- }
2827
- for (const field of REQUIRED_TOP_LEVEL_STRING_FIELDS) {
2828
- if (typeof record[field] !== "string" || record[field] === "") {
2829
- return fail(`"${field}" must be a non-empty string`);
2830
- }
2831
- }
2832
- if ("hostRpcUrl" in record && record.hostRpcUrl !== void 0) {
2833
- if (typeof record.hostRpcUrl !== "string" || record.hostRpcUrl === "") {
2834
- return fail('"hostRpcUrl" must be a non-empty string when present');
2835
- }
2836
- }
2837
- const merchant = record.merchant;
2838
- if (typeof merchant !== "object" || merchant === null || typeof merchant.address !== "string" || typeof merchant.privateKeyLabel !== "string") {
2839
- return fail('"merchant" must be { address: string, privateKeyLabel: string }');
2840
- }
2841
- for (const key of ["buyer", "facilitator"]) {
2842
- const account = record[key];
2843
- if (typeof account !== "object" || account === null || typeof account.address !== "string" || typeof account.privateKey !== "string" || typeof account.note !== "string") {
2844
- return fail(`"${key}" must be { address: string, privateKey: string, note: string }`);
2845
- }
2846
- }
2847
- return record;
2958
+ return result.data;
2848
2959
  }
2849
2960
 
2850
2961
  // src/cli/lib/manifest-env.ts
@@ -2882,12 +2993,63 @@ function maskMiddle(value, keep = 6) {
2882
2993
  if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
2883
2994
  return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
2884
2995
  }
2885
- createRequire(import.meta.url);
2996
+
2997
+ // package.json
2998
+ var package_default = {
2999
+ dependencies: {
3000
+ "@clack/prompts": "1.7.0",
3001
+ "@scalar/openapi-parser": "0.29.0",
3002
+ ajv: "8.20.0",
3003
+ "ajv-formats": "3.0.1",
3004
+ "better-sqlite3": "13.0.3",
3005
+ commander: "15.0.0",
3006
+ fastify: "5.12.0",
3007
+ picocolors: "1.1.1",
3008
+ pino: "10.3.1",
3009
+ yaml: "2.9.0",
3010
+ zod: "3.25.76"
3011
+ },
3012
+ peerDependencies: {
3013
+ "@coinbase/x402": "2.1.0",
3014
+ "@modelcontextprotocol/sdk": "1.30.0",
3015
+ "@sd-jwt/core": "0.20.1",
3016
+ "@x402/core": "2.23.0",
3017
+ "@x402/evm": "2.23.0",
3018
+ canonicalize: "5.0.0",
3019
+ jose: "6.2.12",
3020
+ mppx: "0.10.1",
3021
+ viem: "2.55.18"
3022
+ }};
3023
+
3024
+ // src/cli/lib/versions.ts
3025
+ var PINS = [
3026
+ ["@modelcontextprotocol/sdk", "protocols/mcp"],
3027
+ ["ajv", "protocols/acp"],
3028
+ ["ajv-formats", "protocols/acp"],
3029
+ ["@x402/core", "payments/x402"],
3030
+ ["@x402/evm", "payments/x402"],
3031
+ ["@coinbase/x402", "payments/x402"],
3032
+ ["viem", "payments/x402"],
3033
+ ["mppx", "payments/mpp"],
3034
+ ["jose", "authorization/ap2"],
3035
+ ["@sd-jwt/core", "authorization/ap2"],
3036
+ ["canonicalize", "authorization/ap2"],
3037
+ ["@scalar/openapi-parser", "openapi"],
3038
+ ["zod", "config"],
3039
+ ["fastify", "gateway"],
3040
+ ["pino", "gateway"],
3041
+ ["better-sqlite3", "storage"]
3042
+ ];
2886
3043
  function readVersionReport() {
2887
- const injected = '{"cliVersion":"1.4.1","pinned":[{"name":"@coinbase/x402","version":"2.1.0","via":"@devlab.group/agent-commerce"},{"name":"@modelcontextprotocol/sdk","version":"1.30.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/core","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/evm","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"viem","version":"2.55.18","via":"@devlab.group/agent-commerce"},{"name":"better-sqlite3","version":"13.0.3","via":"@devlab.group/agent-commerce"},{"name":"fastify","version":"5.12.0","via":"@devlab.group/agent-commerce"},{"name":"pino","version":"10.3.1","via":"@devlab.group/agent-commerce"},{"name":"zod","version":"3.25.76","via":"@devlab.group/agent-commerce"}]}' ;
2888
- {
2889
- return JSON.parse(injected);
2890
- }
3044
+ const declared = {
3045
+ ...package_default.peerDependencies,
3046
+ ...package_default.dependencies
3047
+ };
3048
+ const pinned = PINS.flatMap(([name, via]) => {
3049
+ const version = declared[name];
3050
+ return version === void 0 ? [] : [{ name, version, via }];
3051
+ });
3052
+ return { cliVersion: PACKAGE_VERSION, pinned };
2891
3053
  }
2892
3054
 
2893
3055
  // src/cli/commands/doctor.ts
@@ -2905,19 +3067,20 @@ function deriveGatewayUrl(explicit, config) {
2905
3067
  function substitutePathParams(url) {
2906
3068
  let filled = url;
2907
3069
  for (const name of extractPathParameterNames(url)) {
2908
- filled = filled.split(`{${name}}`).join("demo-check");
3070
+ filled = filled.replaceAll(`{${name}}`, "demo-check");
2909
3071
  }
2910
3072
  return filled;
2911
3073
  }
2912
- function extractWellKnownX402(body) {
3074
+ function extractWellKnownX402(body, rail = "x402") {
2913
3075
  const payments = body?.["payments"];
2914
3076
  if (typeof payments !== "object" || payments === null) return void 0;
2915
- const x402 = payments["x402"];
2916
- if (typeof x402 !== "object" || x402 === null) return void 0;
2917
- const rec = x402;
3077
+ const block = payments[rail];
3078
+ if (typeof block !== "object" || block === null) return void 0;
3079
+ const rec = block;
2918
3080
  if (rec["enabled"] === false) return "disabled";
2919
3081
  if (rec["enabled"] !== true) return void 0;
2920
- const { asset, network, payTo } = rec;
3082
+ const { asset, network } = rec;
3083
+ const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
2921
3084
  if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
2922
3085
  return void 0;
2923
3086
  }
@@ -2926,7 +3089,7 @@ function extractWellKnownX402(body) {
2926
3089
  function sameAddress2(a, b) {
2927
3090
  return a.toLowerCase() === b.toLowerCase();
2928
3091
  }
2929
- function findX402Mismatch(configured, live) {
3092
+ function findX402Mismatch(configured, live, payToField = "payTo") {
2930
3093
  const diffs = [];
2931
3094
  if (!sameAddress2(configured.asset, live.asset)) {
2932
3095
  diffs.push(
@@ -2940,11 +3103,26 @@ function findX402Mismatch(configured, live) {
2940
3103
  }
2941
3104
  if (!sameAddress2(configured.payTo, live.payTo)) {
2942
3105
  diffs.push(
2943
- `payTo: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
3106
+ `${payToField}: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
2944
3107
  );
2945
3108
  }
2946
3109
  if (diffs.length === 0) return void 0;
2947
- return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
3110
+ return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
3111
+ }
3112
+ function describeSettlement(network, facilitator) {
3113
+ const profile = findNetworkProfile(network);
3114
+ const mode = profile ? resolveDeploymentMode(profile, facilitator.mode) : void 0;
3115
+ 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})`;
3116
+ const via = facilitator.mode === "local" ? "local (in-process)" : `remote (auth=${facilitator.auth.type})`;
3117
+ return { mode, where, via };
3118
+ }
3119
+ function mainnetSafetyCheck(name, payToField, facilitator) {
3120
+ const unauthenticated = facilitator.mode === "remote" && facilitator.auth.type === "none";
3121
+ return {
3122
+ name,
3123
+ status: unauthenticated ? "WARN" : "INFO",
3124
+ 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.`
3125
+ };
2948
3126
  }
2949
3127
  function describeIssuers(label, issuers) {
2950
3128
  const described = issuers.map(
@@ -2960,72 +3138,26 @@ function isWritableFile(path2) {
2960
3138
  return false;
2961
3139
  }
2962
3140
  }
2963
- function ap2ReplayCheck(path2) {
2964
- const name = "AP2 replay store";
3141
+ function storeFileCheck(name, path2, risk, suffix = "") {
2965
3142
  if (path2 === ":memory:") {
2966
3143
  return {
2967
3144
  name,
2968
3145
  status: "WARN",
2969
- detail: "in-memory store - every spent mandate is forgotten on restart, so one could authorize a second purchase; use a file path in production"
3146
+ detail: `in-memory store: ${risk.inMemory}; use a file path in production${suffix}`
2970
3147
  };
2971
3148
  }
2972
3149
  if (existsSync(path2)) {
2973
- return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"` } : {
3150
+ return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"${suffix}` } : {
2974
3151
  name,
2975
3152
  status: "FAIL",
2976
- detail: `"${path2}" exists but is not writable by this user - no mandate could be recorded as spent`
3153
+ detail: `"${path2}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
2977
3154
  };
2978
3155
  }
2979
- const directory = dirname(path2);
2980
- if (directory !== "" && directory !== "." && !existsSync(directory)) {
2981
- return {
2982
- name,
2983
- status: "WARN",
2984
- detail: `no store yet at "${path2}" and its directory does not exist - it is created when the gateway starts, provided that path is writable`
2985
- };
2986
- }
2987
- return {
2988
- name,
2989
- status: "WARN",
2990
- detail: `no store yet at "${path2}" - it is created the first time the gateway starts`
2991
- };
3156
+ return { name, status: "WARN", detail: `${missingStoreDetail(path2)}${suffix}` };
2992
3157
  }
2993
- function acpIdempotencyCheck(idempotency) {
2994
- const retention = `retention ${idempotency.retentionHours}h`;
2995
- if (idempotency.retentionHours < 24) {
2996
- return {
2997
- name: "ACP idempotency",
2998
- status: "FAIL",
2999
- detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
3000
- };
3001
- }
3002
- if (idempotency.path === ":memory:") {
3003
- return {
3004
- name: "ACP idempotency",
3005
- status: "WARN",
3006
- detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
3007
- };
3008
- }
3009
- const directory = dirname(idempotency.path);
3010
- if (existsSync(idempotency.path)) {
3011
- return isWritableFile(idempotency.path) ? { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` } : {
3012
- name: "ACP idempotency",
3013
- status: "FAIL",
3014
- detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
3015
- };
3016
- }
3017
- if (directory !== "" && directory !== "." && !existsSync(directory)) {
3018
- return {
3019
- name: "ACP idempotency",
3020
- status: "WARN",
3021
- detail: `no store yet at "${idempotency.path}" and its directory does not exist \u2014 it is created when the gateway starts, provided that path is writable`
3022
- };
3023
- }
3024
- return {
3025
- name: "ACP idempotency",
3026
- status: "WARN",
3027
- detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
3028
- };
3158
+ function missingStoreDetail(path2) {
3159
+ const directory = dirname(path2);
3160
+ 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`;
3029
3161
  }
3030
3162
  function acpMappingCheck(config, operations) {
3031
3163
  const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
@@ -3069,11 +3201,11 @@ async function runDoctor(options, deps = {}) {
3069
3201
  checks.push({
3070
3202
  name: "Config",
3071
3203
  status: "PASS",
3072
- detail: `valid \u2014 ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
3204
+ detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
3073
3205
  });
3074
3206
  } catch (err) {
3075
3207
  const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
3076
- const hint = !manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable) ? ` \u2014 run "npm run chain:deploy" (writes ${LOCAL_CHAIN_MANIFEST_PATH}, read automatically for local X402_*/MERCHANT_WALLET placeholders)` : "";
3208
+ 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` : "";
3077
3209
  checks.push({
3078
3210
  name: "Config",
3079
3211
  status: "FAIL",
@@ -3101,7 +3233,7 @@ async function runDoctor(options, deps = {}) {
3101
3233
  );
3102
3234
  }
3103
3235
  if (config === void 0) {
3104
- checks.push({ name: "Backend", status: "WARN", detail: "skipped \u2014 config invalid" });
3236
+ checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
3105
3237
  } else if (config.resources.length === 0) {
3106
3238
  checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
3107
3239
  } else {
@@ -3124,7 +3256,7 @@ async function runDoctor(options, deps = {}) {
3124
3256
  CHECK_TIMEOUT_MS
3125
3257
  ) : void 0;
3126
3258
  if (config === void 0) {
3127
- checks.push({ name: "Protocols", status: "WARN", detail: "skipped \u2014 config invalid" });
3259
+ checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
3128
3260
  } else if (wellKnown === void 0 || !wellKnown.ok) {
3129
3261
  checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
3130
3262
  } else {
@@ -3137,7 +3269,7 @@ async function runDoctor(options, deps = {}) {
3137
3269
  });
3138
3270
  }
3139
3271
  if (config === void 0) {
3140
- checks.push({ name: "A2A", status: "WARN", detail: "skipped \u2014 config invalid" });
3272
+ checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
3141
3273
  } else if (!config.protocols.a2a.enabled) {
3142
3274
  checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
3143
3275
  } else {
@@ -3153,7 +3285,7 @@ async function runDoctor(options, deps = {}) {
3153
3285
  });
3154
3286
  }
3155
3287
  if (config === void 0) {
3156
- checks.push({ name: "ACP", status: "WARN", detail: "skipped \u2014 config invalid" });
3288
+ checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
3157
3289
  } else if (!config.protocols.acp.enabled) {
3158
3290
  checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
3159
3291
  } else {
@@ -3168,7 +3300,17 @@ async function runDoctor(options, deps = {}) {
3168
3300
  status: "PASS",
3169
3301
  detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
3170
3302
  });
3171
- checks.push(acpIdempotencyCheck(acp.idempotency));
3303
+ checks.push(
3304
+ storeFileCheck(
3305
+ "ACP idempotency",
3306
+ acp.idempotency.path,
3307
+ {
3308
+ inMemory: "replay protection is lost on every restart",
3309
+ unwritable: "the adapter cannot claim idempotency keys"
3310
+ },
3311
+ ` (retention ${acp.idempotency.retentionHours}h)`
3312
+ )
3313
+ );
3172
3314
  checks.push(acpMappingCheck(config, acp.checkout.operations));
3173
3315
  checks.push({
3174
3316
  name: "ACP unsupported",
@@ -3178,7 +3320,7 @@ async function runDoctor(options, deps = {}) {
3178
3320
  }
3179
3321
  const ap2 = config?.authorization?.ap2;
3180
3322
  if (config === void 0) {
3181
- checks.push({ name: "AP2", status: "WARN", detail: "skipped \u2014 config invalid" });
3323
+ checks.push({ name: "AP2", status: "WARN", detail: "skipped: config invalid" });
3182
3324
  } else if (ap2 === void 0 || !ap2.enabled) {
3183
3325
  checks.push({ name: "AP2", status: "INFO", detail: "disabled" });
3184
3326
  } else {
@@ -3192,14 +3334,19 @@ async function runDoctor(options, deps = {}) {
3192
3334
  status: "PASS",
3193
3335
  detail: `${describeIssuers("mandate", ap2.trust.mandateIssuers)} \xB7 ${describeIssuers("checkout", ap2.trust.checkoutIssuers)}`
3194
3336
  });
3195
- checks.push(ap2ReplayCheck(ap2.replay.path));
3337
+ checks.push(
3338
+ storeFileCheck("AP2 replay store", ap2.replay.path, {
3339
+ inMemory: "every spent mandate is forgotten on restart, so one could authorize a second purchase",
3340
+ unwritable: "no mandate could be recorded as spent"
3341
+ })
3342
+ );
3196
3343
  const gated = config.resources.filter(
3197
3344
  (resource) => resource.authorization?.required.includes("ap2")
3198
3345
  );
3199
3346
  checks.push({
3200
3347
  name: "AP2 resources",
3201
3348
  status: gated.length > 0 ? "PASS" : "WARN",
3202
- detail: gated.length > 0 ? gated.map((resource) => resource.id).join(", ") : "AP2 is enabled but no resource requires it \u2014 every purchase settles without a mandate"
3349
+ detail: gated.length > 0 ? gated.map((resource) => resource.id).join(", ") : "AP2 is enabled but no resource requires it, so every purchase settles without a mandate"
3203
3350
  });
3204
3351
  checks.push({
3205
3352
  name: "AP2 unsupported",
@@ -3211,23 +3358,20 @@ async function runDoctor(options, deps = {}) {
3211
3358
  if (x402 === void 0 || !x402.enabled) {
3212
3359
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
3213
3360
  } else {
3214
- const profile = findNetworkProfile(x402.network);
3215
- const mode = profile ? resolveDeploymentMode(profile, x402.facilitator.mode) : void 0;
3216
- const where = profile === void 0 || mode === void 0 ? `unknown network ${x402.network}` : mode === "local" ? `LOCAL dev chain (${x402.network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${x402.network})`;
3217
- const facilitatorDetail = x402.facilitator.mode === "local" ? "local" : `remote (auth=${x402.facilitator.auth.type})`;
3218
- const summary = `x402 v2 (scheme=exact) enabled \u2014 ${where}, destination=${maskMiddle(x402.payTo)}, facilitator=${facilitatorDetail}`;
3361
+ const { mode, where, via } = describeSettlement(x402.network, x402.facilitator);
3362
+ const summary = `x402 v2 (scheme=exact) enabled - ${where}, destination=${maskMiddle(x402.payTo)}, facilitator=${via}`;
3219
3363
  const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
3220
3364
  if (sameAddress2(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
3221
3365
  checks.push({
3222
3366
  name: "Payments",
3223
3367
  status: "WARN",
3224
- detail: `${summary}, but the asset is still the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)} \u2014 no token contract exists there, so every paid call will fail. Set payments.x402.asset (\${X402_ASSET} is filled from .deploy/local.json by "npm run chain:deploy")`
3368
+ 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)`
3225
3369
  });
3226
3370
  } else if (live === "disabled") {
3227
3371
  checks.push({
3228
3372
  name: "Payments",
3229
3373
  status: "FAIL",
3230
- detail: `${summary}, but the gateway at ${gatewayUrl} reports x402 disabled \u2014 it is running a different configuration`
3374
+ detail: `${summary}, but the gateway at ${gatewayUrl} reports x402 disabled, so it is running a different configuration`
3231
3375
  });
3232
3376
  } else if (live === void 0) {
3233
3377
  checks.push({
@@ -3242,35 +3386,53 @@ async function runDoctor(options, deps = {}) {
3242
3386
  );
3243
3387
  }
3244
3388
  if (mode === "mainnet") {
3245
- const unauthenticated = x402.facilitator.mode === "remote" && x402.facilitator.auth.type === "none";
3389
+ checks.push(mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator));
3390
+ }
3391
+ }
3392
+ const mpp = config?.payments.mpp;
3393
+ if (mpp === void 0 || !mpp.enabled) {
3394
+ checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
3395
+ } else {
3396
+ const { mode, where, via } = describeSettlement(mpp.network, mpp.facilitator);
3397
+ 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}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
3398
+ const configured = { asset: mpp.asset, network: mpp.network, payTo: mpp.recipient };
3399
+ const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body, "mpp") : void 0;
3400
+ if (live === "disabled") {
3246
3401
  checks.push({
3247
- name: "Mainnet safety",
3248
- status: unauthenticated ? "WARN" : "INFO",
3249
- detail: unauthenticated ? `${describeDeploymentMode("mainnet")} \u2014 settling through a facilitator that takes no credential, accepted via allowUnauthenticatedFacilitator. It sees every payment authorisation you handle, with no account or terms behind it. Everything else is enforced at config load: explicit allowMainnet opt-in, HTTPS, non-development payTo, canonical asset. Payments are fail-closed.` : `${describeDeploymentMode("mainnet")} \u2014 enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development payTo, and the canonical asset for this network. Payments are fail-closed.`
3402
+ name: "Payments (MPP)",
3403
+ status: "FAIL",
3404
+ detail: `Local config enables MPP, but ${gatewayUrl} reports it disabled`
3250
3405
  });
3406
+ } else if (live === void 0) {
3407
+ checks.push({
3408
+ name: "Payments (MPP)",
3409
+ status: "INFO",
3410
+ detail: `${summary} (live MPP settlement config unavailable for comparison)`
3411
+ });
3412
+ } else {
3413
+ const mismatch = findX402Mismatch(configured, live, "recipient");
3414
+ checks.push(
3415
+ mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
3416
+ );
3417
+ }
3418
+ if (mode === "mainnet") {
3419
+ checks.push(mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator));
3251
3420
  }
3252
3421
  }
3253
- checks.push({
3254
- name: "Payments (MPP)",
3255
- status: "INFO",
3256
- detail: "planned \u2014 not implemented in this release"
3257
- });
3258
3422
  if (config === void 0) {
3259
- checks.push({ name: "Storage", status: "WARN", detail: "skipped \u2014 config invalid" });
3423
+ checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
3260
3424
  } else if (
3261
- // Diagnose without creating: createSqliteReceiptStore mkdir+creates the
3262
- // file, so calling it on a wrong path (a typo, or a container path read
3263
- // from the host) silently produces a fresh empty database and a false
3264
- // "healthy, receipts=0" PASS — a read-only command must not have this
3265
- // side effect, and it shadows the real store's real receipts entirely.
3266
- // `:memory:` never exists on disk by definition, so it always falls
3267
- // through to the real open/health/count branch below, same as today.
3425
+ // Diagnose without creating: createSqliteReceiptStore creates the file,
3426
+ // so opening a wrong path (a typo, or a container path read from the
3427
+ // host) would leave an empty database and report it healthy, hiding the
3428
+ // real one. `:memory:` never exists on disk, so it takes the open branch
3429
+ // below.
3268
3430
  config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
3269
3431
  ) {
3270
3432
  checks.push({
3271
3433
  name: "Storage",
3272
3434
  status: "WARN",
3273
- detail: `no store exists yet at "${config.storage.receipts.path}" \u2014 it is created automatically the first time the gateway starts. If the gateway is already running, this path does not match the one it actually uses.`
3435
+ detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
3274
3436
  });
3275
3437
  } else {
3276
3438
  let store;
@@ -3307,7 +3469,7 @@ async function runDoctor(options, deps = {}) {
3307
3469
  await store?.close();
3308
3470
  }
3309
3471
  }
3310
- if (wellKnown !== void 0 && wellKnown.ok) {
3472
+ if (wellKnown?.ok) {
3311
3473
  checks.push({
3312
3474
  name: "Protocol versions",
3313
3475
  status: "PASS",
@@ -3318,7 +3480,7 @@ async function runDoctor(options, deps = {}) {
3318
3480
  checks.push({
3319
3481
  name: "Protocol versions",
3320
3482
  status: "INFO",
3321
- detail: `gateway unreachable \u2014 local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
3483
+ detail: `gateway unreachable; local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
3322
3484
  });
3323
3485
  }
3324
3486
  const scored = checks.filter((c) => c.status !== "INFO");
@@ -3357,6 +3519,8 @@ init_core();
3357
3519
  // src/openapi/discover.ts
3358
3520
  init_errors();
3359
3521
  init_execution();
3522
+ init_is_record();
3523
+ init_constants4();
3360
3524
 
3361
3525
  // src/openapi/refs.ts
3362
3526
  init_errors();
@@ -3397,7 +3561,7 @@ function dereference(document, node, stack = []) {
3397
3561
  }
3398
3562
  function resolvePointer(document, ref) {
3399
3563
  const pointer = ref.slice(1);
3400
- if (pointer === "" || pointer === "/") return document;
3564
+ if (pointer === "") return document;
3401
3565
  if (!pointer.startsWith("/")) {
3402
3566
  throw new CommerceError(
3403
3567
  "CONFIG_INVALID",
@@ -3407,12 +3571,22 @@ function resolvePointer(document, ref) {
3407
3571
  }
3408
3572
  let current = document;
3409
3573
  for (const rawSegment of pointer.slice(1).split("/")) {
3410
- const segment = decodeURIComponent(rawSegment).replaceAll("~1", "/").replaceAll("~0", "~");
3574
+ let decoded;
3575
+ try {
3576
+ decoded = decodeURIComponent(rawSegment);
3577
+ } catch {
3578
+ throw new CommerceError(
3579
+ "CONFIG_INVALID",
3580
+ `OpenAPI reference "${ref}" contains a malformed percent-escape`,
3581
+ { details: { ref } }
3582
+ );
3583
+ }
3584
+ const segment = decoded.replaceAll("~1", "/").replaceAll("~0", "~");
3411
3585
  if (Array.isArray(current)) {
3412
3586
  const index = Number(segment);
3413
3587
  current = Number.isInteger(index) ? current[index] : void 0;
3414
3588
  } else if (typeof current === "object" && current !== null) {
3415
- current = current[segment];
3589
+ current = Object.hasOwn(current, segment) ? current[segment] : void 0;
3416
3590
  } else {
3417
3591
  current = void 0;
3418
3592
  }
@@ -3436,8 +3610,7 @@ var SUPPORTED_METHODS = {
3436
3610
  delete: "DELETE"
3437
3611
  };
3438
3612
  var NON_OPERATION_KEYS = /* @__PURE__ */ new Set(["summary", "description", "servers", "parameters", "$ref"]);
3439
- var ID_ALLOWED = /[^A-Za-z0-9_.-]+/g;
3440
- var MAX_ID_LENGTH = 128;
3613
+ var ID_DISALLOWED = new RegExp(`[^${MCP_TOOL_NAME_CHARS}]+`, "g");
3441
3614
  function discoverOperations(loaded, options = {}) {
3442
3615
  const { document } = loaded;
3443
3616
  const diagnostics = [];
@@ -3463,16 +3636,24 @@ function discoverOperations(loaded, options = {}) {
3463
3636
  }
3464
3637
  const pathItem = dereference(document, rawPathItem).value;
3465
3638
  if (!isRecord(pathItem)) continue;
3639
+ const unsupportedMethod = (name) => {
3640
+ diagnostics.push({
3641
+ severity: "warning",
3642
+ code: "unsupported-method",
3643
+ operation: `${name.toUpperCase()} ${path2}`,
3644
+ message: `Skipped ${name.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
3645
+ });
3646
+ };
3466
3647
  for (const [key, rawOperation] of Object.entries(pathItem)) {
3467
3648
  if (NON_OPERATION_KEYS.has(key) || key.startsWith("x-")) continue;
3649
+ if (key === "additionalOperations") {
3650
+ if (isRecord(rawOperation))
3651
+ for (const name of Object.keys(rawOperation)) unsupportedMethod(name);
3652
+ continue;
3653
+ }
3468
3654
  const method = SUPPORTED_METHODS[key.toLowerCase()];
3469
3655
  if (method === void 0) {
3470
- diagnostics.push({
3471
- severity: "warning",
3472
- code: "unsupported-method",
3473
- operation: `${key.toUpperCase()} ${path2}`,
3474
- message: `Skipped ${key.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
3475
- });
3656
+ unsupportedMethod(key);
3476
3657
  continue;
3477
3658
  }
3478
3659
  const operation = dereference(document, rawOperation).value;
@@ -3522,8 +3703,8 @@ function discoverOperations(loaded, options = {}) {
3522
3703
  ...description !== void 0 ? { description } : {},
3523
3704
  parameters: [
3524
3705
  ...toArray(pathItem["parameters"]),
3525
- // Operation parameters last: Phase 4 lets the later one win, which is
3526
- // the OpenAPI override rule (same name + in).
3706
+ // Operation parameters last: `mergeParameters` lets the later one win,
3707
+ // the OpenAPI override rule (same `name` and `in`)
3527
3708
  ...toArray(operation["parameters"])
3528
3709
  ],
3529
3710
  ...operation["requestBody"] !== void 0 ? { requestBody: operation["requestBody"] } : {},
@@ -3535,12 +3716,12 @@ function discoverOperations(loaded, options = {}) {
3535
3716
  return { operations, diagnostics };
3536
3717
  }
3537
3718
  function toResourceId(operationId, method, path2) {
3538
- const fromOperationId = operationId === void 0 ? "" : normaliseId(operationId);
3719
+ const fromOperationId = operationId === void 0 ? "" : normalizeId(operationId);
3539
3720
  if (fromOperationId !== "") return fromOperationId;
3540
- return normaliseId(`${method.toLowerCase()}_${path2}`);
3721
+ return normalizeId(`${method.toLowerCase()}_${path2}`);
3541
3722
  }
3542
- function normaliseId(value) {
3543
- return value.replace(ID_ALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MAX_ID_LENGTH);
3723
+ function normalizeId(value) {
3724
+ return value.replace(ID_DISALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MCP_TOOL_NAME_MAX_LENGTH);
3544
3725
  }
3545
3726
  function selectServer(document, candidates, context, options, diagnostics) {
3546
3727
  if (options.baseUrl !== void 0) return options.baseUrl;
@@ -3552,12 +3733,13 @@ function selectServer(document, candidates, context, options, diagnostics) {
3552
3733
  if (typeof url !== "string" || url === "") continue;
3553
3734
  const substituted = substituteServerVariables(url, first["variables"], context, diagnostics);
3554
3735
  if (substituted === void 0) return void 0;
3555
- if (!isAbsoluteHttpUrl(substituted)) {
3736
+ const issue = describeServerUrlIssue(substituted);
3737
+ if (issue !== void 0) {
3556
3738
  diagnostics.push({
3557
3739
  severity: "error",
3558
- code: "relative-server-url",
3740
+ code: issue.code,
3559
3741
  operation: context.resourceId,
3560
- message: `Skipped ${context.where}: server URL "${substituted}" is relative, so no backend host is known. Pass --base-url`
3742
+ message: `Skipped ${context.where}: server URL "${substituted}" ${issue.reason}. Pass --base-url`
3561
3743
  });
3562
3744
  return void 0;
3563
3745
  }
@@ -3606,12 +3788,26 @@ function substituteServerVariables(url, variables, context, diagnostics) {
3606
3788
  return substituted;
3607
3789
  }
3608
3790
  function isAbsoluteHttpUrl(value) {
3791
+ return describeServerUrlIssue(value) === void 0;
3792
+ }
3793
+ function describeServerUrlIssue(value) {
3609
3794
  try {
3610
3795
  const parsed = new URL(value);
3611
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
3612
- return parsed.search === "" && parsed.hash === "";
3796
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3797
+ return { code: "invalid-server-url", reason: "must use http:// or https://" };
3798
+ }
3799
+ if (parsed.search !== "" || parsed.hash !== "") {
3800
+ return {
3801
+ code: "invalid-server-url",
3802
+ reason: "must not contain a query string or fragment"
3803
+ };
3804
+ }
3805
+ return void 0;
3613
3806
  } catch {
3614
- return false;
3807
+ return {
3808
+ code: "relative-server-url",
3809
+ reason: "is relative or malformed, so no backend host is known"
3810
+ };
3615
3811
  }
3616
3812
  }
3617
3813
  function assertAbsoluteHttpUrl(value, label) {
@@ -3623,18 +3819,20 @@ function assertAbsoluteHttpUrl(value, label) {
3623
3819
  );
3624
3820
  }
3625
3821
  }
3626
- function isRecord(value) {
3627
- return typeof value === "object" && value !== null && !Array.isArray(value);
3628
- }
3629
3822
  function toArray(value) {
3630
3823
  return Array.isArray(value) ? value : [];
3631
3824
  }
3632
3825
 
3826
+ // src/openapi/draft.ts
3827
+ init_is_record();
3828
+
3633
3829
  // src/openapi/request.ts
3634
3830
  init_execution();
3831
+ init_is_record();
3635
3832
 
3636
3833
  // src/openapi/schema.ts
3637
3834
  init_errors();
3835
+ init_is_record();
3638
3836
  var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
3639
3837
  "object",
3640
3838
  "string",
@@ -3665,7 +3863,7 @@ function convertSchema(document, node, stack = []) {
3665
3863
  const dropped = /* @__PURE__ */ new Set();
3666
3864
  try {
3667
3865
  const schema = convertNode(document, node, stack, dropped);
3668
- return schema === void 0 ? { supported: false, reason: "schema could not be represented" } : { supported: true, schema, dropped: [...dropped] };
3866
+ return { supported: true, schema, dropped: [...dropped] };
3669
3867
  } catch (error) {
3670
3868
  if (error instanceof UnsupportedSchema) return { supported: false, reason: error.message };
3671
3869
  if (isCommerceError(error)) return { supported: false, reason: error.message };
@@ -3678,11 +3876,8 @@ function convertNode(document, node, stack, dropped) {
3678
3876
  if (node === true) return {};
3679
3877
  if (node === false) throw new UnsupportedSchema("schema is `false`, which accepts no value");
3680
3878
  const resolved = dereference(document, node, stack);
3681
- const source = resolved.value;
3682
- if (typeof source !== "object" || source === null || Array.isArray(source)) {
3683
- throw new UnsupportedSchema("schema node is not an object");
3684
- }
3685
- const schemaNode = source;
3879
+ const schemaNode = resolved.value;
3880
+ if (!isRecord(schemaNode)) throw new UnsupportedSchema("schema node is not an object");
3686
3881
  if (Array.isArray(schemaNode["allOf"])) {
3687
3882
  return mergeAllOf(document, schemaNode, resolved.stack, dropped);
3688
3883
  }
@@ -3701,11 +3896,15 @@ function convertNode(document, node, stack, dropped) {
3701
3896
  }
3702
3897
  if (Array.isArray(schemaNode["enum"])) result["enum"] = [...schemaNode["enum"]];
3703
3898
  const properties = schemaNode["properties"];
3704
- if (isRecord2(properties)) {
3899
+ if (isRecord(properties)) {
3705
3900
  const converted = {};
3706
3901
  for (const [name, sub] of Object.entries(properties)) {
3707
- const child = convertNode(document, sub, resolved.stack, dropped);
3708
- if (child !== void 0) converted[name] = child;
3902
+ if (name === "__proto__") {
3903
+ throw new UnsupportedSchema(
3904
+ 'schema declares a "__proto__" property, which cannot be imported'
3905
+ );
3906
+ }
3907
+ converted[name] = convertNode(document, sub, resolved.stack, dropped);
3709
3908
  }
3710
3909
  result["properties"] = converted;
3711
3910
  }
@@ -3718,17 +3917,15 @@ function convertNode(document, node, stack, dropped) {
3718
3917
  if (typeof additional === "boolean") {
3719
3918
  result["additionalProperties"] = additional;
3720
3919
  } else if (additional !== void 0) {
3721
- const child = convertNode(document, additional, resolved.stack, dropped);
3722
- if (child !== void 0) result["additionalProperties"] = child;
3723
- } else if (isRecord2(properties)) {
3920
+ result["additionalProperties"] = convertNode(document, additional, resolved.stack, dropped);
3921
+ } else if (isRecord(properties)) {
3724
3922
  result["additionalProperties"] = false;
3725
3923
  }
3726
3924
  const items = schemaNode["items"];
3727
3925
  if (Array.isArray(items)) {
3728
3926
  dropped.add("items (tuple form)");
3729
3927
  } else if (items !== void 0) {
3730
- const child = convertNode(document, items, resolved.stack, dropped);
3731
- if (child !== void 0) result["items"] = child;
3928
+ result["items"] = convertNode(document, items, resolved.stack, dropped);
3732
3929
  }
3733
3930
  for (const keyword of Object.keys(schemaNode)) {
3734
3931
  if (isCarriedKeyword(keyword) || ANNOTATION_KEYWORDS.has(keyword) || keyword.startsWith("x-")) {
@@ -3765,19 +3962,19 @@ function mergeAllOf(document, schema, stack, dropped) {
3765
3962
  const merged = { type: "object", properties: {}, required: [] };
3766
3963
  const properties = merged["properties"];
3767
3964
  const required = /* @__PURE__ */ new Set();
3965
+ let open2 = false;
3768
3966
  const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
3769
3967
  for (const branch of parts) {
3770
3968
  const converted = convertNode(document, branch, stack, dropped);
3771
- if (converted === void 0) throw new UnsupportedSchema("allOf branch is empty");
3772
3969
  const types = typeList2(converted["type"]);
3773
3970
  if (types !== void 0 && !types.includes("object")) {
3774
3971
  throw new UnsupportedSchema("allOf mixes object and non-object schemas");
3775
3972
  }
3776
3973
  const branchProperties = converted["properties"];
3777
- if (isRecord2(branchProperties)) {
3974
+ if (isRecord(branchProperties)) {
3778
3975
  for (const [name, sub] of Object.entries(branchProperties)) {
3779
3976
  const existing = properties[name];
3780
- if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(sub)) {
3977
+ if (existing !== void 0 && !isDeepStrictEqual(existing, sub)) {
3781
3978
  throw new UnsupportedSchema(
3782
3979
  `allOf branches declare property "${name}" differently, which cannot be merged`
3783
3980
  );
@@ -3793,18 +3990,24 @@ function mergeAllOf(document, schema, stack, dropped) {
3793
3990
  throw new UnsupportedSchema(`allOf branch uses "${keyword}", which cannot be merged`);
3794
3991
  }
3795
3992
  }
3796
- if (typeof converted["description"] === "string" && merged["description"] === void 0) {
3797
- merged["description"] = converted["description"];
3993
+ for (const keyword of METADATA_KEYWORDS) {
3994
+ if (Object.hasOwn(converted, keyword) && !Object.hasOwn(merged, keyword)) {
3995
+ merged[keyword] = converted[keyword];
3996
+ }
3997
+ }
3998
+ const additional = converted["additionalProperties"];
3999
+ if (additional === true) open2 = true;
4000
+ else if (isRecord(additional)) {
4001
+ throw new UnsupportedSchema(
4002
+ "allOf branch constrains additionalProperties, which cannot be merged"
4003
+ );
3798
4004
  }
3799
4005
  }
3800
4006
  if (required.size > 0) merged["required"] = [...required];
3801
4007
  else delete merged["required"];
3802
- merged["additionalProperties"] = false;
4008
+ merged["additionalProperties"] = open2;
3803
4009
  return merged;
3804
4010
  }
3805
- function isRecord2(value) {
3806
- return typeof value === "object" && value !== null && !Array.isArray(value);
3807
- }
3808
4011
 
3809
4012
  // src/openapi/request.ts
3810
4013
  var PATH_GROUP = "path";
@@ -3858,16 +4061,10 @@ function mapRequest(loaded, candidate) {
3858
4061
  warn("unsupported-optional-parameter", `Omitted optional ${label}: unknown location`);
3859
4062
  continue;
3860
4063
  }
3861
- const unsupported = describeUnsupportedParameter(document, parameter, location);
3862
- if (unsupported !== void 0) {
3863
- if (required2) return skip("unsupported-required-parameter", `${label} ${unsupported}`);
3864
- warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${unsupported}`);
3865
- continue;
3866
- }
3867
- const converted = convertSchema(document, parameter["schema"]);
4064
+ const converted = convertParameter(document, parameter, location);
3868
4065
  if (!converted.supported) {
3869
4066
  if (required2) return skip("unsupported-required-parameter", `${label} ${converted.reason}`);
3870
- warn("unsupported-optional-parameter", `Omitted optional ${label}: ${converted.reason}`);
4067
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${converted.reason}`);
3871
4068
  continue;
3872
4069
  }
3873
4070
  for (const keyword of converted.dropped) dropped.add(keyword);
@@ -3936,38 +4133,43 @@ function mapRequest(loaded, candidate) {
3936
4133
  function mergeParameters(document, parameters) {
3937
4134
  const byIdentity = /* @__PURE__ */ new Map();
3938
4135
  for (const raw of parameters) {
3939
- const resolved = dereference(document, raw).value;
3940
- if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) continue;
3941
- const parameter = resolved;
4136
+ const parameter = dereference(document, raw).value;
4137
+ if (!isRecord(parameter)) continue;
3942
4138
  byIdentity.set(`${String(parameter["in"])}:${String(parameter["name"])}`, parameter);
3943
4139
  }
3944
4140
  return [...byIdentity.values()];
3945
4141
  }
3946
- function describeUnsupportedParameter(document, parameter, location) {
4142
+ function convertParameter(document, parameter, location) {
4143
+ const unsupported = (reason) => ({ supported: false, reason });
4144
+ if (parameter["name"] === "__proto__") {
4145
+ return unsupported("has a name that cannot be an input key");
4146
+ }
3947
4147
  if (parameter["content"] !== void 0) {
3948
- return "uses the `content` form, whose media-type serialization the gateway does not perform";
4148
+ return unsupported(
4149
+ "uses the `content` form, whose media-type serialization the gateway does not perform"
4150
+ );
3949
4151
  }
3950
4152
  const style = parameter["style"];
3951
4153
  if (location === "query" && typeof style === "string" && UNSUPPORTED_QUERY_STYLES.has(style)) {
3952
- return `uses style "${style}", which the gateway does not serialize`;
4154
+ return unsupported(`uses style "${style}", which the gateway does not serialize`);
3953
4155
  }
3954
4156
  if (location === "path" && typeof style === "string" && style !== "simple") {
3955
- return `uses style "${style}"; the gateway substitutes plain values only`;
4157
+ return unsupported(`uses style "${style}"; the gateway substitutes plain values only`);
3956
4158
  }
3957
4159
  const converted = convertSchema(document, parameter["schema"]);
3958
- if (!converted.supported) return converted.reason;
4160
+ if (!converted.supported) return unsupported(`has an unusable schema: ${converted.reason}`);
3959
4161
  if (!isPrimitiveSchema(converted.schema)) {
3960
- return "is not a primitive; object and array parameters need serialization the gateway does not perform";
4162
+ return unsupported(
4163
+ "is not a primitive; object and array parameters need serialization the gateway does not perform"
4164
+ );
3961
4165
  }
3962
- return void 0;
4166
+ return converted;
3963
4167
  }
3964
4168
  function resolveBody(document, candidate) {
3965
4169
  if (candidate.requestBody === void 0) return { kind: "none" };
3966
- const resolved = dereference(document, candidate.requestBody).value;
3967
- if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) {
3968
- return { kind: "none" };
3969
- }
3970
- const requestBody = resolved;
4170
+ const resolvedBody = dereference(document, candidate.requestBody);
4171
+ const requestBody = resolvedBody.value;
4172
+ if (!isRecord(requestBody)) return { kind: "none" };
3971
4173
  const required = requestBody["required"] === true;
3972
4174
  if (candidate.method === "GET" || candidate.method === "DELETE") {
3973
4175
  return {
@@ -3977,7 +4179,7 @@ function resolveBody(document, candidate) {
3977
4179
  };
3978
4180
  }
3979
4181
  const content = requestBody["content"];
3980
- if (!isRecord3(content)) return { kind: "none" };
4182
+ if (!isRecord(content)) return { kind: "none" };
3981
4183
  const mediaType = pickJsonMediaType(Object.keys(content));
3982
4184
  if (mediaType === void 0) {
3983
4185
  return {
@@ -3986,8 +4188,8 @@ function resolveBody(document, candidate) {
3986
4188
  reason: `no JSON request body content type (found: ${Object.keys(content).join(", ") || "none"}). Only application/json and application/*+json are supported - multipart and form data are never serialized as JSON`
3987
4189
  };
3988
4190
  }
3989
- const media = content[mediaType];
3990
- const schemaNode = isRecord3(media) ? media["schema"] : void 0;
4191
+ const media = dereference(document, content[mediaType], resolvedBody.stack).value;
4192
+ const schemaNode = isRecord(media) ? media["schema"] : void 0;
3991
4193
  if (schemaNode === void 0) {
3992
4194
  return {
3993
4195
  kind: "schema",
@@ -4008,10 +4210,10 @@ function resolveBody(document, candidate) {
4008
4210
  };
4009
4211
  }
4010
4212
  function pickJsonMediaType(keys) {
4011
- const normalised = keys.map((key) => ({ key, type: key.split(";")[0]?.trim().toLowerCase() }));
4012
- const exact = normalised.find((entry) => entry.type === "application/json");
4213
+ const normalized = keys.map((key) => ({ key, type: key.split(";")[0]?.trim().toLowerCase() }));
4214
+ const exact = normalized.find((entry) => entry.type === "application/json");
4013
4215
  if (exact !== void 0) return exact.key;
4014
- return [...normalised].sort((a, b) => (a.type ?? "").localeCompare(b.type ?? "")).find((entry) => entry.type?.startsWith("application/") && entry.type.endsWith("+json"))?.key;
4216
+ return [...normalized].sort((a, b) => (a.type ?? "").localeCompare(b.type ?? "")).find((entry) => entry.type?.startsWith("application/") && entry.type.endsWith("+json"))?.key;
4015
4217
  }
4016
4218
  function closedObject(properties, required) {
4017
4219
  return {
@@ -4021,9 +4223,6 @@ function closedObject(properties, required) {
4021
4223
  additionalProperties: false
4022
4224
  };
4023
4225
  }
4024
- function isRecord3(value) {
4025
- return typeof value === "object" && value !== null && !Array.isArray(value);
4026
- }
4027
4226
 
4028
4227
  // src/openapi/draft.ts
4029
4228
  function buildResourceDrafts(loaded, options = {}) {
@@ -4031,11 +4230,12 @@ function buildResourceDrafts(loaded, options = {}) {
4031
4230
  loaded,
4032
4231
  options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
4033
4232
  );
4034
- const diagnostics = [...discovery.diagnostics];
4233
+ const diagnostics = [];
4035
4234
  const drafts = [];
4036
4235
  const wanted = options.include?.operationIds;
4037
4236
  const wantedTags = options.include?.tags;
4038
4237
  const matched = /* @__PURE__ */ new Set();
4238
+ const selected = /* @__PURE__ */ new Set();
4039
4239
  for (const candidate of discovery.operations) {
4040
4240
  if (wanted !== void 0 && !selects(wanted, candidate)) continue;
4041
4241
  if (wantedTags !== void 0 && !candidate.tags.some((tag) => wantedTags.includes(tag))) {
@@ -4044,6 +4244,7 @@ function buildResourceDrafts(loaded, options = {}) {
4044
4244
  if (wanted !== void 0) {
4045
4245
  for (const id of wanted) if (selects([id], candidate)) matched.add(id);
4046
4246
  }
4247
+ selected.add(candidate.resourceId);
4047
4248
  const mapping = mapRequest(loaded, candidate);
4048
4249
  diagnostics.push(...mapping.diagnostics);
4049
4250
  if (!mapping.supported) continue;
@@ -4073,8 +4274,8 @@ function buildResourceDrafts(loaded, options = {}) {
4073
4274
  if (options.policy?.pricing === void 0 || options.policy.expose === void 0) {
4074
4275
  review.push(
4075
4276
  "REVIEW: pricing and exposure are not inferred from OpenAPI. Add e.g.",
4076
- ' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC }',
4077
- " expose: [http] # http | mcp | a2a"
4277
+ ' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC } with payments: [x402]',
4278
+ " expose: [http] # http | mcp | a2a | acp"
4078
4279
  );
4079
4280
  }
4080
4281
  drafts.push({
@@ -4094,12 +4295,9 @@ function buildResourceDrafts(loaded, options = {}) {
4094
4295
  ...mapping.contentType !== void 0 ? { headers: { "Content-Type": mapping.contentType } } : {},
4095
4296
  ...Object.keys(mapping.inputBindings).length > 0 ? { inputBindings: mapping.inputBindings } : {}
4096
4297
  },
4097
- // Policy is written only when the operator asked for it. An absent
4098
- // `pricing`/`expose` is what makes the draft fail config validation
4099
- // until a human has decided.
4298
+ // Written only when the operator supplied it (see the file header)
4100
4299
  ...options.policy?.pricing !== void 0 ? { pricing: options.policy.pricing } : {},
4101
- ...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {},
4102
- ...options.policy?.payments !== void 0 ? { payments: [...options.policy.payments] } : {}
4300
+ ...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {}
4103
4301
  },
4104
4302
  review
4105
4303
  });
@@ -4108,10 +4306,19 @@ function buildResourceDrafts(loaded, options = {}) {
4108
4306
  version: loaded.version,
4109
4307
  sourcePath: loaded.sourcePath,
4110
4308
  drafts,
4111
- diagnostics,
4309
+ diagnostics: [
4310
+ ...selectDiscoveryDiagnostics(discovery.diagnostics, options.include, selected),
4311
+ ...diagnostics
4312
+ ],
4112
4313
  unmatchedOperationIds: (wanted ?? []).filter((id) => !matched.has(id))
4113
4314
  };
4114
4315
  }
4316
+ function selectDiscoveryDiagnostics(found, include, selected) {
4317
+ if (include?.operationIds === void 0 && include?.tags === void 0) return found;
4318
+ return found.filter(
4319
+ ({ operation }) => operation === void 0 || selected.has(operation) || include.operationIds?.includes(operation) === true
4320
+ );
4321
+ }
4115
4322
  function selects(ids, candidate) {
4116
4323
  return ids.includes(candidate.resourceId) || candidate.operationId !== void 0 && ids.includes(candidate.operationId);
4117
4324
  }
@@ -4122,22 +4329,31 @@ function declaresSecurity(candidate) {
4122
4329
  }
4123
4330
  function selectOutputSchema(document, candidate, diagnostics, dropped) {
4124
4331
  const responses = candidate.responses;
4125
- if (!isRecord4(responses)) return void 0;
4332
+ if (!isRecord(responses)) return void 0;
4126
4333
  const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
4127
4334
  if (successes.length === 0) return void 0;
4128
- const withBody = successes.filter((status) => {
4129
- const response2 = responses[status];
4130
- const content2 = isRecord4(response2) ? response2["content"] : void 0;
4131
- return isRecord4(content2) && pickJsonMediaType(Object.keys(content2)) !== void 0;
4335
+ const withBody = successes.flatMap((status) => {
4336
+ const response = dereference(document, responses[status]);
4337
+ if (!isRecord(response.value)) return [];
4338
+ const content = response.value["content"];
4339
+ if (!isRecord(content)) return [];
4340
+ const mediaType = pickJsonMediaType(Object.keys(content));
4341
+ if (mediaType === void 0) return [];
4342
+ const media = dereference(document, content[mediaType], response.stack).value;
4343
+ return isRecord(media) ? [{ status, media }] : [];
4132
4344
  });
4133
4345
  const chosen = withBody[0];
4134
4346
  if (chosen === void 0) return void 0;
4135
- const response = responses[chosen];
4136
- const content = response["content"];
4137
- const mediaType = pickJsonMediaType(Object.keys(content));
4138
- const media = content[mediaType];
4139
- const schemaNode = isRecord4(media) ? media["schema"] : void 0;
4140
- if (schemaNode === void 0) return void 0;
4347
+ const schemaNode = chosen.media["schema"];
4348
+ if (schemaNode === void 0) {
4349
+ diagnostics.push({
4350
+ severity: "warning",
4351
+ code: "missing-output-schema",
4352
+ operation: candidate.resourceId,
4353
+ message: `${candidate.resourceId}: response ${chosen.status} has JSON content without a schema; omitted the output schema`
4354
+ });
4355
+ return void 0;
4356
+ }
4141
4357
  const converted = convertSchema(document, schemaNode);
4142
4358
  if (!converted.supported) {
4143
4359
  diagnostics.push({
@@ -4154,7 +4370,7 @@ function selectOutputSchema(document, candidate, diagnostics, dropped) {
4154
4370
  severity: "warning",
4155
4371
  code: "multiple-success-responses",
4156
4372
  operation: candidate.resourceId,
4157
- message: `${candidate.resourceId}: responses ${withBody.join(", ")} all carry a JSON body; used ${chosen}`
4373
+ message: `${candidate.resourceId}: responses ${withBody.map(({ status }) => status).join(", ")} all carry a JSON body; used ${chosen.status}`
4158
4374
  });
4159
4375
  }
4160
4376
  return converted.schema;
@@ -4169,7 +4385,8 @@ function renderResourcesYaml(result) {
4169
4385
  const doc = new Document({ resources });
4170
4386
  doc.commentBefore = [
4171
4387
  " Generated by agent-commerce import openapi. Review before use.",
4172
- ` Source: ${basename(result.sourcePath)} (OpenAPI ${result.version})`,
4388
+ // win32.basename splits on both / and \
4389
+ ` Source: ${win32.basename(result.sourcePath)} (OpenAPI ${result.version})`,
4173
4390
  " Merge the resources below into config.yaml once pricing, exposure and",
4174
4391
  " any backend authentication have been decided."
4175
4392
  ].join("\n");
@@ -4182,12 +4399,6 @@ function renderResourcesYaml(result) {
4182
4399
  });
4183
4400
  return doc.toString({ lineWidth: 0 });
4184
4401
  }
4185
- function basename(path2) {
4186
- return path2.split(/[\\/]/).pop() ?? path2;
4187
- }
4188
- function isRecord4(value) {
4189
- return typeof value === "object" && value !== null && !Array.isArray(value);
4190
- }
4191
4402
 
4192
4403
  // src/openapi/load.ts
4193
4404
  init_errors();
@@ -4200,6 +4411,31 @@ function invalid2(message, details, cause) {
4200
4411
  ...cause !== void 0 ? { cause } : {}
4201
4412
  });
4202
4413
  }
4414
+ async function readCapped(sourcePath, details) {
4415
+ const buffer = Buffer.alloc(MAX_SOURCE_BYTES + 1);
4416
+ let length = 0;
4417
+ try {
4418
+ const handle = await open(sourcePath, "r");
4419
+ try {
4420
+ while (length < buffer.length) {
4421
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
4422
+ if (bytesRead === 0) break;
4423
+ length += bytesRead;
4424
+ }
4425
+ } finally {
4426
+ await handle.close();
4427
+ }
4428
+ } catch (error) {
4429
+ throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
4430
+ }
4431
+ if (length > MAX_SOURCE_BYTES) {
4432
+ throw invalid2(`OpenAPI document "${sourcePath}" is over the ${MAX_SOURCE_BYTES}-byte limit`, {
4433
+ ...details,
4434
+ maxBytes: MAX_SOURCE_BYTES
4435
+ });
4436
+ }
4437
+ return buffer.toString("utf8", 0, length);
4438
+ }
4203
4439
  async function loadOpenApiDocument(sourcePath) {
4204
4440
  const details = { sourcePath };
4205
4441
  let stats;
@@ -4224,7 +4460,7 @@ async function loadOpenApiDocument(sourcePath) {
4224
4460
  details
4225
4461
  );
4226
4462
  }
4227
- const source = await readFile(sourcePath, "utf8");
4463
+ const source = await readCapped(sourcePath, details);
4228
4464
  if (source.trim() === "") {
4229
4465
  throw invalid2(`OpenAPI document "${sourcePath}" is empty`, details);
4230
4466
  }
@@ -4243,7 +4479,7 @@ async function loadOpenApiDocument(sourcePath) {
4243
4479
  }
4244
4480
  const document = parsed;
4245
4481
  const version = readVersion(document, sourcePath);
4246
- rejectExternalReferences(document, sourcePath);
4482
+ rejectUnsupportedReferences(document, sourcePath);
4247
4483
  const result = await validate(document);
4248
4484
  if (!result.valid) {
4249
4485
  const first = result.errors?.[0];
@@ -4275,7 +4511,7 @@ function readVersion(document, sourcePath) {
4275
4511
  }
4276
4512
  return featureVersion;
4277
4513
  }
4278
- function rejectExternalReferences(document, sourcePath) {
4514
+ function rejectUnsupportedReferences(document, sourcePath) {
4279
4515
  const seen = /* @__PURE__ */ new WeakSet();
4280
4516
  const queue = [document];
4281
4517
  while (queue.length > 0) {
@@ -4288,31 +4524,45 @@ function rejectExternalReferences(document, sourcePath) {
4288
4524
  continue;
4289
4525
  }
4290
4526
  for (const [key, value] of Object.entries(node)) {
4291
- if (key === "$ref" && typeof value === "string" && !value.startsWith("#")) {
4292
- throw invalid2(
4293
- `OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
4294
- { sourcePath, ref: value }
4295
- );
4527
+ if (key === "$ref" && typeof value === "string") {
4528
+ if (!value.startsWith("#")) {
4529
+ throw invalid2(
4530
+ `OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
4531
+ { sourcePath, ref: value }
4532
+ );
4533
+ }
4534
+ if (!isPercentDecodable(value)) {
4535
+ throw invalid2(
4536
+ `OpenAPI document "${sourcePath}" contains a reference "${value}" with a malformed percent-escape`,
4537
+ { sourcePath, ref: value }
4538
+ );
4539
+ }
4296
4540
  }
4297
4541
  queue.push(value);
4298
4542
  }
4299
4543
  }
4300
4544
  }
4545
+ function isPercentDecodable(value) {
4546
+ try {
4547
+ decodeURIComponent(value);
4548
+ return true;
4549
+ } catch {
4550
+ return false;
4551
+ }
4552
+ }
4301
4553
 
4302
4554
  // src/cli/commands/import-openapi.ts
4303
4555
  function defaultOutputPath(source) {
4304
- const name = basename$1(source);
4556
+ const name = basename(source);
4305
4557
  const stem = name.slice(0, name.length - extname(name).length) || name;
4306
4558
  return `${stem}.agent-commerce.yaml`;
4307
4559
  }
4308
4560
  async function runImportOpenApi(options, io, deps = {}) {
4309
4561
  const fileExists = deps.fileExists ?? existsSync;
4310
4562
  const outputPath = options.output ?? defaultOutputPath(options.source);
4311
- let policy;
4312
- try {
4313
- policy = buildPolicy(options);
4314
- } catch (error) {
4315
- io.stderr(`FAIL ${error instanceof Error ? error.message : String(error)}`);
4563
+ const policy = buildPolicy(options);
4564
+ if (typeof policy === "string") {
4565
+ io.stderr(`FAIL ${policy}`);
4316
4566
  return 1;
4317
4567
  }
4318
4568
  if (fileExists(outputPath) && options.force !== true) {
@@ -4393,13 +4643,11 @@ function buildPolicy(options) {
4393
4643
  if (options.expose !== void 0) {
4394
4644
  const requested = options.expose.split(",").map((name) => name.trim()).filter((name) => name !== "");
4395
4645
  if (requested.length === 0) {
4396
- throw new Error(`--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`);
4646
+ return `--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`;
4397
4647
  }
4398
4648
  for (const name of requested) {
4399
4649
  if (!PROTOCOL_NAMES.includes(name)) {
4400
- throw new Error(
4401
- `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`
4402
- );
4650
+ return `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`;
4403
4651
  }
4404
4652
  }
4405
4653
  policy.expose = requested;
@@ -4473,14 +4721,6 @@ function formatConfigError(err, manifestFound = false) {
4473
4721
  }
4474
4722
  return lines.join("\n");
4475
4723
  }
4476
- if (err instanceof ZodError) {
4477
- const lines = ["FAIL Configuration schema errors:"];
4478
- for (const issue of err.issues) {
4479
- const path2 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
4480
- lines.push(` - ${path2}: ${issue.message}`);
4481
- }
4482
- return lines.join("\n");
4483
- }
4484
4724
  if (err instanceof Error) {
4485
4725
  return `FAIL ${err.message}`;
4486
4726
  }
@@ -4504,7 +4744,11 @@ async function runValidate(options, io, deps = {}) {
4504
4744
  io.stdout(
4505
4745
  ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"} a2a=${config.protocols.a2a.enabled ? "on" : "off"} acp=${config.protocols.acp.enabled ? "on" : "off"}`
4506
4746
  );
4507
- io.stdout(` payments: x402=${config.payments.x402?.enabled === true ? "on" : "off"}`);
4747
+ const onOff = (enabled) => enabled === true ? "on" : "off";
4748
+ io.stdout(
4749
+ ` payments: x402=${onOff(config.payments.x402?.enabled)} mpp=${onOff(config.payments.mpp?.enabled)}`
4750
+ );
4751
+ io.stdout(` authorization: ap2=${onOff(config.authorization?.ap2.enabled)}`);
4508
4752
  return 0;
4509
4753
  } catch (err) {
4510
4754
  io.stderr(formatConfigError(err, manifestFound));
@@ -4518,8 +4762,7 @@ async function collectAnswersInteractive() {
4518
4762
  const backendBaseUrl = await clack.text({
4519
4763
  message: "Backend base URL (your existing merchant API)",
4520
4764
  initialValue: "http://localhost:3000",
4521
- // shape-checked here, where the user can retype it,
4522
- // rather than surfacing as a config error after the fact.
4765
+ // Checked here, where the user can retype it, rather than as a config error later
4523
4766
  validate: (value) => {
4524
4767
  const trimmed = (value ?? "").trim();
4525
4768
  if (trimmed.length === 0) return "Required";
@@ -4556,12 +4799,8 @@ async function collectAnswersInteractive() {
4556
4799
  { value: "mcp", label: "MCP" }
4557
4800
  ],
4558
4801
  initialValues: ["http", "mcp"],
4559
- // `required: false` permits an empty selection, which renders
4560
- // `expose: []` on every resource and fails the schema's min(1). clack's
4561
- // multiselect has no `validate` hook, so this is caught by the in-memory
4562
- // validation in `runInit` — which now reports the real schema error and
4563
- // writes nothing, instead of leaving an invalid file behind.
4564
- required: false
4802
+ // Every resource needs at least one protocol in `expose`
4803
+ required: true
4565
4804
  });
4566
4805
  if (clack.isCancel(protocols)) {
4567
4806
  clack.cancel("Aborted.");
@@ -4575,7 +4814,7 @@ async function collectAnswersInteractive() {
4575
4814
  let merchantPayTo = defaultInitAnswers().merchantPayTo;
4576
4815
  if (x402Enabled) {
4577
4816
  const answer = await clack.text({
4578
- message: "Merchant settlement destination address (merchant-controlled \u2014 never a gateway-owned wallet)",
4817
+ message: "Merchant settlement address (enter an address you control)",
4579
4818
  initialValue: merchantPayTo,
4580
4819
  validate: (value) => {
4581
4820
  const trimmed = (value ?? "").trim();
@@ -4618,20 +4857,16 @@ async function runInit(options, io, deps = {}) {
4618
4857
  return 1;
4619
4858
  }
4620
4859
  const yamlContent = renderInitConfigYaml(answers);
4621
- const resolvedPath = resolve(outputPath);
4622
- const dir = dirname(resolvedPath);
4623
- if (dir !== "" && !existsSync(dir)) {
4624
- mkdirSync(dir, { recursive: true });
4625
- }
4860
+ mkdirSync(dirname(resolve(outputPath)), { recursive: true });
4626
4861
  await writeFileImpl(outputPath, yamlContent);
4627
4862
  io.stdout(`PASS Wrote ${outputPath}`);
4628
- io.stdout(`PASS ${outputPath} is valid \u2014 ${config.resources.length} resource(s)`);
4863
+ io.stdout(`PASS ${outputPath} is valid: ${config.resources.length} resource(s)`);
4629
4864
  try {
4630
4865
  await loadConfig2({ path: outputPath });
4631
4866
  } catch (err) {
4632
4867
  io.stderr(formatConfigError(err));
4633
4868
  io.stderr(
4634
- `FAIL ${outputPath} was written but does not parse, although the same answers validated in memory \u2014 this is a bug in the config renderer, please report it.`
4869
+ `FAIL ${outputPath} was written but does not parse, although the same answers validated in memory. This is a bug in the config renderer; please report it.`
4635
4870
  );
4636
4871
  return 1;
4637
4872
  }
@@ -4650,8 +4885,9 @@ function runVersion(io, deps = {}) {
4650
4885
  if (report.pinned.length > 0) {
4651
4886
  io.stdout("");
4652
4887
  io.stdout("Pinned protocol / SDK versions:");
4888
+ const width = Math.max(...report.pinned.map((pin) => pin.name.length));
4653
4889
  for (const pin of report.pinned) {
4654
- io.stdout(` ${pin.name.padEnd(24)} ${pin.version.padEnd(12)} (via ${pin.via})`);
4890
+ io.stdout(` ${pin.name.padEnd(width)} ${pin.version.padEnd(12)} (via ${pin.via})`);
4655
4891
  }
4656
4892
  }
4657
4893
  return 0;
@@ -4702,7 +4938,10 @@ function buildProgram(io = processIo) {
4702
4938
  process.exitCode = code;
4703
4939
  });
4704
4940
  const importCommand = program.command("import").description("Generate Agent Commerce resource drafts from an API description.");
4705
- importCommand.command("openapi").description("Convert a local OpenAPI 3.0/3.1/3.2 document into resource drafts.").argument("<source>", "path to a local .yaml, .yml or .json OpenAPI document").option("--output <path>", "output path (default: <source>.agent-commerce.yaml)").option("--force", "overwrite an existing output file", false).option("--base-url <url>", "backend base URL, overriding the document servers").option(
4941
+ importCommand.command("openapi").description("Convert a local OpenAPI 3.0/3.1/3.2 document into resource drafts.").argument("<source>", "path to a local .yaml, .yml or .json OpenAPI document").option(
4942
+ "--output <path>",
4943
+ "output path (default: <source name without extension>.agent-commerce.yaml in the current directory)"
4944
+ ).option("--force", "overwrite an existing output file", false).option("--base-url <url>", "backend base URL, overriding the document servers").option(
4706
4945
  "--operation <operationId>",
4707
4946
  "import only this operation (repeatable)",
4708
4947
  collect,
@@ -4727,7 +4966,7 @@ function buildProgram(io = processIo) {
4727
4966
  }
4728
4967
  );
4729
4968
  program.command("demo").description(
4730
- "Run the documented docker compose + demo agent quickstart, verifying health between steps."
4969
+ "Start the local stack with docker compose (waiting for healthy services), then run the demo agent. Run it from a repo clone."
4731
4970
  ).action(async () => {
4732
4971
  process.exitCode = await runDemo(io);
4733
4972
  });