@devlab.group/agent-commerce 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,14 +2,15 @@
2
2
  import { ZodError, z } from 'zod';
3
3
  import { existsSync, mkdirSync, accessSync, constants, closeSync, openSync, readFileSync, chmodSync, promises } from 'fs';
4
4
  import * as path from 'path';
5
- import { resolve, dirname, join } from 'path';
6
- import { stringify, parse, YAMLError } from 'yaml';
5
+ import { resolve, dirname, basename as basename$1, extname, join } from 'path';
6
+ import { stringify, parse, Document, YAMLError } from 'yaml';
7
7
  import { CommanderError, Command } from 'commander';
8
8
  import picocolors from 'picocolors';
9
9
  import { randomUUID } from 'crypto';
10
10
  import Database from 'better-sqlite3';
11
11
  import { createRequire } from 'module';
12
- import { writeFile } from 'fs/promises';
12
+ import { writeFile, stat, readFile, rename, rm } from 'fs/promises';
13
+ import { validate } from '@scalar/openapi-parser';
13
14
  import * as clack from '@clack/prompts';
14
15
 
15
16
  var __defProp = Object.defineProperty;
@@ -30,12 +31,34 @@ var init_filename = __esm({
30
31
  }
31
32
  });
32
33
 
34
+ // src/core/domain/common.ts
35
+ var PROTOCOL_NAMES;
36
+ var init_common = __esm({
37
+ "src/core/domain/common.ts"() {
38
+ PROTOCOL_NAMES = ["http", "mcp", "a2a"];
39
+ }
40
+ });
41
+
42
+ // src/core/domain/event.ts
43
+ var init_event = __esm({
44
+ "src/core/domain/event.ts"() {
45
+ }
46
+ });
47
+
33
48
  // src/core/domain/resource.ts
34
49
  var init_resource = __esm({
35
50
  "src/core/domain/resource.ts"() {
36
51
  }
37
52
  });
38
53
 
54
+ // src/core/domain/wire.ts
55
+ var PAYMENT_INPUT_FIELD;
56
+ var init_wire = __esm({
57
+ "src/core/domain/wire.ts"() {
58
+ PAYMENT_INPUT_FIELD = "_payment";
59
+ }
60
+ });
61
+
39
62
  // src/core/errors/codes.ts
40
63
  var COMMERCE_ERROR_HTTP_STATUS, RETRYABLE_ERROR_CODES;
41
64
  var init_codes = __esm({
@@ -142,6 +165,45 @@ var init_logger = __esm({
142
165
  }
143
166
  });
144
167
 
168
+ // src/core/interfaces/protocol-adapter.ts
169
+ var init_protocol_adapter = __esm({
170
+ "src/core/interfaces/protocol-adapter.ts"() {
171
+ }
172
+ });
173
+
174
+ // src/core/interfaces/runtime.ts
175
+ var systemClock;
176
+ var init_runtime = __esm({
177
+ "src/core/interfaces/runtime.ts"() {
178
+ systemClock = {
179
+ now: () => /* @__PURE__ */ new Date(),
180
+ nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
181
+ monotonicMs: () => performance.now()
182
+ };
183
+ }
184
+ });
185
+
186
+ // src/core/public-types.ts
187
+ var init_public_types = __esm({
188
+ "src/core/public-types.ts"() {
189
+ init_common();
190
+ init_event();
191
+ init_resource();
192
+ init_wire();
193
+ init_errors();
194
+ init_logger();
195
+ init_protocol_adapter();
196
+ init_runtime();
197
+ }
198
+ });
199
+
200
+ // src/core/index.ts
201
+ var init_core = __esm({
202
+ "src/core/index.ts"() {
203
+ init_public_types();
204
+ }
205
+ });
206
+
145
207
  // src/core/execution/backend-http.ts
146
208
  function extractPathParameterNames(url) {
147
209
  return [...url.matchAll(PATH_PARAM_PATTERN)].map((match) => match[1]);
@@ -169,14 +231,6 @@ var init_event_bus = __esm({
169
231
  }
170
232
  });
171
233
 
172
- // src/core/domain/wire.ts
173
- var PAYMENT_INPUT_FIELD;
174
- var init_wire = __esm({
175
- "src/core/domain/wire.ts"() {
176
- PAYMENT_INPUT_FIELD = "_payment";
177
- }
178
- });
179
-
180
234
  // src/core/execution/validation.ts
181
235
  function typeList(raw) {
182
236
  if (typeof raw === "string") return [raw];
@@ -223,50 +277,6 @@ var init_execution = __esm({
223
277
  }
224
278
  });
225
279
 
226
- // src/core/domain/event.ts
227
- var init_event = __esm({
228
- "src/core/domain/event.ts"() {
229
- }
230
- });
231
-
232
- // src/core/interfaces/protocol-adapter.ts
233
- var init_protocol_adapter = __esm({
234
- "src/core/interfaces/protocol-adapter.ts"() {
235
- }
236
- });
237
-
238
- // src/core/interfaces/runtime.ts
239
- var systemClock;
240
- var init_runtime = __esm({
241
- "src/core/interfaces/runtime.ts"() {
242
- systemClock = {
243
- now: () => /* @__PURE__ */ new Date(),
244
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
245
- monotonicMs: () => performance.now()
246
- };
247
- }
248
- });
249
-
250
- // src/core/public-types.ts
251
- var init_public_types = __esm({
252
- "src/core/public-types.ts"() {
253
- init_event();
254
- init_resource();
255
- init_wire();
256
- init_errors();
257
- init_logger();
258
- init_protocol_adapter();
259
- init_runtime();
260
- }
261
- });
262
-
263
- // src/core/index.ts
264
- var init_core = __esm({
265
- "src/core/index.ts"() {
266
- init_public_types();
267
- }
268
- });
269
-
270
280
  // src/payments/x402/networks.ts
271
281
  function findNetworkProfile(id) {
272
282
  return BY_ID.get(id);
@@ -677,8 +687,13 @@ function normalise(raw) {
677
687
  mcp: {
678
688
  enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
679
689
  mountPath: raw.protocols.mcp.mountPath
690
+ },
691
+ a2a: {
692
+ enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
693
+ mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
680
694
  }
681
695
  };
696
+ validateMountPaths(protocols);
682
697
  const x402Raw = raw.payments.x402;
683
698
  const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
684
699
  mode: "remote",
@@ -753,6 +768,25 @@ function normalise(raw) {
753
768
  }
754
769
  };
755
770
  }
771
+ function validateMountPaths(protocols) {
772
+ const mounts = [
773
+ ["mcp", protocols.mcp],
774
+ ["a2a", protocols.a2a]
775
+ ].filter(([, p]) => p.enabled);
776
+ for (const [index, [nameA, a]] of mounts.entries()) {
777
+ for (const [nameB, b] of mounts.slice(index + 1)) {
778
+ const baseA = a.mountPath.replace(/\/+$/, "");
779
+ const baseB = b.mountPath.replace(/\/+$/, "");
780
+ if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
781
+ throw new CommerceError(
782
+ "CONFIG_INVALID",
783
+ `protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") - each mount registers a wildcard, so overlapping prefixes cannot both be served`,
784
+ { details: { path: `protocols.${nameA}.mountPath` } }
785
+ );
786
+ }
787
+ }
788
+ }
789
+ }
756
790
  function normaliseResource(id, entry, protocols, x402) {
757
791
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
758
792
  const inputProperties = entry.input?.["properties"];
@@ -773,7 +807,7 @@ function normaliseResource(id, entry, protocols, x402) {
773
807
  const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
774
808
  throw new CommerceError(
775
809
  "CONFIG_INVALID",
776
- `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
810
+ `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
777
811
  { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
778
812
  );
779
813
  }
@@ -794,6 +828,13 @@ function normaliseResource(id, entry, protocols, x402) {
794
828
  );
795
829
  }
796
830
  }
831
+ if (entry.expose.includes("a2a") && !protocols.a2a.enabled) {
832
+ throw new CommerceError(
833
+ "CONFIG_INVALID",
834
+ `Resource "${id}" is exposed via "a2a" but protocols.a2a.enabled is false`,
835
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
836
+ );
837
+ }
797
838
  if (entry.expose.includes("http") && !protocols.http.enabled) {
798
839
  throw new CommerceError(
799
840
  "CONFIG_INVALID",
@@ -802,7 +843,8 @@ function normaliseResource(id, entry, protocols, x402) {
802
843
  );
803
844
  }
804
845
  validateBackendUrl(id, entry.backend.url);
805
- validatePathParametersDeclared(id, entry.backend.url, entry.input);
846
+ const pathScope = validateInputBindings(id, entry.backend, entry.input);
847
+ validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
806
848
  if (entry.pricing.type === "dynamic") {
807
849
  throw new CommerceError(
808
850
  "CONFIG_INVALID",
@@ -855,7 +897,8 @@ function normaliseResource(id, entry, protocols, x402) {
855
897
  timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
856
898
  min: 1
857
899
  })
858
- } : {}
900
+ } : {},
901
+ ...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
859
902
  },
860
903
  pricing,
861
904
  exposedVia: entry.expose,
@@ -909,7 +952,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
909
952
  if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
910
953
  throw new CommerceError(
911
954
  "CONFIG_INVALID",
912
- `Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object" \u2014 the validator will never route a value through either check under that type, so this schema claims to constrain input it does not actually enforce. Remove "properties"/"required", or include "object" in "type".`,
955
+ `Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object" - the validator will never route a value through either check under that type, so this schema claims to constrain input it does not actually enforce. Remove "properties"/"required", or include "object" in "type".`,
913
956
  { details: { path: `resources.${id}.${path2}`, resourceId: id } }
914
957
  );
915
958
  }
@@ -943,7 +986,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
943
986
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
944
987
  } else if (Array.isArray(items)) {
945
988
  console.warn(
946
- `[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce \u2014 only a single schema applied to every array element is supported (see src/core/execution/validation.ts). Each position's schema is unenforced; treat it as documentation only.`
989
+ `[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce - only a single schema applied to every array element is supported (see src/core/execution/validation.ts). Each position's schema is unenforced; treat it as documentation only.`
947
990
  );
948
991
  }
949
992
  }
@@ -952,14 +995,14 @@ function validatePricingAmount(id, amount, x402) {
952
995
  if (!PRICING_AMOUNT_PATTERN.test(amount)) {
953
996
  throw new CommerceError(
954
997
  "CONFIG_INVALID",
955
- `Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent \u2014 e.g. "0.01")`,
998
+ `Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent - e.g. "0.01")`,
956
999
  { details: { path: path2, resourceId: id } }
957
1000
  );
958
1001
  }
959
1002
  if (ZERO_AMOUNT_PATTERN.test(amount)) {
960
1003
  throw new CommerceError(
961
1004
  "CONFIG_INVALID",
962
- `Resource "${id}" has pricing.amount "0" \u2014 a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
1005
+ `Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
963
1006
  { details: { path: path2, resourceId: id } }
964
1007
  );
965
1008
  }
@@ -974,7 +1017,86 @@ function validatePricingAmount(id, amount, x402) {
974
1017
  }
975
1018
  }
976
1019
  }
977
- function validatePathParametersDeclared(id, url, input) {
1020
+ function validateInputBindings(id, backend, input) {
1021
+ const legacy = { schema: input, where: "its input schema" };
1022
+ const bindings = backend.inputBindings;
1023
+ const templated = extractPathParameterNames(backend.url).length > 0;
1024
+ if (bindings === void 0) return legacy;
1025
+ const path2 = `resources.${id}.backend.inputBindings`;
1026
+ const fail = (message, details = {}) => {
1027
+ throw new CommerceError("CONFIG_INVALID", `Resource "${id}" ${message}`, {
1028
+ details: { path: path2, resourceId: id, ...details }
1029
+ });
1030
+ };
1031
+ const entries = Object.entries(bindings).filter(
1032
+ (entry) => entry[1] !== void 0
1033
+ );
1034
+ if (entries.length === 0) {
1035
+ fail("has an empty backend.inputBindings - remove the block to use the default mapping");
1036
+ }
1037
+ if (backend.method === "GET" || backend.method === "DELETE") {
1038
+ if (bindings.body !== void 0) {
1039
+ fail(
1040
+ `binds a request body on a ${backend.method}, which sends none - the value would be silently dropped`
1041
+ );
1042
+ }
1043
+ }
1044
+ if (templated && bindings.path === void 0) {
1045
+ fail(
1046
+ 'has backend.url path parameters but no "path" binding - in explicit binding mode nothing else supplies them, so every call would fail to reach the backend'
1047
+ );
1048
+ }
1049
+ const seen = /* @__PURE__ */ new Map();
1050
+ const properties = isPlainObject2(input?.["properties"]) ? input["properties"] : {};
1051
+ const required = new Set(
1052
+ Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1053
+ );
1054
+ for (const [location, property] of entries) {
1055
+ if (property === PAYMENT_INPUT_FIELD) {
1056
+ fail(
1057
+ `binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
1058
+ { location }
1059
+ );
1060
+ }
1061
+ const other = seen.get(property);
1062
+ if (other !== void 0) {
1063
+ fail(`binds both "${other}" and "${location}" to the input property "${property}"`, {
1064
+ location,
1065
+ property
1066
+ });
1067
+ }
1068
+ seen.set(property, location);
1069
+ if (!Object.hasOwn(properties, property)) {
1070
+ fail(
1071
+ `binds "${location}" to input property "${property}", which the input schema does not declare - the schema is closed, so a caller could never supply it`,
1072
+ { location, property }
1073
+ );
1074
+ }
1075
+ const declared = properties[property];
1076
+ if (location !== "body" && isPlainObject2(declared) && !isObjectSchemaNode(declared)) {
1077
+ fail(
1078
+ `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
1079
+ { location, property }
1080
+ );
1081
+ }
1082
+ }
1083
+ if (templated && bindings.path !== void 0 && !required.has(bindings.path)) {
1084
+ fail(
1085
+ `binds path parameters to input property "${bindings.path}" without listing it in the input schema's "required" - a caller that omits it cannot supply any path parameter, so the request could never be built`,
1086
+ { property: bindings.path }
1087
+ );
1088
+ }
1089
+ if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
1090
+ const group = properties[bindings.path];
1091
+ return {
1092
+ schema: isPlainObject2(group) ? group : void 0,
1093
+ where: `input.properties.${bindings.path}`
1094
+ };
1095
+ }
1096
+ function pickDefined(value) {
1097
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1098
+ }
1099
+ function validatePathParametersDeclared(id, url, input, where) {
978
1100
  const stray = findUnparsedBraceToken(url);
979
1101
  if (stray !== void 0) {
980
1102
  throw new CommerceError(
@@ -1013,14 +1135,14 @@ function validatePathParametersDeclared(id, url, input) {
1013
1135
  if (!Object.hasOwn(properties, param)) {
1014
1136
  throw new CommerceError(
1015
1137
  "CONFIG_INVALID",
1016
- `Resource "${id}" has backend.url path parameter "{${param}}" which is not declared in its input schema \u2014 the caller has no way to supply it, so every call would settle payment (if priced) and then fail to reach the backend`,
1138
+ `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`,
1017
1139
  { details: { path: path2, resourceId: id, param } }
1018
1140
  );
1019
1141
  }
1020
1142
  if (!required.has(param)) {
1021
1143
  throw new CommerceError(
1022
1144
  "CONFIG_INVALID",
1023
- `Resource "${id}" has backend.url path parameter "{${param}}" declared in its input schema but not listed in "required" \u2014 a caller that omits it hits the same unservable-request problem as an undeclared parameter`,
1145
+ `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`,
1024
1146
  { details: { path: path2, resourceId: id, param } }
1025
1147
  );
1026
1148
  }
@@ -1063,7 +1185,7 @@ function validateAddress(path2, value) {
1063
1185
  );
1064
1186
  }
1065
1187
  }
1066
- var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, McpMountPathSchema, ProtocolsSchema, BackendMethodSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, PaymentsSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
1188
+ var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, MountPathSchema, ProtocolsSchema, DEFAULT_A2A_MOUNT_PATH, BackendMethodSchema, BackendInputBindingsSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, PaymentsSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
1067
1189
  var init_schema = __esm({
1068
1190
  "src/config/schema.ts"() {
1069
1191
  init_execution();
@@ -1088,15 +1210,15 @@ var init_schema = __esm({
1088
1210
  adminToken: z.string().min(1).optional(),
1089
1211
  /** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
1090
1212
  // Entries are matched literally against the browser's
1091
- // `Origin` header, so `"*"` and a trailing slash match nothing at all —
1213
+ // `Origin` header, so `"*"` and a trailing slash match nothing at all -
1092
1214
  // fail-closed (a lockout, not a bypass), but silently, and a lockout with
1093
1215
  // no explanation is the kind of thing an operator "fixes" by disabling the
1094
1216
  // check. Reject those two shapes with a pointer instead.
1095
1217
  allowedOrigins: z.array(
1096
1218
  z.string().min(1).refine((origin) => origin !== "*", {
1097
- message: `wildcard "*" is not supported \u2014 allowedOrigins entries are matched literally against the browser's Origin header, so "*" would match nothing. List each scheme://host[:port] explicitly.`
1219
+ 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.`
1098
1220
  }).refine((origin) => !origin.endsWith("/"), {
1099
- message: 'must not end with "/" \u2014 a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
1221
+ message: 'must not end with "/" - a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
1100
1222
  })
1101
1223
  ).optional()
1102
1224
  }).strict();
@@ -1110,16 +1232,19 @@ var init_schema = __esm({
1110
1232
  "/health",
1111
1233
  "/ready",
1112
1234
  "/.well-known/agent-commerce",
1235
+ // Fixed by the A2A specification, so it is the adapter's to serve and never
1236
+ // a configurable mount's to claim.
1237
+ "/.well-known/agent-card.json",
1113
1238
  "/api/resources",
1114
1239
  "/api/resources/:id/invoke",
1115
1240
  "/api/receipts",
1116
1241
  "/api/events",
1117
1242
  "/api/events/stream"
1118
1243
  ];
1119
- McpMountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1120
- message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
1244
+ MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1245
+ message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
1121
1246
  }).refine((value) => !/[:*?\s]/.test(value), {
1122
- message: 'must not contain ":", "*", "?" or whitespace \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
1247
+ message: 'must not contain ":", "*", "?" or whitespace - the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
1123
1248
  }).refine(
1124
1249
  (value) => {
1125
1250
  const base = value.replace(/\/+$/, "");
@@ -1135,16 +1260,29 @@ var init_schema = __esm({
1135
1260
  http: z.object({ enabled: BooleanOrString }).strict(),
1136
1261
  mcp: z.object({
1137
1262
  enabled: BooleanOrString,
1138
- mountPath: McpMountPathSchema
1139
- }).strict()
1263
+ mountPath: MountPathSchema
1264
+ }).strict(),
1265
+ // Optional block: A2A is off unless an operator asks for it, and an
1266
+ // existing config predating the adapter stays valid.
1267
+ a2a: z.object({
1268
+ enabled: BooleanOrString,
1269
+ mountPath: MountPathSchema.optional()
1270
+ }).strict().optional()
1140
1271
  }).strict();
1272
+ DEFAULT_A2A_MOUNT_PATH = "/a2a";
1141
1273
  BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1274
+ BackendInputBindingsSchema = z.object({
1275
+ path: z.string().min(1).optional(),
1276
+ query: z.string().min(1).optional(),
1277
+ body: z.string().min(1).optional()
1278
+ }).strict();
1142
1279
  BackendHandlerSchema = z.object({
1143
1280
  type: z.literal("http"),
1144
1281
  method: BackendMethodSchema,
1145
1282
  url: z.string().min(1),
1146
1283
  headers: z.record(z.string(), z.string()).optional(),
1147
- timeoutMs: NumberOrString.optional()
1284
+ timeoutMs: NumberOrString.optional(),
1285
+ inputBindings: BackendInputBindingsSchema.optional()
1148
1286
  }).strict();
1149
1287
  PricingSchema = z.discriminatedUnion("type", [
1150
1288
  z.object({ type: z.literal("free") }).strict(),
@@ -1189,7 +1327,7 @@ var init_schema = __esm({
1189
1327
  z.object({
1190
1328
  mode: z.literal("remote"),
1191
1329
  url: z.string().min(1),
1192
- // Absent means "this facilitator takes no credential" — an explicit
1330
+ // Absent means "this facilitator takes no credential" - an explicit
1193
1331
  // statement, normalised to `{ type: 'none' }` below. On a mainnet that
1194
1332
  // combination is refused outright, so the default can never quietly
1195
1333
  // become an unauthenticated production facilitator.
@@ -1207,7 +1345,7 @@ var init_schema = __esm({
1207
1345
  payTo: z.string().min(1),
1208
1346
  maxTimeoutSeconds: NumberOrString,
1209
1347
  facilitator: FacilitatorSchema,
1210
- /** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
1348
+ /** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
1211
1349
  allowMainnet: BooleanOrString.optional(),
1212
1350
  /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1213
1351
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
@@ -1224,7 +1362,7 @@ var init_schema = __esm({
1224
1362
  resources: ResourcesMapSchema,
1225
1363
  payments: PaymentsSchema
1226
1364
  }).strict();
1227
- SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp"]);
1365
+ SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1228
1366
  SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1229
1367
  EMPTY_CLOSED_OBJECT_SCHEMA = {
1230
1368
  type: "object",
@@ -1346,6 +1484,7 @@ var init_config = __esm({
1346
1484
 
1347
1485
  // src/cli/program.ts
1348
1486
  init_filename();
1487
+ init_core();
1349
1488
 
1350
1489
  // src/cli/commands/demo.ts
1351
1490
  function defaultDemoSteps() {
@@ -1413,9 +1552,46 @@ init_execution();
1413
1552
  init_core();
1414
1553
  init_networks();
1415
1554
 
1555
+ // src/protocols/a2a/constants.ts
1556
+ var A2A_SPEC_VERSION = "1.0.0";
1557
+ var A2A_PROTOCOL_VERSION = "1.0";
1558
+ var A2A_PROTOCOL_BINDING = "JSONRPC";
1559
+ var A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
1560
+ var A2A_UNSUPPORTED_METHODS = [
1561
+ "SendStreamingMessage",
1562
+ "GetTask",
1563
+ "ListTasks",
1564
+ "CancelTask",
1565
+ "SubscribeToTask",
1566
+ "CreateTaskPushNotificationConfig",
1567
+ "GetTaskPushNotificationConfig",
1568
+ "ListTaskPushNotificationConfigs",
1569
+ "DeleteTaskPushNotificationConfig",
1570
+ "GetExtendedAgentCard"
1571
+ ];
1572
+
1573
+ // src/protocols/a2a/descriptor.ts
1574
+ var A2A_UNSUPPORTED = [
1575
+ // Methods, named as the protocol names them. Same list the transport
1576
+ // rejects by name, so the descriptor cannot promise less than it refuses.
1577
+ ...A2A_UNSUPPORTED_METHODS,
1578
+ // Transports other than the one binding served.
1579
+ "HTTP+JSON/REST binding",
1580
+ "gRPC binding",
1581
+ // Behaviours.
1582
+ "SSE",
1583
+ "long-running task persistence",
1584
+ "task resumption",
1585
+ "push notifications",
1586
+ "multi-turn conversational continuation",
1587
+ "authenticated extended agent cards",
1588
+ "A2A authentication schemes",
1589
+ "artifact types beyond Agent Commerce outcome data"
1590
+ ];
1591
+
1416
1592
  // src/storage/receipts/store.ts
1417
1593
  init_core();
1418
- var PACKAGE_VERSION = "1.0.0" ;
1594
+ var PACKAGE_VERSION = "1.2.0" ;
1419
1595
 
1420
1596
  // src/storage/receipts/redact.ts
1421
1597
  var SECRET_KEY_PATTERN = /private[-_ ]?key|secret|password|mnemonic|seed[-_ ]?phrase|api[-_ ]?key|authoriz(e|ation)|^auth$|auth[-_]?header|payment[-_ ]?proof|^proof$|x-payment|signature|signed[-_ ]?message|token|bearer|credential|cookie|session|jwt/i;
@@ -2168,7 +2344,7 @@ function maskMiddle(value, keep = 6) {
2168
2344
  }
2169
2345
  createRequire(import.meta.url);
2170
2346
  function readVersionReport() {
2171
- const injected = '{"cliVersion":"1.0.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"}]}' ;
2347
+ const injected = '{"cliVersion":"1.2.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"}]}' ;
2172
2348
  {
2173
2349
  return JSON.parse(injected);
2174
2350
  }
@@ -2307,10 +2483,27 @@ async function runDoctor(options, deps = {}) {
2307
2483
  checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
2308
2484
  } else {
2309
2485
  const mcpMountPath = config.protocols.mcp.enabled ? config.protocols.mcp.mountPath : void 0;
2486
+ const a2aMountPath = config.protocols.a2a.enabled ? config.protocols.a2a.mountPath : void 0;
2310
2487
  checks.push({
2311
2488
  name: "Protocols",
2312
2489
  status: "PASS",
2313
- detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"}`
2490
+ detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"}`
2491
+ });
2492
+ }
2493
+ if (config === void 0) {
2494
+ checks.push({ name: "A2A", status: "WARN", detail: "skipped \u2014 config invalid" });
2495
+ } else if (!config.protocols.a2a.enabled) {
2496
+ checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
2497
+ } else {
2498
+ checks.push({
2499
+ name: "A2A",
2500
+ status: "PASS",
2501
+ detail: `experimental \xB7 spec ${A2A_SPEC_VERSION} \xB7 protocol ${A2A_PROTOCOL_VERSION} \xB7 binding ${A2A_PROTOCOL_BINDING} \xB7 mount ${config.protocols.a2a.mountPath} \xB7 card ${A2A_AGENT_CARD_PATH}`
2502
+ });
2503
+ checks.push({
2504
+ name: "A2A unsupported",
2505
+ status: "INFO",
2506
+ detail: A2A_UNSUPPORTED.join(", ")
2314
2507
  });
2315
2508
  }
2316
2509
  const x402 = config?.payments.x402;
@@ -2457,6 +2650,1109 @@ function printDoctorReport(report, io, json) {
2457
2650
  io.stdout(`Score: ${report.score.passed}/${report.score.total} checks passed`);
2458
2651
  }
2459
2652
 
2653
+ // src/cli/commands/import-openapi.ts
2654
+ init_core();
2655
+
2656
+ // src/openapi/discover.ts
2657
+ init_errors();
2658
+ init_execution();
2659
+
2660
+ // src/openapi/refs.ts
2661
+ init_errors();
2662
+ var MAX_REF_DEPTH = 100;
2663
+ function isRefNode(value) {
2664
+ return typeof value === "object" && value !== null && typeof value.$ref === "string";
2665
+ }
2666
+ function dereference(document, node, stack = []) {
2667
+ let current = node;
2668
+ let chain = stack;
2669
+ while (isRefNode(current)) {
2670
+ const pointer = current.$ref;
2671
+ if (!pointer.startsWith("#")) {
2672
+ throw new CommerceError(
2673
+ "CONFIG_INVALID",
2674
+ `OpenAPI document contains an external reference "${pointer}". Only internal references (#/...) are supported`,
2675
+ { details: { ref: pointer } }
2676
+ );
2677
+ }
2678
+ if (chain.includes(pointer)) {
2679
+ throw new CommerceError(
2680
+ "CONFIG_INVALID",
2681
+ `OpenAPI reference "${pointer}" is circular (${[...chain, pointer].join(" -> ")})`,
2682
+ { details: { ref: pointer, chain: [...chain, pointer] } }
2683
+ );
2684
+ }
2685
+ if (chain.length >= MAX_REF_DEPTH) {
2686
+ throw new CommerceError(
2687
+ "CONFIG_INVALID",
2688
+ `OpenAPI reference chain exceeds ${MAX_REF_DEPTH} hops at "${pointer}"`,
2689
+ { details: { ref: pointer } }
2690
+ );
2691
+ }
2692
+ current = resolvePointer(document, pointer);
2693
+ chain = [...chain, pointer];
2694
+ }
2695
+ return { value: current, stack: chain };
2696
+ }
2697
+ function resolvePointer(document, ref) {
2698
+ const pointer = ref.slice(1);
2699
+ if (pointer === "" || pointer === "/") return document;
2700
+ if (!pointer.startsWith("/")) {
2701
+ throw new CommerceError(
2702
+ "CONFIG_INVALID",
2703
+ `OpenAPI reference "${ref}" is not a JSON Pointer. Plain names and anchors are not supported`,
2704
+ { details: { ref } }
2705
+ );
2706
+ }
2707
+ let current = document;
2708
+ for (const rawSegment of pointer.slice(1).split("/")) {
2709
+ const segment = decodeURIComponent(rawSegment).replaceAll("~1", "/").replaceAll("~0", "~");
2710
+ if (Array.isArray(current)) {
2711
+ const index = Number(segment);
2712
+ current = Number.isInteger(index) ? current[index] : void 0;
2713
+ } else if (typeof current === "object" && current !== null) {
2714
+ current = current[segment];
2715
+ } else {
2716
+ current = void 0;
2717
+ }
2718
+ if (current === void 0) {
2719
+ throw new CommerceError(
2720
+ "CONFIG_INVALID",
2721
+ `OpenAPI reference "${ref}" does not resolve to anything in the document`,
2722
+ { details: { ref } }
2723
+ );
2724
+ }
2725
+ }
2726
+ return current;
2727
+ }
2728
+
2729
+ // src/openapi/discover.ts
2730
+ var SUPPORTED_METHODS = {
2731
+ get: "GET",
2732
+ post: "POST",
2733
+ put: "PUT",
2734
+ patch: "PATCH",
2735
+ delete: "DELETE"
2736
+ };
2737
+ var NON_OPERATION_KEYS = /* @__PURE__ */ new Set(["summary", "description", "servers", "parameters", "$ref"]);
2738
+ var ID_ALLOWED = /[^A-Za-z0-9_.-]+/g;
2739
+ var MAX_ID_LENGTH = 128;
2740
+ function discoverOperations(loaded, options = {}) {
2741
+ const { document } = loaded;
2742
+ const diagnostics = [];
2743
+ const operations = [];
2744
+ const claimed = /* @__PURE__ */ new Map();
2745
+ if (options.baseUrl !== void 0) assertAbsoluteHttpUrl(options.baseUrl, "--base-url");
2746
+ const paths = document["paths"];
2747
+ if (!isRecord(paths)) {
2748
+ return { operations, diagnostics };
2749
+ }
2750
+ const rootServers = document["servers"];
2751
+ const rootSecurity = document["security"];
2752
+ for (const [path2, rawPathItem] of Object.entries(paths)) {
2753
+ if (path2.startsWith("x-")) continue;
2754
+ if (!path2.startsWith("/")) {
2755
+ diagnostics.push({
2756
+ severity: "warning",
2757
+ code: "invalid-path-key",
2758
+ operation: path2,
2759
+ message: `Skipped "${path2}": a Paths key must start with "/"`
2760
+ });
2761
+ continue;
2762
+ }
2763
+ const pathItem = dereference(document, rawPathItem).value;
2764
+ if (!isRecord(pathItem)) continue;
2765
+ for (const [key, rawOperation] of Object.entries(pathItem)) {
2766
+ if (NON_OPERATION_KEYS.has(key) || key.startsWith("x-")) continue;
2767
+ const method = SUPPORTED_METHODS[key.toLowerCase()];
2768
+ if (method === void 0) {
2769
+ diagnostics.push({
2770
+ severity: "warning",
2771
+ code: "unsupported-method",
2772
+ operation: `${key.toUpperCase()} ${path2}`,
2773
+ message: `Skipped ${key.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
2774
+ });
2775
+ continue;
2776
+ }
2777
+ const operation = dereference(document, rawOperation).value;
2778
+ if (!isRecord(operation)) continue;
2779
+ const where = `${method} ${path2}`;
2780
+ const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : void 0;
2781
+ const resourceId = toResourceId(operationId, method, path2);
2782
+ const previous = claimed.get(resourceId);
2783
+ if (previous !== void 0) {
2784
+ throw new CommerceError(
2785
+ "CONFIG_INVALID",
2786
+ `Operations "${previous}" and "${where}" both produce the resource id "${resourceId}". Give one of them a distinct operationId - ids are what agents discover, so the importer will not rename either`,
2787
+ { details: { resourceId, operations: [previous, where] } }
2788
+ );
2789
+ }
2790
+ claimed.set(resourceId, where);
2791
+ const server = selectServer(
2792
+ document,
2793
+ [operation["servers"], pathItem["servers"], rootServers],
2794
+ { where, resourceId },
2795
+ options,
2796
+ diagnostics
2797
+ );
2798
+ if (server === void 0) continue;
2799
+ const backendUrl = `${server.replace(/\/+$/, "")}${path2}`;
2800
+ const stray = findUnparsedBraceToken(backendUrl);
2801
+ if (stray !== void 0) {
2802
+ diagnostics.push({
2803
+ severity: "error",
2804
+ code: "unsupported-path-template",
2805
+ operation: resourceId,
2806
+ message: `Skipped ${where}: path template "${stray}" uses characters the gateway cannot substitute (allowed: A-Z a-z 0-9 _ . -)`
2807
+ });
2808
+ continue;
2809
+ }
2810
+ const summary = typeof operation["summary"] === "string" ? operation["summary"] : void 0;
2811
+ const description = typeof operation["description"] === "string" ? operation["description"] : summary;
2812
+ const security = operation["security"] ?? rootSecurity;
2813
+ operations.push({
2814
+ resourceId,
2815
+ method,
2816
+ path: path2,
2817
+ backendUrl,
2818
+ ...operationId !== void 0 ? { operationId } : {},
2819
+ tags: toArray(operation["tags"]).filter((tag) => typeof tag === "string"),
2820
+ name: summary ?? operationId ?? resourceId,
2821
+ ...description !== void 0 ? { description } : {},
2822
+ parameters: [
2823
+ ...toArray(pathItem["parameters"]),
2824
+ // Operation parameters last: Phase 4 lets the later one win, which is
2825
+ // the OpenAPI override rule (same name + in).
2826
+ ...toArray(operation["parameters"])
2827
+ ],
2828
+ ...operation["requestBody"] !== void 0 ? { requestBody: operation["requestBody"] } : {},
2829
+ ...operation["responses"] !== void 0 ? { responses: operation["responses"] } : {},
2830
+ security: toArray(security)
2831
+ });
2832
+ }
2833
+ }
2834
+ return { operations, diagnostics };
2835
+ }
2836
+ function toResourceId(operationId, method, path2) {
2837
+ const fromOperationId = operationId === void 0 ? "" : normaliseId(operationId);
2838
+ if (fromOperationId !== "") return fromOperationId;
2839
+ return normaliseId(`${method.toLowerCase()}_${path2}`);
2840
+ }
2841
+ function normaliseId(value) {
2842
+ return value.replace(ID_ALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MAX_ID_LENGTH);
2843
+ }
2844
+ function selectServer(document, candidates, context, options, diagnostics) {
2845
+ if (options.baseUrl !== void 0) return options.baseUrl;
2846
+ for (const candidate of candidates) {
2847
+ const servers = toArray(candidate);
2848
+ const first = servers.length > 0 ? dereference(document, servers[0]).value : void 0;
2849
+ if (!isRecord(first)) continue;
2850
+ const url = first["url"];
2851
+ if (typeof url !== "string" || url === "") continue;
2852
+ const substituted = substituteServerVariables(url, first["variables"], context, diagnostics);
2853
+ if (substituted === void 0) return void 0;
2854
+ if (!isAbsoluteHttpUrl(substituted)) {
2855
+ diagnostics.push({
2856
+ severity: "error",
2857
+ code: "relative-server-url",
2858
+ operation: context.resourceId,
2859
+ message: `Skipped ${context.where}: server URL "${substituted}" is relative, so no backend host is known. Pass --base-url`
2860
+ });
2861
+ return void 0;
2862
+ }
2863
+ return substituted;
2864
+ }
2865
+ diagnostics.push({
2866
+ severity: "error",
2867
+ code: "no-server-url",
2868
+ operation: context.resourceId,
2869
+ message: `Skipped ${context.where}: the document declares no server URL. Pass --base-url`
2870
+ });
2871
+ return void 0;
2872
+ }
2873
+ function substituteServerVariables(url, variables, context, diagnostics) {
2874
+ if (!url.includes("{")) return url;
2875
+ const declared = isRecord(variables) ? variables : {};
2876
+ const used = [];
2877
+ let missing;
2878
+ const substituted = url.replace(/\{([^{}]*)\}/g, (match, name) => {
2879
+ const variable = declared[name];
2880
+ const fallback = isRecord(variable) ? variable["default"] : void 0;
2881
+ if (typeof fallback !== "string") {
2882
+ missing = name;
2883
+ return match;
2884
+ }
2885
+ used.push(`${name}=${fallback}`);
2886
+ return fallback;
2887
+ });
2888
+ if (missing !== void 0) {
2889
+ diagnostics.push({
2890
+ severity: "error",
2891
+ code: "server-variable-without-default",
2892
+ operation: context.resourceId,
2893
+ message: `Skipped ${context.where}: server variable "{${missing}}" has no default value. Pass --base-url`
2894
+ });
2895
+ return void 0;
2896
+ }
2897
+ if (used.length > 0) {
2898
+ diagnostics.push({
2899
+ severity: "warning",
2900
+ code: "server-variable-default",
2901
+ operation: context.resourceId,
2902
+ message: `Server URL for ${context.where} uses declared defaults (${used.join(", ")}); override with --base-url if that is not the deployment you mean`
2903
+ });
2904
+ }
2905
+ return substituted;
2906
+ }
2907
+ function isAbsoluteHttpUrl(value) {
2908
+ try {
2909
+ const parsed = new URL(value);
2910
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
2911
+ return parsed.search === "" && parsed.hash === "";
2912
+ } catch {
2913
+ return false;
2914
+ }
2915
+ }
2916
+ function assertAbsoluteHttpUrl(value, label) {
2917
+ if (!isAbsoluteHttpUrl(value)) {
2918
+ throw new CommerceError(
2919
+ "CONFIG_INVALID",
2920
+ `${label} "${value}" must be an absolute http:// or https:// URL with no query string or fragment`,
2921
+ { details: { value } }
2922
+ );
2923
+ }
2924
+ }
2925
+ function isRecord(value) {
2926
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2927
+ }
2928
+ function toArray(value) {
2929
+ return Array.isArray(value) ? value : [];
2930
+ }
2931
+
2932
+ // src/openapi/request.ts
2933
+ init_execution();
2934
+
2935
+ // src/openapi/schema.ts
2936
+ init_errors();
2937
+ var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
2938
+ "object",
2939
+ "string",
2940
+ "number",
2941
+ "integer",
2942
+ "boolean",
2943
+ "array",
2944
+ "null"
2945
+ ]);
2946
+ var METADATA_KEYWORDS = ["title", "description", "default", "example", "examples", "deprecated"];
2947
+ var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
2948
+ "xml",
2949
+ "externalDocs",
2950
+ "readOnly",
2951
+ "writeOnly",
2952
+ "$schema",
2953
+ "$id",
2954
+ "$comment",
2955
+ "$defs",
2956
+ "definitions"
2957
+ ]);
2958
+ function isPrimitiveSchema(schema) {
2959
+ const types = typeList2(schema["type"]);
2960
+ if (types === void 0) return false;
2961
+ return types.every((type) => type !== "object" && type !== "array" && SUPPORTED_TYPES.has(type));
2962
+ }
2963
+ function convertSchema(document, node, stack = []) {
2964
+ const dropped = /* @__PURE__ */ new Set();
2965
+ try {
2966
+ const schema = convertNode(document, node, stack, dropped);
2967
+ return schema === void 0 ? { supported: false, reason: "schema could not be represented" } : { supported: true, schema, dropped: [...dropped] };
2968
+ } catch (error) {
2969
+ if (error instanceof UnsupportedSchema) return { supported: false, reason: error.message };
2970
+ if (isCommerceError(error)) return { supported: false, reason: error.message };
2971
+ throw error;
2972
+ }
2973
+ }
2974
+ var UnsupportedSchema = class extends Error {
2975
+ };
2976
+ function convertNode(document, node, stack, dropped) {
2977
+ if (node === true) return {};
2978
+ if (node === false) throw new UnsupportedSchema("schema is `false`, which accepts no value");
2979
+ const resolved = dereference(document, node, stack);
2980
+ const source = resolved.value;
2981
+ if (typeof source !== "object" || source === null || Array.isArray(source)) {
2982
+ throw new UnsupportedSchema("schema node is not an object");
2983
+ }
2984
+ const schemaNode = source;
2985
+ if (Array.isArray(schemaNode["allOf"])) {
2986
+ return mergeAllOf(document, schemaNode, resolved.stack, dropped);
2987
+ }
2988
+ for (const keyword of ["oneOf", "anyOf", "not", "discriminator"]) {
2989
+ if (Object.hasOwn(schemaNode, keyword)) {
2990
+ throw new UnsupportedSchema(
2991
+ `schema uses "${keyword}", which this gateway cannot enforce - accepting it would advertise validation that never runs`
2992
+ );
2993
+ }
2994
+ }
2995
+ const result = {};
2996
+ const types = resolveTypes(schemaNode);
2997
+ if (types !== void 0) result["type"] = types.length === 1 ? types[0] : types;
2998
+ for (const keyword of METADATA_KEYWORDS) {
2999
+ if (Object.hasOwn(schemaNode, keyword)) result[keyword] = schemaNode[keyword];
3000
+ }
3001
+ if (Array.isArray(schemaNode["enum"])) result["enum"] = [...schemaNode["enum"]];
3002
+ const properties = schemaNode["properties"];
3003
+ if (isRecord2(properties)) {
3004
+ const converted = {};
3005
+ for (const [name, sub] of Object.entries(properties)) {
3006
+ const child = convertNode(document, sub, resolved.stack, dropped);
3007
+ if (child !== void 0) converted[name] = child;
3008
+ }
3009
+ result["properties"] = converted;
3010
+ }
3011
+ const required = schemaNode["required"];
3012
+ if (Array.isArray(required)) {
3013
+ const names = required.filter((name) => typeof name === "string");
3014
+ if (names.length > 0) result["required"] = names;
3015
+ }
3016
+ const additional = schemaNode["additionalProperties"];
3017
+ if (typeof additional === "boolean") {
3018
+ result["additionalProperties"] = additional;
3019
+ } else if (additional !== void 0) {
3020
+ const child = convertNode(document, additional, resolved.stack, dropped);
3021
+ if (child !== void 0) result["additionalProperties"] = child;
3022
+ } else if (isRecord2(properties)) {
3023
+ result["additionalProperties"] = false;
3024
+ }
3025
+ const items = schemaNode["items"];
3026
+ if (Array.isArray(items)) {
3027
+ dropped.add("items (tuple form)");
3028
+ } else if (items !== void 0) {
3029
+ const child = convertNode(document, items, resolved.stack, dropped);
3030
+ if (child !== void 0) result["items"] = child;
3031
+ }
3032
+ for (const keyword of Object.keys(schemaNode)) {
3033
+ if (isCarriedKeyword(keyword) || ANNOTATION_KEYWORDS.has(keyword) || keyword.startsWith("x-")) {
3034
+ continue;
3035
+ }
3036
+ dropped.add(keyword);
3037
+ }
3038
+ return result;
3039
+ }
3040
+ function isCarriedKeyword(keyword) {
3041
+ return keyword === "type" || keyword === "properties" || keyword === "required" || keyword === "additionalProperties" || keyword === "enum" || keyword === "items" || keyword === "nullable" || keyword === "$ref" || METADATA_KEYWORDS.includes(keyword);
3042
+ }
3043
+ function resolveTypes(schema) {
3044
+ const declared = typeList2(schema["type"]);
3045
+ if (declared === void 0) return void 0;
3046
+ const known = declared.filter((type) => SUPPORTED_TYPES.has(type));
3047
+ if (known.length === 0) {
3048
+ throw new UnsupportedSchema(`schema type "${String(schema["type"])}" is not a JSON type`);
3049
+ }
3050
+ if (schema["nullable"] === true && !known.includes("null")) known.push("null");
3051
+ return known;
3052
+ }
3053
+ function typeList2(raw) {
3054
+ if (typeof raw === "string") return [raw];
3055
+ if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
3056
+ return raw;
3057
+ }
3058
+ return void 0;
3059
+ }
3060
+ function mergeAllOf(document, schema, stack, dropped) {
3061
+ const branches = schema["allOf"];
3062
+ const siblings = { ...schema };
3063
+ delete siblings["allOf"];
3064
+ const merged = { type: "object", properties: {}, required: [] };
3065
+ const properties = merged["properties"];
3066
+ const required = /* @__PURE__ */ new Set();
3067
+ const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
3068
+ for (const branch of parts) {
3069
+ const converted = convertNode(document, branch, stack, dropped);
3070
+ if (converted === void 0) throw new UnsupportedSchema("allOf branch is empty");
3071
+ const types = typeList2(converted["type"]);
3072
+ if (types !== void 0 && !types.includes("object")) {
3073
+ throw new UnsupportedSchema("allOf mixes object and non-object schemas");
3074
+ }
3075
+ const branchProperties = converted["properties"];
3076
+ if (isRecord2(branchProperties)) {
3077
+ for (const [name, sub] of Object.entries(branchProperties)) {
3078
+ const existing = properties[name];
3079
+ if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(sub)) {
3080
+ throw new UnsupportedSchema(
3081
+ `allOf branches declare property "${name}" differently, which cannot be merged`
3082
+ );
3083
+ }
3084
+ properties[name] = sub;
3085
+ }
3086
+ }
3087
+ for (const name of Array.isArray(converted["required"]) ? converted["required"] : []) {
3088
+ if (typeof name === "string") required.add(name);
3089
+ }
3090
+ for (const keyword of ["enum", "items"]) {
3091
+ if (Object.hasOwn(converted, keyword)) {
3092
+ throw new UnsupportedSchema(`allOf branch uses "${keyword}", which cannot be merged`);
3093
+ }
3094
+ }
3095
+ if (typeof converted["description"] === "string" && merged["description"] === void 0) {
3096
+ merged["description"] = converted["description"];
3097
+ }
3098
+ }
3099
+ if (required.size > 0) merged["required"] = [...required];
3100
+ else delete merged["required"];
3101
+ merged["additionalProperties"] = false;
3102
+ return merged;
3103
+ }
3104
+ function isRecord2(value) {
3105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3106
+ }
3107
+
3108
+ // src/openapi/request.ts
3109
+ var PATH_GROUP = "path";
3110
+ var QUERY_GROUP = "query";
3111
+ var BODY_GROUP = "body";
3112
+ var IGNORED_HEADER_NAMES = /* @__PURE__ */ new Set(["accept", "content-type", "authorization"]);
3113
+ var UNSUPPORTED_QUERY_STYLES = /* @__PURE__ */ new Set(["deepObject", "spaceDelimited", "pipeDelimited"]);
3114
+ function mapRequest(loaded, candidate) {
3115
+ const { document } = loaded;
3116
+ const diagnostics = [];
3117
+ const dropped = /* @__PURE__ */ new Set();
3118
+ const operation = candidate.resourceId;
3119
+ const warn = (code, message) => {
3120
+ diagnostics.push({ severity: "warning", code, operation, message });
3121
+ };
3122
+ const skip = (code, message) => {
3123
+ diagnostics.push({
3124
+ severity: "error",
3125
+ code,
3126
+ operation,
3127
+ message: `Skipped ${candidate.method} ${candidate.path}: ${message}`
3128
+ });
3129
+ return { supported: false, diagnostics };
3130
+ };
3131
+ const pathProperties = {};
3132
+ const queryProperties = {};
3133
+ const requiredQuery = [];
3134
+ const templateParams = new Set(extractPathParameterNames(candidate.path));
3135
+ for (const parameter of mergeParameters(document, candidate.parameters)) {
3136
+ const name = typeof parameter["name"] === "string" ? parameter["name"] : void 0;
3137
+ const location = typeof parameter["in"] === "string" ? parameter["in"] : void 0;
3138
+ if (name === void 0 || location === void 0) continue;
3139
+ const required2 = parameter["required"] === true || location === "path";
3140
+ const label = `${location} parameter "${name}"`;
3141
+ if (location === "header" || location === "cookie") {
3142
+ if (location === "header" && IGNORED_HEADER_NAMES.has(name.toLowerCase())) continue;
3143
+ if (required2) {
3144
+ return skip(
3145
+ "unsupported-required-parameter",
3146
+ `${label} is required, and ${location} parameters are operator configuration in this release, not agent input`
3147
+ );
3148
+ }
3149
+ warn(
3150
+ "unsupported-optional-parameter",
3151
+ `Omitted optional ${label}: ${location} parameters are not imported. Configure it under backend.headers if the API needs it`
3152
+ );
3153
+ continue;
3154
+ }
3155
+ if (location !== "path" && location !== "query") {
3156
+ if (required2) return skip("unsupported-required-parameter", `${label} has unknown location`);
3157
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: unknown location`);
3158
+ continue;
3159
+ }
3160
+ const unsupported = describeUnsupportedParameter(document, parameter, location);
3161
+ if (unsupported !== void 0) {
3162
+ if (required2) return skip("unsupported-required-parameter", `${label} ${unsupported}`);
3163
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${unsupported}`);
3164
+ continue;
3165
+ }
3166
+ const converted = convertSchema(document, parameter["schema"]);
3167
+ if (!converted.supported) {
3168
+ if (required2) return skip("unsupported-required-parameter", `${label} ${converted.reason}`);
3169
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: ${converted.reason}`);
3170
+ continue;
3171
+ }
3172
+ for (const keyword of converted.dropped) dropped.add(keyword);
3173
+ if (location === "path") {
3174
+ if (!templateParams.has(name)) {
3175
+ warn(
3176
+ "path-parameter-not-in-template",
3177
+ `Ignored path parameter "${name}": it does not appear in "${candidate.path}"`
3178
+ );
3179
+ continue;
3180
+ }
3181
+ pathProperties[name] = converted.schema;
3182
+ } else {
3183
+ queryProperties[name] = converted.schema;
3184
+ if (required2) requiredQuery.push(name);
3185
+ }
3186
+ }
3187
+ for (const param of templateParams) {
3188
+ if (!Object.hasOwn(pathProperties, param)) {
3189
+ return skip(
3190
+ "undeclared-path-parameter",
3191
+ `"{${param}}" appears in the path but is not declared as a path parameter, so a caller could never supply it`
3192
+ );
3193
+ }
3194
+ }
3195
+ const body = resolveBody(document, candidate);
3196
+ if (body.kind === "unsupported") {
3197
+ if (body.required) return skip("unsupported-request-body", body.reason);
3198
+ warn("unsupported-request-body", `Omitted the request body: ${body.reason}`);
3199
+ }
3200
+ if (body.kind === "schema") {
3201
+ for (const keyword of body.dropped) dropped.add(keyword);
3202
+ }
3203
+ const properties = {};
3204
+ const required = [];
3205
+ const bindings = {};
3206
+ if (Object.keys(pathProperties).length > 0) {
3207
+ properties[PATH_GROUP] = closedObject(pathProperties, Object.keys(pathProperties));
3208
+ required.push(PATH_GROUP);
3209
+ bindings.path = PATH_GROUP;
3210
+ }
3211
+ if (Object.keys(queryProperties).length > 0) {
3212
+ properties[QUERY_GROUP] = closedObject(queryProperties, requiredQuery);
3213
+ if (requiredQuery.length > 0) required.push(QUERY_GROUP);
3214
+ bindings.query = QUERY_GROUP;
3215
+ }
3216
+ if (body.kind === "schema") {
3217
+ properties[BODY_GROUP] = body.schema;
3218
+ if (body.required) required.push(BODY_GROUP);
3219
+ bindings.body = BODY_GROUP;
3220
+ }
3221
+ return {
3222
+ supported: true,
3223
+ inputSchema: {
3224
+ type: "object",
3225
+ properties,
3226
+ ...required.length > 0 ? { required } : {},
3227
+ additionalProperties: false
3228
+ },
3229
+ inputBindings: bindings,
3230
+ ...body.kind === "schema" && body.contentType !== void 0 ? { contentType: body.contentType } : {},
3231
+ droppedKeywords: [...dropped],
3232
+ diagnostics
3233
+ };
3234
+ }
3235
+ function mergeParameters(document, parameters) {
3236
+ const byIdentity = /* @__PURE__ */ new Map();
3237
+ for (const raw of parameters) {
3238
+ const resolved = dereference(document, raw).value;
3239
+ if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) continue;
3240
+ const parameter = resolved;
3241
+ byIdentity.set(`${String(parameter["in"])}:${String(parameter["name"])}`, parameter);
3242
+ }
3243
+ return [...byIdentity.values()];
3244
+ }
3245
+ function describeUnsupportedParameter(document, parameter, location) {
3246
+ if (parameter["content"] !== void 0) {
3247
+ return "uses the `content` form, whose media-type serialization the gateway does not perform";
3248
+ }
3249
+ const style = parameter["style"];
3250
+ if (location === "query" && typeof style === "string" && UNSUPPORTED_QUERY_STYLES.has(style)) {
3251
+ return `uses style "${style}", which the gateway does not serialize`;
3252
+ }
3253
+ if (location === "path" && typeof style === "string" && style !== "simple") {
3254
+ return `uses style "${style}"; the gateway substitutes plain values only`;
3255
+ }
3256
+ const converted = convertSchema(document, parameter["schema"]);
3257
+ if (!converted.supported) return converted.reason;
3258
+ if (!isPrimitiveSchema(converted.schema)) {
3259
+ return "is not a primitive; object and array parameters need serialization the gateway does not perform";
3260
+ }
3261
+ return void 0;
3262
+ }
3263
+ function resolveBody(document, candidate) {
3264
+ if (candidate.requestBody === void 0) return { kind: "none" };
3265
+ const resolved = dereference(document, candidate.requestBody).value;
3266
+ if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) {
3267
+ return { kind: "none" };
3268
+ }
3269
+ const requestBody = resolved;
3270
+ const required = requestBody["required"] === true;
3271
+ if (candidate.method === "GET" || candidate.method === "DELETE") {
3272
+ return {
3273
+ kind: "unsupported",
3274
+ required: false,
3275
+ reason: `a ${candidate.method} request body is not sent by the gateway`
3276
+ };
3277
+ }
3278
+ const content = requestBody["content"];
3279
+ if (!isRecord3(content)) return { kind: "none" };
3280
+ const mediaType = pickJsonMediaType(Object.keys(content));
3281
+ if (mediaType === void 0) {
3282
+ return {
3283
+ kind: "unsupported",
3284
+ required,
3285
+ 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`
3286
+ };
3287
+ }
3288
+ const media = content[mediaType];
3289
+ const schemaNode = isRecord3(media) ? media["schema"] : void 0;
3290
+ if (schemaNode === void 0) {
3291
+ return {
3292
+ kind: "schema",
3293
+ schema: { type: "object", additionalProperties: true },
3294
+ required,
3295
+ ...mediaType === "application/json" ? {} : { contentType: mediaType },
3296
+ dropped: []
3297
+ };
3298
+ }
3299
+ const converted = convertSchema(document, schemaNode);
3300
+ if (!converted.supported) return { kind: "unsupported", required, reason: converted.reason };
3301
+ return {
3302
+ kind: "schema",
3303
+ schema: converted.schema,
3304
+ required,
3305
+ ...mediaType === "application/json" ? {} : { contentType: mediaType },
3306
+ dropped: converted.dropped
3307
+ };
3308
+ }
3309
+ function pickJsonMediaType(keys) {
3310
+ const normalised = keys.map((key) => ({ key, type: key.split(";")[0]?.trim().toLowerCase() }));
3311
+ const exact = normalised.find((entry) => entry.type === "application/json");
3312
+ if (exact !== void 0) return exact.key;
3313
+ return [...normalised].sort((a, b) => (a.type ?? "").localeCompare(b.type ?? "")).find((entry) => entry.type?.startsWith("application/") && entry.type.endsWith("+json"))?.key;
3314
+ }
3315
+ function closedObject(properties, required) {
3316
+ return {
3317
+ type: "object",
3318
+ properties,
3319
+ ...required.length > 0 ? { required: [...required] } : {},
3320
+ additionalProperties: false
3321
+ };
3322
+ }
3323
+ function isRecord3(value) {
3324
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3325
+ }
3326
+
3327
+ // src/openapi/draft.ts
3328
+ function buildResourceDrafts(loaded, options = {}) {
3329
+ const discovery = discoverOperations(
3330
+ loaded,
3331
+ options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
3332
+ );
3333
+ const diagnostics = [...discovery.diagnostics];
3334
+ const drafts = [];
3335
+ const wanted = options.include?.operationIds;
3336
+ const wantedTags = options.include?.tags;
3337
+ const matched = /* @__PURE__ */ new Set();
3338
+ for (const candidate of discovery.operations) {
3339
+ if (wanted !== void 0 && !selects(wanted, candidate)) continue;
3340
+ if (wantedTags !== void 0 && !candidate.tags.some((tag) => wantedTags.includes(tag))) {
3341
+ continue;
3342
+ }
3343
+ if (wanted !== void 0) {
3344
+ for (const id of wanted) if (selects([id], candidate)) matched.add(id);
3345
+ }
3346
+ const mapping = mapRequest(loaded, candidate);
3347
+ diagnostics.push(...mapping.diagnostics);
3348
+ if (!mapping.supported) continue;
3349
+ const dropped = new Set(mapping.droppedKeywords);
3350
+ const output = selectOutputSchema(loaded.document, candidate, diagnostics, dropped);
3351
+ if (dropped.size > 0) {
3352
+ diagnostics.push({
3353
+ severity: "warning",
3354
+ code: "unenforced-schema-constraints",
3355
+ operation: candidate.resourceId,
3356
+ message: `${candidate.resourceId}: dropped ${[...dropped].join(", ")} - this gateway validates structure only, and keeping them would advertise checks it never performs`
3357
+ });
3358
+ }
3359
+ const review = [];
3360
+ if (declaresSecurity(candidate)) {
3361
+ diagnostics.push({
3362
+ severity: "warning",
3363
+ code: "backend-authentication-required",
3364
+ operation: candidate.resourceId,
3365
+ message: `${candidate.resourceId} declares backend authentication. OpenAPI credentials were not imported. Configure backend.headers with environment placeholders before enabling the resource`
3366
+ });
3367
+ review.push(
3368
+ "This operation declares backend authentication. No credential was imported.",
3369
+ "Add it under backend.headers with an ${ENV_VAR} placeholder before enabling."
3370
+ );
3371
+ }
3372
+ if (options.policy?.pricing === void 0 || options.policy.expose === void 0) {
3373
+ review.push(
3374
+ "REVIEW: pricing and exposure are not inferred from OpenAPI. Add e.g.",
3375
+ ' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC }',
3376
+ " expose: [http] # http | mcp | a2a"
3377
+ );
3378
+ }
3379
+ drafts.push({
3380
+ id: candidate.resourceId,
3381
+ ...candidate.operationId !== void 0 ? { operationId: candidate.operationId } : {},
3382
+ tags: candidate.tags,
3383
+ source: `${candidate.method} ${candidate.path}`,
3384
+ resource: {
3385
+ name: candidate.name,
3386
+ ...candidate.description !== void 0 ? { description: candidate.description } : {},
3387
+ input: mapping.inputSchema,
3388
+ ...output !== void 0 ? { output } : {},
3389
+ backend: {
3390
+ type: "http",
3391
+ method: candidate.method,
3392
+ url: candidate.backendUrl,
3393
+ ...mapping.contentType !== void 0 ? { headers: { "Content-Type": mapping.contentType } } : {},
3394
+ ...Object.keys(mapping.inputBindings).length > 0 ? { inputBindings: mapping.inputBindings } : {}
3395
+ },
3396
+ // Policy is written only when the operator asked for it. An absent
3397
+ // `pricing`/`expose` is what makes the draft fail config validation
3398
+ // until a human has decided.
3399
+ ...options.policy?.pricing !== void 0 ? { pricing: options.policy.pricing } : {},
3400
+ ...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {},
3401
+ ...options.policy?.payments !== void 0 ? { payments: [...options.policy.payments] } : {}
3402
+ },
3403
+ review
3404
+ });
3405
+ }
3406
+ return {
3407
+ version: loaded.version,
3408
+ sourcePath: loaded.sourcePath,
3409
+ drafts,
3410
+ diagnostics,
3411
+ unmatchedOperationIds: (wanted ?? []).filter((id) => !matched.has(id))
3412
+ };
3413
+ }
3414
+ function selects(ids, candidate) {
3415
+ return ids.includes(candidate.resourceId) || candidate.operationId !== void 0 && ids.includes(candidate.operationId);
3416
+ }
3417
+ function declaresSecurity(candidate) {
3418
+ return candidate.security.some(
3419
+ (requirement) => typeof requirement === "object" && requirement !== null && Object.keys(requirement).length > 0
3420
+ );
3421
+ }
3422
+ function selectOutputSchema(document, candidate, diagnostics, dropped) {
3423
+ const responses = candidate.responses;
3424
+ if (!isRecord4(responses)) return void 0;
3425
+ const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
3426
+ if (successes.length === 0) return void 0;
3427
+ const withBody = successes.filter((status) => {
3428
+ const response2 = responses[status];
3429
+ const content2 = isRecord4(response2) ? response2["content"] : void 0;
3430
+ return isRecord4(content2) && pickJsonMediaType(Object.keys(content2)) !== void 0;
3431
+ });
3432
+ const chosen = withBody[0];
3433
+ if (chosen === void 0) return void 0;
3434
+ const response = responses[chosen];
3435
+ const content = response["content"];
3436
+ const mediaType = pickJsonMediaType(Object.keys(content));
3437
+ const media = content[mediaType];
3438
+ const schemaNode = isRecord4(media) ? media["schema"] : void 0;
3439
+ if (schemaNode === void 0) return void 0;
3440
+ const converted = convertSchema(document, schemaNode);
3441
+ if (!converted.supported) {
3442
+ diagnostics.push({
3443
+ severity: "warning",
3444
+ code: "unsupported-output-schema",
3445
+ operation: candidate.resourceId,
3446
+ message: `${candidate.resourceId}: omitted the output schema (${converted.reason})`
3447
+ });
3448
+ return void 0;
3449
+ }
3450
+ for (const keyword of converted.dropped) dropped.add(keyword);
3451
+ if (withBody.length > 1) {
3452
+ diagnostics.push({
3453
+ severity: "warning",
3454
+ code: "multiple-success-responses",
3455
+ operation: candidate.resourceId,
3456
+ message: `${candidate.resourceId}: responses ${withBody.join(", ")} all carry a JSON body; used ${chosen}`
3457
+ });
3458
+ }
3459
+ return converted.schema;
3460
+ }
3461
+ function rank(status) {
3462
+ const preferred = ["200", "201", "202"].indexOf(status);
3463
+ return preferred === -1 ? 100 + Number(status) : preferred;
3464
+ }
3465
+ function renderResourcesYaml(result) {
3466
+ const resources = {};
3467
+ for (const draft of result.drafts) resources[draft.id] = draft.resource;
3468
+ const doc = new Document({ resources });
3469
+ doc.commentBefore = [
3470
+ " Generated by agent-commerce import openapi. Review before use.",
3471
+ ` Source: ${basename(result.sourcePath)} (OpenAPI ${result.version})`,
3472
+ " Merge the resources below into config.yaml once pricing, exposure and",
3473
+ " any backend authentication have been decided."
3474
+ ].join("\n");
3475
+ const map = doc.getIn(["resources"], true);
3476
+ map?.items.forEach((item, index) => {
3477
+ const draft = result.drafts[index];
3478
+ const key = item.key;
3479
+ if (draft === void 0 || draft.review.length === 0 || key === null) return;
3480
+ key.commentBefore = draft.review.map((line) => ` ${line}`).join("\n");
3481
+ });
3482
+ return doc.toString({ lineWidth: 0 });
3483
+ }
3484
+ function basename(path2) {
3485
+ return path2.split(/[\\/]/).pop() ?? path2;
3486
+ }
3487
+ function isRecord4(value) {
3488
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3489
+ }
3490
+
3491
+ // src/openapi/load.ts
3492
+ init_errors();
3493
+ var MAX_SOURCE_BYTES = 10 * 1024 * 1024;
3494
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
3495
+ var SUPPORTED_VERSIONS = /* @__PURE__ */ new Set(["3.0", "3.1", "3.2"]);
3496
+ function invalid2(message, details, cause) {
3497
+ return new CommerceError("CONFIG_INVALID", message, {
3498
+ details,
3499
+ ...cause !== void 0 ? { cause } : {}
3500
+ });
3501
+ }
3502
+ async function loadOpenApiDocument(sourcePath) {
3503
+ const details = { sourcePath };
3504
+ let stats;
3505
+ try {
3506
+ stats = await stat(sourcePath);
3507
+ } catch (error) {
3508
+ throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
3509
+ }
3510
+ if (stats.isDirectory()) {
3511
+ throw invalid2(`"${sourcePath}" is a directory, not an OpenAPI document`, details);
3512
+ }
3513
+ if (stats.size > MAX_SOURCE_BYTES) {
3514
+ throw invalid2(
3515
+ `OpenAPI document "${sourcePath}" is ${stats.size} bytes, over the ${MAX_SOURCE_BYTES}-byte limit`,
3516
+ { ...details, size: stats.size, maxBytes: MAX_SOURCE_BYTES }
3517
+ );
3518
+ }
3519
+ const extension = extname(sourcePath).toLowerCase();
3520
+ if (!SUPPORTED_EXTENSIONS.has(extension)) {
3521
+ throw invalid2(
3522
+ `OpenAPI document "${sourcePath}" has an unsupported extension "${extension || "(none)"}". Supported: .yaml, .yml, .json`,
3523
+ details
3524
+ );
3525
+ }
3526
+ const source = await readFile(sourcePath, "utf8");
3527
+ if (source.trim() === "") {
3528
+ throw invalid2(`OpenAPI document "${sourcePath}" is empty`, details);
3529
+ }
3530
+ let parsed;
3531
+ try {
3532
+ parsed = extension === ".json" ? JSON.parse(source) : parse(source);
3533
+ } catch (error) {
3534
+ throw invalid2(
3535
+ `OpenAPI document "${sourcePath}" is not valid ${extension === ".json" ? "JSON" : "YAML"}: ${error instanceof Error ? error.message : String(error)}`,
3536
+ details,
3537
+ error
3538
+ );
3539
+ }
3540
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3541
+ throw invalid2(`OpenAPI document "${sourcePath}" is not an object`, details);
3542
+ }
3543
+ const document = parsed;
3544
+ const version = readVersion(document, sourcePath);
3545
+ rejectExternalReferences(document, sourcePath);
3546
+ const result = await validate(document);
3547
+ if (!result.valid) {
3548
+ const first = result.errors?.[0];
3549
+ throw invalid2(
3550
+ `OpenAPI document "${sourcePath}" is not a valid OpenAPI ${version} description: ${first ? `${first.message}${first.path ? ` (at ${first.path})` : ""}` : "unknown validation error"}`,
3551
+ { ...details, errors: result.errors?.slice(0, 10) ?? [] }
3552
+ );
3553
+ }
3554
+ return { version, document, sourcePath };
3555
+ }
3556
+ function readVersion(document, sourcePath) {
3557
+ const details = { sourcePath };
3558
+ if (typeof document["swagger"] === "string") {
3559
+ throw invalid2(
3560
+ `"${sourcePath}" is a Swagger ${document["swagger"]} document. Only OpenAPI 3.0, 3.1 and 3.2 are supported - convert it first`,
3561
+ details
3562
+ );
3563
+ }
3564
+ const declared = document["openapi"];
3565
+ if (typeof declared !== "string") {
3566
+ throw invalid2(`"${sourcePath}" has no "openapi" version field`, details);
3567
+ }
3568
+ const featureVersion = /^(\d+\.\d+)(?:\.|$)/.exec(declared)?.[1];
3569
+ if (featureVersion === void 0 || !SUPPORTED_VERSIONS.has(featureVersion)) {
3570
+ throw invalid2(
3571
+ `OpenAPI version "${declared}" is not supported. Supported: 3.0.x, 3.1.x, 3.2.x`,
3572
+ { ...details, version: declared }
3573
+ );
3574
+ }
3575
+ return featureVersion;
3576
+ }
3577
+ function rejectExternalReferences(document, sourcePath) {
3578
+ const seen = /* @__PURE__ */ new WeakSet();
3579
+ const queue = [document];
3580
+ while (queue.length > 0) {
3581
+ const node = queue.pop();
3582
+ if (typeof node !== "object" || node === null) continue;
3583
+ if (seen.has(node)) continue;
3584
+ seen.add(node);
3585
+ if (Array.isArray(node)) {
3586
+ queue.push(...node);
3587
+ continue;
3588
+ }
3589
+ for (const [key, value] of Object.entries(node)) {
3590
+ if (key === "$ref" && typeof value === "string" && !value.startsWith("#")) {
3591
+ throw invalid2(
3592
+ `OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
3593
+ { sourcePath, ref: value }
3594
+ );
3595
+ }
3596
+ queue.push(value);
3597
+ }
3598
+ }
3599
+ }
3600
+
3601
+ // src/cli/commands/import-openapi.ts
3602
+ function defaultOutputPath(source) {
3603
+ const name = basename$1(source);
3604
+ const stem = name.slice(0, name.length - extname(name).length) || name;
3605
+ return `${stem}.agent-commerce.yaml`;
3606
+ }
3607
+ async function runImportOpenApi(options, io, deps = {}) {
3608
+ const fileExists = deps.fileExists ?? existsSync;
3609
+ const outputPath = options.output ?? defaultOutputPath(options.source);
3610
+ let policy;
3611
+ try {
3612
+ policy = buildPolicy(options);
3613
+ } catch (error) {
3614
+ io.stderr(`FAIL ${error instanceof Error ? error.message : String(error)}`);
3615
+ return 1;
3616
+ }
3617
+ if (fileExists(outputPath) && options.force !== true) {
3618
+ io.stderr(`FAIL ${outputPath} already exists. Re-run with --force to overwrite.`);
3619
+ return 1;
3620
+ }
3621
+ let result;
3622
+ try {
3623
+ const loaded = await loadOpenApiDocument(options.source);
3624
+ result = buildResourceDrafts(loaded, {
3625
+ ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
3626
+ ...Object.keys(policy).length > 0 ? { policy } : {},
3627
+ ...options.operations !== void 0 || options.tags !== void 0 ? {
3628
+ include: {
3629
+ ...options.operations !== void 0 ? { operationIds: options.operations } : {},
3630
+ ...options.tags !== void 0 ? { tags: options.tags } : {}
3631
+ }
3632
+ } : {}
3633
+ });
3634
+ } catch (error) {
3635
+ io.stderr(`FAIL ${error instanceof Error ? error.message : String(error)}`);
3636
+ return 1;
3637
+ }
3638
+ const skipped = result.diagnostics.filter((entry) => entry.severity === "error");
3639
+ const warnings = result.diagnostics.filter((entry) => entry.severity === "warning");
3640
+ const failures = [];
3641
+ if (result.unmatchedOperationIds.length > 0) {
3642
+ failures.push(`no operation matched ${result.unmatchedOperationIds.join(", ")}`);
3643
+ }
3644
+ if (result.drafts.length === 0) {
3645
+ failures.push("no supported operations were imported");
3646
+ }
3647
+ if (options.strict === true && warnings.length > 0) {
3648
+ failures.push(`${warnings.length} warning(s) with --strict`);
3649
+ }
3650
+ const wrote = failures.length === 0;
3651
+ if (wrote) {
3652
+ try {
3653
+ await writeAtomically(outputPath, renderResourcesYaml(result), deps);
3654
+ } catch (error) {
3655
+ io.stderr(`FAIL ${outputPath} could not be written: ${describe(error)}`);
3656
+ return 1;
3657
+ }
3658
+ }
3659
+ if (options.json === true) {
3660
+ io.stdout(
3661
+ JSON.stringify(
3662
+ {
3663
+ source: options.source,
3664
+ openapi: result.version,
3665
+ output: wrote ? outputPath : null,
3666
+ imported: result.drafts.length,
3667
+ skipped: skipped.length,
3668
+ warnings: warnings.length,
3669
+ resources: result.drafts.map((draft) => ({
3670
+ id: draft.id,
3671
+ source: draft.source,
3672
+ ...draft.operationId !== void 0 ? { operationId: draft.operationId } : {},
3673
+ tags: draft.tags
3674
+ })),
3675
+ diagnostics: result.diagnostics,
3676
+ unmatchedOperationIds: result.unmatchedOperationIds,
3677
+ exitCode: failures.length === 0 ? 0 : 1
3678
+ },
3679
+ null,
3680
+ 2
3681
+ )
3682
+ );
3683
+ } else {
3684
+ printSummary(result, { outputPath, wrote, policy }, io);
3685
+ }
3686
+ for (const failure of failures) io.stderr(`FAIL ${failure}`);
3687
+ return failures.length === 0 ? 0 : 1;
3688
+ }
3689
+ function buildPolicy(options) {
3690
+ const policy = {};
3691
+ if (options.free === true) policy.pricing = { type: "free" };
3692
+ if (options.expose !== void 0) {
3693
+ const requested = options.expose.split(",").map((name) => name.trim()).filter((name) => name !== "");
3694
+ if (requested.length === 0) {
3695
+ throw new Error(`--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`);
3696
+ }
3697
+ for (const name of requested) {
3698
+ if (!PROTOCOL_NAMES.includes(name)) {
3699
+ throw new Error(
3700
+ `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`
3701
+ );
3702
+ }
3703
+ }
3704
+ policy.expose = requested;
3705
+ }
3706
+ return policy;
3707
+ }
3708
+ async function writeAtomically(outputPath, content, deps) {
3709
+ if (deps.writeFile !== void 0) {
3710
+ await deps.writeFile(outputPath, content);
3711
+ return;
3712
+ }
3713
+ const target = resolve(outputPath);
3714
+ const temporary = `${target}.${process.pid}.tmp`;
3715
+ try {
3716
+ await writeFile(temporary, content, "utf8");
3717
+ await rename(temporary, target);
3718
+ } catch (error) {
3719
+ await rm(temporary, { force: true });
3720
+ throw error;
3721
+ }
3722
+ }
3723
+ function printSummary(result, context, io) {
3724
+ const skipped = result.diagnostics.filter((entry) => entry.severity === "error");
3725
+ const warnings = result.diagnostics.filter((entry) => entry.severity === "warning");
3726
+ io.stdout(`OpenAPI ${result.version}: ${result.sourcePath}`);
3727
+ io.stdout("");
3728
+ io.stdout(`Imported: ${result.drafts.length}`);
3729
+ io.stdout(`Skipped: ${skipped.length}`);
3730
+ io.stdout(`Warnings: ${warnings.length}`);
3731
+ if (skipped.length > 0) {
3732
+ io.stdout("");
3733
+ io.stdout("Skipped operations:");
3734
+ for (const entry of skipped) io.stdout(` ${entry.message}`);
3735
+ }
3736
+ if (warnings.length > 0) {
3737
+ io.stdout("");
3738
+ io.stdout("Warnings:");
3739
+ for (const entry of warnings) io.stdout(` ${entry.message}`);
3740
+ }
3741
+ if (context.wrote) {
3742
+ io.stdout("");
3743
+ io.stdout("Generated:");
3744
+ io.stdout(` ${context.outputPath}`);
3745
+ }
3746
+ io.stdout("");
3747
+ if (context.policy.pricing === void 0 || context.policy.expose === void 0) {
3748
+ io.stdout("Pricing/exposure were not inferred.");
3749
+ }
3750
+ io.stdout("Review the generated resources before merging them into config.yaml.");
3751
+ }
3752
+ function describe(error) {
3753
+ return error instanceof Error ? error.message : String(error);
3754
+ }
3755
+
2460
3756
  // src/cli/commands/init.ts
2461
3757
  init_filename();
2462
3758
 
@@ -2505,7 +3801,7 @@ async function runValidate(options, io, deps = {}) {
2505
3801
  io.stdout(` merchant: ${config.merchant.name} (${config.merchant.id})`);
2506
3802
  io.stdout(` resources: ${config.resources.length}`);
2507
3803
  io.stdout(
2508
- ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"}`
3804
+ ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"} a2a=${config.protocols.a2a.enabled ? "on" : "off"}`
2509
3805
  );
2510
3806
  io.stdout(` payments: x402=${config.payments.x402?.enabled === true ? "on" : "off"}`);
2511
3807
  return 0;
@@ -2675,7 +3971,7 @@ var processIo = {
2675
3971
  // src/cli/program.ts
2676
3972
  function buildProgram(io = processIo) {
2677
3973
  const program = new Command();
2678
- program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, validate, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
3974
+ program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, import, validate, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
2679
3975
  writeOut: (str) => io.stdout(str.replace(/\n+$/, "")),
2680
3976
  writeErr: (str) => io.stderr(str.replace(/\n+$/, ""))
2681
3977
  });
@@ -2704,6 +4000,31 @@ function buildProgram(io = processIo) {
2704
4000
  const code = await runInit({ outputPath: opts.output, force: opts.force, yes: opts.yes }, io);
2705
4001
  process.exitCode = code;
2706
4002
  });
4003
+ const importCommand = program.command("import").description("Generate Agent Commerce resource drafts from an API description.");
4004
+ 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(
4005
+ "--operation <operationId>",
4006
+ "import only this operation (repeatable)",
4007
+ collect,
4008
+ void 0
4009
+ ).option("--tag <tag>", "import only operations with this tag (repeatable, OR-ed)", collect).option("--free", "mark generated resources as pricing.type free", false).option("--expose <protocols>", `comma-separated: ${PROTOCOL_NAMES.join(",")}`).option("--strict", "exit non-zero when the import produced warnings", false).option("--json", "emit a machine-readable summary instead of a report", false).action(
4010
+ async (source, opts) => {
4011
+ process.exitCode = await runImportOpenApi(
4012
+ {
4013
+ source,
4014
+ ...opts.output !== void 0 ? { output: opts.output } : {},
4015
+ force: opts.force,
4016
+ ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {},
4017
+ ...opts.operation !== void 0 ? { operations: opts.operation } : {},
4018
+ ...opts.tag !== void 0 ? { tags: opts.tag } : {},
4019
+ free: opts.free,
4020
+ ...opts.expose !== void 0 ? { expose: opts.expose } : {},
4021
+ strict: opts.strict,
4022
+ json: opts.json
4023
+ },
4024
+ io
4025
+ );
4026
+ }
4027
+ );
2707
4028
  program.command("demo").description(
2708
4029
  "Run the documented docker compose + demo agent quickstart, verifying health between steps."
2709
4030
  ).action(async () => {
@@ -2711,6 +4032,9 @@ function buildProgram(io = processIo) {
2711
4032
  });
2712
4033
  return program;
2713
4034
  }
4035
+ function collect(value, previous) {
4036
+ return [...previous ?? [], value];
4037
+ }
2714
4038
 
2715
4039
  // src/cli/index.ts
2716
4040
  for (const stream of [process.stdout, process.stderr]) {