@devlab.group/agent-commerce 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -9,7 +9,7 @@ import picocolors from 'picocolors';
9
9
  import { randomUUID } from 'crypto';
10
10
  import { createRequire } from 'module';
11
11
  import Database from 'better-sqlite3';
12
- import { writeFile, stat, readFile, rename, rm } from 'fs/promises';
12
+ import { writeFile, stat, rename, rm, open } from 'fs/promises';
13
13
  import { validate } from '@scalar/openapi-parser';
14
14
  import * as clack from '@clack/prompts';
15
15
 
@@ -32,10 +32,11 @@ var init_filename = __esm({
32
32
  });
33
33
 
34
34
  // src/core/domain/common.ts
35
- var PROTOCOL_NAMES;
35
+ var PROTOCOL_NAMES, PAYMENT_METHOD_NAMES;
36
36
  var init_common = __esm({
37
37
  "src/core/domain/common.ts"() {
38
38
  PROTOCOL_NAMES = ["http", "mcp", "a2a", "acp"];
39
+ PAYMENT_METHOD_NAMES = ["x402", "mpp"];
39
40
  }
40
41
  });
41
42
 
@@ -312,6 +313,34 @@ var init_execution = __esm({
312
313
  }
313
314
  });
314
315
 
316
+ // src/payments/mpp/constants.ts
317
+ function isMppNetwork(value) {
318
+ return MPP_NETWORKS.includes(value);
319
+ }
320
+ var MPP_SPEC_COMMIT, MPP_SPEC_DRAFTS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPPX_VERSION, MPP_PROFILE, MPP_NETWORKS, MPP_DEFAULT_NETWORK;
321
+ var init_constants2 = __esm({
322
+ "src/payments/mpp/constants.ts"() {
323
+ MPP_SPEC_COMMIT = "806fdb8b8c92cda5c84b8660558921b6bbeef7e0";
324
+ MPP_SPEC_DRAFTS = {
325
+ core: "draft-httpauth-payment-00",
326
+ intent: "draft-payment-intent-charge-00",
327
+ method: "draft-evm-charge-00"
328
+ };
329
+ MPP_MIN_CHALLENGE_SECRET_LENGTH = 32;
330
+ MPPX_VERSION = "0.10.1";
331
+ MPP_PROFILE = {
332
+ intent: "charge",
333
+ method: "evm",
334
+ credentialType: "authorization",
335
+ assetSymbol: "USDC",
336
+ assetDecimals: 6
337
+ };
338
+ MPP_NETWORKS = ["eip155:84532", "eip155:8453"];
339
+ MPP_DEFAULT_NETWORK = "eip155:84532";
340
+ `mpp/${MPP_SPEC_DRAFTS.core} intent=${MPP_PROFILE.intent} method=${MPP_PROFILE.method}`;
341
+ }
342
+ });
343
+
315
344
  // src/payments/x402/networks.ts
316
345
  function findNetworkProfile(id) {
317
346
  return BY_ID.get(id);
@@ -372,7 +401,7 @@ var init_networks = __esm({
372
401
 
373
402
  // src/protocols/acp/constants.ts
374
403
  var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
375
- var init_constants2 = __esm({
404
+ var init_constants3 = __esm({
376
405
  "src/protocols/acp/constants.ts"() {
377
406
  ACP_SPEC_VERSION = "2026-04-17";
378
407
  ACP_API_VERSION = "2026-04-17";
@@ -524,93 +553,95 @@ function invalid(message, path2) {
524
553
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
525
554
  }
526
555
  function resolveX402Deployment(input) {
527
- const profile = requireNetworkProfile(input.network, "payments.x402.network");
556
+ const at = input.configPath ?? "payments.x402";
557
+ const payTo = input.payToField ?? "payTo";
558
+ const profile = requireNetworkProfile(input.network, `${at}.network`);
528
559
  const mode = resolveDeploymentMode(profile, input.facilitator.mode);
529
560
  if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
530
561
  throw invalid(
531
- `payments.x402: network "${profile.id}" (${profile.displayName}) is a mainnet and cannot be served by facilitator.mode "local". A mainnet deployment must settle through a remote facilitator.`,
532
- "payments.x402.facilitator.mode"
562
+ `${at}: network "${profile.id}" (${profile.displayName}) is a mainnet and cannot be served by facilitator.mode "local". A mainnet deployment must settle through a remote facilitator.`,
563
+ `${at}.facilitator.mode`
533
564
  );
534
565
  }
535
566
  if (mode === "mainnet" && input.allowMainnet !== true) {
536
567
  throw invalid(
537
- `payments.x402: network "${profile.id}" (${profile.displayName}) settles real funds. Set payments.x402.allowMainnet: true to acknowledge this explicitly \u2014 it is never the default.`,
538
- "payments.x402.allowMainnet"
568
+ `${at}: network "${profile.id}" (${profile.displayName}) settles real funds. Set ${at}.allowMainnet: true to acknowledge this explicitly - it is never the default.`,
569
+ `${at}.allowMainnet`
539
570
  );
540
571
  }
541
572
  if (input.facilitator.mode === "remote") {
542
- assertFacilitatorUrlIsSafe(input.facilitator.url, mode);
573
+ assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
543
574
  if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
544
575
  throw invalid(
545
- `payments.x402: facilitator ${describeOrigin(input.facilitator.url)} takes no credential, and this is a mainnet deployment. It will see every payment authorisation you handle, with no account, terms or support behind it. Set payments.x402.allowUnauthenticatedFacilitator: true to accept that, or configure facilitator.auth.`,
546
- "payments.x402.allowUnauthenticatedFacilitator"
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.`,
577
+ `${at}.allowUnauthenticatedFacilitator`
547
578
  );
548
579
  }
549
580
  for (const [field, value] of credentialFields(input.facilitator.auth)) {
550
581
  if (value.trim() === "") {
551
582
  throw invalid(
552
- `payments.x402: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
553
- `payments.x402.facilitator.auth.${field}`
583
+ `${at}: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
584
+ `${at}.facilitator.auth.${field}`
554
585
  );
555
586
  }
556
587
  }
557
588
  }
558
589
  if (/^0x0{40}$/i.test(input.payTo)) {
559
590
  throw invalid(
560
- 'payments.x402: "payTo" is the zero address. Every payment settled there is destroyed.',
561
- "payments.x402.payTo"
591
+ `${at}: "${payTo}" is the zero address. Every payment settled there is destroyed.`,
592
+ `${at}.${payTo}`
562
593
  );
563
594
  }
564
595
  if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
565
596
  throw invalid(
566
- `payments.x402: "payTo" (${input.payTo}) is a well-known Anvil development address and this is a ${mode} deployment. Anyone can spend what settles there. Set payTo to your own merchant wallet.`,
567
- "payments.x402.payTo"
597
+ `${at}: "${payTo}" (${input.payTo}) is a well-known Anvil development address and this is a ${mode} deployment. Anyone can spend what settles there. Set ${payTo} to your own merchant wallet.`,
598
+ `${at}.${payTo}`
568
599
  );
569
600
  }
570
601
  const canonical = profile.canonicalAsset;
571
602
  if (mode === "mainnet" && canonical) {
572
603
  if (!sameAddress(input.asset, canonical.address)) {
573
604
  throw invalid(
574
- `payments.x402: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
575
- "payments.x402.asset"
605
+ `${at}: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
606
+ `${at}.asset`
576
607
  );
577
608
  }
578
609
  if (input.assetName !== void 0 && input.assetName !== canonical.name) {
579
610
  throw invalid(
580
- `payments.x402: assetName "${input.assetName}" is not the EIP-712 domain name ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.name}"). Every payment would be refused after the buyer signed.`,
581
- "payments.x402.assetName"
611
+ `${at}: assetName "${input.assetName}" is not the EIP-712 domain name ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.name}"). Every payment would be refused after the buyer signed.`,
612
+ `${at}.assetName`
582
613
  );
583
614
  }
584
615
  if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
585
616
  throw invalid(
586
- `payments.x402: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
587
- "payments.x402.assetVersion"
617
+ `${at}: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
618
+ `${at}.assetVersion`
588
619
  );
589
620
  }
590
621
  }
591
622
  return { profile, mode };
592
623
  }
593
- function assertFacilitatorUrlIsSafe(url, mode) {
624
+ function assertFacilitatorUrlIsSafe(url, mode, at) {
594
625
  let parsed;
595
626
  try {
596
627
  parsed = new URL(url);
597
628
  } catch (cause) {
598
- throw new CommerceError("CONFIG_INVALID", `payments.x402: facilitator.url is not a valid URL`, {
629
+ throw new CommerceError("CONFIG_INVALID", `${at}: facilitator.url is not a valid URL`, {
599
630
  cause,
600
- details: { path: "payments.x402.facilitator.url" }
631
+ details: { path: `${at}.facilitator.url` }
601
632
  });
602
633
  }
603
634
  if (parsed.protocol === "https:") return;
604
635
  if (parsed.protocol !== "http:") {
605
636
  throw invalid(
606
- `payments.x402: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
607
- "payments.x402.facilitator.url"
637
+ `${at}: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
638
+ `${at}.facilitator.url`
608
639
  );
609
640
  }
610
641
  if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
611
642
  throw invalid(
612
- `payments.x402: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorisations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
613
- "payments.x402.facilitator.url"
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.`,
644
+ `${at}.facilitator.url`
614
645
  );
615
646
  }
616
647
  function describeOrigin(url) {
@@ -755,12 +786,7 @@ function normalise(raw) {
755
786
  };
756
787
  validateMountPaths(protocols);
757
788
  const x402Raw = raw.payments.x402;
758
- const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
759
- mode: "remote",
760
- url: x402Raw.facilitator.url,
761
- auth: x402Raw.facilitator.auth ?? { type: "none" }
762
- };
763
- const x402 = x402Raw && facilitator ? {
789
+ const x402 = x402Raw !== void 0 ? {
764
790
  enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
765
791
  network: x402Raw.network,
766
792
  rpcUrl: x402Raw.rpcUrl,
@@ -779,7 +805,7 @@ function normalise(raw) {
779
805
  min: 1
780
806
  }
781
807
  ),
782
- facilitator,
808
+ facilitator: normaliseFacilitator(x402Raw.facilitator),
783
809
  ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
784
810
  ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
785
811
  allowUnauthenticatedFacilitator: toBoolean(
@@ -802,12 +828,13 @@ function normalise(raw) {
802
828
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
803
829
  });
804
830
  }
831
+ const mpp = normaliseMpp(raw.payments.mpp);
805
832
  const ap2 = normaliseAp2(raw.authorization?.ap2);
806
833
  if (ap2?.enabled) {
807
834
  validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
808
835
  }
809
836
  const resources = Object.entries(raw.resources).map(
810
- ([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
837
+ ([id, entry]) => normaliseResource(id, entry, protocols, x402, mpp, ap2)
811
838
  );
812
839
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
813
840
  return {
@@ -829,7 +856,8 @@ function normalise(raw) {
829
856
  protocols,
830
857
  resources,
831
858
  payments: {
832
- ...x402 !== void 0 ? { x402 } : {}
859
+ ...x402 !== void 0 ? { x402 } : {},
860
+ ...mpp !== void 0 ? { mpp } : {}
833
861
  },
834
862
  ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
835
863
  };
@@ -1181,7 +1209,74 @@ function validateAcpOperationInput(path2, operation, resource) {
1181
1209
  }
1182
1210
  }
1183
1211
  }
1184
- function normaliseResource(id, entry, protocols, x402, ap2) {
1212
+ function normaliseFacilitator(raw) {
1213
+ return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
1214
+ }
1215
+ function normaliseMpp(raw) {
1216
+ if (raw === void 0) return void 0;
1217
+ const network = raw.network ?? MPP_DEFAULT_NETWORK;
1218
+ if (!isMppNetwork(network)) {
1219
+ throw new CommerceError(
1220
+ "CONFIG_INVALID",
1221
+ `payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
1222
+ { details: { path: "payments.mpp.network" } }
1223
+ );
1224
+ }
1225
+ const mpp = {
1226
+ enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
1227
+ network,
1228
+ rpcUrl: raw.rpcUrl,
1229
+ asset: raw.asset,
1230
+ assetName: raw.assetName,
1231
+ assetVersion: raw.assetVersion,
1232
+ recipient: raw.recipient,
1233
+ realm: raw.realm,
1234
+ challengeSecret: raw.challengeSecret,
1235
+ ...raw.challengeTtlSeconds !== void 0 ? {
1236
+ challengeTtlSeconds: toNumber(
1237
+ raw.challengeTtlSeconds,
1238
+ "payments.mpp.challengeTtlSeconds",
1239
+ { min: 1 }
1240
+ )
1241
+ } : {},
1242
+ facilitator: normaliseFacilitator(raw.facilitator),
1243
+ ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
1244
+ ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
1245
+ allowUnauthenticatedFacilitator: toBoolean(
1246
+ raw.allowUnauthenticatedFacilitator,
1247
+ "payments.mpp.allowUnauthenticatedFacilitator"
1248
+ )
1249
+ } : {}
1250
+ };
1251
+ validateAddress("payments.mpp.recipient", mpp.recipient);
1252
+ validateAddress("payments.mpp.asset", mpp.asset);
1253
+ if (/[\r\n]/.test(mpp.realm)) {
1254
+ throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
1255
+ details: { path: "payments.mpp.realm" }
1256
+ });
1257
+ }
1258
+ if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
1259
+ throw new CommerceError(
1260
+ "CONFIG_INVALID",
1261
+ `payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
1262
+ { details: { path: "payments.mpp.challengeSecret" } }
1263
+ );
1264
+ }
1265
+ resolveX402Deployment({
1266
+ network: mpp.network,
1267
+ payTo: mpp.recipient,
1268
+ asset: mpp.asset,
1269
+ assetName: mpp.assetName,
1270
+ assetVersion: mpp.assetVersion,
1271
+ facilitator: mpp.facilitator,
1272
+ ...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
1273
+ ...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
1274
+ configPath: "payments.mpp",
1275
+ payToField: "recipient"
1276
+ });
1277
+ return mpp;
1278
+ }
1279
+ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
1185
1280
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1186
1281
  const inputProperties = entry.input?.["properties"];
1187
1282
  if (inputProperties && typeof inputProperties === "object") {
@@ -1257,7 +1352,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
1257
1352
  );
1258
1353
  }
1259
1354
  if (entry.pricing.type === "fixed") {
1260
- validatePricingAmount(id, entry.pricing.amount, x402);
1355
+ validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
1261
1356
  }
1262
1357
  const paymentMethods = entry.payments ?? [];
1263
1358
  if (entry.pricing.type === "fixed") {
@@ -1272,17 +1367,28 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
1272
1367
  if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
1273
1368
  throw new CommerceError(
1274
1369
  "CONFIG_INVALID",
1275
- `Resource "${id}" names unsupported payment method "${method}". Supported: x402.`,
1370
+ `Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
1276
1371
  { details: { path: `resources.${id}.payments`, resourceId: id, method } }
1277
1372
  );
1278
1373
  }
1279
- if (method === "x402" && (!x402 || !x402.enabled)) {
1374
+ }
1375
+ const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
1376
+ if (!paymentMethods.some((method) => enabledRails.includes(method))) {
1377
+ throw new CommerceError(
1378
+ "CONFIG_INVALID",
1379
+ `Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
1380
+ { details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
1381
+ );
1382
+ }
1383
+ if (mpp?.enabled && paymentMethods.includes("mpp")) {
1384
+ if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
1280
1385
  throw new CommerceError(
1281
1386
  "CONFIG_INVALID",
1282
- `Resource "${id}" names payment method "x402" which is not configured or not enabled under payments.x402`,
1283
- { details: { path: `resources.${id}.payments`, resourceId: id, method } }
1387
+ `Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
1388
+ { details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
1284
1389
  );
1285
1390
  }
1391
+ validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
1286
1392
  }
1287
1393
  }
1288
1394
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
@@ -1396,7 +1502,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1396
1502
  );
1397
1503
  }
1398
1504
  }
1399
- function validatePricingAmount(id, amount, x402) {
1505
+ function validatePricingAmount(id, amount, decimals) {
1400
1506
  const path2 = `resources.${id}.pricing.amount`;
1401
1507
  if (!PRICING_AMOUNT_PATTERN.test(amount)) {
1402
1508
  throw new CommerceError(
@@ -1412,12 +1518,12 @@ function validatePricingAmount(id, amount, x402) {
1412
1518
  { details: { path: path2, resourceId: id } }
1413
1519
  );
1414
1520
  }
1415
- if (x402 !== void 0) {
1521
+ if (decimals !== void 0) {
1416
1522
  const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
1417
- if (fractionalDigits > x402.assetDecimals) {
1523
+ if (fractionalDigits > decimals) {
1418
1524
  throw new CommerceError(
1419
1525
  "CONFIG_INVALID",
1420
- `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${x402.assetDecimals} decimals)`,
1526
+ `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
1421
1527
  { details: { path: path2, resourceId: id } }
1422
1528
  );
1423
1529
  }
@@ -1588,14 +1694,15 @@ function validateAddress(path2, value) {
1588
1694
  );
1589
1695
  }
1590
1696
  }
1591
- var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, MountPathSchema, AcpAuthSchema, AcpIdempotencySchema, AcpCheckoutSchema, AcpDiscoverySchema, ProtocolsSchema, DEFAULT_A2A_MOUNT_PATH, DEFAULT_ACP_MOUNT_PATH, ACP_RETENTION_HOURS, BackendMethodSchema, BackendInputBindingsSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, PaymentsSchema, Ap2KeySchema, Ap2IssuerSchema, Ap2Schema, AuthorizationSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, SUPPORTED_AUTHORIZATION_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
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;
1592
1698
  var init_schema = __esm({
1593
1699
  "src/config/schema.ts"() {
1594
1700
  init_constants();
1595
1701
  init_execution();
1596
1702
  init_core();
1597
- init_guardrails();
1598
1703
  init_constants2();
1704
+ init_guardrails();
1705
+ init_constants3();
1599
1706
  init_env();
1600
1707
  SUPPORTED_CONFIG_VERSION = 1;
1601
1708
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
@@ -1781,8 +1888,26 @@ var init_schema = __esm({
1781
1888
  /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1782
1889
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
1783
1890
  }).strict();
1891
+ MppSchema = z.object({
1892
+ enabled: BooleanOrString,
1893
+ network: z.string().min(1).optional(),
1894
+ rpcUrl: z.string().min(1),
1895
+ asset: z.string().min(1),
1896
+ assetName: z.string().min(1),
1897
+ assetVersion: z.string().min(1),
1898
+ recipient: z.string().min(1),
1899
+ realm: z.string().min(1),
1900
+ challengeSecret: z.string().min(1),
1901
+ challengeTtlSeconds: NumberOrString.optional(),
1902
+ facilitator: FacilitatorSchema,
1903
+ // Real funds. Never defaulted - see src/payments/x402/guardrails.ts
1904
+ allowMainnet: BooleanOrString.optional(),
1905
+ // Accepts a mainnet facilitator that takes no credential. Never defaulted
1906
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
1907
+ }).strict();
1784
1908
  PaymentsSchema = z.object({
1785
- x402: X402Schema.optional()
1909
+ x402: X402Schema.optional(),
1910
+ mpp: MppSchema.optional()
1786
1911
  }).strict();
1787
1912
  Ap2KeySchema = z.object({
1788
1913
  kid: z.string().min(1),
@@ -1821,7 +1946,7 @@ var init_schema = __esm({
1821
1946
  authorization: AuthorizationSchema.optional()
1822
1947
  }).strict();
1823
1948
  SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1824
- SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1949
+ SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
1825
1950
  SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
1826
1951
  EMPTY_CLOSED_OBJECT_SCHEMA = {
1827
1952
  type: "object",
@@ -2029,13 +2154,13 @@ var AP2_UNSUPPORTED = [
2029
2154
  "digest algorithms other than sha-256",
2030
2155
  // Roles this gateway does not play
2031
2156
  "mandate issuance",
2032
- "merchant checkout JWT issuance",
2033
2157
  "AP2 over the ACP checkout adapter"
2034
2158
  ];
2035
2159
 
2036
2160
  // src/cli/commands/doctor.ts
2037
2161
  init_execution();
2038
2162
  init_core();
2163
+ init_constants2();
2039
2164
  init_networks();
2040
2165
 
2041
2166
  // src/protocols/a2a/constants.ts
@@ -2076,10 +2201,10 @@ var A2A_UNSUPPORTED = [
2076
2201
  ];
2077
2202
 
2078
2203
  // src/cli/commands/doctor.ts
2079
- init_constants2();
2204
+ init_constants3();
2080
2205
 
2081
2206
  // src/protocols/acp/descriptor.ts
2082
- init_constants2();
2207
+ init_constants3();
2083
2208
  [
2084
2209
  "rest transport",
2085
2210
  "well-known discovery",
@@ -2113,7 +2238,7 @@ var ACP_UNSUPPORTED = [
2113
2238
 
2114
2239
  // src/storage/receipts/store.ts
2115
2240
  init_core();
2116
- var PACKAGE_VERSION = "1.4.0" ;
2241
+ var PACKAGE_VERSION = "1.5.0" ;
2117
2242
 
2118
2243
  // src/storage/sqlite.ts
2119
2244
  init_core();
@@ -2884,7 +3009,7 @@ function maskMiddle(value, keep = 6) {
2884
3009
  }
2885
3010
  createRequire(import.meta.url);
2886
3011
  function readVersionReport() {
2887
- const injected = '{"cliVersion":"1.4.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"}]}' ;
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"}]}' ;
2888
3013
  {
2889
3014
  return JSON.parse(injected);
2890
3015
  }
@@ -2909,15 +3034,16 @@ function substitutePathParams(url) {
2909
3034
  }
2910
3035
  return filled;
2911
3036
  }
2912
- function extractWellKnownX402(body) {
3037
+ function extractWellKnownX402(body, rail = "x402") {
2913
3038
  const payments = body?.["payments"];
2914
3039
  if (typeof payments !== "object" || payments === null) return void 0;
2915
- const x402 = payments["x402"];
2916
- if (typeof x402 !== "object" || x402 === null) return void 0;
2917
- const rec = x402;
3040
+ const block = payments[rail];
3041
+ if (typeof block !== "object" || block === null) return void 0;
3042
+ const rec = block;
2918
3043
  if (rec["enabled"] === false) return "disabled";
2919
3044
  if (rec["enabled"] !== true) return void 0;
2920
- const { asset, network, payTo } = rec;
3045
+ const { asset, network } = rec;
3046
+ const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
2921
3047
  if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
2922
3048
  return void 0;
2923
3049
  }
@@ -2926,7 +3052,7 @@ function extractWellKnownX402(body) {
2926
3052
  function sameAddress2(a, b) {
2927
3053
  return a.toLowerCase() === b.toLowerCase();
2928
3054
  }
2929
- function findX402Mismatch(configured, live) {
3055
+ function findX402Mismatch(configured, live, payToField = "payTo") {
2930
3056
  const diffs = [];
2931
3057
  if (!sameAddress2(configured.asset, live.asset)) {
2932
3058
  diffs.push(
@@ -2940,11 +3066,11 @@ function findX402Mismatch(configured, live) {
2940
3066
  }
2941
3067
  if (!sameAddress2(configured.payTo, live.payTo)) {
2942
3068
  diffs.push(
2943
- `payTo: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
3069
+ `${payToField}: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
2944
3070
  );
2945
3071
  }
2946
3072
  if (diffs.length === 0) return void 0;
2947
- return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
3073
+ return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
2948
3074
  }
2949
3075
  function describeIssuers(label, issuers) {
2950
3076
  const described = issuers.map(
@@ -3250,11 +3376,36 @@ async function runDoctor(options, deps = {}) {
3250
3376
  });
3251
3377
  }
3252
3378
  }
3253
- checks.push({
3254
- name: "Payments (MPP)",
3255
- status: "INFO",
3256
- detail: "planned \u2014 not implemented in this release"
3257
- });
3379
+ const mpp = config?.payments.mpp;
3380
+ if (mpp === void 0 || !mpp.enabled) {
3381
+ checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
3382
+ } 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}`;
3388
+ const configured = { asset: mpp.asset, network: mpp.network, payTo: mpp.recipient };
3389
+ const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body, "mpp") : void 0;
3390
+ if (live === "disabled") {
3391
+ checks.push({
3392
+ name: "Payments (MPP)",
3393
+ status: "FAIL",
3394
+ detail: `Local config enables MPP, but ${gatewayUrl} reports it disabled`
3395
+ });
3396
+ } else if (live === void 0) {
3397
+ checks.push({
3398
+ name: "Payments (MPP)",
3399
+ status: "INFO",
3400
+ detail: `${summary} (live MPP settlement config unavailable for comparison)`
3401
+ });
3402
+ } else {
3403
+ const mismatch = findX402Mismatch(configured, live, "recipient");
3404
+ checks.push(
3405
+ mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
3406
+ );
3407
+ }
3408
+ }
3258
3409
  if (config === void 0) {
3259
3410
  checks.push({ name: "Storage", status: "WARN", detail: "skipped \u2014 config invalid" });
3260
3411
  } else if (
@@ -3463,16 +3614,24 @@ function discoverOperations(loaded, options = {}) {
3463
3614
  }
3464
3615
  const pathItem = dereference(document, rawPathItem).value;
3465
3616
  if (!isRecord(pathItem)) continue;
3617
+ const unsupportedMethod = (name) => {
3618
+ diagnostics.push({
3619
+ severity: "warning",
3620
+ code: "unsupported-method",
3621
+ operation: `${name.toUpperCase()} ${path2}`,
3622
+ message: `Skipped ${name.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
3623
+ });
3624
+ };
3466
3625
  for (const [key, rawOperation] of Object.entries(pathItem)) {
3467
3626
  if (NON_OPERATION_KEYS.has(key) || key.startsWith("x-")) continue;
3627
+ if (key === "additionalOperations") {
3628
+ if (isRecord(rawOperation))
3629
+ for (const name of Object.keys(rawOperation)) unsupportedMethod(name);
3630
+ continue;
3631
+ }
3468
3632
  const method = SUPPORTED_METHODS[key.toLowerCase()];
3469
3633
  if (method === void 0) {
3470
- diagnostics.push({
3471
- severity: "warning",
3472
- code: "unsupported-method",
3473
- operation: `${key.toUpperCase()} ${path2}`,
3474
- message: `Skipped ${key.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
3475
- });
3634
+ unsupportedMethod(key);
3476
3635
  continue;
3477
3636
  }
3478
3637
  const operation = dereference(document, rawOperation).value;
@@ -3552,12 +3711,13 @@ function selectServer(document, candidates, context, options, diagnostics) {
3552
3711
  if (typeof url !== "string" || url === "") continue;
3553
3712
  const substituted = substituteServerVariables(url, first["variables"], context, diagnostics);
3554
3713
  if (substituted === void 0) return void 0;
3555
- if (!isAbsoluteHttpUrl(substituted)) {
3714
+ const issue = describeServerUrlIssue(substituted);
3715
+ if (issue !== void 0) {
3556
3716
  diagnostics.push({
3557
3717
  severity: "error",
3558
- code: "relative-server-url",
3718
+ code: issue.code,
3559
3719
  operation: context.resourceId,
3560
- message: `Skipped ${context.where}: server URL "${substituted}" is relative, so no backend host is known. Pass --base-url`
3720
+ message: `Skipped ${context.where}: server URL "${substituted}" ${issue.reason}. Pass --base-url`
3561
3721
  });
3562
3722
  return void 0;
3563
3723
  }
@@ -3606,12 +3766,26 @@ function substituteServerVariables(url, variables, context, diagnostics) {
3606
3766
  return substituted;
3607
3767
  }
3608
3768
  function isAbsoluteHttpUrl(value) {
3769
+ return describeServerUrlIssue(value) === void 0;
3770
+ }
3771
+ function describeServerUrlIssue(value) {
3609
3772
  try {
3610
3773
  const parsed = new URL(value);
3611
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
3612
- return parsed.search === "" && parsed.hash === "";
3774
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3775
+ return { code: "invalid-server-url", reason: "must use http:// or https://" };
3776
+ }
3777
+ if (parsed.search !== "" || parsed.hash !== "") {
3778
+ return {
3779
+ code: "invalid-server-url",
3780
+ reason: "must not contain a query string or fragment"
3781
+ };
3782
+ }
3783
+ return void 0;
3613
3784
  } catch {
3614
- return false;
3785
+ return {
3786
+ code: "relative-server-url",
3787
+ reason: "is relative or malformed, so no backend host is known"
3788
+ };
3615
3789
  }
3616
3790
  }
3617
3791
  function assertAbsoluteHttpUrl(value, label) {
@@ -3765,6 +3939,7 @@ function mergeAllOf(document, schema, stack, dropped) {
3765
3939
  const merged = { type: "object", properties: {}, required: [] };
3766
3940
  const properties = merged["properties"];
3767
3941
  const required = /* @__PURE__ */ new Set();
3942
+ let open2 = false;
3768
3943
  const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
3769
3944
  for (const branch of parts) {
3770
3945
  const converted = convertNode(document, branch, stack, dropped);
@@ -3793,13 +3968,22 @@ function mergeAllOf(document, schema, stack, dropped) {
3793
3968
  throw new UnsupportedSchema(`allOf branch uses "${keyword}", which cannot be merged`);
3794
3969
  }
3795
3970
  }
3796
- if (typeof converted["description"] === "string" && merged["description"] === void 0) {
3797
- merged["description"] = converted["description"];
3971
+ for (const keyword of METADATA_KEYWORDS) {
3972
+ if (Object.hasOwn(converted, keyword) && !Object.hasOwn(merged, keyword)) {
3973
+ merged[keyword] = converted[keyword];
3974
+ }
3975
+ }
3976
+ const additional = converted["additionalProperties"];
3977
+ if (additional === true) open2 = true;
3978
+ else if (isRecord2(additional)) {
3979
+ throw new UnsupportedSchema(
3980
+ "allOf branch constrains additionalProperties, which cannot be merged"
3981
+ );
3798
3982
  }
3799
3983
  }
3800
3984
  if (required.size > 0) merged["required"] = [...required];
3801
3985
  else delete merged["required"];
3802
- merged["additionalProperties"] = false;
3986
+ merged["additionalProperties"] = open2;
3803
3987
  return merged;
3804
3988
  }
3805
3989
  function isRecord2(value) {
@@ -3963,11 +4147,11 @@ function describeUnsupportedParameter(document, parameter, location) {
3963
4147
  }
3964
4148
  function resolveBody(document, candidate) {
3965
4149
  if (candidate.requestBody === void 0) return { kind: "none" };
3966
- const resolved = dereference(document, candidate.requestBody).value;
3967
- if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) {
4150
+ const resolvedBody = dereference(document, candidate.requestBody);
4151
+ if (typeof resolvedBody.value !== "object" || resolvedBody.value === null || Array.isArray(resolvedBody.value)) {
3968
4152
  return { kind: "none" };
3969
4153
  }
3970
- const requestBody = resolved;
4154
+ const requestBody = resolvedBody.value;
3971
4155
  const required = requestBody["required"] === true;
3972
4156
  if (candidate.method === "GET" || candidate.method === "DELETE") {
3973
4157
  return {
@@ -3986,7 +4170,7 @@ function resolveBody(document, candidate) {
3986
4170
  reason: `no JSON request body content type (found: ${Object.keys(content).join(", ") || "none"}). Only application/json and application/*+json are supported - multipart and form data are never serialized as JSON`
3987
4171
  };
3988
4172
  }
3989
- const media = content[mediaType];
4173
+ const media = dereference(document, content[mediaType], resolvedBody.stack).value;
3990
4174
  const schemaNode = isRecord3(media) ? media["schema"] : void 0;
3991
4175
  if (schemaNode === void 0) {
3992
4176
  return {
@@ -4031,11 +4215,12 @@ function buildResourceDrafts(loaded, options = {}) {
4031
4215
  loaded,
4032
4216
  options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
4033
4217
  );
4034
- const diagnostics = [...discovery.diagnostics];
4218
+ const diagnostics = [];
4035
4219
  const drafts = [];
4036
4220
  const wanted = options.include?.operationIds;
4037
4221
  const wantedTags = options.include?.tags;
4038
4222
  const matched = /* @__PURE__ */ new Set();
4223
+ const selected = /* @__PURE__ */ new Set();
4039
4224
  for (const candidate of discovery.operations) {
4040
4225
  if (wanted !== void 0 && !selects(wanted, candidate)) continue;
4041
4226
  if (wantedTags !== void 0 && !candidate.tags.some((tag) => wantedTags.includes(tag))) {
@@ -4044,6 +4229,7 @@ function buildResourceDrafts(loaded, options = {}) {
4044
4229
  if (wanted !== void 0) {
4045
4230
  for (const id of wanted) if (selects([id], candidate)) matched.add(id);
4046
4231
  }
4232
+ selected.add(candidate.resourceId);
4047
4233
  const mapping = mapRequest(loaded, candidate);
4048
4234
  diagnostics.push(...mapping.diagnostics);
4049
4235
  if (!mapping.supported) continue;
@@ -4073,8 +4259,8 @@ function buildResourceDrafts(loaded, options = {}) {
4073
4259
  if (options.policy?.pricing === void 0 || options.policy.expose === void 0) {
4074
4260
  review.push(
4075
4261
  "REVIEW: pricing and exposure are not inferred from OpenAPI. Add e.g.",
4076
- ' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC }',
4077
- " expose: [http] # http | mcp | a2a"
4262
+ ' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC } with payments: [x402]',
4263
+ " expose: [http] # http | mcp | a2a | acp"
4078
4264
  );
4079
4265
  }
4080
4266
  drafts.push({
@@ -4108,10 +4294,19 @@ function buildResourceDrafts(loaded, options = {}) {
4108
4294
  version: loaded.version,
4109
4295
  sourcePath: loaded.sourcePath,
4110
4296
  drafts,
4111
- diagnostics,
4297
+ diagnostics: [
4298
+ ...selectDiscoveryDiagnostics(discovery.diagnostics, options.include, selected),
4299
+ ...diagnostics
4300
+ ],
4112
4301
  unmatchedOperationIds: (wanted ?? []).filter((id) => !matched.has(id))
4113
4302
  };
4114
4303
  }
4304
+ function selectDiscoveryDiagnostics(found, include, selected) {
4305
+ if (include?.operationIds === void 0 && include?.tags === void 0) return found;
4306
+ return found.filter(
4307
+ ({ operation }) => operation === void 0 || selected.has(operation) || include.operationIds?.includes(operation) === true
4308
+ );
4309
+ }
4115
4310
  function selects(ids, candidate) {
4116
4311
  return ids.includes(candidate.resourceId) || candidate.operationId !== void 0 && ids.includes(candidate.operationId);
4117
4312
  }
@@ -4125,19 +4320,28 @@ function selectOutputSchema(document, candidate, diagnostics, dropped) {
4125
4320
  if (!isRecord4(responses)) return void 0;
4126
4321
  const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
4127
4322
  if (successes.length === 0) return void 0;
4128
- const withBody = successes.filter((status) => {
4129
- const response2 = responses[status];
4130
- const content2 = isRecord4(response2) ? response2["content"] : void 0;
4131
- return isRecord4(content2) && pickJsonMediaType(Object.keys(content2)) !== void 0;
4323
+ const withBody = successes.flatMap((status) => {
4324
+ const response = dereference(document, responses[status]);
4325
+ if (!isRecord4(response.value)) return [];
4326
+ const content = response.value["content"];
4327
+ if (!isRecord4(content)) return [];
4328
+ const mediaType = pickJsonMediaType(Object.keys(content));
4329
+ if (mediaType === void 0) return [];
4330
+ const media = dereference(document, content[mediaType], response.stack).value;
4331
+ return isRecord4(media) ? [{ status, media }] : [];
4132
4332
  });
4133
4333
  const chosen = withBody[0];
4134
4334
  if (chosen === void 0) return void 0;
4135
- const response = responses[chosen];
4136
- const content = response["content"];
4137
- const mediaType = pickJsonMediaType(Object.keys(content));
4138
- const media = content[mediaType];
4139
- const schemaNode = isRecord4(media) ? media["schema"] : void 0;
4140
- if (schemaNode === void 0) return void 0;
4335
+ const schemaNode = chosen.media["schema"];
4336
+ if (schemaNode === void 0) {
4337
+ diagnostics.push({
4338
+ severity: "warning",
4339
+ code: "missing-output-schema",
4340
+ operation: candidate.resourceId,
4341
+ message: `${candidate.resourceId}: response ${chosen.status} has JSON content without a schema; omitted the output schema`
4342
+ });
4343
+ return void 0;
4344
+ }
4141
4345
  const converted = convertSchema(document, schemaNode);
4142
4346
  if (!converted.supported) {
4143
4347
  diagnostics.push({
@@ -4154,7 +4358,7 @@ function selectOutputSchema(document, candidate, diagnostics, dropped) {
4154
4358
  severity: "warning",
4155
4359
  code: "multiple-success-responses",
4156
4360
  operation: candidate.resourceId,
4157
- message: `${candidate.resourceId}: responses ${withBody.join(", ")} all carry a JSON body; used ${chosen}`
4361
+ message: `${candidate.resourceId}: responses ${withBody.map(({ status }) => status).join(", ")} all carry a JSON body; used ${chosen.status}`
4158
4362
  });
4159
4363
  }
4160
4364
  return converted.schema;
@@ -4200,6 +4404,31 @@ function invalid2(message, details, cause) {
4200
4404
  ...cause !== void 0 ? { cause } : {}
4201
4405
  });
4202
4406
  }
4407
+ async function readCapped(sourcePath, details) {
4408
+ const buffer = Buffer.alloc(MAX_SOURCE_BYTES + 1);
4409
+ let length = 0;
4410
+ try {
4411
+ const handle = await open(sourcePath, "r");
4412
+ try {
4413
+ while (length < buffer.length) {
4414
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
4415
+ if (bytesRead === 0) break;
4416
+ length += bytesRead;
4417
+ }
4418
+ } finally {
4419
+ await handle.close();
4420
+ }
4421
+ } catch (error) {
4422
+ throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
4423
+ }
4424
+ if (length > MAX_SOURCE_BYTES) {
4425
+ throw invalid2(`OpenAPI document "${sourcePath}" is over the ${MAX_SOURCE_BYTES}-byte limit`, {
4426
+ ...details,
4427
+ maxBytes: MAX_SOURCE_BYTES
4428
+ });
4429
+ }
4430
+ return buffer.toString("utf8", 0, length);
4431
+ }
4203
4432
  async function loadOpenApiDocument(sourcePath) {
4204
4433
  const details = { sourcePath };
4205
4434
  let stats;
@@ -4224,7 +4453,7 @@ async function loadOpenApiDocument(sourcePath) {
4224
4453
  details
4225
4454
  );
4226
4455
  }
4227
- const source = await readFile(sourcePath, "utf8");
4456
+ const source = await readCapped(sourcePath, details);
4228
4457
  if (source.trim() === "") {
4229
4458
  throw invalid2(`OpenAPI document "${sourcePath}" is empty`, details);
4230
4459
  }
@@ -4702,7 +4931,10 @@ function buildProgram(io = processIo) {
4702
4931
  process.exitCode = code;
4703
4932
  });
4704
4933
  const importCommand = program.command("import").description("Generate Agent Commerce resource drafts from an API description.");
4705
- importCommand.command("openapi").description("Convert a local OpenAPI 3.0/3.1/3.2 document into resource drafts.").argument("<source>", "path to a local .yaml, .yml or .json OpenAPI document").option("--output <path>", "output path (default: <source>.agent-commerce.yaml)").option("--force", "overwrite an existing output file", false).option("--base-url <url>", "backend base URL, overriding the document servers").option(
4934
+ importCommand.command("openapi").description("Convert a local OpenAPI 3.0/3.1/3.2 document into resource drafts.").argument("<source>", "path to a local .yaml, .yml or .json OpenAPI document").option(
4935
+ "--output <path>",
4936
+ "output path (default: <source name without extension>.agent-commerce.yaml in the current directory)"
4937
+ ).option("--force", "overwrite an existing output file", false).option("--base-url <url>", "backend base URL, overriding the document servers").option(
4706
4938
  "--operation <operationId>",
4707
4939
  "import only this operation (repeatable)",
4708
4940
  collect,