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