@devlab.group/agent-commerce 1.1.0 → 1.3.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
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { ZodError, z } from 'zod';
3
- import { existsSync, mkdirSync, accessSync, constants, closeSync, openSync, readFileSync, chmodSync, promises } from 'fs';
3
+ import { existsSync, mkdirSync, accessSync, constants, readFileSync, closeSync, openSync, 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
- import Database from 'better-sqlite3';
11
10
  import { createRequire } from 'module';
12
- import { writeFile } from 'fs/promises';
11
+ import Database from 'better-sqlite3';
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", "acp"];
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);
@@ -325,6 +335,30 @@ var init_networks = __esm({
325
335
  }
326
336
  });
327
337
 
338
+ // src/protocols/acp/constants.ts
339
+ var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
340
+ var init_constants = __esm({
341
+ "src/protocols/acp/constants.ts"() {
342
+ ACP_SPEC_VERSION = "2026-04-17";
343
+ ACP_API_VERSION = "2026-04-17";
344
+ ACP_WELL_KNOWN_PATH = "/.well-known/acp.json";
345
+ ACP_CHECKOUT_OPERATIONS = [
346
+ "createCheckoutSession",
347
+ "updateCheckoutSession",
348
+ "getCheckoutSession",
349
+ "completeCheckoutSession",
350
+ "cancelCheckoutSession"
351
+ ];
352
+ ACP_OPERATION_INPUT_KEYS = {
353
+ createCheckoutSession: ["body"],
354
+ updateCheckoutSession: ["path", "body"],
355
+ getCheckoutSession: ["path"],
356
+ completeCheckoutSession: ["path", "body"],
357
+ cancelCheckoutSession: ["path"]
358
+ };
359
+ }
360
+ });
361
+
328
362
  // src/config/env.ts
329
363
  function substituteEnv(value, env, path2 = "$") {
330
364
  if (typeof value === "string") {
@@ -681,7 +715,8 @@ function normalise(raw) {
681
715
  a2a: {
682
716
  enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
683
717
  mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
684
- }
718
+ },
719
+ acp: normaliseAcp(raw.protocols.acp)
685
720
  };
686
721
  validateMountPaths(protocols);
687
722
  const x402Raw = raw.payments.x402;
@@ -735,6 +770,7 @@ function normalise(raw) {
735
770
  const resources = Object.entries(raw.resources).map(
736
771
  ([id, entry]) => normaliseResource(id, entry, protocols, x402)
737
772
  );
773
+ if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
738
774
  return {
739
775
  version: SUPPORTED_CONFIG_VERSION,
740
776
  merchant: {
@@ -761,7 +797,8 @@ function normalise(raw) {
761
797
  function validateMountPaths(protocols) {
762
798
  const mounts = [
763
799
  ["mcp", protocols.mcp],
764
- ["a2a", protocols.a2a]
800
+ ["a2a", protocols.a2a],
801
+ ["acp", protocols.acp]
765
802
  ].filter(([, p]) => p.enabled);
766
803
  for (const [index, [nameA, a]] of mounts.entries()) {
767
804
  for (const [nameB, b] of mounts.slice(index + 1)) {
@@ -770,13 +807,152 @@ function validateMountPaths(protocols) {
770
807
  if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
771
808
  throw new CommerceError(
772
809
  "CONFIG_INVALID",
773
- `protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") \u2014 each mount registers a wildcard, so overlapping prefixes cannot both be served`,
810
+ `protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") - each mount registers a wildcard, so overlapping prefixes cannot both be served`,
774
811
  { details: { path: `protocols.${nameA}.mountPath` } }
775
812
  );
776
813
  }
777
814
  }
778
815
  }
779
816
  }
817
+ function acpInvalid(path2, message, extra = {}) {
818
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
819
+ }
820
+ function normaliseAcp(raw) {
821
+ const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
822
+ if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
823
+ return { enabled: false, mountPath };
824
+ }
825
+ if (raw.auth === void 0) {
826
+ throw acpInvalid(
827
+ "protocols.acp.auth",
828
+ 'protocols.acp.auth is required when ACP is enabled - every ACP checkout endpoint is authenticated, and "bearer" is the only scheme supported in this release'
829
+ );
830
+ }
831
+ if (raw.idempotency === void 0) {
832
+ throw acpInvalid(
833
+ "protocols.acp.idempotency",
834
+ "protocols.acp.idempotency is required when ACP is enabled - it names the SQLite file holding checkout idempotency records, and ACP makes the Idempotency-Key mandatory on every checkout POST"
835
+ );
836
+ }
837
+ const discovery = raw.discovery;
838
+ return {
839
+ enabled: true,
840
+ mountPath,
841
+ auth: raw.auth,
842
+ idempotency: {
843
+ path: raw.idempotency.path,
844
+ retentionHours: toNumber(
845
+ raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
846
+ "protocols.acp.idempotency.retentionHours",
847
+ { min: ACP_RETENTION_HOURS }
848
+ )
849
+ },
850
+ checkout: { operations: normaliseAcpOperations(raw.checkout) },
851
+ // Built key by key rather than passed through: absent metadata must stay
852
+ // absent from the discovery document, not appear there as `undefined`.
853
+ ...discovery !== void 0 ? {
854
+ discovery: {
855
+ ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
856
+ ...discovery.supportedCurrencies !== void 0 ? { supportedCurrencies: discovery.supportedCurrencies } : {},
857
+ ...discovery.supportedLocales !== void 0 ? { supportedLocales: discovery.supportedLocales } : {},
858
+ ...discovery.interventionTypes !== void 0 ? { interventionTypes: discovery.interventionTypes } : {}
859
+ }
860
+ } : {}
861
+ };
862
+ }
863
+ function normaliseAcpOperations(checkout) {
864
+ const configured = checkout?.operations ?? {};
865
+ for (const key of Object.keys(configured)) {
866
+ if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
867
+ throw acpInvalid(
868
+ `protocols.acp.checkout.operations.${key}`,
869
+ `protocols.acp.checkout.operations names unknown ACP checkout operation "${key}". Supported: ${ACP_CHECKOUT_OPERATIONS.join(", ")}.`
870
+ );
871
+ }
872
+ }
873
+ const operations = {};
874
+ const claimedBy = /* @__PURE__ */ new Map();
875
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
876
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
877
+ const resourceId = configured[operation];
878
+ if (resourceId === void 0) {
879
+ throw acpInvalid(
880
+ path2,
881
+ `${path2} is required when ACP is enabled - all five checkout operations must be mapped, since ACP discovery advertises the checkout service as a whole`
882
+ );
883
+ }
884
+ const claimed = claimedBy.get(resourceId);
885
+ if (claimed !== void 0) {
886
+ throw acpInvalid(
887
+ path2,
888
+ `${path2} maps resource "${resourceId}", which is already mapped to "${claimed}" - each ACP checkout operation needs its own resource`,
889
+ { resourceId }
890
+ );
891
+ }
892
+ claimedBy.set(resourceId, operation);
893
+ operations[operation] = resourceId;
894
+ }
895
+ return operations;
896
+ }
897
+ function validateAcpCheckoutMapping(acp, resources) {
898
+ const byId = new Map(resources.map((resource) => [resource.id, resource]));
899
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
900
+ const resourceId = acp.checkout.operations[operation];
901
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
902
+ const resource = byId.get(resourceId);
903
+ if (resource === void 0) {
904
+ throw acpInvalid(
905
+ path2,
906
+ `${path2} maps resource "${resourceId}", which is not defined under "resources"`,
907
+ { resourceId }
908
+ );
909
+ }
910
+ if (!resource.exposedVia.includes("acp")) {
911
+ throw acpInvalid(
912
+ path2,
913
+ `Resource "${resourceId}" implements ACP operation "${operation}" but does not list "acp" in its expose - the mapping says which resource serves the operation, expose says ACP may invoke it, and both are required`,
914
+ { resourceId }
915
+ );
916
+ }
917
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
918
+ throw acpInvalid(
919
+ path2,
920
+ `Resource "${resourceId}" implements ACP operation "${operation}" and must use pricing.type "free" with no "payments" - ACP checkout carries the merchant's own purchase payment, so the gateway does not also charge for the invocation`,
921
+ { resourceId }
922
+ );
923
+ }
924
+ validateAcpOperationInput(path2, operation, resource);
925
+ }
926
+ }
927
+ function validateAcpOperationInput(path2, operation, resource) {
928
+ const schema = resource.inputSchema;
929
+ if (schema === void 0) return;
930
+ const keys = ACP_OPERATION_INPUT_KEYS[operation];
931
+ const properties = schema["properties"];
932
+ if (schema["additionalProperties"] === false && isPlainObject2(properties)) {
933
+ for (const key of keys) {
934
+ if (!Object.hasOwn(properties, key)) {
935
+ throw acpInvalid(
936
+ path2,
937
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter always sends for this operation`,
938
+ { resourceId: resource.id }
939
+ );
940
+ }
941
+ }
942
+ }
943
+ const required = schema["required"];
944
+ if (Array.isArray(required)) {
945
+ for (const entry of required) {
946
+ if (typeof entry === "string" && !keys.includes(entry)) {
947
+ throw acpInvalid(
948
+ path2,
949
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation never supplies (it sends: ${keys.join(", ")})`,
950
+ { resourceId: resource.id }
951
+ );
952
+ }
953
+ }
954
+ }
955
+ }
780
956
  function normaliseResource(id, entry, protocols, x402) {
781
957
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
782
958
  const inputProperties = entry.input?.["properties"];
@@ -797,7 +973,7 @@ function normaliseResource(id, entry, protocols, x402) {
797
973
  const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
798
974
  throw new CommerceError(
799
975
  "CONFIG_INVALID",
800
- `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
976
+ `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: ${PROTOCOL_NAMES.join(", ")}.`,
801
977
  { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
802
978
  );
803
979
  }
@@ -825,6 +1001,13 @@ function normaliseResource(id, entry, protocols, x402) {
825
1001
  { details: { path: `resources.${id}.expose`, resourceId: id } }
826
1002
  );
827
1003
  }
1004
+ if (entry.expose.includes("acp") && !protocols.acp.enabled) {
1005
+ throw new CommerceError(
1006
+ "CONFIG_INVALID",
1007
+ `Resource "${id}" is exposed via "acp" but protocols.acp.enabled is false`,
1008
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1009
+ );
1010
+ }
828
1011
  if (entry.expose.includes("http") && !protocols.http.enabled) {
829
1012
  throw new CommerceError(
830
1013
  "CONFIG_INVALID",
@@ -833,7 +1016,8 @@ function normaliseResource(id, entry, protocols, x402) {
833
1016
  );
834
1017
  }
835
1018
  validateBackendUrl(id, entry.backend.url);
836
- validatePathParametersDeclared(id, entry.backend.url, entry.input);
1019
+ const pathScope = validateInputBindings(id, entry.backend, entry.input);
1020
+ validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
837
1021
  if (entry.pricing.type === "dynamic") {
838
1022
  throw new CommerceError(
839
1023
  "CONFIG_INVALID",
@@ -886,7 +1070,8 @@ function normaliseResource(id, entry, protocols, x402) {
886
1070
  timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
887
1071
  min: 1
888
1072
  })
889
- } : {}
1073
+ } : {},
1074
+ ...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
890
1075
  },
891
1076
  pricing,
892
1077
  exposedVia: entry.expose,
@@ -940,7 +1125,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
940
1125
  if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
941
1126
  throw new CommerceError(
942
1127
  "CONFIG_INVALID",
943
- `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".`,
1128
+ `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".`,
944
1129
  { details: { path: `resources.${id}.${path2}`, resourceId: id } }
945
1130
  );
946
1131
  }
@@ -974,7 +1159,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
974
1159
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
975
1160
  } else if (Array.isArray(items)) {
976
1161
  console.warn(
977
- `[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.`
1162
+ `[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.`
978
1163
  );
979
1164
  }
980
1165
  }
@@ -983,14 +1168,14 @@ function validatePricingAmount(id, amount, x402) {
983
1168
  if (!PRICING_AMOUNT_PATTERN.test(amount)) {
984
1169
  throw new CommerceError(
985
1170
  "CONFIG_INVALID",
986
- `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")`,
1171
+ `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")`,
987
1172
  { details: { path: path2, resourceId: id } }
988
1173
  );
989
1174
  }
990
1175
  if (ZERO_AMOUNT_PATTERN.test(amount)) {
991
1176
  throw new CommerceError(
992
1177
  "CONFIG_INVALID",
993
- `Resource "${id}" has pricing.amount "0" \u2014 a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
1178
+ `Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
994
1179
  { details: { path: path2, resourceId: id } }
995
1180
  );
996
1181
  }
@@ -1005,7 +1190,86 @@ function validatePricingAmount(id, amount, x402) {
1005
1190
  }
1006
1191
  }
1007
1192
  }
1008
- function validatePathParametersDeclared(id, url, input) {
1193
+ function validateInputBindings(id, backend, input) {
1194
+ const legacy = { schema: input, where: "its input schema" };
1195
+ const bindings = backend.inputBindings;
1196
+ const templated = extractPathParameterNames(backend.url).length > 0;
1197
+ if (bindings === void 0) return legacy;
1198
+ const path2 = `resources.${id}.backend.inputBindings`;
1199
+ const fail = (message, details = {}) => {
1200
+ throw new CommerceError("CONFIG_INVALID", `Resource "${id}" ${message}`, {
1201
+ details: { path: path2, resourceId: id, ...details }
1202
+ });
1203
+ };
1204
+ const entries = Object.entries(bindings).filter(
1205
+ (entry) => entry[1] !== void 0
1206
+ );
1207
+ if (entries.length === 0) {
1208
+ fail("has an empty backend.inputBindings - remove the block to use the default mapping");
1209
+ }
1210
+ if (backend.method === "GET" || backend.method === "DELETE") {
1211
+ if (bindings.body !== void 0) {
1212
+ fail(
1213
+ `binds a request body on a ${backend.method}, which sends none - the value would be silently dropped`
1214
+ );
1215
+ }
1216
+ }
1217
+ if (templated && bindings.path === void 0) {
1218
+ fail(
1219
+ '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'
1220
+ );
1221
+ }
1222
+ const seen = /* @__PURE__ */ new Map();
1223
+ const properties = isPlainObject2(input?.["properties"]) ? input["properties"] : {};
1224
+ const required = new Set(
1225
+ Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1226
+ );
1227
+ for (const [location, property] of entries) {
1228
+ if (property === PAYMENT_INPUT_FIELD) {
1229
+ fail(
1230
+ `binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
1231
+ { location }
1232
+ );
1233
+ }
1234
+ const other = seen.get(property);
1235
+ if (other !== void 0) {
1236
+ fail(`binds both "${other}" and "${location}" to the input property "${property}"`, {
1237
+ location,
1238
+ property
1239
+ });
1240
+ }
1241
+ seen.set(property, location);
1242
+ if (!Object.hasOwn(properties, property)) {
1243
+ fail(
1244
+ `binds "${location}" to input property "${property}", which the input schema does not declare - the schema is closed, so a caller could never supply it`,
1245
+ { location, property }
1246
+ );
1247
+ }
1248
+ const declared = properties[property];
1249
+ if (location !== "body" && isPlainObject2(declared) && !isObjectSchemaNode(declared)) {
1250
+ fail(
1251
+ `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
1252
+ { location, property }
1253
+ );
1254
+ }
1255
+ }
1256
+ if (templated && bindings.path !== void 0 && !required.has(bindings.path)) {
1257
+ fail(
1258
+ `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`,
1259
+ { property: bindings.path }
1260
+ );
1261
+ }
1262
+ if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
1263
+ const group = properties[bindings.path];
1264
+ return {
1265
+ schema: isPlainObject2(group) ? group : void 0,
1266
+ where: `input.properties.${bindings.path}`
1267
+ };
1268
+ }
1269
+ function pickDefined(value) {
1270
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1271
+ }
1272
+ function validatePathParametersDeclared(id, url, input, where) {
1009
1273
  const stray = findUnparsedBraceToken(url);
1010
1274
  if (stray !== void 0) {
1011
1275
  throw new CommerceError(
@@ -1044,14 +1308,14 @@ function validatePathParametersDeclared(id, url, input) {
1044
1308
  if (!Object.hasOwn(properties, param)) {
1045
1309
  throw new CommerceError(
1046
1310
  "CONFIG_INVALID",
1047
- `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`,
1311
+ `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`,
1048
1312
  { details: { path: path2, resourceId: id, param } }
1049
1313
  );
1050
1314
  }
1051
1315
  if (!required.has(param)) {
1052
1316
  throw new CommerceError(
1053
1317
  "CONFIG_INVALID",
1054
- `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`,
1318
+ `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`,
1055
1319
  { details: { path: path2, resourceId: id, param } }
1056
1320
  );
1057
1321
  }
@@ -1094,12 +1358,13 @@ function validateAddress(path2, value) {
1094
1358
  );
1095
1359
  }
1096
1360
  }
1097
- 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, 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;
1361
+ 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, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
1098
1362
  var init_schema = __esm({
1099
1363
  "src/config/schema.ts"() {
1100
1364
  init_execution();
1101
1365
  init_core();
1102
1366
  init_guardrails();
1367
+ init_constants();
1103
1368
  init_env();
1104
1369
  SUPPORTED_CONFIG_VERSION = 1;
1105
1370
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
@@ -1119,15 +1384,15 @@ var init_schema = __esm({
1119
1384
  adminToken: z.string().min(1).optional(),
1120
1385
  /** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
1121
1386
  // Entries are matched literally against the browser's
1122
- // `Origin` header, so `"*"` and a trailing slash match nothing at all —
1387
+ // `Origin` header, so `"*"` and a trailing slash match nothing at all -
1123
1388
  // fail-closed (a lockout, not a bypass), but silently, and a lockout with
1124
1389
  // no explanation is the kind of thing an operator "fixes" by disabling the
1125
1390
  // check. Reject those two shapes with a pointer instead.
1126
1391
  allowedOrigins: z.array(
1127
1392
  z.string().min(1).refine((origin) => origin !== "*", {
1128
- 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.`
1393
+ 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.`
1129
1394
  }).refine((origin) => !origin.endsWith("/"), {
1130
- 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".'
1395
+ 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".'
1131
1396
  })
1132
1397
  ).optional()
1133
1398
  }).strict();
@@ -1144,6 +1409,8 @@ var init_schema = __esm({
1144
1409
  // Fixed by the A2A specification, so it is the adapter's to serve and never
1145
1410
  // a configurable mount's to claim.
1146
1411
  "/.well-known/agent-card.json",
1412
+ // Likewise fixed by the ACP specification.
1413
+ ACP_WELL_KNOWN_PATH,
1147
1414
  "/api/resources",
1148
1415
  "/api/resources/:id/invoke",
1149
1416
  "/api/receipts",
@@ -1151,9 +1418,9 @@ var init_schema = __esm({
1151
1418
  "/api/events/stream"
1152
1419
  ];
1153
1420
  MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1154
- message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
1421
+ message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
1155
1422
  }).refine((value) => !/[:*?\s]/.test(value), {
1156
- message: 'must not contain ":", "*", "?" or whitespace \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
1423
+ message: 'must not contain ":", "*", "?" or whitespace - the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
1157
1424
  }).refine(
1158
1425
  (value) => {
1159
1426
  const base = value.replace(/\/+$/, "");
@@ -1165,6 +1432,15 @@ var init_schema = __esm({
1165
1432
  message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
1166
1433
  }
1167
1434
  );
1435
+ AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
1436
+ AcpIdempotencySchema = z.object({ path: z.string().min(1), retentionHours: NumberOrString.optional() }).strict();
1437
+ AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
1438
+ AcpDiscoverySchema = z.object({
1439
+ documentationUrl: z.string().min(1).optional(),
1440
+ supportedCurrencies: z.array(z.string().min(1)).optional(),
1441
+ supportedLocales: z.array(z.string().min(1)).optional(),
1442
+ interventionTypes: z.array(z.string().min(1)).optional()
1443
+ }).strict();
1168
1444
  ProtocolsSchema = z.object({
1169
1445
  http: z.object({ enabled: BooleanOrString }).strict(),
1170
1446
  mcp: z.object({
@@ -1176,16 +1452,36 @@ var init_schema = __esm({
1176
1452
  a2a: z.object({
1177
1453
  enabled: BooleanOrString,
1178
1454
  mountPath: MountPathSchema.optional()
1455
+ }).strict().optional(),
1456
+ // Likewise optional and off by default. The sub-blocks are optional here
1457
+ // and required by the business pass only when ACP is enabled, so a
1458
+ // disabled placeholder block stays writable and an enabled one gets a
1459
+ // message naming the piece it is missing.
1460
+ acp: z.object({
1461
+ enabled: BooleanOrString,
1462
+ mountPath: MountPathSchema.optional(),
1463
+ auth: AcpAuthSchema.optional(),
1464
+ idempotency: AcpIdempotencySchema.optional(),
1465
+ checkout: AcpCheckoutSchema.optional(),
1466
+ discovery: AcpDiscoverySchema.optional()
1179
1467
  }).strict().optional()
1180
1468
  }).strict();
1181
1469
  DEFAULT_A2A_MOUNT_PATH = "/a2a";
1470
+ DEFAULT_ACP_MOUNT_PATH = "/acp";
1471
+ ACP_RETENTION_HOURS = 24;
1182
1472
  BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1473
+ BackendInputBindingsSchema = z.object({
1474
+ path: z.string().min(1).optional(),
1475
+ query: z.string().min(1).optional(),
1476
+ body: z.string().min(1).optional()
1477
+ }).strict();
1183
1478
  BackendHandlerSchema = z.object({
1184
1479
  type: z.literal("http"),
1185
1480
  method: BackendMethodSchema,
1186
1481
  url: z.string().min(1),
1187
1482
  headers: z.record(z.string(), z.string()).optional(),
1188
- timeoutMs: NumberOrString.optional()
1483
+ timeoutMs: NumberOrString.optional(),
1484
+ inputBindings: BackendInputBindingsSchema.optional()
1189
1485
  }).strict();
1190
1486
  PricingSchema = z.discriminatedUnion("type", [
1191
1487
  z.object({ type: z.literal("free") }).strict(),
@@ -1230,7 +1526,7 @@ var init_schema = __esm({
1230
1526
  z.object({
1231
1527
  mode: z.literal("remote"),
1232
1528
  url: z.string().min(1),
1233
- // Absent means "this facilitator takes no credential" — an explicit
1529
+ // Absent means "this facilitator takes no credential" - an explicit
1234
1530
  // statement, normalised to `{ type: 'none' }` below. On a mainnet that
1235
1531
  // combination is refused outright, so the default can never quietly
1236
1532
  // become an unauthenticated production facilitator.
@@ -1248,7 +1544,7 @@ var init_schema = __esm({
1248
1544
  payTo: z.string().min(1),
1249
1545
  maxTimeoutSeconds: NumberOrString,
1250
1546
  facilitator: FacilitatorSchema,
1251
- /** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
1547
+ /** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
1252
1548
  allowMainnet: BooleanOrString.optional(),
1253
1549
  /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1254
1550
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
@@ -1265,7 +1561,7 @@ var init_schema = __esm({
1265
1561
  resources: ResourcesMapSchema,
1266
1562
  payments: PaymentsSchema
1267
1563
  }).strict();
1268
- SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp", "a2a"]);
1564
+ SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1269
1565
  SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1270
1566
  EMPTY_CLOSED_OBJECT_SCHEMA = {
1271
1567
  type: "object",
@@ -1387,6 +1683,7 @@ var init_config = __esm({
1387
1683
 
1388
1684
  // src/cli/program.ts
1389
1685
  init_filename();
1686
+ init_core();
1390
1687
 
1391
1688
  // src/cli/commands/demo.ts
1392
1689
  function defaultDemoSteps() {
@@ -1491,9 +1788,94 @@ var A2A_UNSUPPORTED = [
1491
1788
  "artifact types beyond Agent Commerce outcome data"
1492
1789
  ];
1493
1790
 
1791
+ // src/cli/commands/doctor.ts
1792
+ init_constants();
1793
+
1794
+ // src/protocols/acp/descriptor.ts
1795
+ init_constants();
1796
+ [
1797
+ "rest transport",
1798
+ "well-known discovery",
1799
+ "bearer authentication",
1800
+ "API-Version negotiation",
1801
+ "checkout service",
1802
+ ...ACP_CHECKOUT_OPERATIONS
1803
+ ];
1804
+ var ACP_UNSUPPORTED = [
1805
+ // Services this seller does not implement, named as ACP names them. The
1806
+ // discovery document advertises `checkout` alone, and these are the reason.
1807
+ "carts service",
1808
+ "feed service",
1809
+ "standalone orders service",
1810
+ "delegate_payment",
1811
+ "delegate_authentication",
1812
+ // Bindings and delivery directions.
1813
+ "ACP MCP transport binding",
1814
+ "webhooks",
1815
+ "outbound merchant-to-agent delivery",
1816
+ "ACP client role",
1817
+ // Request authentication beyond the bearer token.
1818
+ "Signature header verification",
1819
+ "Timestamp replay-window verification",
1820
+ // Extensions and versioning.
1821
+ "discount extension",
1822
+ "general ACP extension framework",
1823
+ "multiple ACP API versions",
1824
+ "seller-backed payment handler integration"
1825
+ ];
1826
+
1494
1827
  // src/storage/receipts/store.ts
1495
1828
  init_core();
1496
- var PACKAGE_VERSION = "1.1.0" ;
1829
+ var PACKAGE_VERSION = "1.3.0" ;
1830
+
1831
+ // src/storage/sqlite.ts
1832
+ init_core();
1833
+ function openSqliteDatabase(options) {
1834
+ const { path: path2, label, logger } = options;
1835
+ const isFileBacked = path2 !== ":memory:";
1836
+ if (isFileBacked) {
1837
+ const dir = dirname(path2);
1838
+ if (dir !== "" && dir !== "." && !existsSync(dir)) {
1839
+ mkdirSync(dir, { recursive: true, mode: 448 });
1840
+ }
1841
+ }
1842
+ if (isFileBacked && existsSync(path2)) {
1843
+ try {
1844
+ accessSync(path2, constants.W_OK);
1845
+ } catch {
1846
+ throw new CommerceError(
1847
+ "STORAGE_ERROR",
1848
+ `${label} "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one - recreate it (docker compose down -v) or fix its ownership.`,
1849
+ { details: { path: path2 } }
1850
+ );
1851
+ }
1852
+ }
1853
+ if (isFileBacked) {
1854
+ try {
1855
+ closeSync(openSync(path2, "a", 384));
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ const db = new Database(path2);
1860
+ db.pragma("journal_mode = WAL");
1861
+ db.pragma("foreign_keys = ON");
1862
+ if (isFileBacked) restrictDatabasePermissions(path2, label, logger);
1863
+ return db;
1864
+ }
1865
+ function restrictDatabasePermissions(path2, label, logger) {
1866
+ for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
1867
+ try {
1868
+ chmodSync(file, 384);
1869
+ } catch (error) {
1870
+ const code = error.code;
1871
+ if (code === "ENOENT") continue;
1872
+ logger.warn(
1873
+ { file, label, error: code ?? "unknown" },
1874
+ "could not restrict database permissions to owner-only"
1875
+ );
1876
+ }
1877
+ }
1878
+ }
1497
1879
 
1498
1880
  // src/storage/receipts/redact.ts
1499
1881
  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;
@@ -1681,54 +2063,14 @@ var defaultIds = {
1681
2063
  function isUniqueConstraintOn(err, column) {
1682
2064
  return err instanceof Error && "code" in err && err.code === "SQLITE_CONSTRAINT_UNIQUE" && err.message.includes(column);
1683
2065
  }
1684
- function restrictDatabasePermissions(path2, logger) {
1685
- for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
1686
- try {
1687
- chmodSync(file, 384);
1688
- } catch (error) {
1689
- const code = error.code;
1690
- if (code === "ENOENT") continue;
1691
- logger.warn(
1692
- { file, error: code ?? "unknown" },
1693
- "could not restrict receipt database permissions to owner-only"
1694
- );
1695
- }
1696
- }
1697
- }
1698
2066
  function createSqliteReceiptStore(options) {
1699
2067
  const logger = options.logger ?? NOOP_LOGGER;
1700
2068
  const clock = options.clock ?? systemClock;
1701
2069
  const ids = options.ids ?? defaultIds;
1702
2070
  const path2 = options.path;
1703
2071
  const isFileBacked = path2 !== ":memory:";
1704
- if (isFileBacked) {
1705
- const dir = dirname(path2);
1706
- if (dir !== "" && dir !== "." && !existsSync(dir)) {
1707
- mkdirSync(dir, { recursive: true, mode: 448 });
1708
- }
1709
- }
1710
- if (isFileBacked && existsSync(path2)) {
1711
- try {
1712
- accessSync(path2, constants.W_OK);
1713
- } catch {
1714
- throw new CommerceError(
1715
- "STORAGE_ERROR",
1716
- `Receipt database "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one \u2014 recreate it (docker compose down -v) or fix its ownership.`,
1717
- { details: { path: path2 } }
1718
- );
1719
- }
1720
- }
1721
- if (isFileBacked) {
1722
- try {
1723
- closeSync(openSync(path2, "a", 384));
1724
- } catch {
1725
- }
1726
- }
1727
- const db = new Database(path2);
1728
- db.pragma("journal_mode = WAL");
1729
- db.pragma("foreign_keys = ON");
2072
+ const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
1730
2073
  migrate(db);
1731
- if (isFileBacked) restrictDatabasePermissions(path2, logger);
1732
2074
  const insertReceiptStmt = db.prepare(
1733
2075
  `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json)
1734
2076
  VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json)`
@@ -2246,7 +2588,7 @@ function maskMiddle(value, keep = 6) {
2246
2588
  }
2247
2589
  createRequire(import.meta.url);
2248
2590
  function readVersionReport() {
2249
- const injected = '{"cliVersion":"1.1.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"}]}' ;
2591
+ const injected = '{"cliVersion":"1.3.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"}]}' ;
2250
2592
  {
2251
2593
  return JSON.parse(injected);
2252
2594
  }
@@ -2308,6 +2650,73 @@ function findX402Mismatch(configured, live) {
2308
2650
  if (diffs.length === 0) return void 0;
2309
2651
  return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
2310
2652
  }
2653
+ function acpIdempotencyCheck(idempotency) {
2654
+ const retention = `retention ${idempotency.retentionHours}h`;
2655
+ if (idempotency.retentionHours < 24) {
2656
+ return {
2657
+ name: "ACP idempotency",
2658
+ status: "FAIL",
2659
+ detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
2660
+ };
2661
+ }
2662
+ if (idempotency.path === ":memory:") {
2663
+ return {
2664
+ name: "ACP idempotency",
2665
+ status: "WARN",
2666
+ detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
2667
+ };
2668
+ }
2669
+ const directory = dirname(idempotency.path);
2670
+ if (existsSync(idempotency.path)) {
2671
+ try {
2672
+ accessSync(idempotency.path, constants.W_OK);
2673
+ return { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` };
2674
+ } catch {
2675
+ return {
2676
+ name: "ACP idempotency",
2677
+ status: "FAIL",
2678
+ detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
2679
+ };
2680
+ }
2681
+ }
2682
+ if (directory !== "" && directory !== "." && !existsSync(directory)) {
2683
+ return {
2684
+ name: "ACP idempotency",
2685
+ status: "WARN",
2686
+ detail: `no store yet at "${idempotency.path}" and its directory does not exist \u2014 it is created when the gateway starts, provided that path is writable`
2687
+ };
2688
+ }
2689
+ return {
2690
+ name: "ACP idempotency",
2691
+ status: "WARN",
2692
+ detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
2693
+ };
2694
+ }
2695
+ function acpMappingCheck(config, operations) {
2696
+ const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
2697
+ const problems = [];
2698
+ for (const [operation, resourceId] of Object.entries(operations)) {
2699
+ const resource = byId.get(resourceId);
2700
+ if (resource === void 0) {
2701
+ problems.push(`${operation} -> "${resourceId}" does not exist`);
2702
+ continue;
2703
+ }
2704
+ if (!resource.exposedVia.includes("acp")) {
2705
+ problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
2706
+ }
2707
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
2708
+ problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
2709
+ }
2710
+ }
2711
+ if (problems.length > 0) {
2712
+ return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
2713
+ }
2714
+ return {
2715
+ name: "ACP checkout mapping",
2716
+ status: "PASS",
2717
+ detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
2718
+ };
2719
+ }
2311
2720
  async function runDoctor(options, deps = {}) {
2312
2721
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
2313
2722
  const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
@@ -2389,7 +2798,7 @@ async function runDoctor(options, deps = {}) {
2389
2798
  checks.push({
2390
2799
  name: "Protocols",
2391
2800
  status: "PASS",
2392
- detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"}`
2801
+ detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"} acp=${config.protocols.acp.enabled ? `on (${config.protocols.acp.mountPath})` : "off"}`
2393
2802
  });
2394
2803
  }
2395
2804
  if (config === void 0) {
@@ -2408,6 +2817,30 @@ async function runDoctor(options, deps = {}) {
2408
2817
  detail: A2A_UNSUPPORTED.join(", ")
2409
2818
  });
2410
2819
  }
2820
+ if (config === void 0) {
2821
+ checks.push({ name: "ACP", status: "WARN", detail: "skipped \u2014 config invalid" });
2822
+ } else if (!config.protocols.acp.enabled) {
2823
+ checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
2824
+ } else {
2825
+ const acp = config.protocols.acp;
2826
+ checks.push({
2827
+ name: "ACP",
2828
+ status: "PASS",
2829
+ detail: `experimental \xB7 spec ${ACP_SPEC_VERSION} \xB7 API-Version ${ACP_API_VERSION} \xB7 service checkout \xB7 mount ${acp.mountPath} \xB7 discovery ${ACP_WELL_KNOWN_PATH}`
2830
+ });
2831
+ checks.push({
2832
+ name: "ACP auth",
2833
+ status: "PASS",
2834
+ detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
2835
+ });
2836
+ checks.push(acpIdempotencyCheck(acp.idempotency));
2837
+ checks.push(acpMappingCheck(config, acp.checkout.operations));
2838
+ checks.push({
2839
+ name: "ACP unsupported",
2840
+ status: "INFO",
2841
+ detail: ACP_UNSUPPORTED.join(", ")
2842
+ });
2843
+ }
2411
2844
  const x402 = config?.payments.x402;
2412
2845
  if (x402 === void 0 || !x402.enabled) {
2413
2846
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
@@ -2552,6 +2985,1109 @@ function printDoctorReport(report, io, json) {
2552
2985
  io.stdout(`Score: ${report.score.passed}/${report.score.total} checks passed`);
2553
2986
  }
2554
2987
 
2988
+ // src/cli/commands/import-openapi.ts
2989
+ init_core();
2990
+
2991
+ // src/openapi/discover.ts
2992
+ init_errors();
2993
+ init_execution();
2994
+
2995
+ // src/openapi/refs.ts
2996
+ init_errors();
2997
+ var MAX_REF_DEPTH = 100;
2998
+ function isRefNode(value) {
2999
+ return typeof value === "object" && value !== null && typeof value.$ref === "string";
3000
+ }
3001
+ function dereference(document, node, stack = []) {
3002
+ let current = node;
3003
+ let chain = stack;
3004
+ while (isRefNode(current)) {
3005
+ const pointer = current.$ref;
3006
+ if (!pointer.startsWith("#")) {
3007
+ throw new CommerceError(
3008
+ "CONFIG_INVALID",
3009
+ `OpenAPI document contains an external reference "${pointer}". Only internal references (#/...) are supported`,
3010
+ { details: { ref: pointer } }
3011
+ );
3012
+ }
3013
+ if (chain.includes(pointer)) {
3014
+ throw new CommerceError(
3015
+ "CONFIG_INVALID",
3016
+ `OpenAPI reference "${pointer}" is circular (${[...chain, pointer].join(" -> ")})`,
3017
+ { details: { ref: pointer, chain: [...chain, pointer] } }
3018
+ );
3019
+ }
3020
+ if (chain.length >= MAX_REF_DEPTH) {
3021
+ throw new CommerceError(
3022
+ "CONFIG_INVALID",
3023
+ `OpenAPI reference chain exceeds ${MAX_REF_DEPTH} hops at "${pointer}"`,
3024
+ { details: { ref: pointer } }
3025
+ );
3026
+ }
3027
+ current = resolvePointer(document, pointer);
3028
+ chain = [...chain, pointer];
3029
+ }
3030
+ return { value: current, stack: chain };
3031
+ }
3032
+ function resolvePointer(document, ref) {
3033
+ const pointer = ref.slice(1);
3034
+ if (pointer === "" || pointer === "/") return document;
3035
+ if (!pointer.startsWith("/")) {
3036
+ throw new CommerceError(
3037
+ "CONFIG_INVALID",
3038
+ `OpenAPI reference "${ref}" is not a JSON Pointer. Plain names and anchors are not supported`,
3039
+ { details: { ref } }
3040
+ );
3041
+ }
3042
+ let current = document;
3043
+ for (const rawSegment of pointer.slice(1).split("/")) {
3044
+ const segment = decodeURIComponent(rawSegment).replaceAll("~1", "/").replaceAll("~0", "~");
3045
+ if (Array.isArray(current)) {
3046
+ const index = Number(segment);
3047
+ current = Number.isInteger(index) ? current[index] : void 0;
3048
+ } else if (typeof current === "object" && current !== null) {
3049
+ current = current[segment];
3050
+ } else {
3051
+ current = void 0;
3052
+ }
3053
+ if (current === void 0) {
3054
+ throw new CommerceError(
3055
+ "CONFIG_INVALID",
3056
+ `OpenAPI reference "${ref}" does not resolve to anything in the document`,
3057
+ { details: { ref } }
3058
+ );
3059
+ }
3060
+ }
3061
+ return current;
3062
+ }
3063
+
3064
+ // src/openapi/discover.ts
3065
+ var SUPPORTED_METHODS = {
3066
+ get: "GET",
3067
+ post: "POST",
3068
+ put: "PUT",
3069
+ patch: "PATCH",
3070
+ delete: "DELETE"
3071
+ };
3072
+ var NON_OPERATION_KEYS = /* @__PURE__ */ new Set(["summary", "description", "servers", "parameters", "$ref"]);
3073
+ var ID_ALLOWED = /[^A-Za-z0-9_.-]+/g;
3074
+ var MAX_ID_LENGTH = 128;
3075
+ function discoverOperations(loaded, options = {}) {
3076
+ const { document } = loaded;
3077
+ const diagnostics = [];
3078
+ const operations = [];
3079
+ const claimed = /* @__PURE__ */ new Map();
3080
+ if (options.baseUrl !== void 0) assertAbsoluteHttpUrl(options.baseUrl, "--base-url");
3081
+ const paths = document["paths"];
3082
+ if (!isRecord(paths)) {
3083
+ return { operations, diagnostics };
3084
+ }
3085
+ const rootServers = document["servers"];
3086
+ const rootSecurity = document["security"];
3087
+ for (const [path2, rawPathItem] of Object.entries(paths)) {
3088
+ if (path2.startsWith("x-")) continue;
3089
+ if (!path2.startsWith("/")) {
3090
+ diagnostics.push({
3091
+ severity: "warning",
3092
+ code: "invalid-path-key",
3093
+ operation: path2,
3094
+ message: `Skipped "${path2}": a Paths key must start with "/"`
3095
+ });
3096
+ continue;
3097
+ }
3098
+ const pathItem = dereference(document, rawPathItem).value;
3099
+ if (!isRecord(pathItem)) continue;
3100
+ for (const [key, rawOperation] of Object.entries(pathItem)) {
3101
+ if (NON_OPERATION_KEYS.has(key) || key.startsWith("x-")) continue;
3102
+ const method = SUPPORTED_METHODS[key.toLowerCase()];
3103
+ if (method === void 0) {
3104
+ diagnostics.push({
3105
+ severity: "warning",
3106
+ code: "unsupported-method",
3107
+ operation: `${key.toUpperCase()} ${path2}`,
3108
+ message: `Skipped ${key.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
3109
+ });
3110
+ continue;
3111
+ }
3112
+ const operation = dereference(document, rawOperation).value;
3113
+ if (!isRecord(operation)) continue;
3114
+ const where = `${method} ${path2}`;
3115
+ const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : void 0;
3116
+ const resourceId = toResourceId(operationId, method, path2);
3117
+ const previous = claimed.get(resourceId);
3118
+ if (previous !== void 0) {
3119
+ throw new CommerceError(
3120
+ "CONFIG_INVALID",
3121
+ `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`,
3122
+ { details: { resourceId, operations: [previous, where] } }
3123
+ );
3124
+ }
3125
+ claimed.set(resourceId, where);
3126
+ const server = selectServer(
3127
+ document,
3128
+ [operation["servers"], pathItem["servers"], rootServers],
3129
+ { where, resourceId },
3130
+ options,
3131
+ diagnostics
3132
+ );
3133
+ if (server === void 0) continue;
3134
+ const backendUrl = `${server.replace(/\/+$/, "")}${path2}`;
3135
+ const stray = findUnparsedBraceToken(backendUrl);
3136
+ if (stray !== void 0) {
3137
+ diagnostics.push({
3138
+ severity: "error",
3139
+ code: "unsupported-path-template",
3140
+ operation: resourceId,
3141
+ message: `Skipped ${where}: path template "${stray}" uses characters the gateway cannot substitute (allowed: A-Z a-z 0-9 _ . -)`
3142
+ });
3143
+ continue;
3144
+ }
3145
+ const summary = typeof operation["summary"] === "string" ? operation["summary"] : void 0;
3146
+ const description = typeof operation["description"] === "string" ? operation["description"] : summary;
3147
+ const security = operation["security"] ?? rootSecurity;
3148
+ operations.push({
3149
+ resourceId,
3150
+ method,
3151
+ path: path2,
3152
+ backendUrl,
3153
+ ...operationId !== void 0 ? { operationId } : {},
3154
+ tags: toArray(operation["tags"]).filter((tag) => typeof tag === "string"),
3155
+ name: summary ?? operationId ?? resourceId,
3156
+ ...description !== void 0 ? { description } : {},
3157
+ parameters: [
3158
+ ...toArray(pathItem["parameters"]),
3159
+ // Operation parameters last: Phase 4 lets the later one win, which is
3160
+ // the OpenAPI override rule (same name + in).
3161
+ ...toArray(operation["parameters"])
3162
+ ],
3163
+ ...operation["requestBody"] !== void 0 ? { requestBody: operation["requestBody"] } : {},
3164
+ ...operation["responses"] !== void 0 ? { responses: operation["responses"] } : {},
3165
+ security: toArray(security)
3166
+ });
3167
+ }
3168
+ }
3169
+ return { operations, diagnostics };
3170
+ }
3171
+ function toResourceId(operationId, method, path2) {
3172
+ const fromOperationId = operationId === void 0 ? "" : normaliseId(operationId);
3173
+ if (fromOperationId !== "") return fromOperationId;
3174
+ return normaliseId(`${method.toLowerCase()}_${path2}`);
3175
+ }
3176
+ function normaliseId(value) {
3177
+ return value.replace(ID_ALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MAX_ID_LENGTH);
3178
+ }
3179
+ function selectServer(document, candidates, context, options, diagnostics) {
3180
+ if (options.baseUrl !== void 0) return options.baseUrl;
3181
+ for (const candidate of candidates) {
3182
+ const servers = toArray(candidate);
3183
+ const first = servers.length > 0 ? dereference(document, servers[0]).value : void 0;
3184
+ if (!isRecord(first)) continue;
3185
+ const url = first["url"];
3186
+ if (typeof url !== "string" || url === "") continue;
3187
+ const substituted = substituteServerVariables(url, first["variables"], context, diagnostics);
3188
+ if (substituted === void 0) return void 0;
3189
+ if (!isAbsoluteHttpUrl(substituted)) {
3190
+ diagnostics.push({
3191
+ severity: "error",
3192
+ code: "relative-server-url",
3193
+ operation: context.resourceId,
3194
+ message: `Skipped ${context.where}: server URL "${substituted}" is relative, so no backend host is known. Pass --base-url`
3195
+ });
3196
+ return void 0;
3197
+ }
3198
+ return substituted;
3199
+ }
3200
+ diagnostics.push({
3201
+ severity: "error",
3202
+ code: "no-server-url",
3203
+ operation: context.resourceId,
3204
+ message: `Skipped ${context.where}: the document declares no server URL. Pass --base-url`
3205
+ });
3206
+ return void 0;
3207
+ }
3208
+ function substituteServerVariables(url, variables, context, diagnostics) {
3209
+ if (!url.includes("{")) return url;
3210
+ const declared = isRecord(variables) ? variables : {};
3211
+ const used = [];
3212
+ let missing;
3213
+ const substituted = url.replace(/\{([^{}]*)\}/g, (match, name) => {
3214
+ const variable = declared[name];
3215
+ const fallback = isRecord(variable) ? variable["default"] : void 0;
3216
+ if (typeof fallback !== "string") {
3217
+ missing = name;
3218
+ return match;
3219
+ }
3220
+ used.push(`${name}=${fallback}`);
3221
+ return fallback;
3222
+ });
3223
+ if (missing !== void 0) {
3224
+ diagnostics.push({
3225
+ severity: "error",
3226
+ code: "server-variable-without-default",
3227
+ operation: context.resourceId,
3228
+ message: `Skipped ${context.where}: server variable "{${missing}}" has no default value. Pass --base-url`
3229
+ });
3230
+ return void 0;
3231
+ }
3232
+ if (used.length > 0) {
3233
+ diagnostics.push({
3234
+ severity: "warning",
3235
+ code: "server-variable-default",
3236
+ operation: context.resourceId,
3237
+ message: `Server URL for ${context.where} uses declared defaults (${used.join(", ")}); override with --base-url if that is not the deployment you mean`
3238
+ });
3239
+ }
3240
+ return substituted;
3241
+ }
3242
+ function isAbsoluteHttpUrl(value) {
3243
+ try {
3244
+ const parsed = new URL(value);
3245
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
3246
+ return parsed.search === "" && parsed.hash === "";
3247
+ } catch {
3248
+ return false;
3249
+ }
3250
+ }
3251
+ function assertAbsoluteHttpUrl(value, label) {
3252
+ if (!isAbsoluteHttpUrl(value)) {
3253
+ throw new CommerceError(
3254
+ "CONFIG_INVALID",
3255
+ `${label} "${value}" must be an absolute http:// or https:// URL with no query string or fragment`,
3256
+ { details: { value } }
3257
+ );
3258
+ }
3259
+ }
3260
+ function isRecord(value) {
3261
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3262
+ }
3263
+ function toArray(value) {
3264
+ return Array.isArray(value) ? value : [];
3265
+ }
3266
+
3267
+ // src/openapi/request.ts
3268
+ init_execution();
3269
+
3270
+ // src/openapi/schema.ts
3271
+ init_errors();
3272
+ var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
3273
+ "object",
3274
+ "string",
3275
+ "number",
3276
+ "integer",
3277
+ "boolean",
3278
+ "array",
3279
+ "null"
3280
+ ]);
3281
+ var METADATA_KEYWORDS = ["title", "description", "default", "example", "examples", "deprecated"];
3282
+ var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
3283
+ "xml",
3284
+ "externalDocs",
3285
+ "readOnly",
3286
+ "writeOnly",
3287
+ "$schema",
3288
+ "$id",
3289
+ "$comment",
3290
+ "$defs",
3291
+ "definitions"
3292
+ ]);
3293
+ function isPrimitiveSchema(schema) {
3294
+ const types = typeList2(schema["type"]);
3295
+ if (types === void 0) return false;
3296
+ return types.every((type) => type !== "object" && type !== "array" && SUPPORTED_TYPES.has(type));
3297
+ }
3298
+ function convertSchema(document, node, stack = []) {
3299
+ const dropped = /* @__PURE__ */ new Set();
3300
+ try {
3301
+ const schema = convertNode(document, node, stack, dropped);
3302
+ return schema === void 0 ? { supported: false, reason: "schema could not be represented" } : { supported: true, schema, dropped: [...dropped] };
3303
+ } catch (error) {
3304
+ if (error instanceof UnsupportedSchema) return { supported: false, reason: error.message };
3305
+ if (isCommerceError(error)) return { supported: false, reason: error.message };
3306
+ throw error;
3307
+ }
3308
+ }
3309
+ var UnsupportedSchema = class extends Error {
3310
+ };
3311
+ function convertNode(document, node, stack, dropped) {
3312
+ if (node === true) return {};
3313
+ if (node === false) throw new UnsupportedSchema("schema is `false`, which accepts no value");
3314
+ const resolved = dereference(document, node, stack);
3315
+ const source = resolved.value;
3316
+ if (typeof source !== "object" || source === null || Array.isArray(source)) {
3317
+ throw new UnsupportedSchema("schema node is not an object");
3318
+ }
3319
+ const schemaNode = source;
3320
+ if (Array.isArray(schemaNode["allOf"])) {
3321
+ return mergeAllOf(document, schemaNode, resolved.stack, dropped);
3322
+ }
3323
+ for (const keyword of ["oneOf", "anyOf", "not", "discriminator"]) {
3324
+ if (Object.hasOwn(schemaNode, keyword)) {
3325
+ throw new UnsupportedSchema(
3326
+ `schema uses "${keyword}", which this gateway cannot enforce - accepting it would advertise validation that never runs`
3327
+ );
3328
+ }
3329
+ }
3330
+ const result = {};
3331
+ const types = resolveTypes(schemaNode);
3332
+ if (types !== void 0) result["type"] = types.length === 1 ? types[0] : types;
3333
+ for (const keyword of METADATA_KEYWORDS) {
3334
+ if (Object.hasOwn(schemaNode, keyword)) result[keyword] = schemaNode[keyword];
3335
+ }
3336
+ if (Array.isArray(schemaNode["enum"])) result["enum"] = [...schemaNode["enum"]];
3337
+ const properties = schemaNode["properties"];
3338
+ if (isRecord2(properties)) {
3339
+ const converted = {};
3340
+ for (const [name, sub] of Object.entries(properties)) {
3341
+ const child = convertNode(document, sub, resolved.stack, dropped);
3342
+ if (child !== void 0) converted[name] = child;
3343
+ }
3344
+ result["properties"] = converted;
3345
+ }
3346
+ const required = schemaNode["required"];
3347
+ if (Array.isArray(required)) {
3348
+ const names = required.filter((name) => typeof name === "string");
3349
+ if (names.length > 0) result["required"] = names;
3350
+ }
3351
+ const additional = schemaNode["additionalProperties"];
3352
+ if (typeof additional === "boolean") {
3353
+ result["additionalProperties"] = additional;
3354
+ } else if (additional !== void 0) {
3355
+ const child = convertNode(document, additional, resolved.stack, dropped);
3356
+ if (child !== void 0) result["additionalProperties"] = child;
3357
+ } else if (isRecord2(properties)) {
3358
+ result["additionalProperties"] = false;
3359
+ }
3360
+ const items = schemaNode["items"];
3361
+ if (Array.isArray(items)) {
3362
+ dropped.add("items (tuple form)");
3363
+ } else if (items !== void 0) {
3364
+ const child = convertNode(document, items, resolved.stack, dropped);
3365
+ if (child !== void 0) result["items"] = child;
3366
+ }
3367
+ for (const keyword of Object.keys(schemaNode)) {
3368
+ if (isCarriedKeyword(keyword) || ANNOTATION_KEYWORDS.has(keyword) || keyword.startsWith("x-")) {
3369
+ continue;
3370
+ }
3371
+ dropped.add(keyword);
3372
+ }
3373
+ return result;
3374
+ }
3375
+ function isCarriedKeyword(keyword) {
3376
+ return keyword === "type" || keyword === "properties" || keyword === "required" || keyword === "additionalProperties" || keyword === "enum" || keyword === "items" || keyword === "nullable" || keyword === "$ref" || METADATA_KEYWORDS.includes(keyword);
3377
+ }
3378
+ function resolveTypes(schema) {
3379
+ const declared = typeList2(schema["type"]);
3380
+ if (declared === void 0) return void 0;
3381
+ const known = declared.filter((type) => SUPPORTED_TYPES.has(type));
3382
+ if (known.length === 0) {
3383
+ throw new UnsupportedSchema(`schema type "${String(schema["type"])}" is not a JSON type`);
3384
+ }
3385
+ if (schema["nullable"] === true && !known.includes("null")) known.push("null");
3386
+ return known;
3387
+ }
3388
+ function typeList2(raw) {
3389
+ if (typeof raw === "string") return [raw];
3390
+ if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
3391
+ return raw;
3392
+ }
3393
+ return void 0;
3394
+ }
3395
+ function mergeAllOf(document, schema, stack, dropped) {
3396
+ const branches = schema["allOf"];
3397
+ const siblings = { ...schema };
3398
+ delete siblings["allOf"];
3399
+ const merged = { type: "object", properties: {}, required: [] };
3400
+ const properties = merged["properties"];
3401
+ const required = /* @__PURE__ */ new Set();
3402
+ const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
3403
+ for (const branch of parts) {
3404
+ const converted = convertNode(document, branch, stack, dropped);
3405
+ if (converted === void 0) throw new UnsupportedSchema("allOf branch is empty");
3406
+ const types = typeList2(converted["type"]);
3407
+ if (types !== void 0 && !types.includes("object")) {
3408
+ throw new UnsupportedSchema("allOf mixes object and non-object schemas");
3409
+ }
3410
+ const branchProperties = converted["properties"];
3411
+ if (isRecord2(branchProperties)) {
3412
+ for (const [name, sub] of Object.entries(branchProperties)) {
3413
+ const existing = properties[name];
3414
+ if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(sub)) {
3415
+ throw new UnsupportedSchema(
3416
+ `allOf branches declare property "${name}" differently, which cannot be merged`
3417
+ );
3418
+ }
3419
+ properties[name] = sub;
3420
+ }
3421
+ }
3422
+ for (const name of Array.isArray(converted["required"]) ? converted["required"] : []) {
3423
+ if (typeof name === "string") required.add(name);
3424
+ }
3425
+ for (const keyword of ["enum", "items"]) {
3426
+ if (Object.hasOwn(converted, keyword)) {
3427
+ throw new UnsupportedSchema(`allOf branch uses "${keyword}", which cannot be merged`);
3428
+ }
3429
+ }
3430
+ if (typeof converted["description"] === "string" && merged["description"] === void 0) {
3431
+ merged["description"] = converted["description"];
3432
+ }
3433
+ }
3434
+ if (required.size > 0) merged["required"] = [...required];
3435
+ else delete merged["required"];
3436
+ merged["additionalProperties"] = false;
3437
+ return merged;
3438
+ }
3439
+ function isRecord2(value) {
3440
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3441
+ }
3442
+
3443
+ // src/openapi/request.ts
3444
+ var PATH_GROUP = "path";
3445
+ var QUERY_GROUP = "query";
3446
+ var BODY_GROUP = "body";
3447
+ var IGNORED_HEADER_NAMES = /* @__PURE__ */ new Set(["accept", "content-type", "authorization"]);
3448
+ var UNSUPPORTED_QUERY_STYLES = /* @__PURE__ */ new Set(["deepObject", "spaceDelimited", "pipeDelimited"]);
3449
+ function mapRequest(loaded, candidate) {
3450
+ const { document } = loaded;
3451
+ const diagnostics = [];
3452
+ const dropped = /* @__PURE__ */ new Set();
3453
+ const operation = candidate.resourceId;
3454
+ const warn = (code, message) => {
3455
+ diagnostics.push({ severity: "warning", code, operation, message });
3456
+ };
3457
+ const skip = (code, message) => {
3458
+ diagnostics.push({
3459
+ severity: "error",
3460
+ code,
3461
+ operation,
3462
+ message: `Skipped ${candidate.method} ${candidate.path}: ${message}`
3463
+ });
3464
+ return { supported: false, diagnostics };
3465
+ };
3466
+ const pathProperties = {};
3467
+ const queryProperties = {};
3468
+ const requiredQuery = [];
3469
+ const templateParams = new Set(extractPathParameterNames(candidate.path));
3470
+ for (const parameter of mergeParameters(document, candidate.parameters)) {
3471
+ const name = typeof parameter["name"] === "string" ? parameter["name"] : void 0;
3472
+ const location = typeof parameter["in"] === "string" ? parameter["in"] : void 0;
3473
+ if (name === void 0 || location === void 0) continue;
3474
+ const required2 = parameter["required"] === true || location === "path";
3475
+ const label = `${location} parameter "${name}"`;
3476
+ if (location === "header" || location === "cookie") {
3477
+ if (location === "header" && IGNORED_HEADER_NAMES.has(name.toLowerCase())) continue;
3478
+ if (required2) {
3479
+ return skip(
3480
+ "unsupported-required-parameter",
3481
+ `${label} is required, and ${location} parameters are operator configuration in this release, not agent input`
3482
+ );
3483
+ }
3484
+ warn(
3485
+ "unsupported-optional-parameter",
3486
+ `Omitted optional ${label}: ${location} parameters are not imported. Configure it under backend.headers if the API needs it`
3487
+ );
3488
+ continue;
3489
+ }
3490
+ if (location !== "path" && location !== "query") {
3491
+ if (required2) return skip("unsupported-required-parameter", `${label} has unknown location`);
3492
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: unknown location`);
3493
+ continue;
3494
+ }
3495
+ const unsupported = describeUnsupportedParameter(document, parameter, location);
3496
+ if (unsupported !== void 0) {
3497
+ if (required2) return skip("unsupported-required-parameter", `${label} ${unsupported}`);
3498
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${unsupported}`);
3499
+ continue;
3500
+ }
3501
+ const converted = convertSchema(document, parameter["schema"]);
3502
+ if (!converted.supported) {
3503
+ if (required2) return skip("unsupported-required-parameter", `${label} ${converted.reason}`);
3504
+ warn("unsupported-optional-parameter", `Omitted optional ${label}: ${converted.reason}`);
3505
+ continue;
3506
+ }
3507
+ for (const keyword of converted.dropped) dropped.add(keyword);
3508
+ if (location === "path") {
3509
+ if (!templateParams.has(name)) {
3510
+ warn(
3511
+ "path-parameter-not-in-template",
3512
+ `Ignored path parameter "${name}": it does not appear in "${candidate.path}"`
3513
+ );
3514
+ continue;
3515
+ }
3516
+ pathProperties[name] = converted.schema;
3517
+ } else {
3518
+ queryProperties[name] = converted.schema;
3519
+ if (required2) requiredQuery.push(name);
3520
+ }
3521
+ }
3522
+ for (const param of templateParams) {
3523
+ if (!Object.hasOwn(pathProperties, param)) {
3524
+ return skip(
3525
+ "undeclared-path-parameter",
3526
+ `"{${param}}" appears in the path but is not declared as a path parameter, so a caller could never supply it`
3527
+ );
3528
+ }
3529
+ }
3530
+ const body = resolveBody(document, candidate);
3531
+ if (body.kind === "unsupported") {
3532
+ if (body.required) return skip("unsupported-request-body", body.reason);
3533
+ warn("unsupported-request-body", `Omitted the request body: ${body.reason}`);
3534
+ }
3535
+ if (body.kind === "schema") {
3536
+ for (const keyword of body.dropped) dropped.add(keyword);
3537
+ }
3538
+ const properties = {};
3539
+ const required = [];
3540
+ const bindings = {};
3541
+ if (Object.keys(pathProperties).length > 0) {
3542
+ properties[PATH_GROUP] = closedObject(pathProperties, Object.keys(pathProperties));
3543
+ required.push(PATH_GROUP);
3544
+ bindings.path = PATH_GROUP;
3545
+ }
3546
+ if (Object.keys(queryProperties).length > 0) {
3547
+ properties[QUERY_GROUP] = closedObject(queryProperties, requiredQuery);
3548
+ if (requiredQuery.length > 0) required.push(QUERY_GROUP);
3549
+ bindings.query = QUERY_GROUP;
3550
+ }
3551
+ if (body.kind === "schema") {
3552
+ properties[BODY_GROUP] = body.schema;
3553
+ if (body.required) required.push(BODY_GROUP);
3554
+ bindings.body = BODY_GROUP;
3555
+ }
3556
+ return {
3557
+ supported: true,
3558
+ inputSchema: {
3559
+ type: "object",
3560
+ properties,
3561
+ ...required.length > 0 ? { required } : {},
3562
+ additionalProperties: false
3563
+ },
3564
+ inputBindings: bindings,
3565
+ ...body.kind === "schema" && body.contentType !== void 0 ? { contentType: body.contentType } : {},
3566
+ droppedKeywords: [...dropped],
3567
+ diagnostics
3568
+ };
3569
+ }
3570
+ function mergeParameters(document, parameters) {
3571
+ const byIdentity = /* @__PURE__ */ new Map();
3572
+ for (const raw of parameters) {
3573
+ const resolved = dereference(document, raw).value;
3574
+ if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) continue;
3575
+ const parameter = resolved;
3576
+ byIdentity.set(`${String(parameter["in"])}:${String(parameter["name"])}`, parameter);
3577
+ }
3578
+ return [...byIdentity.values()];
3579
+ }
3580
+ function describeUnsupportedParameter(document, parameter, location) {
3581
+ if (parameter["content"] !== void 0) {
3582
+ return "uses the `content` form, whose media-type serialization the gateway does not perform";
3583
+ }
3584
+ const style = parameter["style"];
3585
+ if (location === "query" && typeof style === "string" && UNSUPPORTED_QUERY_STYLES.has(style)) {
3586
+ return `uses style "${style}", which the gateway does not serialize`;
3587
+ }
3588
+ if (location === "path" && typeof style === "string" && style !== "simple") {
3589
+ return `uses style "${style}"; the gateway substitutes plain values only`;
3590
+ }
3591
+ const converted = convertSchema(document, parameter["schema"]);
3592
+ if (!converted.supported) return converted.reason;
3593
+ if (!isPrimitiveSchema(converted.schema)) {
3594
+ return "is not a primitive; object and array parameters need serialization the gateway does not perform";
3595
+ }
3596
+ return void 0;
3597
+ }
3598
+ function resolveBody(document, candidate) {
3599
+ if (candidate.requestBody === void 0) return { kind: "none" };
3600
+ const resolved = dereference(document, candidate.requestBody).value;
3601
+ if (typeof resolved !== "object" || resolved === null || Array.isArray(resolved)) {
3602
+ return { kind: "none" };
3603
+ }
3604
+ const requestBody = resolved;
3605
+ const required = requestBody["required"] === true;
3606
+ if (candidate.method === "GET" || candidate.method === "DELETE") {
3607
+ return {
3608
+ kind: "unsupported",
3609
+ required: false,
3610
+ reason: `a ${candidate.method} request body is not sent by the gateway`
3611
+ };
3612
+ }
3613
+ const content = requestBody["content"];
3614
+ if (!isRecord3(content)) return { kind: "none" };
3615
+ const mediaType = pickJsonMediaType(Object.keys(content));
3616
+ if (mediaType === void 0) {
3617
+ return {
3618
+ kind: "unsupported",
3619
+ required,
3620
+ 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`
3621
+ };
3622
+ }
3623
+ const media = content[mediaType];
3624
+ const schemaNode = isRecord3(media) ? media["schema"] : void 0;
3625
+ if (schemaNode === void 0) {
3626
+ return {
3627
+ kind: "schema",
3628
+ schema: { type: "object", additionalProperties: true },
3629
+ required,
3630
+ ...mediaType === "application/json" ? {} : { contentType: mediaType },
3631
+ dropped: []
3632
+ };
3633
+ }
3634
+ const converted = convertSchema(document, schemaNode);
3635
+ if (!converted.supported) return { kind: "unsupported", required, reason: converted.reason };
3636
+ return {
3637
+ kind: "schema",
3638
+ schema: converted.schema,
3639
+ required,
3640
+ ...mediaType === "application/json" ? {} : { contentType: mediaType },
3641
+ dropped: converted.dropped
3642
+ };
3643
+ }
3644
+ function pickJsonMediaType(keys) {
3645
+ const normalised = keys.map((key) => ({ key, type: key.split(";")[0]?.trim().toLowerCase() }));
3646
+ const exact = normalised.find((entry) => entry.type === "application/json");
3647
+ if (exact !== void 0) return exact.key;
3648
+ return [...normalised].sort((a, b) => (a.type ?? "").localeCompare(b.type ?? "")).find((entry) => entry.type?.startsWith("application/") && entry.type.endsWith("+json"))?.key;
3649
+ }
3650
+ function closedObject(properties, required) {
3651
+ return {
3652
+ type: "object",
3653
+ properties,
3654
+ ...required.length > 0 ? { required: [...required] } : {},
3655
+ additionalProperties: false
3656
+ };
3657
+ }
3658
+ function isRecord3(value) {
3659
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3660
+ }
3661
+
3662
+ // src/openapi/draft.ts
3663
+ function buildResourceDrafts(loaded, options = {}) {
3664
+ const discovery = discoverOperations(
3665
+ loaded,
3666
+ options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
3667
+ );
3668
+ const diagnostics = [...discovery.diagnostics];
3669
+ const drafts = [];
3670
+ const wanted = options.include?.operationIds;
3671
+ const wantedTags = options.include?.tags;
3672
+ const matched = /* @__PURE__ */ new Set();
3673
+ for (const candidate of discovery.operations) {
3674
+ if (wanted !== void 0 && !selects(wanted, candidate)) continue;
3675
+ if (wantedTags !== void 0 && !candidate.tags.some((tag) => wantedTags.includes(tag))) {
3676
+ continue;
3677
+ }
3678
+ if (wanted !== void 0) {
3679
+ for (const id of wanted) if (selects([id], candidate)) matched.add(id);
3680
+ }
3681
+ const mapping = mapRequest(loaded, candidate);
3682
+ diagnostics.push(...mapping.diagnostics);
3683
+ if (!mapping.supported) continue;
3684
+ const dropped = new Set(mapping.droppedKeywords);
3685
+ const output = selectOutputSchema(loaded.document, candidate, diagnostics, dropped);
3686
+ if (dropped.size > 0) {
3687
+ diagnostics.push({
3688
+ severity: "warning",
3689
+ code: "unenforced-schema-constraints",
3690
+ operation: candidate.resourceId,
3691
+ message: `${candidate.resourceId}: dropped ${[...dropped].join(", ")} - this gateway validates structure only, and keeping them would advertise checks it never performs`
3692
+ });
3693
+ }
3694
+ const review = [];
3695
+ if (declaresSecurity(candidate)) {
3696
+ diagnostics.push({
3697
+ severity: "warning",
3698
+ code: "backend-authentication-required",
3699
+ operation: candidate.resourceId,
3700
+ message: `${candidate.resourceId} declares backend authentication. OpenAPI credentials were not imported. Configure backend.headers with environment placeholders before enabling the resource`
3701
+ });
3702
+ review.push(
3703
+ "This operation declares backend authentication. No credential was imported.",
3704
+ "Add it under backend.headers with an ${ENV_VAR} placeholder before enabling."
3705
+ );
3706
+ }
3707
+ if (options.policy?.pricing === void 0 || options.policy.expose === void 0) {
3708
+ review.push(
3709
+ "REVIEW: pricing and exposure are not inferred from OpenAPI. Add e.g.",
3710
+ ' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC }',
3711
+ " expose: [http] # http | mcp | a2a"
3712
+ );
3713
+ }
3714
+ drafts.push({
3715
+ id: candidate.resourceId,
3716
+ ...candidate.operationId !== void 0 ? { operationId: candidate.operationId } : {},
3717
+ tags: candidate.tags,
3718
+ source: `${candidate.method} ${candidate.path}`,
3719
+ resource: {
3720
+ name: candidate.name,
3721
+ ...candidate.description !== void 0 ? { description: candidate.description } : {},
3722
+ input: mapping.inputSchema,
3723
+ ...output !== void 0 ? { output } : {},
3724
+ backend: {
3725
+ type: "http",
3726
+ method: candidate.method,
3727
+ url: candidate.backendUrl,
3728
+ ...mapping.contentType !== void 0 ? { headers: { "Content-Type": mapping.contentType } } : {},
3729
+ ...Object.keys(mapping.inputBindings).length > 0 ? { inputBindings: mapping.inputBindings } : {}
3730
+ },
3731
+ // Policy is written only when the operator asked for it. An absent
3732
+ // `pricing`/`expose` is what makes the draft fail config validation
3733
+ // until a human has decided.
3734
+ ...options.policy?.pricing !== void 0 ? { pricing: options.policy.pricing } : {},
3735
+ ...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {},
3736
+ ...options.policy?.payments !== void 0 ? { payments: [...options.policy.payments] } : {}
3737
+ },
3738
+ review
3739
+ });
3740
+ }
3741
+ return {
3742
+ version: loaded.version,
3743
+ sourcePath: loaded.sourcePath,
3744
+ drafts,
3745
+ diagnostics,
3746
+ unmatchedOperationIds: (wanted ?? []).filter((id) => !matched.has(id))
3747
+ };
3748
+ }
3749
+ function selects(ids, candidate) {
3750
+ return ids.includes(candidate.resourceId) || candidate.operationId !== void 0 && ids.includes(candidate.operationId);
3751
+ }
3752
+ function declaresSecurity(candidate) {
3753
+ return candidate.security.some(
3754
+ (requirement) => typeof requirement === "object" && requirement !== null && Object.keys(requirement).length > 0
3755
+ );
3756
+ }
3757
+ function selectOutputSchema(document, candidate, diagnostics, dropped) {
3758
+ const responses = candidate.responses;
3759
+ if (!isRecord4(responses)) return void 0;
3760
+ const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
3761
+ if (successes.length === 0) return void 0;
3762
+ const withBody = successes.filter((status) => {
3763
+ const response2 = responses[status];
3764
+ const content2 = isRecord4(response2) ? response2["content"] : void 0;
3765
+ return isRecord4(content2) && pickJsonMediaType(Object.keys(content2)) !== void 0;
3766
+ });
3767
+ const chosen = withBody[0];
3768
+ if (chosen === void 0) return void 0;
3769
+ const response = responses[chosen];
3770
+ const content = response["content"];
3771
+ const mediaType = pickJsonMediaType(Object.keys(content));
3772
+ const media = content[mediaType];
3773
+ const schemaNode = isRecord4(media) ? media["schema"] : void 0;
3774
+ if (schemaNode === void 0) return void 0;
3775
+ const converted = convertSchema(document, schemaNode);
3776
+ if (!converted.supported) {
3777
+ diagnostics.push({
3778
+ severity: "warning",
3779
+ code: "unsupported-output-schema",
3780
+ operation: candidate.resourceId,
3781
+ message: `${candidate.resourceId}: omitted the output schema (${converted.reason})`
3782
+ });
3783
+ return void 0;
3784
+ }
3785
+ for (const keyword of converted.dropped) dropped.add(keyword);
3786
+ if (withBody.length > 1) {
3787
+ diagnostics.push({
3788
+ severity: "warning",
3789
+ code: "multiple-success-responses",
3790
+ operation: candidate.resourceId,
3791
+ message: `${candidate.resourceId}: responses ${withBody.join(", ")} all carry a JSON body; used ${chosen}`
3792
+ });
3793
+ }
3794
+ return converted.schema;
3795
+ }
3796
+ function rank(status) {
3797
+ const preferred = ["200", "201", "202"].indexOf(status);
3798
+ return preferred === -1 ? 100 + Number(status) : preferred;
3799
+ }
3800
+ function renderResourcesYaml(result) {
3801
+ const resources = {};
3802
+ for (const draft of result.drafts) resources[draft.id] = draft.resource;
3803
+ const doc = new Document({ resources });
3804
+ doc.commentBefore = [
3805
+ " Generated by agent-commerce import openapi. Review before use.",
3806
+ ` Source: ${basename(result.sourcePath)} (OpenAPI ${result.version})`,
3807
+ " Merge the resources below into config.yaml once pricing, exposure and",
3808
+ " any backend authentication have been decided."
3809
+ ].join("\n");
3810
+ const map = doc.getIn(["resources"], true);
3811
+ map?.items.forEach((item, index) => {
3812
+ const draft = result.drafts[index];
3813
+ const key = item.key;
3814
+ if (draft === void 0 || draft.review.length === 0 || key === null) return;
3815
+ key.commentBefore = draft.review.map((line) => ` ${line}`).join("\n");
3816
+ });
3817
+ return doc.toString({ lineWidth: 0 });
3818
+ }
3819
+ function basename(path2) {
3820
+ return path2.split(/[\\/]/).pop() ?? path2;
3821
+ }
3822
+ function isRecord4(value) {
3823
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3824
+ }
3825
+
3826
+ // src/openapi/load.ts
3827
+ init_errors();
3828
+ var MAX_SOURCE_BYTES = 10 * 1024 * 1024;
3829
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
3830
+ var SUPPORTED_VERSIONS = /* @__PURE__ */ new Set(["3.0", "3.1", "3.2"]);
3831
+ function invalid2(message, details, cause) {
3832
+ return new CommerceError("CONFIG_INVALID", message, {
3833
+ details,
3834
+ ...cause !== void 0 ? { cause } : {}
3835
+ });
3836
+ }
3837
+ async function loadOpenApiDocument(sourcePath) {
3838
+ const details = { sourcePath };
3839
+ let stats;
3840
+ try {
3841
+ stats = await stat(sourcePath);
3842
+ } catch (error) {
3843
+ throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
3844
+ }
3845
+ if (stats.isDirectory()) {
3846
+ throw invalid2(`"${sourcePath}" is a directory, not an OpenAPI document`, details);
3847
+ }
3848
+ if (stats.size > MAX_SOURCE_BYTES) {
3849
+ throw invalid2(
3850
+ `OpenAPI document "${sourcePath}" is ${stats.size} bytes, over the ${MAX_SOURCE_BYTES}-byte limit`,
3851
+ { ...details, size: stats.size, maxBytes: MAX_SOURCE_BYTES }
3852
+ );
3853
+ }
3854
+ const extension = extname(sourcePath).toLowerCase();
3855
+ if (!SUPPORTED_EXTENSIONS.has(extension)) {
3856
+ throw invalid2(
3857
+ `OpenAPI document "${sourcePath}" has an unsupported extension "${extension || "(none)"}". Supported: .yaml, .yml, .json`,
3858
+ details
3859
+ );
3860
+ }
3861
+ const source = await readFile(sourcePath, "utf8");
3862
+ if (source.trim() === "") {
3863
+ throw invalid2(`OpenAPI document "${sourcePath}" is empty`, details);
3864
+ }
3865
+ let parsed;
3866
+ try {
3867
+ parsed = extension === ".json" ? JSON.parse(source) : parse(source);
3868
+ } catch (error) {
3869
+ throw invalid2(
3870
+ `OpenAPI document "${sourcePath}" is not valid ${extension === ".json" ? "JSON" : "YAML"}: ${error instanceof Error ? error.message : String(error)}`,
3871
+ details,
3872
+ error
3873
+ );
3874
+ }
3875
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3876
+ throw invalid2(`OpenAPI document "${sourcePath}" is not an object`, details);
3877
+ }
3878
+ const document = parsed;
3879
+ const version = readVersion(document, sourcePath);
3880
+ rejectExternalReferences(document, sourcePath);
3881
+ const result = await validate(document);
3882
+ if (!result.valid) {
3883
+ const first = result.errors?.[0];
3884
+ throw invalid2(
3885
+ `OpenAPI document "${sourcePath}" is not a valid OpenAPI ${version} description: ${first ? `${first.message}${first.path ? ` (at ${first.path})` : ""}` : "unknown validation error"}`,
3886
+ { ...details, errors: result.errors?.slice(0, 10) ?? [] }
3887
+ );
3888
+ }
3889
+ return { version, document, sourcePath };
3890
+ }
3891
+ function readVersion(document, sourcePath) {
3892
+ const details = { sourcePath };
3893
+ if (typeof document["swagger"] === "string") {
3894
+ throw invalid2(
3895
+ `"${sourcePath}" is a Swagger ${document["swagger"]} document. Only OpenAPI 3.0, 3.1 and 3.2 are supported - convert it first`,
3896
+ details
3897
+ );
3898
+ }
3899
+ const declared = document["openapi"];
3900
+ if (typeof declared !== "string") {
3901
+ throw invalid2(`"${sourcePath}" has no "openapi" version field`, details);
3902
+ }
3903
+ const featureVersion = /^(\d+\.\d+)(?:\.|$)/.exec(declared)?.[1];
3904
+ if (featureVersion === void 0 || !SUPPORTED_VERSIONS.has(featureVersion)) {
3905
+ throw invalid2(
3906
+ `OpenAPI version "${declared}" is not supported. Supported: 3.0.x, 3.1.x, 3.2.x`,
3907
+ { ...details, version: declared }
3908
+ );
3909
+ }
3910
+ return featureVersion;
3911
+ }
3912
+ function rejectExternalReferences(document, sourcePath) {
3913
+ const seen = /* @__PURE__ */ new WeakSet();
3914
+ const queue = [document];
3915
+ while (queue.length > 0) {
3916
+ const node = queue.pop();
3917
+ if (typeof node !== "object" || node === null) continue;
3918
+ if (seen.has(node)) continue;
3919
+ seen.add(node);
3920
+ if (Array.isArray(node)) {
3921
+ queue.push(...node);
3922
+ continue;
3923
+ }
3924
+ for (const [key, value] of Object.entries(node)) {
3925
+ if (key === "$ref" && typeof value === "string" && !value.startsWith("#")) {
3926
+ throw invalid2(
3927
+ `OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
3928
+ { sourcePath, ref: value }
3929
+ );
3930
+ }
3931
+ queue.push(value);
3932
+ }
3933
+ }
3934
+ }
3935
+
3936
+ // src/cli/commands/import-openapi.ts
3937
+ function defaultOutputPath(source) {
3938
+ const name = basename$1(source);
3939
+ const stem = name.slice(0, name.length - extname(name).length) || name;
3940
+ return `${stem}.agent-commerce.yaml`;
3941
+ }
3942
+ async function runImportOpenApi(options, io, deps = {}) {
3943
+ const fileExists = deps.fileExists ?? existsSync;
3944
+ const outputPath = options.output ?? defaultOutputPath(options.source);
3945
+ let policy;
3946
+ try {
3947
+ policy = buildPolicy(options);
3948
+ } catch (error) {
3949
+ io.stderr(`FAIL ${error instanceof Error ? error.message : String(error)}`);
3950
+ return 1;
3951
+ }
3952
+ if (fileExists(outputPath) && options.force !== true) {
3953
+ io.stderr(`FAIL ${outputPath} already exists. Re-run with --force to overwrite.`);
3954
+ return 1;
3955
+ }
3956
+ let result;
3957
+ try {
3958
+ const loaded = await loadOpenApiDocument(options.source);
3959
+ result = buildResourceDrafts(loaded, {
3960
+ ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
3961
+ ...Object.keys(policy).length > 0 ? { policy } : {},
3962
+ ...options.operations !== void 0 || options.tags !== void 0 ? {
3963
+ include: {
3964
+ ...options.operations !== void 0 ? { operationIds: options.operations } : {},
3965
+ ...options.tags !== void 0 ? { tags: options.tags } : {}
3966
+ }
3967
+ } : {}
3968
+ });
3969
+ } catch (error) {
3970
+ io.stderr(`FAIL ${error instanceof Error ? error.message : String(error)}`);
3971
+ return 1;
3972
+ }
3973
+ const skipped = result.diagnostics.filter((entry) => entry.severity === "error");
3974
+ const warnings = result.diagnostics.filter((entry) => entry.severity === "warning");
3975
+ const failures = [];
3976
+ if (result.unmatchedOperationIds.length > 0) {
3977
+ failures.push(`no operation matched ${result.unmatchedOperationIds.join(", ")}`);
3978
+ }
3979
+ if (result.drafts.length === 0) {
3980
+ failures.push("no supported operations were imported");
3981
+ }
3982
+ if (options.strict === true && warnings.length > 0) {
3983
+ failures.push(`${warnings.length} warning(s) with --strict`);
3984
+ }
3985
+ const wrote = failures.length === 0;
3986
+ if (wrote) {
3987
+ try {
3988
+ await writeAtomically(outputPath, renderResourcesYaml(result), deps);
3989
+ } catch (error) {
3990
+ io.stderr(`FAIL ${outputPath} could not be written: ${describe(error)}`);
3991
+ return 1;
3992
+ }
3993
+ }
3994
+ if (options.json === true) {
3995
+ io.stdout(
3996
+ JSON.stringify(
3997
+ {
3998
+ source: options.source,
3999
+ openapi: result.version,
4000
+ output: wrote ? outputPath : null,
4001
+ imported: result.drafts.length,
4002
+ skipped: skipped.length,
4003
+ warnings: warnings.length,
4004
+ resources: result.drafts.map((draft) => ({
4005
+ id: draft.id,
4006
+ source: draft.source,
4007
+ ...draft.operationId !== void 0 ? { operationId: draft.operationId } : {},
4008
+ tags: draft.tags
4009
+ })),
4010
+ diagnostics: result.diagnostics,
4011
+ unmatchedOperationIds: result.unmatchedOperationIds,
4012
+ exitCode: failures.length === 0 ? 0 : 1
4013
+ },
4014
+ null,
4015
+ 2
4016
+ )
4017
+ );
4018
+ } else {
4019
+ printSummary(result, { outputPath, wrote, policy }, io);
4020
+ }
4021
+ for (const failure of failures) io.stderr(`FAIL ${failure}`);
4022
+ return failures.length === 0 ? 0 : 1;
4023
+ }
4024
+ function buildPolicy(options) {
4025
+ const policy = {};
4026
+ if (options.free === true) policy.pricing = { type: "free" };
4027
+ if (options.expose !== void 0) {
4028
+ const requested = options.expose.split(",").map((name) => name.trim()).filter((name) => name !== "");
4029
+ if (requested.length === 0) {
4030
+ throw new Error(`--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`);
4031
+ }
4032
+ for (const name of requested) {
4033
+ if (!PROTOCOL_NAMES.includes(name)) {
4034
+ throw new Error(
4035
+ `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`
4036
+ );
4037
+ }
4038
+ }
4039
+ policy.expose = requested;
4040
+ }
4041
+ return policy;
4042
+ }
4043
+ async function writeAtomically(outputPath, content, deps) {
4044
+ if (deps.writeFile !== void 0) {
4045
+ await deps.writeFile(outputPath, content);
4046
+ return;
4047
+ }
4048
+ const target = resolve(outputPath);
4049
+ const temporary = `${target}.${process.pid}.tmp`;
4050
+ try {
4051
+ await writeFile(temporary, content, "utf8");
4052
+ await rename(temporary, target);
4053
+ } catch (error) {
4054
+ await rm(temporary, { force: true });
4055
+ throw error;
4056
+ }
4057
+ }
4058
+ function printSummary(result, context, io) {
4059
+ const skipped = result.diagnostics.filter((entry) => entry.severity === "error");
4060
+ const warnings = result.diagnostics.filter((entry) => entry.severity === "warning");
4061
+ io.stdout(`OpenAPI ${result.version}: ${result.sourcePath}`);
4062
+ io.stdout("");
4063
+ io.stdout(`Imported: ${result.drafts.length}`);
4064
+ io.stdout(`Skipped: ${skipped.length}`);
4065
+ io.stdout(`Warnings: ${warnings.length}`);
4066
+ if (skipped.length > 0) {
4067
+ io.stdout("");
4068
+ io.stdout("Skipped operations:");
4069
+ for (const entry of skipped) io.stdout(` ${entry.message}`);
4070
+ }
4071
+ if (warnings.length > 0) {
4072
+ io.stdout("");
4073
+ io.stdout("Warnings:");
4074
+ for (const entry of warnings) io.stdout(` ${entry.message}`);
4075
+ }
4076
+ if (context.wrote) {
4077
+ io.stdout("");
4078
+ io.stdout("Generated:");
4079
+ io.stdout(` ${context.outputPath}`);
4080
+ }
4081
+ io.stdout("");
4082
+ if (context.policy.pricing === void 0 || context.policy.expose === void 0) {
4083
+ io.stdout("Pricing/exposure were not inferred.");
4084
+ }
4085
+ io.stdout("Review the generated resources before merging them into config.yaml.");
4086
+ }
4087
+ function describe(error) {
4088
+ return error instanceof Error ? error.message : String(error);
4089
+ }
4090
+
2555
4091
  // src/cli/commands/init.ts
2556
4092
  init_filename();
2557
4093
 
@@ -2600,7 +4136,7 @@ async function runValidate(options, io, deps = {}) {
2600
4136
  io.stdout(` merchant: ${config.merchant.name} (${config.merchant.id})`);
2601
4137
  io.stdout(` resources: ${config.resources.length}`);
2602
4138
  io.stdout(
2603
- ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"} a2a=${config.protocols.a2a.enabled ? "on" : "off"}`
4139
+ ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"} a2a=${config.protocols.a2a.enabled ? "on" : "off"} acp=${config.protocols.acp.enabled ? "on" : "off"}`
2604
4140
  );
2605
4141
  io.stdout(` payments: x402=${config.payments.x402?.enabled === true ? "on" : "off"}`);
2606
4142
  return 0;
@@ -2770,7 +4306,7 @@ var processIo = {
2770
4306
  // src/cli/program.ts
2771
4307
  function buildProgram(io = processIo) {
2772
4308
  const program = new Command();
2773
- 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({
4309
+ 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({
2774
4310
  writeOut: (str) => io.stdout(str.replace(/\n+$/, "")),
2775
4311
  writeErr: (str) => io.stderr(str.replace(/\n+$/, ""))
2776
4312
  });
@@ -2799,6 +4335,31 @@ function buildProgram(io = processIo) {
2799
4335
  const code = await runInit({ outputPath: opts.output, force: opts.force, yes: opts.yes }, io);
2800
4336
  process.exitCode = code;
2801
4337
  });
4338
+ const importCommand = program.command("import").description("Generate Agent Commerce resource drafts from an API description.");
4339
+ 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(
4340
+ "--operation <operationId>",
4341
+ "import only this operation (repeatable)",
4342
+ collect,
4343
+ void 0
4344
+ ).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(
4345
+ async (source, opts) => {
4346
+ process.exitCode = await runImportOpenApi(
4347
+ {
4348
+ source,
4349
+ ...opts.output !== void 0 ? { output: opts.output } : {},
4350
+ force: opts.force,
4351
+ ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {},
4352
+ ...opts.operation !== void 0 ? { operations: opts.operation } : {},
4353
+ ...opts.tag !== void 0 ? { tags: opts.tag } : {},
4354
+ free: opts.free,
4355
+ ...opts.expose !== void 0 ? { expose: opts.expose } : {},
4356
+ strict: opts.strict,
4357
+ json: opts.json
4358
+ },
4359
+ io
4360
+ );
4361
+ }
4362
+ );
2802
4363
  program.command("demo").description(
2803
4364
  "Run the documented docker compose + demo agent quickstart, verifying health between steps."
2804
4365
  ).action(async () => {
@@ -2806,6 +4367,9 @@ function buildProgram(io = processIo) {
2806
4367
  });
2807
4368
  return program;
2808
4369
  }
4370
+ function collect(value, previous) {
4371
+ return [...previous ?? [], value];
4372
+ }
2809
4373
 
2810
4374
  // src/cli/index.ts
2811
4375
  for (const stream of [process.stdout, process.stderr]) {