@devlab.group/agent-commerce 1.5.0 → 1.6.1

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 (45) hide show
  1. package/README.md +73 -71
  2. package/dist/ap2.d.ts +20 -23
  3. package/dist/ap2.js +53 -75
  4. package/dist/ap2.js.map +1 -1
  5. package/dist/authorization-BpbWGGrf.d.ts +115 -0
  6. package/dist/chunk-AFI2CEZS.js +10 -0
  7. package/dist/chunk-AFI2CEZS.js.map +1 -0
  8. package/dist/{chunk-PBYN7EK2.js → chunk-CBJIT7QP.js} +10 -9
  9. package/dist/chunk-CBJIT7QP.js.map +1 -0
  10. package/dist/{chunk-NKNXVDHN.js → chunk-JKEKKXRJ.js} +45 -66
  11. package/dist/chunk-JKEKKXRJ.js.map +1 -0
  12. package/dist/{chunk-5GEWKNIH.js → chunk-N5KRK7YA.js} +63 -27
  13. package/dist/chunk-N5KRK7YA.js.map +1 -0
  14. package/dist/{chunk-6D2EZ5ME.js → chunk-V2V2E7HX.js} +2 -2
  15. package/dist/{chunk-6D2EZ5ME.js.map → chunk-V2V2E7HX.js.map} +1 -1
  16. package/dist/{chunk-US622BKF.js → chunk-VKDXMXHL.js} +5 -8
  17. package/dist/chunk-VKDXMXHL.js.map +1 -0
  18. package/dist/cli/index.js +501 -494
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/{constants-6mypC60h.d.ts → constants-Bkq04FRZ.d.ts} +1 -3
  21. package/dist/guardrails-71XwtXlT.d.ts +31 -0
  22. package/dist/index.d.ts +230 -265
  23. package/dist/index.js +1050 -607
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp.d.ts +6 -59
  26. package/dist/mcp.js +61 -53
  27. package/dist/mcp.js.map +1 -1
  28. package/dist/mpp.d.ts +4 -4
  29. package/dist/mpp.js +21 -19
  30. package/dist/mpp.js.map +1 -1
  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 +10 -9
  36. package/dist/x402.js.map +1 -1
  37. package/package.json +7 -5
  38. package/dist/authorization-DA7mjy_O.d.ts +0 -123
  39. package/dist/chunk-5GEWKNIH.js.map +0 -1
  40. package/dist/chunk-NKNXVDHN.js.map +0 -1
  41. package/dist/chunk-PBYN7EK2.js.map +0 -1
  42. package/dist/chunk-US622BKF.js.map +0 -1
  43. package/dist/guardrails-Dmwi63KB.d.ts +0 -38
  44. package/dist/protocol-adapter-APRjs1E2.d.ts +0 -273
  45. package/dist/runtime-DC5XQ9Cc.d.ts +0 -351
package/dist/cli/index.js CHANGED
@@ -1,13 +1,14 @@
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
13
  import { writeFile, stat, rename, rm, open } from 'fs/promises';
13
14
  import { validate } from '@scalar/openapi-parser';
@@ -32,11 +33,22 @@ var init_filename = __esm({
32
33
  });
33
34
 
34
35
  // src/core/domain/common.ts
35
- var PROTOCOL_NAMES, PAYMENT_METHOD_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
- PAYMENT_METHOD_NAMES = ["x402", "mpp"];
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
+ );
40
52
  }
41
53
  });
42
54
 
@@ -65,10 +77,9 @@ var init_codes = __esm({
65
77
  PAYMENT_REPLAYED: 409,
66
78
  PAYMENT_PROVIDER_UNAVAILABLE: 503,
67
79
  PAYMENT_SETTLEMENT_FAILED: 502,
68
- // 403, not 402: the buyer's money is not the problem. A 402 tells a client
69
- // "pay and retry", which cannot fix a missing or rejected mandate, and a
70
- // client that auto-pays on 402 would be charged for a request that was
71
- // 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
72
83
  AUTHORIZATION_REQUIRED: 403,
73
84
  AUTHORIZATION_INVALID: 403,
74
85
  AUTHORIZATION_REPLAYED: 409,
@@ -82,17 +93,13 @@ var init_codes = __esm({
82
93
  };
83
94
  RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
84
95
  "PAYMENT_PROVIDER_UNAVAILABLE",
85
- // Our verifier or our replay store was unreachable, so no verdict on the
86
- // mandate was ever reached. That is an outage on our side, and the same
87
- // 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.
88
98
  "AUTHORIZATION_PROVIDER_UNAVAILABLE",
89
99
  "BACKEND_TIMEOUT",
90
- // Load shedding is transient by definition: the caller should back off and
91
- // try again. Before this code existed the MCP adapter's queue-full path threw
92
- // PROTOCOL_UNSUPPORTED — HTTP 501, retryable:false — so an agent framework
93
- // keyed on the machine-readable flag (which exists precisely so clients need
94
- // not parse prose) would read a momentary throttle as a permanent capability
95
- // 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.
96
103
  "GATEWAY_BUSY"
97
104
  ]);
98
105
  }
@@ -128,7 +135,7 @@ var init_commerce_error = __esm({
128
135
  if (options.resourceId !== void 0) this.resourceId = options.resourceId;
129
136
  Error.captureStackTrace?.(this, _CommerceError);
130
137
  }
131
- /** Non-secret, client-safe representation. */
138
+ /** Non-secret, client-safe representation. `cause` is never included */
132
139
  toInfo() {
133
140
  return {
134
141
  code: this.code,
@@ -152,11 +159,21 @@ var init_errors = __esm({
152
159
  }
153
160
  });
154
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
+
155
171
  // src/core/domain/wire.ts
156
172
  var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
157
173
  var init_wire = __esm({
158
174
  "src/core/domain/wire.ts"() {
159
175
  init_errors();
176
+ init_is_record();
160
177
  PAYMENT_INPUT_FIELD = "_payment";
161
178
  AUTHORIZATION_INPUT_FIELD = "_authorization";
162
179
  RESERVED_INPUT_FIELDS = [
@@ -258,17 +275,10 @@ var init_backend_http = __esm({
258
275
  init_resource();
259
276
  init_errors();
260
277
  init_logger();
278
+ init_is_record();
261
279
  PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
262
280
  }
263
281
  });
264
-
265
- // src/core/execution/event-bus.ts
266
- var init_event_bus = __esm({
267
- "src/core/execution/event-bus.ts"() {
268
- }
269
- });
270
-
271
- // src/core/execution/validation.ts
272
282
  function typeList(raw) {
273
283
  if (typeof raw === "string") return [raw];
274
284
  if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
@@ -283,15 +293,17 @@ function isObjectSchemaNode(schema) {
283
293
  }
284
294
  var init_validation = __esm({
285
295
  "src/core/execution/validation.ts"() {
296
+ init_is_record();
286
297
  }
287
298
  });
288
299
  var init_pipeline = __esm({
289
300
  "src/core/execution/pipeline.ts"() {
290
301
  init_wire();
291
302
  init_errors();
303
+ init_is_record();
292
304
  init_backend_http();
293
305
  init_validation();
294
- /* @__PURE__ */ new Set([...RESERVED_INPUT_FIELDS, "__proto__"]);
306
+ [...RESERVED_INPUT_FIELDS, "__proto__"];
295
307
  }
296
308
  });
297
309
 
@@ -302,13 +314,20 @@ var init_registry = __esm({
302
314
  }
303
315
  });
304
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
+
305
324
  // src/core/execution/index.ts
306
325
  var init_execution = __esm({
307
326
  "src/core/execution/index.ts"() {
308
327
  init_backend_http();
309
- init_event_bus();
310
328
  init_pipeline();
311
329
  init_registry();
330
+ init_store_event_sink();
312
331
  init_validation();
313
332
  }
314
333
  });
@@ -395,12 +414,12 @@ var init_networks = __esm({
395
414
  ];
396
415
  BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
397
416
  SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
398
- LIVE_MAINNET_BANNER = "LIVE MAINNET MODE \u2014 REAL FUNDS";
417
+ LIVE_MAINNET_BANNER = "LIVE MAINNET MODE - REAL FUNDS";
399
418
  }
400
419
  });
401
420
 
402
421
  // src/protocols/acp/constants.ts
403
- var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
422
+ var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS;
404
423
  var init_constants3 = __esm({
405
424
  "src/protocols/acp/constants.ts"() {
406
425
  ACP_SPEC_VERSION = "2026-04-17";
@@ -420,6 +439,9 @@ var init_constants3 = __esm({
420
439
  completeCheckoutSession: ["path", "body"],
421
440
  cancelCheckoutSession: ["path"]
422
441
  };
442
+ ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
443
+ cancelCheckoutSession: ["body"]
444
+ };
423
445
  }
424
446
  });
425
447
 
@@ -431,7 +453,7 @@ function substituteEnv(value, env, path2 = "$") {
431
453
  if (Array.isArray(value)) {
432
454
  return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
433
455
  }
434
- if (isPlainObject(value)) {
456
+ if (isRecord(value)) {
435
457
  const result = {};
436
458
  for (const [key, entry] of Object.entries(value)) {
437
459
  result[key] = substituteEnv(entry, env, `${path2}.${key}`);
@@ -448,16 +470,15 @@ function assertTemplateIsSupported(value, path2) {
448
470
  if (NESTED_PLACEHOLDER.test(value)) {
449
471
  throw new CommerceError(
450
472
  "CONFIG_INVALID",
451
- `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.`,
452
474
  { details: { path: path2 } }
453
475
  );
454
476
  }
455
- BRACE_TOKEN.lastIndex = 0;
456
477
  for (const [token] of value.matchAll(BRACE_TOKEN)) {
457
478
  if (SUPPORTED_TOKEN.test(token)) continue;
458
479
  throw new CommerceError(
459
480
  "CONFIG_INVALID",
460
- `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.`,
461
482
  { details: { path: path2, token } }
462
483
  );
463
484
  }
@@ -482,13 +503,11 @@ function substituteOnce(value, env, path2) {
482
503
  }
483
504
  );
484
505
  }
485
- function isPlainObject(value) {
486
- return typeof value === "object" && value !== null && !Array.isArray(value);
487
- }
488
506
  var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
489
507
  var init_env = __esm({
490
508
  "src/config/env.ts"() {
491
509
  init_core();
510
+ init_is_record();
492
511
  PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
493
512
  BRACE_TOKEN = /\$\{[^{}]*\}/g;
494
513
  SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
@@ -526,7 +545,7 @@ function isWellKnownDevAddress(address) {
526
545
  }
527
546
  function isLikelyLocalOrPrivateHost(hostname) {
528
547
  const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
529
- if (h === "localhost" || h === "127.0.0.1" || h === "::1") return true;
548
+ if (h === "localhost" || h === "::1") return true;
530
549
  if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
531
550
  if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
532
551
  if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
@@ -573,7 +592,7 @@ function resolveX402Deployment(input) {
573
592
  assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
574
593
  if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
575
594
  throw invalid(
576
- `${at}: 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 ${at}.allowUnauthenticatedFacilitator: true to accept that, or configure facilitator.auth.`,
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.`,
577
596
  `${at}.allowUnauthenticatedFacilitator`
578
597
  );
579
598
  }
@@ -640,7 +659,7 @@ function assertFacilitatorUrlIsSafe(url, mode, at) {
640
659
  }
641
660
  if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
642
661
  throw invalid(
643
- `${at}: 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.`,
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.`,
644
663
  `${at}.facilitator.url`
645
664
  );
646
665
  }
@@ -674,8 +693,20 @@ var init_guardrails = __esm({
674
693
  init_networks();
675
694
  }
676
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
+ });
677
708
  function parseConfig(raw, env) {
678
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
709
+ if (!isRecord(raw)) {
679
710
  throw new CommerceError(
680
711
  "CONFIG_INVALID",
681
712
  'Configuration root must be a mapping (object) at "$"',
@@ -687,7 +718,7 @@ function parseConfig(raw, env) {
687
718
  checkVersion(raw);
688
719
  const substituted = substituteEnv(raw, env);
689
720
  const parsed = parseWithZod(RawConfigSchema, substituted);
690
- return normalise(parsed);
721
+ return normalize(parsed);
691
722
  }
692
723
  function checkVersion(raw) {
693
724
  if (!("version" in raw)) {
@@ -695,12 +726,12 @@ function checkVersion(raw) {
695
726
  details: { path: "version" }
696
727
  });
697
728
  }
698
- const rawVersion = raw["version"];
699
- const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
729
+ const version = raw["version"];
700
730
  if (version !== SUPPORTED_CONFIG_VERSION) {
731
+ const hint = typeof version === "string" ? ", written as a number without quotes" : "";
701
732
  throw new CommerceError(
702
733
  "CONFIG_INVALID",
703
- `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}`,
704
735
  { details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
705
736
  );
706
737
  }
@@ -771,7 +802,7 @@ function toBoolean(value, path2) {
771
802
  { details: { path: path2 } }
772
803
  );
773
804
  }
774
- function normalise(raw) {
805
+ function normalize(raw) {
775
806
  const protocols = {
776
807
  http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
777
808
  mcp: {
@@ -782,7 +813,7 @@ function normalise(raw) {
782
813
  enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
783
814
  mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
784
815
  },
785
- acp: normaliseAcp(raw.protocols.acp)
816
+ acp: normalizeAcp(raw.protocols.acp)
786
817
  };
787
818
  validateMountPaths(protocols);
788
819
  const x402Raw = raw.payments.x402;
@@ -805,7 +836,7 @@ function normalise(raw) {
805
836
  min: 1
806
837
  }
807
838
  ),
808
- facilitator: normaliseFacilitator(x402Raw.facilitator),
839
+ facilitator: normalizeFacilitator(x402Raw.facilitator),
809
840
  ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
810
841
  ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
811
842
  allowUnauthenticatedFacilitator: toBoolean(
@@ -828,13 +859,13 @@ function normalise(raw) {
828
859
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
829
860
  });
830
861
  }
831
- const mpp = normaliseMpp(raw.payments.mpp);
832
- const ap2 = normaliseAp2(raw.authorization?.ap2);
862
+ const mpp = normalizeMpp(raw.payments.mpp);
863
+ const ap2 = normalizeAp2(raw.authorization?.ap2);
833
864
  if (ap2?.enabled) {
834
865
  validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
835
866
  }
836
867
  const resources = Object.entries(raw.resources).map(
837
- ([id, entry]) => normaliseResource(id, entry, protocols, x402, mpp, ap2)
868
+ ([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
838
869
  );
839
870
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
840
871
  return {
@@ -844,8 +875,7 @@ function normalise(raw) {
844
875
  name: raw.merchant.name,
845
876
  publicBaseUrl: raw.merchant.publicBaseUrl
846
877
  },
847
- // min 0, not 1: port 0 is the standard "let the OS pick a free port"
848
- // convention, useful for tests and ephemeral dev/demo instances.
878
+ // Port 0 lets the OS pick a free port, for tests and throwaway instances
849
879
  server: {
850
880
  port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
851
881
  host: raw.server.host,
@@ -885,7 +915,7 @@ function validateMountPaths(protocols) {
885
915
  function ap2Invalid(path2, message, extra = {}) {
886
916
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
887
917
  }
888
- function normaliseAp2(raw) {
918
+ function normalizeAp2(raw) {
889
919
  if (raw === void 0) return void 0;
890
920
  if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
891
921
  const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
@@ -905,14 +935,14 @@ function normaliseAp2(raw) {
905
935
  if (raw.replay === void 0) {
906
936
  throw ap2Invalid(
907
937
  "authorization.ap2.replay",
908
- "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"
909
939
  );
910
940
  }
911
- const mandateIssuers = normaliseIssuers(
941
+ const mandateIssuers = normalizeIssuers(
912
942
  raw.trust?.mandateIssuers,
913
943
  "authorization.ap2.trust.mandateIssuers"
914
944
  );
915
- const checkoutIssuers = normaliseIssuers(
945
+ const checkoutIssuers = normalizeIssuers(
916
946
  raw.trust?.checkoutIssuers,
917
947
  "authorization.ap2.trust.checkoutIssuers"
918
948
  );
@@ -929,7 +959,7 @@ function normaliseAp2(raw) {
929
959
  replay: { path: raw.replay.path }
930
960
  };
931
961
  }
932
- function normaliseIssuers(raw, path2) {
962
+ function normalizeIssuers(raw, path2) {
933
963
  if (raw === void 0 || raw.length === 0) {
934
964
  throw ap2Invalid(
935
965
  path2,
@@ -1015,11 +1045,11 @@ function validateJwk(jwk, kid, path2) {
1015
1045
  if (jwk["kid"] !== void 0 && value("kid") !== kid) {
1016
1046
  fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
1017
1047
  }
1018
- const normalised = {};
1048
+ const normalized = {};
1019
1049
  for (const member of AP2_JWK_MEMBERS) {
1020
- if (jwk[member] !== void 0) normalised[member] = value(member);
1050
+ if (jwk[member] !== void 0) normalized[member] = value(member);
1021
1051
  }
1022
- return normalised;
1052
+ return normalized;
1023
1053
  }
1024
1054
  function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1025
1055
  if (replayPath === ":memory:") return;
@@ -1033,7 +1063,7 @@ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1033
1063
  );
1034
1064
  }
1035
1065
  }
1036
- function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1066
+ function normalizeResourceAuthorization(id, entry, pricing, ap2) {
1037
1067
  const required = entry.authorization?.required;
1038
1068
  if (required === void 0) return void 0;
1039
1069
  const path2 = `resources.${id}.authorization.required`;
@@ -1073,7 +1103,7 @@ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1073
1103
  function acpInvalid(path2, message, extra = {}) {
1074
1104
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1075
1105
  }
1076
- function normaliseAcp(raw) {
1106
+ function normalizeAcp(raw) {
1077
1107
  const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
1078
1108
  if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
1079
1109
  return { enabled: false, mountPath };
@@ -1103,9 +1133,8 @@ function normaliseAcp(raw) {
1103
1133
  { min: ACP_RETENTION_HOURS }
1104
1134
  )
1105
1135
  },
1106
- checkout: { operations: normaliseAcpOperations(raw.checkout) },
1107
- // Built key by key rather than passed through: absent metadata must stay
1108
- // 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`
1109
1138
  ...discovery !== void 0 ? {
1110
1139
  discovery: {
1111
1140
  ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
@@ -1116,7 +1145,7 @@ function normaliseAcp(raw) {
1116
1145
  } : {}
1117
1146
  };
1118
1147
  }
1119
- function normaliseAcpOperations(checkout) {
1148
+ function normalizeAcpOperations(checkout) {
1120
1149
  const configured = checkout?.operations ?? {};
1121
1150
  for (const key of Object.keys(configured)) {
1122
1151
  if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
@@ -1184,13 +1213,15 @@ function validateAcpOperationInput(path2, operation, resource) {
1184
1213
  const schema = resource.inputSchema;
1185
1214
  if (schema === void 0) return;
1186
1215
  const keys = ACP_OPERATION_INPUT_KEYS[operation];
1216
+ const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
1187
1217
  const properties = schema["properties"];
1188
- if (schema["additionalProperties"] === false && isPlainObject2(properties)) {
1189
- for (const key of keys) {
1218
+ if (schema["additionalProperties"] === false && isRecord(properties)) {
1219
+ for (const key of [...keys, ...optionalKeys]) {
1190
1220
  if (!Object.hasOwn(properties, key)) {
1221
+ const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
1191
1222
  throw acpInvalid(
1192
1223
  path2,
1193
- `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`,
1194
1225
  { resourceId: resource.id }
1195
1226
  );
1196
1227
  }
@@ -1202,17 +1233,17 @@ function validateAcpOperationInput(path2, operation, resource) {
1202
1233
  if (typeof entry === "string" && !keys.includes(entry)) {
1203
1234
  throw acpInvalid(
1204
1235
  path2,
1205
- `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(", ")})`,
1206
1237
  { resourceId: resource.id }
1207
1238
  );
1208
1239
  }
1209
1240
  }
1210
1241
  }
1211
1242
  }
1212
- function normaliseFacilitator(raw) {
1243
+ function normalizeFacilitator(raw) {
1213
1244
  return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
1214
1245
  }
1215
- function normaliseMpp(raw) {
1246
+ function normalizeMpp(raw) {
1216
1247
  if (raw === void 0) return void 0;
1217
1248
  const network = raw.network ?? MPP_DEFAULT_NETWORK;
1218
1249
  if (!isMppNetwork(network)) {
@@ -1239,7 +1270,7 @@ function normaliseMpp(raw) {
1239
1270
  { min: 1 }
1240
1271
  )
1241
1272
  } : {},
1242
- facilitator: normaliseFacilitator(raw.facilitator),
1273
+ facilitator: normalizeFacilitator(raw.facilitator),
1243
1274
  ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
1244
1275
  ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
1245
1276
  allowUnauthenticatedFacilitator: toBoolean(
@@ -1276,7 +1307,7 @@ function normaliseMpp(raw) {
1276
1307
  });
1277
1308
  return mpp;
1278
1309
  }
1279
- function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
1310
+ function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
1280
1311
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1281
1312
  const inputProperties = entry.input?.["properties"];
1282
1313
  if (inputProperties && typeof inputProperties === "object") {
@@ -1342,6 +1373,7 @@ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
1342
1373
  );
1343
1374
  }
1344
1375
  validateBackendUrl(id, entry.backend.url);
1376
+ validateBackendHeaders(id, entry.backend.headers);
1345
1377
  const pathScope = validateInputBindings(id, entry.backend, entry.input);
1346
1378
  validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
1347
1379
  if (entry.pricing.type === "dynamic") {
@@ -1392,7 +1424,7 @@ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
1392
1424
  }
1393
1425
  }
1394
1426
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
1395
- const authorization = normaliseResourceAuthorization(id, entry, pricing, ap2);
1427
+ const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
1396
1428
  return {
1397
1429
  id,
1398
1430
  name: entry.name,
@@ -1422,11 +1454,11 @@ function defaultClosedObjectSchema(schema) {
1422
1454
  const result = { ...schema };
1423
1455
  if (isObjectSchema) {
1424
1456
  const properties = schema["properties"];
1425
- if (isPlainObject2(properties)) {
1457
+ if (isRecord(properties)) {
1426
1458
  result["properties"] = Object.fromEntries(
1427
1459
  Object.entries(properties).map(([key, sub]) => [
1428
1460
  key,
1429
- isPlainObject2(sub) ? defaultClosedObjectSchema(sub) : sub
1461
+ isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
1430
1462
  ])
1431
1463
  );
1432
1464
  }
@@ -1435,18 +1467,15 @@ function defaultClosedObjectSchema(schema) {
1435
1467
  }
1436
1468
  }
1437
1469
  const items = schema["items"];
1438
- if (isPlainObject2(items)) {
1470
+ if (isRecord(items)) {
1439
1471
  result["items"] = defaultClosedObjectSchema(items);
1440
1472
  }
1441
1473
  const additional = schema["additionalProperties"];
1442
- if (isPlainObject2(additional)) {
1474
+ if (isRecord(additional)) {
1443
1475
  result["additionalProperties"] = defaultClosedObjectSchema(additional);
1444
1476
  }
1445
1477
  return result;
1446
1478
  }
1447
- function isPlainObject2(value) {
1448
- return typeof value === "object" && value !== null && !Array.isArray(value);
1449
- }
1450
1479
  function excludesObjectType(schema) {
1451
1480
  const type = schema["type"];
1452
1481
  if (type === void 0 || type === "object") return false;
@@ -1471,7 +1500,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1471
1500
  const requiredRaw = schema["required"];
1472
1501
  const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
1473
1502
  if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
1474
- const declared = isPlainObject2(schema["properties"]) ? schema["properties"] : {};
1503
+ const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
1475
1504
  const undeclared = requiredRaw.filter(
1476
1505
  (name) => typeof name === "string" && !Object.hasOwn(declared, name)
1477
1506
  );
@@ -1484,17 +1513,17 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1484
1513
  }
1485
1514
  }
1486
1515
  const properties = schema["properties"];
1487
- if (isPlainObject2(properties)) {
1516
+ if (isRecord(properties)) {
1488
1517
  for (const [key, sub] of Object.entries(properties)) {
1489
- if (isPlainObject2(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1518
+ if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1490
1519
  }
1491
1520
  }
1492
1521
  const additional = schema["additionalProperties"];
1493
- if (isPlainObject2(additional)) {
1522
+ if (isRecord(additional)) {
1494
1523
  validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
1495
1524
  }
1496
1525
  const items = schema["items"];
1497
- if (isPlainObject2(items)) {
1526
+ if (isRecord(items)) {
1498
1527
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
1499
1528
  } else if (Array.isArray(items)) {
1500
1529
  console.warn(
@@ -1559,7 +1588,7 @@ function validateInputBindings(id, backend, input) {
1559
1588
  );
1560
1589
  }
1561
1590
  const seen = /* @__PURE__ */ new Map();
1562
- const properties = isPlainObject2(input?.["properties"]) ? input["properties"] : {};
1591
+ const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
1563
1592
  const required = new Set(
1564
1593
  Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1565
1594
  );
@@ -1582,7 +1611,7 @@ function validateInputBindings(id, backend, input) {
1582
1611
  );
1583
1612
  }
1584
1613
  const declared = properties[property];
1585
- if (location !== "body" && isPlainObject2(declared) && !isObjectSchemaNode(declared)) {
1614
+ if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
1586
1615
  fail(
1587
1616
  `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
1588
1617
  { location, property }
@@ -1598,7 +1627,7 @@ function validateInputBindings(id, backend, input) {
1598
1627
  if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
1599
1628
  const group = properties[bindings.path];
1600
1629
  return {
1601
- schema: isPlainObject2(group) ? group : void 0,
1630
+ schema: isRecord(group) ? group : void 0,
1602
1631
  where: `input.properties.${bindings.path}`
1603
1632
  };
1604
1633
  }
@@ -1610,7 +1639,7 @@ function validatePathParametersDeclared(id, url, input, where) {
1610
1639
  if (stray !== void 0) {
1611
1640
  throw new CommerceError(
1612
1641
  "CONFIG_INVALID",
1613
- `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`,
1614
1643
  { details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
1615
1644
  );
1616
1645
  }
@@ -1628,13 +1657,13 @@ function validatePathParametersDeclared(id, url, input, where) {
1628
1657
  if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
1629
1658
  throw new CommerceError(
1630
1659
  "CONFIG_INVALID",
1631
- `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`,
1632
1661
  { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1633
1662
  );
1634
1663
  }
1635
1664
  }
1636
1665
  const propertiesRaw = input?.["properties"];
1637
- const properties = isPlainObject2(propertiesRaw) ? propertiesRaw : {};
1666
+ const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
1638
1667
  const requiredRaw = input?.["required"];
1639
1668
  const required = new Set(
1640
1669
  Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
@@ -1644,14 +1673,14 @@ function validatePathParametersDeclared(id, url, input, where) {
1644
1673
  if (!Object.hasOwn(properties, param)) {
1645
1674
  throw new CommerceError(
1646
1675
  "CONFIG_INVALID",
1647
- `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`,
1648
1677
  { details: { path: path2, resourceId: id, param } }
1649
1678
  );
1650
1679
  }
1651
1680
  if (!required.has(param)) {
1652
1681
  throw new CommerceError(
1653
1682
  "CONFIG_INVALID",
1654
- `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`,
1655
1684
  { details: { path: path2, resourceId: id, param } }
1656
1685
  );
1657
1686
  }
@@ -1664,7 +1693,7 @@ function validateBackendUrl(id, url) {
1664
1693
  } catch {
1665
1694
  throw new CommerceError(
1666
1695
  "CONFIG_INVALID",
1667
- `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`,
1668
1697
  { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1669
1698
  );
1670
1699
  }
@@ -1676,6 +1705,19 @@ function validateBackendUrl(id, url) {
1676
1705
  );
1677
1706
  }
1678
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
+ }
1679
1721
  function validateAddress(path2, value) {
1680
1722
  if (!ADDRESS_PATTERN.test(value)) {
1681
1723
  throw new CommerceError(
@@ -1694,20 +1736,21 @@ function validateAddress(path2, value) {
1694
1736
  );
1695
1737
  }
1696
1738
  }
1697
- 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, 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;
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;
1698
1740
  var init_schema = __esm({
1699
1741
  "src/config/schema.ts"() {
1700
1742
  init_constants();
1701
- init_execution();
1702
1743
  init_core();
1744
+ init_execution();
1745
+ init_is_record();
1703
1746
  init_constants2();
1704
1747
  init_guardrails();
1705
1748
  init_constants3();
1749
+ init_constants4();
1706
1750
  init_env();
1707
1751
  SUPPORTED_CONFIG_VERSION = 1;
1708
1752
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
1709
1753
  ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
1710
- MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
1711
1754
  NumberOrString = z.union([z.number(), z.string()]);
1712
1755
  BooleanOrString = z.union([z.boolean(), z.string()]);
1713
1756
  MerchantSchema = z.object({
@@ -1718,14 +1761,14 @@ var init_schema = __esm({
1718
1761
  ServerSchema = z.object({
1719
1762
  port: NumberOrString,
1720
1763
  host: z.string().min(1),
1721
- /** 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.
1722
1766
  adminToken: z.string().min(1).optional(),
1723
- /** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
1724
- // Entries are matched literally against the browser's
1725
- // `Origin` header, so `"*"` and a trailing slash match nothing at all -
1726
- // fail-closed (a lockout, not a bypass), but silently, and a lockout with
1727
- // no explanation is the kind of thing an operator "fixes" by disabling the
1728
- // 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.
1729
1772
  allowedOrigins: z.array(
1730
1773
  z.string().min(1).refine((origin) => origin !== "*", {
1731
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.`
@@ -1744,16 +1787,14 @@ var init_schema = __esm({
1744
1787
  "/health",
1745
1788
  "/ready",
1746
1789
  "/.well-known/agent-commerce",
1747
- // Fixed by the A2A specification, so it is the adapter's to serve and never
1748
- // a configurable mount's to claim.
1790
+ // Fixed by the A2A specification and served by the adapter
1749
1791
  "/.well-known/agent-card.json",
1750
- // Likewise fixed by the ACP specification.
1792
+ // Fixed by the ACP specification
1751
1793
  ACP_WELL_KNOWN_PATH,
1752
1794
  "/api/resources",
1753
1795
  "/api/resources/:id/invoke",
1754
1796
  "/api/receipts",
1755
- "/api/events",
1756
- "/api/events/stream"
1797
+ "/api/events"
1757
1798
  ];
1758
1799
  MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1759
1800
  message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
@@ -1785,16 +1826,14 @@ var init_schema = __esm({
1785
1826
  enabled: BooleanOrString,
1786
1827
  mountPath: MountPathSchema
1787
1828
  }).strict(),
1788
- // Optional block: A2A is off unless an operator asks for it, and an
1789
- // existing config predating the adapter stays valid.
1829
+ // Optional; A2A is off unless configured
1790
1830
  a2a: z.object({
1791
1831
  enabled: BooleanOrString,
1792
1832
  mountPath: MountPathSchema.optional()
1793
1833
  }).strict().optional(),
1794
- // Likewise optional and off by default. The sub-blocks are optional here
1795
- // and required by the business pass only when ACP is enabled, so a
1796
- // disabled placeholder block stays writable and an enabled one gets a
1797
- // 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.
1798
1837
  acp: z.object({
1799
1838
  enabled: BooleanOrString,
1800
1839
  mountPath: MountPathSchema.optional(),
@@ -1850,7 +1889,7 @@ var init_schema = __esm({
1850
1889
  z.object({ type: z.literal("none") }).strict(),
1851
1890
  z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
1852
1891
  // Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
1853
- // which signs a fresh JWT per request; a static header cannot express it.
1892
+ // which signs a fresh JWT per request.
1854
1893
  z.object({
1855
1894
  type: z.literal("cdp"),
1856
1895
  apiKeyId: z.string().min(1),
@@ -1865,10 +1904,8 @@ var init_schema = __esm({
1865
1904
  z.object({
1866
1905
  mode: z.literal("remote"),
1867
1906
  url: z.string().min(1),
1868
- // Absent means "this facilitator takes no credential" - an explicit
1869
- // statement, normalised to `{ type: 'none' }` below. On a mainnet that
1870
- // combination is refused outright, so the default can never quietly
1871
- // become an unauthenticated production facilitator.
1907
+ // Absent auth sends no credential and normalizes to `{ type: 'none' }`,
1908
+ // which on mainnet requires `allowUnauthenticatedFacilitator`
1872
1909
  auth: FacilitatorAuthSchema.optional()
1873
1910
  }).strict()
1874
1911
  ]);
@@ -1883,9 +1920,9 @@ var init_schema = __esm({
1883
1920
  payTo: z.string().min(1),
1884
1921
  maxTimeoutSeconds: NumberOrString,
1885
1922
  facilitator: FacilitatorSchema,
1886
- /** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
1923
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1887
1924
  allowMainnet: BooleanOrString.optional(),
1888
- /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1925
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
1889
1926
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
1890
1927
  }).strict();
1891
1928
  MppSchema = z.object({
@@ -1900,9 +1937,9 @@ var init_schema = __esm({
1900
1937
  challengeSecret: z.string().min(1),
1901
1938
  challengeTtlSeconds: NumberOrString.optional(),
1902
1939
  facilitator: FacilitatorSchema,
1903
- // Real funds. Never defaulted - see src/payments/x402/guardrails.ts
1940
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1904
1941
  allowMainnet: BooleanOrString.optional(),
1905
- // Accepts a mainnet facilitator that takes no credential. Never defaulted
1942
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
1906
1943
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
1907
1944
  }).strict();
1908
1945
  PaymentsSchema = z.object({
@@ -1915,11 +1952,8 @@ var init_schema = __esm({
1915
1952
  }).strict();
1916
1953
  Ap2IssuerSchema = z.object({
1917
1954
  issuer: z.string().min(1),
1918
- /**
1919
- * Required, not defaulted. Without it, a mandate minted for another
1920
- * merchant would verify here, and there is no value worth guessing for
1921
- * something that decides that.
1922
- */
1955
+ // Required, not defaulted: without it a mandate minted for another
1956
+ // merchant would verify here
1923
1957
  audience: z.string().min(1),
1924
1958
  keys: z.array(Ap2KeySchema).min(1)
1925
1959
  }).strict();
@@ -2001,9 +2035,8 @@ async function loadConfig(options = {}) {
2001
2035
  `Configuration file "${configPath}" is not valid YAML: ${reason}`,
2002
2036
  {
2003
2037
  details: { path: configPath, ...details },
2004
- // Retained for programmatic callers. Nothing in the CLI prints a
2005
- // `cause`, and nothing should start: this is the object whose
2006
- // `.message` carries the source excerpt (see below).
2038
+ // Kept for programmatic callers. Never print it: its `.message` quotes
2039
+ // the source line (see describeYamlError).
2007
2040
  cause: error
2008
2041
  }
2009
2042
  );
@@ -2074,9 +2107,9 @@ init_core();
2074
2107
  function defaultDemoSteps() {
2075
2108
  return [
2076
2109
  {
2077
- name: "Start local chain + gateway + merchant (docker compose)",
2110
+ name: "Start the local stack: chain, merchant, gateway and dashboard (docker compose)",
2078
2111
  command: "docker",
2079
- args: ["compose", "up", "--build", "-d"]
2112
+ args: ["compose", "up", "--build", "--detach", "--wait"]
2080
2113
  },
2081
2114
  { name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
2082
2115
  ];
@@ -2084,7 +2117,7 @@ function defaultDemoSteps() {
2084
2117
  async function runDemo(io, deps = {}) {
2085
2118
  const steps = deps.steps ?? defaultDemoSteps();
2086
2119
  const run = deps.run ?? execCommand;
2087
- 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");
2088
2121
  for (const step of steps) {
2089
2122
  io.stdout("");
2090
2123
  io.stdout(`==> ${step.name}`);
@@ -2100,13 +2133,6 @@ async function runDemo(io, deps = {}) {
2100
2133
  io.stderr(` ${step.command} ${step.args.join(" ")}`);
2101
2134
  return 1;
2102
2135
  }
2103
- if (step.healthCheck !== void 0) {
2104
- const healthy = await step.healthCheck();
2105
- if (!healthy) {
2106
- io.stderr(`FAIL "${step.name}" completed but its health check did not pass.`);
2107
- return 1;
2108
- }
2109
- }
2110
2136
  io.stdout(`OK ${step.name}`);
2111
2137
  }
2112
2138
  io.stdout("");
@@ -2137,8 +2163,7 @@ init_constants();
2137
2163
  // src/authorization/ap2/descriptor.ts
2138
2164
  init_constants();
2139
2165
  var AP2_UNSUPPORTED = [
2140
- // Mandate kinds. Open mandates carry spending constraints nothing here
2141
- // evaluates, so accepting one would tell a buyer their limits were checked.
2166
+ // Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
2142
2167
  "autonomous mode",
2143
2168
  "open checkout mandates (mandate.checkout.open.1)",
2144
2169
  "intent mandates",
@@ -2158,8 +2183,8 @@ var AP2_UNSUPPORTED = [
2158
2183
  ];
2159
2184
 
2160
2185
  // src/cli/commands/doctor.ts
2161
- init_execution();
2162
2186
  init_core();
2187
+ init_execution();
2163
2188
  init_constants2();
2164
2189
  init_networks();
2165
2190
 
@@ -2183,13 +2208,12 @@ var A2A_UNSUPPORTED_METHODS = [
2183
2208
 
2184
2209
  // src/protocols/a2a/descriptor.ts
2185
2210
  var A2A_UNSUPPORTED = [
2186
- // Methods, named as the protocol names them. Same list the transport
2187
- // 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
2188
2212
  ...A2A_UNSUPPORTED_METHODS,
2189
- // Transports other than the one binding served.
2213
+ // Transports other than the one binding served
2190
2214
  "HTTP+JSON/REST binding",
2191
2215
  "gRPC binding",
2192
- // Behaviours.
2216
+ // Behaviors
2193
2217
  "SSE",
2194
2218
  "long-running task persistence",
2195
2219
  "task resumption",
@@ -2221,15 +2245,15 @@ var ACP_UNSUPPORTED = [
2221
2245
  "standalone orders service",
2222
2246
  "delegate_payment",
2223
2247
  "delegate_authentication",
2224
- // Bindings and delivery directions.
2248
+ // Bindings and delivery directions
2225
2249
  "ACP MCP transport binding",
2226
2250
  "webhooks",
2227
2251
  "outbound merchant-to-agent delivery",
2228
2252
  "ACP client role",
2229
- // Request authentication beyond the bearer token.
2253
+ // Request authentication beyond the bearer token
2230
2254
  "Signature header verification",
2231
2255
  "Timestamp replay-window verification",
2232
- // Extensions and versioning.
2256
+ // Extensions, versioning and payment handlers
2233
2257
  "discount extension",
2234
2258
  "general ACP extension framework",
2235
2259
  "multiple ACP API versions",
@@ -2238,7 +2262,7 @@ var ACP_UNSUPPORTED = [
2238
2262
 
2239
2263
  // src/storage/receipts/store.ts
2240
2264
  init_core();
2241
- var PACKAGE_VERSION = "1.5.0" ;
2265
+ var PACKAGE_VERSION = "1.6.1" ;
2242
2266
 
2243
2267
  // src/storage/sqlite.ts
2244
2268
  init_core();
@@ -2246,10 +2270,7 @@ function openSqliteDatabase(options) {
2246
2270
  const { path: path2, label, logger } = options;
2247
2271
  const isFileBacked = path2 !== ":memory:";
2248
2272
  if (isFileBacked) {
2249
- const dir = dirname(path2);
2250
- if (dir !== "" && dir !== "." && !existsSync(dir)) {
2251
- mkdirSync(dir, { recursive: true, mode: 448 });
2252
- }
2273
+ mkdirSync(dirname(path2), { recursive: true, mode: 448 });
2253
2274
  }
2254
2275
  if (isFileBacked && existsSync(path2)) {
2255
2276
  try {
@@ -2321,8 +2342,8 @@ function receiptToRow(receipt) {
2321
2342
  payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
2322
2343
  delivered_at: receipt.deliveredAt,
2323
2344
  backend_status: receipt.backendStatus,
2324
- duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
2325
- protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
2345
+ duration_ms: receipt.durationMs ?? null,
2346
+ protocol: receipt.protocol ?? null,
2326
2347
  metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
2327
2348
  authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
2328
2349
  };
@@ -2349,12 +2370,12 @@ function eventToRow(event) {
2349
2370
  id: event.id,
2350
2371
  type: event.type,
2351
2372
  request_id: event.requestId,
2352
- resource_id: event.resourceId !== void 0 ? event.resourceId : null,
2373
+ resource_id: event.resourceId ?? null,
2353
2374
  at: event.at,
2354
- adapter: event.adapter !== void 0 ? event.adapter : null,
2355
- payment_provider: event.paymentProvider !== void 0 ? event.paymentProvider : null,
2356
- duration_ms: event.durationMs !== void 0 ? event.durationMs : null,
2357
- 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,
2358
2379
  data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
2359
2380
  };
2360
2381
  }
@@ -2458,7 +2479,7 @@ var MIGRATIONS = [
2458
2479
  }
2459
2480
  }
2460
2481
  ];
2461
- var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
2482
+ var SCHEMA_VERSION = MIGRATIONS.at(-1)?.version ?? 0;
2462
2483
  function migrate(db) {
2463
2484
  const current = db.pragma("user_version", { simple: true });
2464
2485
  for (const migration of MIGRATIONS) {
@@ -2521,10 +2542,8 @@ function createSqliteReceiptStore(options) {
2521
2542
  );
2522
2543
  const insertPaymentAttemptStmt = db.prepare(
2523
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)
2524
- VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)`
2525
- );
2526
- const getPaymentAttemptByIdStmt = db.prepare(
2527
- "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 *`
2528
2547
  );
2529
2548
  const updatePaymentAttemptStmt = db.prepare(
2530
2549
  `UPDATE payment_attempts SET status = @status,
@@ -2575,8 +2594,9 @@ function createSqliteReceiptStore(options) {
2575
2594
  async reservePaymentAttempt(reservation) {
2576
2595
  const now = clock.nowIso();
2577
2596
  const id = ids.next("attempt");
2597
+ let row;
2578
2598
  try {
2579
- insertPaymentAttemptStmt.run({
2599
+ row = insertPaymentAttemptStmt.get({
2580
2600
  id,
2581
2601
  request_id: reservation.requestId,
2582
2602
  resource_id: reservation.resourceId,
@@ -2596,19 +2616,12 @@ function createSqliteReceiptStore(options) {
2596
2616
  if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
2597
2617
  throw new CommerceError(
2598
2618
  "PAYMENT_REPLAYED",
2599
- `Payment authorisation has already been used (replayKey=${reservation.replayKey})`,
2619
+ `Payment authorization has already been used (replayKey=${reservation.replayKey})`,
2600
2620
  { requestId: reservation.requestId, resourceId: reservation.resourceId }
2601
2621
  );
2602
2622
  }
2603
2623
  throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
2604
2624
  }
2605
- const row = getPaymentAttemptByIdStmt.get(id);
2606
- if (row === void 0) {
2607
- throw new CommerceError(
2608
- "STORAGE_ERROR",
2609
- "Payment attempt vanished immediately after insert"
2610
- );
2611
- }
2612
2625
  return rowToPaymentAttempt(row);
2613
2626
  },
2614
2627
  async updatePaymentAttempt(update) {
@@ -2646,11 +2659,11 @@ function createSqliteReceiptStore(options) {
2646
2659
  const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
2647
2660
  return rows.map(rowToReceipt);
2648
2661
  },
2649
- /** 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)
2650
2663
  async countReceipts() {
2651
2664
  return countReceiptsStmt.get()?.count ?? 0;
2652
2665
  },
2653
- /** Exact count of non-2xx receipts — see the interface doc for why this exists. */
2666
+ // Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
2654
2667
  async countUndeliveredReceipts() {
2655
2668
  return countUndeliveredReceiptsStmt.get()?.count ?? 0;
2656
2669
  },
@@ -2730,35 +2743,28 @@ function createSqliteReceiptStore(options) {
2730
2743
  }
2731
2744
 
2732
2745
  // src/cli/lib/config-client.ts
2733
- async function loadConfigDynamic(options = {}) {
2734
- let mod;
2746
+ init_core();
2747
+ async function importConfig() {
2735
2748
  try {
2736
- mod = await Promise.resolve().then(() => (init_config(), config_exports));
2749
+ return await Promise.resolve().then(() => (init_config(), config_exports));
2737
2750
  } catch (err) {
2738
- throw new Error(
2751
+ throw new CommerceError(
2752
+ "INTERNAL_ERROR",
2739
2753
  `the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
2740
2754
  );
2741
2755
  }
2742
- return mod.loadConfig(options);
2756
+ }
2757
+ async function loadConfigDynamic(options = {}) {
2758
+ return (await importConfig()).loadConfig(options);
2743
2759
  }
2744
2760
  async function parseConfigDynamic(raw, env = process.env) {
2745
- let mod;
2746
- try {
2747
- mod = await Promise.resolve().then(() => (init_config(), config_exports));
2748
- } catch (err) {
2749
- throw new Error(
2750
- `the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
2751
- );
2752
- }
2753
- return mod.parseConfig(raw, env);
2761
+ return (await importConfig()).parseConfig(raw, env);
2754
2762
  }
2755
2763
 
2756
2764
  // src/cli/lib/http.ts
2757
2765
  async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
2758
- const controller = new AbortController();
2759
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2760
2766
  try {
2761
- const res = await fetchImpl(url, { signal: controller.signal });
2767
+ const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
2762
2768
  let body;
2763
2769
  try {
2764
2770
  body = await res.json();
@@ -2772,8 +2778,6 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
2772
2778
  status: 0,
2773
2779
  error: err instanceof Error ? err.message : "unknown fetch error"
2774
2780
  };
2775
- } finally {
2776
- clearTimeout(timer);
2777
2781
  }
2778
2782
  }
2779
2783
 
@@ -2818,9 +2822,8 @@ function buildResourcesMap(answers) {
2818
2822
  resources.report = {
2819
2823
  name: "Get market report",
2820
2824
  description: "Deterministic demo premium market report.",
2821
- // No caller-supplied fields for this resource: an explicit empty,
2822
- // closed schema (rather than omitting `input`) so a missing schema can
2823
- // 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
2824
2827
  input: {
2825
2828
  type: "object",
2826
2829
  properties: {},
@@ -2871,25 +2874,22 @@ function buildInitConfigObject(answers) {
2871
2874
  }
2872
2875
  var HEADER = `# Generated by \`agent-commerce init\`.
2873
2876
  #
2874
- # Any address/key below labelled "LOCAL DEVELOPMENT ONLY" or "placeholder" is
2875
- # either one of Anvil's well-known, public dev accounts (never fund them,
2876
- # never use them outside a local chain) or a
2877
- # not-yet-real address. Before running against the local chain, run
2878
- # \`npm run chain:deploy\` and replace \`payments.x402.asset\` with the deployed
2879
- # MockUSDC address from \`.deploy/local.json\`. Replace \`payments.x402.payTo\`
2880
- # with your real merchant-controlled settlement address before ever pointing
2881
- # 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.
2882
2884
  #
2883
- # server.host is loopback (127.0.0.1) on purpose: binding every interface
2884
- # 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.
2885
2887
  #
2886
- # server.adminToken / server.allowedOrigins are left unset here \u2014 a generated
2887
- # secret in a file you might commit is its own problem, so this is left as a
2888
- # placeholder for you to fill in rather than a real token. Without a token,
2889
- # the operator routes (/api/receipts, /api/events, /api/events/stream) \u2014
2890
- # the merchant's commerce ledger: payer addresses, amounts, settlement
2891
- # hashes \u2014 return 404 rather than serving openly. To enable them, uncomment
2892
- # 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:
2893
2893
  # server:
2894
2894
  # adminToken: \${ADMIN_TOKEN}
2895
2895
  # allowedOrigins:
@@ -2901,18 +2901,30 @@ function renderInitConfigYaml(answers) {
2901
2901
  return `${HEADER}
2902
2902
  ${body}`;
2903
2903
  }
2904
+
2905
+ // src/payments/x402/local-chain/manifest.ts
2906
+ init_core();
2904
2907
  var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
2905
- var REQUIRED_TOP_LEVEL_STRING_FIELDS = [
2906
- "rpcUrl",
2907
- "asset",
2908
- "assetName",
2909
- "assetVersion",
2910
- "buyerInitialBalance"
2911
- ];
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
+ });
2912
2923
  function readLocalChainManifest(cwd = process.cwd()) {
2913
2924
  const path2 = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
2914
2925
  if (!existsSync(path2)) {
2915
- throw new Error(
2926
+ throw new CommerceError(
2927
+ "CONFIG_INVALID",
2916
2928
  `Local chain manifest not found at "${path2}". Run "npm run chain:start" then "npm run chain:deploy" first.`
2917
2929
  );
2918
2930
  }
@@ -2920,56 +2932,30 @@ function readLocalChainManifest(cwd = process.cwd()) {
2920
2932
  try {
2921
2933
  raw = readFileSync(path2, "utf8");
2922
2934
  } catch (cause) {
2923
- 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
+ });
2924
2938
  }
2925
2939
  let parsed;
2926
2940
  try {
2927
2941
  parsed = JSON.parse(raw);
2928
2942
  } catch (cause) {
2929
- throw new Error(
2943
+ throw new CommerceError(
2944
+ "CONFIG_INVALID",
2930
2945
  `Local chain manifest at "${path2}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
2931
2946
  { cause }
2932
2947
  );
2933
2948
  }
2934
- return validateManifestShape(parsed, path2);
2935
- }
2936
- function validateManifestShape(value, path2) {
2937
- const fail = (detail) => {
2938
- throw new Error(
2939
- `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".`
2940
2956
  );
2941
- };
2942
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2943
- return fail("expected a JSON object");
2944
- }
2945
- const record = value;
2946
- if (typeof record.chainId !== "number") {
2947
- return fail('"chainId" must be a number');
2948
- }
2949
- if (typeof record.assetDecimals !== "number") {
2950
- return fail('"assetDecimals" must be a number');
2951
- }
2952
- for (const field of REQUIRED_TOP_LEVEL_STRING_FIELDS) {
2953
- if (typeof record[field] !== "string" || record[field] === "") {
2954
- return fail(`"${field}" must be a non-empty string`);
2955
- }
2956
2957
  }
2957
- if ("hostRpcUrl" in record && record.hostRpcUrl !== void 0) {
2958
- if (typeof record.hostRpcUrl !== "string" || record.hostRpcUrl === "") {
2959
- return fail('"hostRpcUrl" must be a non-empty string when present');
2960
- }
2961
- }
2962
- const merchant = record.merchant;
2963
- if (typeof merchant !== "object" || merchant === null || typeof merchant.address !== "string" || typeof merchant.privateKeyLabel !== "string") {
2964
- return fail('"merchant" must be { address: string, privateKeyLabel: string }');
2965
- }
2966
- for (const key of ["buyer", "facilitator"]) {
2967
- const account = record[key];
2968
- if (typeof account !== "object" || account === null || typeof account.address !== "string" || typeof account.privateKey !== "string" || typeof account.note !== "string") {
2969
- return fail(`"${key}" must be { address: string, privateKey: string, note: string }`);
2970
- }
2971
- }
2972
- return record;
2958
+ return result.data;
2973
2959
  }
2974
2960
 
2975
2961
  // src/cli/lib/manifest-env.ts
@@ -3007,12 +2993,63 @@ function maskMiddle(value, keep = 6) {
3007
2993
  if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
3008
2994
  return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
3009
2995
  }
3010
- 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.5",
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
+ ];
3011
3043
  function readVersionReport() {
3012
- const injected = '{"cliVersion":"1.5.0","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"}]}' ;
3013
- {
3014
- return JSON.parse(injected);
3015
- }
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 };
3016
3053
  }
3017
3054
 
3018
3055
  // src/cli/commands/doctor.ts
@@ -3030,7 +3067,7 @@ function deriveGatewayUrl(explicit, config) {
3030
3067
  function substitutePathParams(url) {
3031
3068
  let filled = url;
3032
3069
  for (const name of extractPathParameterNames(url)) {
3033
- filled = filled.split(`{${name}}`).join("demo-check");
3070
+ filled = filled.replaceAll(`{${name}}`, "demo-check");
3034
3071
  }
3035
3072
  return filled;
3036
3073
  }
@@ -3072,6 +3109,21 @@ function findX402Mismatch(configured, live, payToField = "payTo") {
3072
3109
  if (diffs.length === 0) return void 0;
3073
3110
  return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
3074
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
+ };
3126
+ }
3075
3127
  function describeIssuers(label, issuers) {
3076
3128
  const described = issuers.map(
3077
3129
  (entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
@@ -3086,72 +3138,26 @@ function isWritableFile(path2) {
3086
3138
  return false;
3087
3139
  }
3088
3140
  }
3089
- function ap2ReplayCheck(path2) {
3090
- const name = "AP2 replay store";
3141
+ function storeFileCheck(name, path2, risk, suffix = "") {
3091
3142
  if (path2 === ":memory:") {
3092
3143
  return {
3093
3144
  name,
3094
3145
  status: "WARN",
3095
- 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}`
3096
3147
  };
3097
3148
  }
3098
3149
  if (existsSync(path2)) {
3099
- return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"` } : {
3150
+ return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"${suffix}` } : {
3100
3151
  name,
3101
3152
  status: "FAIL",
3102
- detail: `"${path2}" exists but is not writable by this user - no mandate could be recorded as spent`
3103
- };
3104
- }
3105
- const directory = dirname(path2);
3106
- if (directory !== "" && directory !== "." && !existsSync(directory)) {
3107
- return {
3108
- name,
3109
- status: "WARN",
3110
- detail: `no store yet at "${path2}" and its directory does not exist - it is created when the gateway starts, provided that path is writable`
3153
+ detail: `"${path2}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
3111
3154
  };
3112
3155
  }
3113
- return {
3114
- name,
3115
- status: "WARN",
3116
- detail: `no store yet at "${path2}" - it is created the first time the gateway starts`
3117
- };
3156
+ return { name, status: "WARN", detail: `${missingStoreDetail(path2)}${suffix}` };
3118
3157
  }
3119
- function acpIdempotencyCheck(idempotency) {
3120
- const retention = `retention ${idempotency.retentionHours}h`;
3121
- if (idempotency.retentionHours < 24) {
3122
- return {
3123
- name: "ACP idempotency",
3124
- status: "FAIL",
3125
- detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
3126
- };
3127
- }
3128
- if (idempotency.path === ":memory:") {
3129
- return {
3130
- name: "ACP idempotency",
3131
- status: "WARN",
3132
- detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
3133
- };
3134
- }
3135
- const directory = dirname(idempotency.path);
3136
- if (existsSync(idempotency.path)) {
3137
- return isWritableFile(idempotency.path) ? { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` } : {
3138
- name: "ACP idempotency",
3139
- status: "FAIL",
3140
- detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
3141
- };
3142
- }
3143
- if (directory !== "" && directory !== "." && !existsSync(directory)) {
3144
- return {
3145
- name: "ACP idempotency",
3146
- status: "WARN",
3147
- 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`
3148
- };
3149
- }
3150
- return {
3151
- name: "ACP idempotency",
3152
- status: "WARN",
3153
- detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
3154
- };
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`;
3155
3161
  }
3156
3162
  function acpMappingCheck(config, operations) {
3157
3163
  const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
@@ -3195,11 +3201,11 @@ async function runDoctor(options, deps = {}) {
3195
3201
  checks.push({
3196
3202
  name: "Config",
3197
3203
  status: "PASS",
3198
- 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}`
3199
3205
  });
3200
3206
  } catch (err) {
3201
3207
  const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
3202
- 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` : "";
3203
3209
  checks.push({
3204
3210
  name: "Config",
3205
3211
  status: "FAIL",
@@ -3227,7 +3233,7 @@ async function runDoctor(options, deps = {}) {
3227
3233
  );
3228
3234
  }
3229
3235
  if (config === void 0) {
3230
- checks.push({ name: "Backend", status: "WARN", detail: "skipped \u2014 config invalid" });
3236
+ checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
3231
3237
  } else if (config.resources.length === 0) {
3232
3238
  checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
3233
3239
  } else {
@@ -3250,7 +3256,7 @@ async function runDoctor(options, deps = {}) {
3250
3256
  CHECK_TIMEOUT_MS
3251
3257
  ) : void 0;
3252
3258
  if (config === void 0) {
3253
- checks.push({ name: "Protocols", status: "WARN", detail: "skipped \u2014 config invalid" });
3259
+ checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
3254
3260
  } else if (wellKnown === void 0 || !wellKnown.ok) {
3255
3261
  checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
3256
3262
  } else {
@@ -3263,7 +3269,7 @@ async function runDoctor(options, deps = {}) {
3263
3269
  });
3264
3270
  }
3265
3271
  if (config === void 0) {
3266
- checks.push({ name: "A2A", status: "WARN", detail: "skipped \u2014 config invalid" });
3272
+ checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
3267
3273
  } else if (!config.protocols.a2a.enabled) {
3268
3274
  checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
3269
3275
  } else {
@@ -3279,7 +3285,7 @@ async function runDoctor(options, deps = {}) {
3279
3285
  });
3280
3286
  }
3281
3287
  if (config === void 0) {
3282
- checks.push({ name: "ACP", status: "WARN", detail: "skipped \u2014 config invalid" });
3288
+ checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
3283
3289
  } else if (!config.protocols.acp.enabled) {
3284
3290
  checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
3285
3291
  } else {
@@ -3294,7 +3300,17 @@ async function runDoctor(options, deps = {}) {
3294
3300
  status: "PASS",
3295
3301
  detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
3296
3302
  });
3297
- 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
+ );
3298
3314
  checks.push(acpMappingCheck(config, acp.checkout.operations));
3299
3315
  checks.push({
3300
3316
  name: "ACP unsupported",
@@ -3304,7 +3320,7 @@ async function runDoctor(options, deps = {}) {
3304
3320
  }
3305
3321
  const ap2 = config?.authorization?.ap2;
3306
3322
  if (config === void 0) {
3307
- checks.push({ name: "AP2", status: "WARN", detail: "skipped \u2014 config invalid" });
3323
+ checks.push({ name: "AP2", status: "WARN", detail: "skipped: config invalid" });
3308
3324
  } else if (ap2 === void 0 || !ap2.enabled) {
3309
3325
  checks.push({ name: "AP2", status: "INFO", detail: "disabled" });
3310
3326
  } else {
@@ -3318,14 +3334,19 @@ async function runDoctor(options, deps = {}) {
3318
3334
  status: "PASS",
3319
3335
  detail: `${describeIssuers("mandate", ap2.trust.mandateIssuers)} \xB7 ${describeIssuers("checkout", ap2.trust.checkoutIssuers)}`
3320
3336
  });
3321
- 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
+ );
3322
3343
  const gated = config.resources.filter(
3323
3344
  (resource) => resource.authorization?.required.includes("ap2")
3324
3345
  );
3325
3346
  checks.push({
3326
3347
  name: "AP2 resources",
3327
3348
  status: gated.length > 0 ? "PASS" : "WARN",
3328
- 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"
3329
3350
  });
3330
3351
  checks.push({
3331
3352
  name: "AP2 unsupported",
@@ -3337,23 +3358,20 @@ async function runDoctor(options, deps = {}) {
3337
3358
  if (x402 === void 0 || !x402.enabled) {
3338
3359
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
3339
3360
  } else {
3340
- const profile = findNetworkProfile(x402.network);
3341
- const mode = profile ? resolveDeploymentMode(profile, x402.facilitator.mode) : void 0;
3342
- 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})`;
3343
- const facilitatorDetail = x402.facilitator.mode === "local" ? "local" : `remote (auth=${x402.facilitator.auth.type})`;
3344
- 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}`;
3345
3363
  const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
3346
3364
  if (sameAddress2(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
3347
3365
  checks.push({
3348
3366
  name: "Payments",
3349
3367
  status: "WARN",
3350
- 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)`
3351
3369
  });
3352
3370
  } else if (live === "disabled") {
3353
3371
  checks.push({
3354
3372
  name: "Payments",
3355
3373
  status: "FAIL",
3356
- 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`
3357
3375
  });
3358
3376
  } else if (live === void 0) {
3359
3377
  checks.push({
@@ -3368,23 +3386,15 @@ async function runDoctor(options, deps = {}) {
3368
3386
  );
3369
3387
  }
3370
3388
  if (mode === "mainnet") {
3371
- const unauthenticated = x402.facilitator.mode === "remote" && x402.facilitator.auth.type === "none";
3372
- checks.push({
3373
- name: "Mainnet safety",
3374
- status: unauthenticated ? "WARN" : "INFO",
3375
- 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.`
3376
- });
3389
+ checks.push(mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator));
3377
3390
  }
3378
3391
  }
3379
3392
  const mpp = config?.payments.mpp;
3380
3393
  if (mpp === void 0 || !mpp.enabled) {
3381
3394
  checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
3382
3395
  } else {
3383
- const profile = findNetworkProfile(mpp.network);
3384
- const mode = profile ? resolveDeploymentMode(profile, mpp.facilitator.mode) : void 0;
3385
- const where = profile === void 0 || mode === void 0 ? `unknown network ${mpp.network}` : mode === "local" ? `LOCAL dev chain (${mpp.network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${mpp.network})`;
3386
- const facilitatorDetail = mpp.facilitator.mode === "local" ? "local" : `remote (auth=${mpp.facilitator.auth.type})`;
3387
- 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=${facilitatorDetail}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
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}`;
3388
3398
  const configured = { asset: mpp.asset, network: mpp.network, payTo: mpp.recipient };
3389
3399
  const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body, "mpp") : void 0;
3390
3400
  if (live === "disabled") {
@@ -3405,23 +3415,24 @@ async function runDoctor(options, deps = {}) {
3405
3415
  mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
3406
3416
  );
3407
3417
  }
3418
+ if (mode === "mainnet") {
3419
+ checks.push(mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator));
3420
+ }
3408
3421
  }
3409
3422
  if (config === void 0) {
3410
- checks.push({ name: "Storage", status: "WARN", detail: "skipped \u2014 config invalid" });
3423
+ checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
3411
3424
  } else if (
3412
- // Diagnose without creating: createSqliteReceiptStore mkdir+creates the
3413
- // file, so calling it on a wrong path (a typo, or a container path read
3414
- // from the host) silently produces a fresh empty database and a false
3415
- // "healthy, receipts=0" PASS — a read-only command must not have this
3416
- // side effect, and it shadows the real store's real receipts entirely.
3417
- // `:memory:` never exists on disk by definition, so it always falls
3418
- // 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.
3419
3430
  config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
3420
3431
  ) {
3421
3432
  checks.push({
3422
3433
  name: "Storage",
3423
3434
  status: "WARN",
3424
- 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.`
3425
3436
  });
3426
3437
  } else {
3427
3438
  let store;
@@ -3458,7 +3469,7 @@ async function runDoctor(options, deps = {}) {
3458
3469
  await store?.close();
3459
3470
  }
3460
3471
  }
3461
- if (wellKnown !== void 0 && wellKnown.ok) {
3472
+ if (wellKnown?.ok) {
3462
3473
  checks.push({
3463
3474
  name: "Protocol versions",
3464
3475
  status: "PASS",
@@ -3469,7 +3480,7 @@ async function runDoctor(options, deps = {}) {
3469
3480
  checks.push({
3470
3481
  name: "Protocol versions",
3471
3482
  status: "INFO",
3472
- 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(", ")}`
3473
3484
  });
3474
3485
  }
3475
3486
  const scored = checks.filter((c) => c.status !== "INFO");
@@ -3508,6 +3519,8 @@ init_core();
3508
3519
  // src/openapi/discover.ts
3509
3520
  init_errors();
3510
3521
  init_execution();
3522
+ init_is_record();
3523
+ init_constants4();
3511
3524
 
3512
3525
  // src/openapi/refs.ts
3513
3526
  init_errors();
@@ -3548,7 +3561,7 @@ function dereference(document, node, stack = []) {
3548
3561
  }
3549
3562
  function resolvePointer(document, ref) {
3550
3563
  const pointer = ref.slice(1);
3551
- if (pointer === "" || pointer === "/") return document;
3564
+ if (pointer === "") return document;
3552
3565
  if (!pointer.startsWith("/")) {
3553
3566
  throw new CommerceError(
3554
3567
  "CONFIG_INVALID",
@@ -3558,12 +3571,22 @@ function resolvePointer(document, ref) {
3558
3571
  }
3559
3572
  let current = document;
3560
3573
  for (const rawSegment of pointer.slice(1).split("/")) {
3561
- 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", "~");
3562
3585
  if (Array.isArray(current)) {
3563
3586
  const index = Number(segment);
3564
3587
  current = Number.isInteger(index) ? current[index] : void 0;
3565
3588
  } else if (typeof current === "object" && current !== null) {
3566
- current = current[segment];
3589
+ current = Object.hasOwn(current, segment) ? current[segment] : void 0;
3567
3590
  } else {
3568
3591
  current = void 0;
3569
3592
  }
@@ -3587,8 +3610,7 @@ var SUPPORTED_METHODS = {
3587
3610
  delete: "DELETE"
3588
3611
  };
3589
3612
  var NON_OPERATION_KEYS = /* @__PURE__ */ new Set(["summary", "description", "servers", "parameters", "$ref"]);
3590
- var ID_ALLOWED = /[^A-Za-z0-9_.-]+/g;
3591
- var MAX_ID_LENGTH = 128;
3613
+ var ID_DISALLOWED = new RegExp(`[^${MCP_TOOL_NAME_CHARS}]+`, "g");
3592
3614
  function discoverOperations(loaded, options = {}) {
3593
3615
  const { document } = loaded;
3594
3616
  const diagnostics = [];
@@ -3681,8 +3703,8 @@ function discoverOperations(loaded, options = {}) {
3681
3703
  ...description !== void 0 ? { description } : {},
3682
3704
  parameters: [
3683
3705
  ...toArray(pathItem["parameters"]),
3684
- // Operation parameters last: Phase 4 lets the later one win, which is
3685
- // 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`)
3686
3708
  ...toArray(operation["parameters"])
3687
3709
  ],
3688
3710
  ...operation["requestBody"] !== void 0 ? { requestBody: operation["requestBody"] } : {},
@@ -3694,12 +3716,12 @@ function discoverOperations(loaded, options = {}) {
3694
3716
  return { operations, diagnostics };
3695
3717
  }
3696
3718
  function toResourceId(operationId, method, path2) {
3697
- const fromOperationId = operationId === void 0 ? "" : normaliseId(operationId);
3719
+ const fromOperationId = operationId === void 0 ? "" : normalizeId(operationId);
3698
3720
  if (fromOperationId !== "") return fromOperationId;
3699
- return normaliseId(`${method.toLowerCase()}_${path2}`);
3721
+ return normalizeId(`${method.toLowerCase()}_${path2}`);
3700
3722
  }
3701
- function normaliseId(value) {
3702
- 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);
3703
3725
  }
3704
3726
  function selectServer(document, candidates, context, options, diagnostics) {
3705
3727
  if (options.baseUrl !== void 0) return options.baseUrl;
@@ -3797,18 +3819,20 @@ function assertAbsoluteHttpUrl(value, label) {
3797
3819
  );
3798
3820
  }
3799
3821
  }
3800
- function isRecord(value) {
3801
- return typeof value === "object" && value !== null && !Array.isArray(value);
3802
- }
3803
3822
  function toArray(value) {
3804
3823
  return Array.isArray(value) ? value : [];
3805
3824
  }
3806
3825
 
3826
+ // src/openapi/draft.ts
3827
+ init_is_record();
3828
+
3807
3829
  // src/openapi/request.ts
3808
3830
  init_execution();
3831
+ init_is_record();
3809
3832
 
3810
3833
  // src/openapi/schema.ts
3811
3834
  init_errors();
3835
+ init_is_record();
3812
3836
  var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
3813
3837
  "object",
3814
3838
  "string",
@@ -3839,7 +3863,7 @@ function convertSchema(document, node, stack = []) {
3839
3863
  const dropped = /* @__PURE__ */ new Set();
3840
3864
  try {
3841
3865
  const schema = convertNode(document, node, stack, dropped);
3842
- return schema === void 0 ? { supported: false, reason: "schema could not be represented" } : { supported: true, schema, dropped: [...dropped] };
3866
+ return { supported: true, schema, dropped: [...dropped] };
3843
3867
  } catch (error) {
3844
3868
  if (error instanceof UnsupportedSchema) return { supported: false, reason: error.message };
3845
3869
  if (isCommerceError(error)) return { supported: false, reason: error.message };
@@ -3852,11 +3876,8 @@ function convertNode(document, node, stack, dropped) {
3852
3876
  if (node === true) return {};
3853
3877
  if (node === false) throw new UnsupportedSchema("schema is `false`, which accepts no value");
3854
3878
  const resolved = dereference(document, node, stack);
3855
- const source = resolved.value;
3856
- if (typeof source !== "object" || source === null || Array.isArray(source)) {
3857
- throw new UnsupportedSchema("schema node is not an object");
3858
- }
3859
- const schemaNode = source;
3879
+ const schemaNode = resolved.value;
3880
+ if (!isRecord(schemaNode)) throw new UnsupportedSchema("schema node is not an object");
3860
3881
  if (Array.isArray(schemaNode["allOf"])) {
3861
3882
  return mergeAllOf(document, schemaNode, resolved.stack, dropped);
3862
3883
  }
@@ -3875,11 +3896,15 @@ function convertNode(document, node, stack, dropped) {
3875
3896
  }
3876
3897
  if (Array.isArray(schemaNode["enum"])) result["enum"] = [...schemaNode["enum"]];
3877
3898
  const properties = schemaNode["properties"];
3878
- if (isRecord2(properties)) {
3899
+ if (isRecord(properties)) {
3879
3900
  const converted = {};
3880
3901
  for (const [name, sub] of Object.entries(properties)) {
3881
- const child = convertNode(document, sub, resolved.stack, dropped);
3882
- 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);
3883
3908
  }
3884
3909
  result["properties"] = converted;
3885
3910
  }
@@ -3892,17 +3917,15 @@ function convertNode(document, node, stack, dropped) {
3892
3917
  if (typeof additional === "boolean") {
3893
3918
  result["additionalProperties"] = additional;
3894
3919
  } else if (additional !== void 0) {
3895
- const child = convertNode(document, additional, resolved.stack, dropped);
3896
- if (child !== void 0) result["additionalProperties"] = child;
3897
- } else if (isRecord2(properties)) {
3920
+ result["additionalProperties"] = convertNode(document, additional, resolved.stack, dropped);
3921
+ } else if (isRecord(properties)) {
3898
3922
  result["additionalProperties"] = false;
3899
3923
  }
3900
3924
  const items = schemaNode["items"];
3901
3925
  if (Array.isArray(items)) {
3902
3926
  dropped.add("items (tuple form)");
3903
3927
  } else if (items !== void 0) {
3904
- const child = convertNode(document, items, resolved.stack, dropped);
3905
- if (child !== void 0) result["items"] = child;
3928
+ result["items"] = convertNode(document, items, resolved.stack, dropped);
3906
3929
  }
3907
3930
  for (const keyword of Object.keys(schemaNode)) {
3908
3931
  if (isCarriedKeyword(keyword) || ANNOTATION_KEYWORDS.has(keyword) || keyword.startsWith("x-")) {
@@ -3943,16 +3966,15 @@ function mergeAllOf(document, schema, stack, dropped) {
3943
3966
  const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
3944
3967
  for (const branch of parts) {
3945
3968
  const converted = convertNode(document, branch, stack, dropped);
3946
- if (converted === void 0) throw new UnsupportedSchema("allOf branch is empty");
3947
3969
  const types = typeList2(converted["type"]);
3948
3970
  if (types !== void 0 && !types.includes("object")) {
3949
3971
  throw new UnsupportedSchema("allOf mixes object and non-object schemas");
3950
3972
  }
3951
3973
  const branchProperties = converted["properties"];
3952
- if (isRecord2(branchProperties)) {
3974
+ if (isRecord(branchProperties)) {
3953
3975
  for (const [name, sub] of Object.entries(branchProperties)) {
3954
3976
  const existing = properties[name];
3955
- if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(sub)) {
3977
+ if (existing !== void 0 && !isDeepStrictEqual(existing, sub)) {
3956
3978
  throw new UnsupportedSchema(
3957
3979
  `allOf branches declare property "${name}" differently, which cannot be merged`
3958
3980
  );
@@ -3975,7 +3997,7 @@ function mergeAllOf(document, schema, stack, dropped) {
3975
3997
  }
3976
3998
  const additional = converted["additionalProperties"];
3977
3999
  if (additional === true) open2 = true;
3978
- else if (isRecord2(additional)) {
4000
+ else if (isRecord(additional)) {
3979
4001
  throw new UnsupportedSchema(
3980
4002
  "allOf branch constrains additionalProperties, which cannot be merged"
3981
4003
  );
@@ -3986,9 +4008,6 @@ function mergeAllOf(document, schema, stack, dropped) {
3986
4008
  merged["additionalProperties"] = open2;
3987
4009
  return merged;
3988
4010
  }
3989
- function isRecord2(value) {
3990
- return typeof value === "object" && value !== null && !Array.isArray(value);
3991
- }
3992
4011
 
3993
4012
  // src/openapi/request.ts
3994
4013
  var PATH_GROUP = "path";
@@ -4042,16 +4061,10 @@ function mapRequest(loaded, candidate) {
4042
4061
  warn("unsupported-optional-parameter", `Omitted optional ${label}: unknown location`);
4043
4062
  continue;
4044
4063
  }
4045
- const unsupported = describeUnsupportedParameter(document, parameter, location);
4046
- if (unsupported !== void 0) {
4047
- if (required2) return skip("unsupported-required-parameter", `${label} ${unsupported}`);
4048
- warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${unsupported}`);
4049
- continue;
4050
- }
4051
- const converted = convertSchema(document, parameter["schema"]);
4064
+ const converted = convertParameter(document, parameter, location);
4052
4065
  if (!converted.supported) {
4053
4066
  if (required2) return skip("unsupported-required-parameter", `${label} ${converted.reason}`);
4054
- warn("unsupported-optional-parameter", `Omitted optional ${label}: ${converted.reason}`);
4067
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${converted.reason}`);
4055
4068
  continue;
4056
4069
  }
4057
4070
  for (const keyword of converted.dropped) dropped.add(keyword);
@@ -4120,38 +4133,43 @@ function mapRequest(loaded, candidate) {
4120
4133
  function mergeParameters(document, parameters) {
4121
4134
  const byIdentity = /* @__PURE__ */ new Map();
4122
4135
  for (const raw of parameters) {
4123
- const resolved = dereference(document, raw).value;
4124
- if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) continue;
4125
- const parameter = resolved;
4136
+ const parameter = dereference(document, raw).value;
4137
+ if (!isRecord(parameter)) continue;
4126
4138
  byIdentity.set(`${String(parameter["in"])}:${String(parameter["name"])}`, parameter);
4127
4139
  }
4128
4140
  return [...byIdentity.values()];
4129
4141
  }
4130
- 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
+ }
4131
4147
  if (parameter["content"] !== void 0) {
4132
- 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
+ );
4133
4151
  }
4134
4152
  const style = parameter["style"];
4135
4153
  if (location === "query" && typeof style === "string" && UNSUPPORTED_QUERY_STYLES.has(style)) {
4136
- return `uses style "${style}", which the gateway does not serialize`;
4154
+ return unsupported(`uses style "${style}", which the gateway does not serialize`);
4137
4155
  }
4138
4156
  if (location === "path" && typeof style === "string" && style !== "simple") {
4139
- return `uses style "${style}"; the gateway substitutes plain values only`;
4157
+ return unsupported(`uses style "${style}"; the gateway substitutes plain values only`);
4140
4158
  }
4141
4159
  const converted = convertSchema(document, parameter["schema"]);
4142
- if (!converted.supported) return converted.reason;
4160
+ if (!converted.supported) return unsupported(`has an unusable schema: ${converted.reason}`);
4143
4161
  if (!isPrimitiveSchema(converted.schema)) {
4144
- 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
+ );
4145
4165
  }
4146
- return void 0;
4166
+ return converted;
4147
4167
  }
4148
4168
  function resolveBody(document, candidate) {
4149
4169
  if (candidate.requestBody === void 0) return { kind: "none" };
4150
4170
  const resolvedBody = dereference(document, candidate.requestBody);
4151
- if (typeof resolvedBody.value !== "object" || resolvedBody.value === null || Array.isArray(resolvedBody.value)) {
4152
- return { kind: "none" };
4153
- }
4154
4171
  const requestBody = resolvedBody.value;
4172
+ if (!isRecord(requestBody)) return { kind: "none" };
4155
4173
  const required = requestBody["required"] === true;
4156
4174
  if (candidate.method === "GET" || candidate.method === "DELETE") {
4157
4175
  return {
@@ -4161,7 +4179,7 @@ function resolveBody(document, candidate) {
4161
4179
  };
4162
4180
  }
4163
4181
  const content = requestBody["content"];
4164
- if (!isRecord3(content)) return { kind: "none" };
4182
+ if (!isRecord(content)) return { kind: "none" };
4165
4183
  const mediaType = pickJsonMediaType(Object.keys(content));
4166
4184
  if (mediaType === void 0) {
4167
4185
  return {
@@ -4171,7 +4189,7 @@ function resolveBody(document, candidate) {
4171
4189
  };
4172
4190
  }
4173
4191
  const media = dereference(document, content[mediaType], resolvedBody.stack).value;
4174
- const schemaNode = isRecord3(media) ? media["schema"] : void 0;
4192
+ const schemaNode = isRecord(media) ? media["schema"] : void 0;
4175
4193
  if (schemaNode === void 0) {
4176
4194
  return {
4177
4195
  kind: "schema",
@@ -4192,10 +4210,10 @@ function resolveBody(document, candidate) {
4192
4210
  };
4193
4211
  }
4194
4212
  function pickJsonMediaType(keys) {
4195
- const normalised = keys.map((key) => ({ key, type: key.split(";")[0]?.trim().toLowerCase() }));
4196
- 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");
4197
4215
  if (exact !== void 0) return exact.key;
4198
- 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;
4199
4217
  }
4200
4218
  function closedObject(properties, required) {
4201
4219
  return {
@@ -4205,9 +4223,6 @@ function closedObject(properties, required) {
4205
4223
  additionalProperties: false
4206
4224
  };
4207
4225
  }
4208
- function isRecord3(value) {
4209
- return typeof value === "object" && value !== null && !Array.isArray(value);
4210
- }
4211
4226
 
4212
4227
  // src/openapi/draft.ts
4213
4228
  function buildResourceDrafts(loaded, options = {}) {
@@ -4280,12 +4295,9 @@ function buildResourceDrafts(loaded, options = {}) {
4280
4295
  ...mapping.contentType !== void 0 ? { headers: { "Content-Type": mapping.contentType } } : {},
4281
4296
  ...Object.keys(mapping.inputBindings).length > 0 ? { inputBindings: mapping.inputBindings } : {}
4282
4297
  },
4283
- // Policy is written only when the operator asked for it. An absent
4284
- // `pricing`/`expose` is what makes the draft fail config validation
4285
- // until a human has decided.
4298
+ // Written only when the operator supplied it (see the file header)
4286
4299
  ...options.policy?.pricing !== void 0 ? { pricing: options.policy.pricing } : {},
4287
- ...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {},
4288
- ...options.policy?.payments !== void 0 ? { payments: [...options.policy.payments] } : {}
4300
+ ...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {}
4289
4301
  },
4290
4302
  review
4291
4303
  });
@@ -4317,18 +4329,18 @@ function declaresSecurity(candidate) {
4317
4329
  }
4318
4330
  function selectOutputSchema(document, candidate, diagnostics, dropped) {
4319
4331
  const responses = candidate.responses;
4320
- if (!isRecord4(responses)) return void 0;
4332
+ if (!isRecord(responses)) return void 0;
4321
4333
  const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
4322
4334
  if (successes.length === 0) return void 0;
4323
4335
  const withBody = successes.flatMap((status) => {
4324
4336
  const response = dereference(document, responses[status]);
4325
- if (!isRecord4(response.value)) return [];
4337
+ if (!isRecord(response.value)) return [];
4326
4338
  const content = response.value["content"];
4327
- if (!isRecord4(content)) return [];
4339
+ if (!isRecord(content)) return [];
4328
4340
  const mediaType = pickJsonMediaType(Object.keys(content));
4329
4341
  if (mediaType === void 0) return [];
4330
4342
  const media = dereference(document, content[mediaType], response.stack).value;
4331
- return isRecord4(media) ? [{ status, media }] : [];
4343
+ return isRecord(media) ? [{ status, media }] : [];
4332
4344
  });
4333
4345
  const chosen = withBody[0];
4334
4346
  if (chosen === void 0) return void 0;
@@ -4373,7 +4385,8 @@ function renderResourcesYaml(result) {
4373
4385
  const doc = new Document({ resources });
4374
4386
  doc.commentBefore = [
4375
4387
  " Generated by agent-commerce import openapi. Review before use.",
4376
- ` Source: ${basename(result.sourcePath)} (OpenAPI ${result.version})`,
4388
+ // win32.basename splits on both / and \
4389
+ ` Source: ${win32.basename(result.sourcePath)} (OpenAPI ${result.version})`,
4377
4390
  " Merge the resources below into config.yaml once pricing, exposure and",
4378
4391
  " any backend authentication have been decided."
4379
4392
  ].join("\n");
@@ -4386,12 +4399,6 @@ function renderResourcesYaml(result) {
4386
4399
  });
4387
4400
  return doc.toString({ lineWidth: 0 });
4388
4401
  }
4389
- function basename(path2) {
4390
- return path2.split(/[\\/]/).pop() ?? path2;
4391
- }
4392
- function isRecord4(value) {
4393
- return typeof value === "object" && value !== null && !Array.isArray(value);
4394
- }
4395
4402
 
4396
4403
  // src/openapi/load.ts
4397
4404
  init_errors();
@@ -4472,7 +4479,7 @@ async function loadOpenApiDocument(sourcePath) {
4472
4479
  }
4473
4480
  const document = parsed;
4474
4481
  const version = readVersion(document, sourcePath);
4475
- rejectExternalReferences(document, sourcePath);
4482
+ rejectUnsupportedReferences(document, sourcePath);
4476
4483
  const result = await validate(document);
4477
4484
  if (!result.valid) {
4478
4485
  const first = result.errors?.[0];
@@ -4504,7 +4511,7 @@ function readVersion(document, sourcePath) {
4504
4511
  }
4505
4512
  return featureVersion;
4506
4513
  }
4507
- function rejectExternalReferences(document, sourcePath) {
4514
+ function rejectUnsupportedReferences(document, sourcePath) {
4508
4515
  const seen = /* @__PURE__ */ new WeakSet();
4509
4516
  const queue = [document];
4510
4517
  while (queue.length > 0) {
@@ -4517,31 +4524,45 @@ function rejectExternalReferences(document, sourcePath) {
4517
4524
  continue;
4518
4525
  }
4519
4526
  for (const [key, value] of Object.entries(node)) {
4520
- if (key === "$ref" && typeof value === "string" && !value.startsWith("#")) {
4521
- throw invalid2(
4522
- `OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
4523
- { sourcePath, ref: value }
4524
- );
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
+ }
4525
4540
  }
4526
4541
  queue.push(value);
4527
4542
  }
4528
4543
  }
4529
4544
  }
4545
+ function isPercentDecodable(value) {
4546
+ try {
4547
+ decodeURIComponent(value);
4548
+ return true;
4549
+ } catch {
4550
+ return false;
4551
+ }
4552
+ }
4530
4553
 
4531
4554
  // src/cli/commands/import-openapi.ts
4532
4555
  function defaultOutputPath(source) {
4533
- const name = basename$1(source);
4556
+ const name = basename(source);
4534
4557
  const stem = name.slice(0, name.length - extname(name).length) || name;
4535
4558
  return `${stem}.agent-commerce.yaml`;
4536
4559
  }
4537
4560
  async function runImportOpenApi(options, io, deps = {}) {
4538
4561
  const fileExists = deps.fileExists ?? existsSync;
4539
4562
  const outputPath = options.output ?? defaultOutputPath(options.source);
4540
- let policy;
4541
- try {
4542
- policy = buildPolicy(options);
4543
- } catch (error) {
4544
- 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}`);
4545
4566
  return 1;
4546
4567
  }
4547
4568
  if (fileExists(outputPath) && options.force !== true) {
@@ -4622,13 +4643,11 @@ function buildPolicy(options) {
4622
4643
  if (options.expose !== void 0) {
4623
4644
  const requested = options.expose.split(",").map((name) => name.trim()).filter((name) => name !== "");
4624
4645
  if (requested.length === 0) {
4625
- throw new Error(`--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`);
4646
+ return `--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`;
4626
4647
  }
4627
4648
  for (const name of requested) {
4628
4649
  if (!PROTOCOL_NAMES.includes(name)) {
4629
- throw new Error(
4630
- `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`
4631
- );
4650
+ return `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`;
4632
4651
  }
4633
4652
  }
4634
4653
  policy.expose = requested;
@@ -4702,14 +4721,6 @@ function formatConfigError(err, manifestFound = false) {
4702
4721
  }
4703
4722
  return lines.join("\n");
4704
4723
  }
4705
- if (err instanceof ZodError) {
4706
- const lines = ["FAIL Configuration schema errors:"];
4707
- for (const issue of err.issues) {
4708
- const path2 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
4709
- lines.push(` - ${path2}: ${issue.message}`);
4710
- }
4711
- return lines.join("\n");
4712
- }
4713
4724
  if (err instanceof Error) {
4714
4725
  return `FAIL ${err.message}`;
4715
4726
  }
@@ -4733,7 +4744,11 @@ async function runValidate(options, io, deps = {}) {
4733
4744
  io.stdout(
4734
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"}`
4735
4746
  );
4736
- 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)}`);
4737
4752
  return 0;
4738
4753
  } catch (err) {
4739
4754
  io.stderr(formatConfigError(err, manifestFound));
@@ -4747,8 +4762,7 @@ async function collectAnswersInteractive() {
4747
4762
  const backendBaseUrl = await clack.text({
4748
4763
  message: "Backend base URL (your existing merchant API)",
4749
4764
  initialValue: "http://localhost:3000",
4750
- // shape-checked here, where the user can retype it,
4751
- // 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
4752
4766
  validate: (value) => {
4753
4767
  const trimmed = (value ?? "").trim();
4754
4768
  if (trimmed.length === 0) return "Required";
@@ -4785,12 +4799,8 @@ async function collectAnswersInteractive() {
4785
4799
  { value: "mcp", label: "MCP" }
4786
4800
  ],
4787
4801
  initialValues: ["http", "mcp"],
4788
- // `required: false` permits an empty selection, which renders
4789
- // `expose: []` on every resource and fails the schema's min(1). clack's
4790
- // multiselect has no `validate` hook, so this is caught by the in-memory
4791
- // validation in `runInit` — which now reports the real schema error and
4792
- // writes nothing, instead of leaving an invalid file behind.
4793
- required: false
4802
+ // Every resource needs at least one protocol in `expose`
4803
+ required: true
4794
4804
  });
4795
4805
  if (clack.isCancel(protocols)) {
4796
4806
  clack.cancel("Aborted.");
@@ -4804,7 +4814,7 @@ async function collectAnswersInteractive() {
4804
4814
  let merchantPayTo = defaultInitAnswers().merchantPayTo;
4805
4815
  if (x402Enabled) {
4806
4816
  const answer = await clack.text({
4807
- message: "Merchant settlement destination address (merchant-controlled \u2014 never a gateway-owned wallet)",
4817
+ message: "Merchant settlement address (enter an address you control)",
4808
4818
  initialValue: merchantPayTo,
4809
4819
  validate: (value) => {
4810
4820
  const trimmed = (value ?? "").trim();
@@ -4847,20 +4857,16 @@ async function runInit(options, io, deps = {}) {
4847
4857
  return 1;
4848
4858
  }
4849
4859
  const yamlContent = renderInitConfigYaml(answers);
4850
- const resolvedPath = resolve(outputPath);
4851
- const dir = dirname(resolvedPath);
4852
- if (dir !== "" && !existsSync(dir)) {
4853
- mkdirSync(dir, { recursive: true });
4854
- }
4860
+ mkdirSync(dirname(resolve(outputPath)), { recursive: true });
4855
4861
  await writeFileImpl(outputPath, yamlContent);
4856
4862
  io.stdout(`PASS Wrote ${outputPath}`);
4857
- io.stdout(`PASS ${outputPath} is valid \u2014 ${config.resources.length} resource(s)`);
4863
+ io.stdout(`PASS ${outputPath} is valid: ${config.resources.length} resource(s)`);
4858
4864
  try {
4859
4865
  await loadConfig2({ path: outputPath });
4860
4866
  } catch (err) {
4861
4867
  io.stderr(formatConfigError(err));
4862
4868
  io.stderr(
4863
- `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.`
4864
4870
  );
4865
4871
  return 1;
4866
4872
  }
@@ -4879,8 +4885,9 @@ function runVersion(io, deps = {}) {
4879
4885
  if (report.pinned.length > 0) {
4880
4886
  io.stdout("");
4881
4887
  io.stdout("Pinned protocol / SDK versions:");
4888
+ const width = Math.max(...report.pinned.map((pin) => pin.name.length));
4882
4889
  for (const pin of report.pinned) {
4883
- 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})`);
4884
4891
  }
4885
4892
  }
4886
4893
  return 0;
@@ -4959,7 +4966,7 @@ function buildProgram(io = processIo) {
4959
4966
  }
4960
4967
  );
4961
4968
  program.command("demo").description(
4962
- "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."
4963
4970
  ).action(async () => {
4964
4971
  process.exitCode = await runDemo(io);
4965
4972
  });