@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/README.md +70 -47
- package/dist/{chunk-64UZVJEO.js → chunk-N3DNLXGC.js} +7 -4
- package/dist/chunk-N3DNLXGC.js.map +1 -0
- package/dist/{chunk-M4I7I2UY.js → chunk-TBZA6Z5B.js} +3 -3
- package/dist/{chunk-M4I7I2UY.js.map → chunk-TBZA6Z5B.js.map} +1 -1
- package/dist/cli/index.js +1690 -126
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +154 -7
- package/dist/index.js +5499 -271
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{protocol-adapter-DZ8qkkAa.d.ts → protocol-adapter-DTFGWGXq.d.ts} +2 -2
- package/dist/{runtime-CC6EFmLP.d.ts → runtime-Plk5Peqi.d.ts} +34 -3
- package/dist/x402.d.ts +1 -1
- package/dist/x402.js +3 -3
- package/package.json +14 -3
- package/dist/chunk-64UZVJEO.js.map +0 -1
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,
|
|
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
|
|
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}")
|
|
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:
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 "/"
|
|
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 "/"
|
|
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
|
|
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"
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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]) {
|