mcp-from-openapi 2.5.1 → 2.6.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 +73 -67
- package/annotations.d.ts +57 -0
- package/client-targets.d.ts +55 -0
- package/errors.d.ts +7 -0
- package/esm/index.mjs +2341 -1191
- package/esm/package.json +1 -1
- package/generator.d.ts +1 -1
- package/index.d.ts +10 -2
- package/index.js +2356 -1192
- package/package.json +1 -1
- package/parameter-resolver.d.ts +23 -2
- package/request-builder.d.ts +52 -0
- package/response-builder.d.ts +1 -0
- package/schema-builder.d.ts +17 -0
- package/sdk.d.ts +22 -0
- package/types.d.ts +164 -19
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
OpenAPIToolGenerator: () => OpenAPIToolGenerator,
|
|
39
39
|
ParameterResolver: () => ParameterResolver,
|
|
40
40
|
ParseError: () => ParseError,
|
|
41
|
+
RequestBuildError: () => RequestBuildError,
|
|
41
42
|
ResponseBuilder: () => ResponseBuilder,
|
|
42
43
|
SchemaBuilder: () => SchemaBuilder,
|
|
43
44
|
SchemaError: () => SchemaError,
|
|
@@ -45,17 +46,30 @@ __export(index_exports, {
|
|
|
45
46
|
SsrfError: () => SsrfError,
|
|
46
47
|
ValidationError: () => ValidationError,
|
|
47
48
|
Validator: () => Validator,
|
|
49
|
+
applyClientTarget: () => applyClientTarget,
|
|
48
50
|
assertUrlSafe: () => assertUrlSafe,
|
|
51
|
+
buildHttpRequest: () => buildHttpRequest,
|
|
52
|
+
collapseNestedUnions: () => collapseNestedUnions,
|
|
53
|
+
collapseRootCompositions: () => collapseRootCompositions,
|
|
49
54
|
createSecurityContext: () => createSecurityContext,
|
|
50
55
|
decodeIpv4MappedIpv6: () => decodeIpv4MappedIpv6,
|
|
51
56
|
defaultLookup: () => defaultLookup,
|
|
57
|
+
demoteFormats: () => demoteFormats,
|
|
58
|
+
enforceClosedObjects: () => enforceClosedObjects,
|
|
59
|
+
ensureArrayItems: () => ensureArrayItems,
|
|
60
|
+
extractExtensionOverrides: () => extractExtensionOverrides,
|
|
61
|
+
inferAnnotationsFromMethod: () => inferAnnotationsFromMethod,
|
|
62
|
+
inlineLocalRefs: () => inlineLocalRefs,
|
|
52
63
|
isBlockedAddress: () => isBlockedAddress,
|
|
53
64
|
isBlockedHostname: () => isBlockedHostname,
|
|
54
65
|
isReferenceObject: () => isReferenceObject,
|
|
55
66
|
normalizeSsrfOptions: () => normalizeSsrfOptions,
|
|
67
|
+
requireAllProperties: () => requireAllProperties,
|
|
68
|
+
resolveExtensionEnabled: () => resolveExtensionEnabled,
|
|
56
69
|
resolveSchemaFormats: () => resolveSchemaFormats,
|
|
57
70
|
safeFetch: () => safeFetch,
|
|
58
|
-
toJsonSchema: () => toJsonSchema
|
|
71
|
+
toJsonSchema: () => toJsonSchema,
|
|
72
|
+
toSdkTool: () => toSdkTool
|
|
59
73
|
});
|
|
60
74
|
module.exports = __toCommonJS(index_exports);
|
|
61
75
|
|
|
@@ -71,7 +85,25 @@ function toJsonSchema(schema) {
|
|
|
71
85
|
return { $ref: schema.$ref };
|
|
72
86
|
}
|
|
73
87
|
const { exclusiveMaximum, exclusiveMinimum, maximum, minimum, ...rest } = schema;
|
|
74
|
-
const
|
|
88
|
+
const { nullable, example, ...cleanRest } = rest;
|
|
89
|
+
const result = { ...cleanRest };
|
|
90
|
+
delete result["xml"];
|
|
91
|
+
let wrapNullable = false;
|
|
92
|
+
if (nullable === true) {
|
|
93
|
+
const type = result["type"];
|
|
94
|
+
if (type === void 0) {
|
|
95
|
+
wrapNullable = true;
|
|
96
|
+
} else if (Array.isArray(type)) {
|
|
97
|
+
if (!type.includes("null")) {
|
|
98
|
+
result["type"] = [...type, "null"];
|
|
99
|
+
}
|
|
100
|
+
} else if (type !== "null") {
|
|
101
|
+
result["type"] = [type, "null"];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (example !== void 0 && !Array.isArray(result["examples"])) {
|
|
105
|
+
result["examples"] = [example];
|
|
106
|
+
}
|
|
75
107
|
if (typeof exclusiveMaximum === "boolean") {
|
|
76
108
|
if (exclusiveMaximum && maximum !== void 0) {
|
|
77
109
|
result["exclusiveMaximum"] = maximum;
|
|
@@ -125,16 +157,57 @@ function toJsonSchema(schema) {
|
|
|
125
157
|
if (result["not"]) {
|
|
126
158
|
result["not"] = toJsonSchema(result["not"]);
|
|
127
159
|
}
|
|
160
|
+
for (const key of ["patternProperties", "$defs", "definitions", "dependentSchemas"]) {
|
|
161
|
+
const value = result[key];
|
|
162
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
163
|
+
const mapped = {};
|
|
164
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
165
|
+
mapped[name] = toJsonSchema(sub);
|
|
166
|
+
}
|
|
167
|
+
result[key] = mapped;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const key of [
|
|
171
|
+
"contains",
|
|
172
|
+
"propertyNames",
|
|
173
|
+
"if",
|
|
174
|
+
"then",
|
|
175
|
+
"else",
|
|
176
|
+
"contentSchema",
|
|
177
|
+
"unevaluatedItems",
|
|
178
|
+
"unevaluatedProperties"
|
|
179
|
+
]) {
|
|
180
|
+
const value = result[key];
|
|
181
|
+
if (value && typeof value === "object") {
|
|
182
|
+
result[key] = toJsonSchema(value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(result["prefixItems"])) {
|
|
186
|
+
result["prefixItems"] = result["prefixItems"].map(toJsonSchema);
|
|
187
|
+
}
|
|
188
|
+
if (wrapNullable) {
|
|
189
|
+
const wrapper = {};
|
|
190
|
+
for (const key of ["title", "description", "deprecated", "examples"]) {
|
|
191
|
+
if (result[key] !== void 0) {
|
|
192
|
+
wrapper[key] = result[key];
|
|
193
|
+
delete result[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
wrapper["anyOf"] = [result, { type: "null" }];
|
|
197
|
+
return wrapper;
|
|
198
|
+
}
|
|
128
199
|
return result;
|
|
129
200
|
}
|
|
130
201
|
|
|
131
202
|
// src/parameter-resolver.ts
|
|
132
203
|
var ParameterResolver = class {
|
|
133
204
|
namingStrategy;
|
|
134
|
-
|
|
205
|
+
includeExamples;
|
|
206
|
+
constructor(namingStrategy, options) {
|
|
135
207
|
this.namingStrategy = namingStrategy ?? {
|
|
136
208
|
conflictResolver: this.defaultConflictResolver
|
|
137
209
|
};
|
|
210
|
+
this.includeExamples = options?.includeExamples ?? false;
|
|
138
211
|
}
|
|
139
212
|
/**
|
|
140
213
|
* Default conflict resolver: prefix with location
|
|
@@ -166,7 +239,8 @@ var ParameterResolver = class {
|
|
|
166
239
|
style: param.style,
|
|
167
240
|
explode: param.explode,
|
|
168
241
|
allowReserved: param.allowReserved,
|
|
169
|
-
deprecated: param.deprecated
|
|
242
|
+
deprecated: param.deprecated,
|
|
243
|
+
examples: this.includeExamples ? collectExampleValues(param.example, param.examples) : void 0
|
|
170
244
|
};
|
|
171
245
|
if (!parametersByName.has(param.name)) {
|
|
172
246
|
parametersByName.set(param.name, []);
|
|
@@ -177,7 +251,8 @@ var ParameterResolver = class {
|
|
|
177
251
|
const contentType = this.selectContentType(requestBody.content);
|
|
178
252
|
const mediaType = requestBody.content[contentType];
|
|
179
253
|
if (mediaType?.schema) {
|
|
180
|
-
this.
|
|
254
|
+
const mediaExamples = this.includeExamples ? collectExampleValues(mediaType.example, mediaType.examples) : void 0;
|
|
255
|
+
this.extractBodyParameters(mediaType.schema, parametersByName, requestBody.required ?? false, contentType, mediaExamples, mediaType.encoding);
|
|
181
256
|
}
|
|
182
257
|
}
|
|
183
258
|
const properties = {};
|
|
@@ -198,7 +273,9 @@ var ParameterResolver = class {
|
|
|
198
273
|
required: param.required,
|
|
199
274
|
style: param.style,
|
|
200
275
|
explode: param.explode,
|
|
201
|
-
|
|
276
|
+
allowReserved: param.allowReserved,
|
|
277
|
+
serialization: param.serialization,
|
|
278
|
+
...param.wholeBody && { wholeBody: true }
|
|
202
279
|
});
|
|
203
280
|
} else {
|
|
204
281
|
params.forEach((param, index) => {
|
|
@@ -214,7 +291,9 @@ var ParameterResolver = class {
|
|
|
214
291
|
required: param.required,
|
|
215
292
|
style: param.style,
|
|
216
293
|
explode: param.explode,
|
|
217
|
-
|
|
294
|
+
allowReserved: param.allowReserved,
|
|
295
|
+
serialization: param.serialization,
|
|
296
|
+
...param.wholeBody && { wholeBody: true }
|
|
218
297
|
});
|
|
219
298
|
});
|
|
220
299
|
}
|
|
@@ -239,23 +318,31 @@ var ParameterResolver = class {
|
|
|
239
318
|
/**
|
|
240
319
|
* Extract parameters from request body schema
|
|
241
320
|
*/
|
|
242
|
-
extractBodyParameters(schema, parametersByName, required, contentType, prefix = "") {
|
|
321
|
+
extractBodyParameters(schema, parametersByName, required, contentType, mediaExamples, encoding, prefix = "") {
|
|
243
322
|
if (!schema || typeof schema !== "object") return;
|
|
244
323
|
const jsonSchema = toJsonSchema(schema);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
324
|
+
const flattened = flattenObjectBody(jsonSchema);
|
|
325
|
+
if (flattened) {
|
|
326
|
+
const requiredFields = flattened.required;
|
|
327
|
+
for (const [propName, propSchema] of Object.entries(flattened.properties)) {
|
|
248
328
|
const fullName = prefix ? `${prefix}.${propName}` : propName;
|
|
249
329
|
const isRequired = required && requiredFields.has(propName);
|
|
250
330
|
if (typeof propSchema === "object") {
|
|
331
|
+
const propEncoding = encoding?.[propName];
|
|
332
|
+
const propExamples = mediaExamples?.map(
|
|
333
|
+
(ex) => ex !== null && typeof ex === "object" && !Array.isArray(ex) ? ex[propName] : void 0
|
|
334
|
+
).filter((value) => value !== void 0);
|
|
251
335
|
const info = {
|
|
252
336
|
name: fullName,
|
|
253
337
|
location: "body",
|
|
254
338
|
required: isRequired,
|
|
255
339
|
schema: propSchema,
|
|
256
340
|
description: propSchema.description,
|
|
341
|
+
examples: propExamples && propExamples.length > 0 ? propExamples : void 0,
|
|
257
342
|
serialization: {
|
|
258
|
-
contentType
|
|
343
|
+
contentType,
|
|
344
|
+
...propEncoding && { encoding: { [propName]: propEncoding } },
|
|
345
|
+
...isBinarySchema(propSchema) && { binary: true }
|
|
259
346
|
}
|
|
260
347
|
};
|
|
261
348
|
if (!parametersByName.has(fullName)) {
|
|
@@ -270,9 +357,13 @@ var ParameterResolver = class {
|
|
|
270
357
|
name: bodyParamName,
|
|
271
358
|
location: "body",
|
|
272
359
|
required,
|
|
273
|
-
schema,
|
|
360
|
+
schema: jsonSchema,
|
|
361
|
+
examples: mediaExamples,
|
|
362
|
+
wholeBody: true,
|
|
274
363
|
serialization: {
|
|
275
|
-
contentType
|
|
364
|
+
contentType,
|
|
365
|
+
...encoding && Object.keys(encoding).length > 0 && { encoding },
|
|
366
|
+
...isBinarySchema(jsonSchema) && { binary: true }
|
|
276
367
|
}
|
|
277
368
|
};
|
|
278
369
|
if (!parametersByName.has(bodyParamName)) {
|
|
@@ -289,6 +380,9 @@ var ParameterResolver = class {
|
|
|
289
380
|
if (param.description) {
|
|
290
381
|
schema.description = param.description;
|
|
291
382
|
}
|
|
383
|
+
if (param.examples && param.examples.length > 0) {
|
|
384
|
+
schema.examples = param.examples;
|
|
385
|
+
}
|
|
292
386
|
if (param.deprecated) {
|
|
293
387
|
schema["deprecated"] = true;
|
|
294
388
|
}
|
|
@@ -385,21 +479,68 @@ var ParameterResolver = class {
|
|
|
385
479
|
required: true,
|
|
386
480
|
security: securityInfo
|
|
387
481
|
});
|
|
388
|
-
|
|
482
|
+
const schemeInInput = includeInInput === true || Array.isArray(includeInInput) && includeInInput.includes(scheme);
|
|
483
|
+
if (schemeInInput) {
|
|
389
484
|
properties[inputKey] = schema;
|
|
390
485
|
required.push(inputKey);
|
|
391
486
|
}
|
|
392
487
|
}
|
|
393
488
|
}
|
|
394
489
|
};
|
|
490
|
+
function collectObjectMembers(schema) {
|
|
491
|
+
if (!schema || typeof schema !== "object") return { properties: {}, required: /* @__PURE__ */ new Set() };
|
|
492
|
+
if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) return "union";
|
|
493
|
+
const properties = {};
|
|
494
|
+
const required = /* @__PURE__ */ new Set();
|
|
495
|
+
if (Array.isArray(schema.allOf)) {
|
|
496
|
+
for (const member of schema.allOf) {
|
|
497
|
+
const collected = collectObjectMembers(member);
|
|
498
|
+
if (collected === "union") return "union";
|
|
499
|
+
Object.assign(properties, collected.properties);
|
|
500
|
+
collected.required.forEach((field) => required.add(field));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
504
|
+
Object.assign(properties, schema.properties);
|
|
505
|
+
}
|
|
506
|
+
if (Array.isArray(schema.required)) {
|
|
507
|
+
schema.required.forEach((field) => required.add(field));
|
|
508
|
+
}
|
|
509
|
+
return { properties, required };
|
|
510
|
+
}
|
|
511
|
+
function flattenObjectBody(schema) {
|
|
512
|
+
const collected = collectObjectMembers(schema);
|
|
513
|
+
if (collected === "union") return void 0;
|
|
514
|
+
return Object.keys(collected.properties).length > 0 ? collected : void 0;
|
|
515
|
+
}
|
|
516
|
+
function isBinarySchema(schema) {
|
|
517
|
+
if (!schema || typeof schema !== "object") return false;
|
|
518
|
+
const record = schema;
|
|
519
|
+
if (record["format"] === "binary") return true;
|
|
520
|
+
return typeof record["contentMediaType"] === "string" && record["contentEncoding"] === void 0 && record["type"] === void 0;
|
|
521
|
+
}
|
|
522
|
+
function collectExampleValues(example, examples) {
|
|
523
|
+
if (examples && !Array.isArray(examples)) {
|
|
524
|
+
const values = Object.values(examples).filter((entry) => entry !== null && typeof entry === "object" && !isReferenceObject(entry)).map((entry) => entry.value).filter((value) => value !== void 0);
|
|
525
|
+
if (values.length > 0) {
|
|
526
|
+
return values;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (example !== void 0) {
|
|
530
|
+
return [example];
|
|
531
|
+
}
|
|
532
|
+
return void 0;
|
|
533
|
+
}
|
|
395
534
|
|
|
396
535
|
// src/response-builder.ts
|
|
397
536
|
var ResponseBuilder = class {
|
|
398
537
|
preferredStatusCodes;
|
|
399
538
|
includeAllResponses;
|
|
539
|
+
includeExamples;
|
|
400
540
|
constructor(options = {}) {
|
|
401
541
|
this.preferredStatusCodes = options.preferredStatusCodes ?? [200, 201, 204, 202, 203, 206];
|
|
402
542
|
this.includeAllResponses = options.includeAllResponses ?? true;
|
|
543
|
+
this.includeExamples = options.includeExamples ?? false;
|
|
403
544
|
}
|
|
404
545
|
/**
|
|
405
546
|
* Build output schema from responses
|
|
@@ -477,6 +618,12 @@ var ResponseBuilder = class {
|
|
|
477
618
|
if (!schema.description && response.description) {
|
|
478
619
|
schema.description = response.description;
|
|
479
620
|
}
|
|
621
|
+
if (this.includeExamples) {
|
|
622
|
+
const mediaExamples = collectExampleValues(mediaType.example, mediaType.examples);
|
|
623
|
+
if (mediaExamples) {
|
|
624
|
+
schema.examples = mediaExamples;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
480
627
|
schema["x-content-type"] = contentType;
|
|
481
628
|
return { statusCode, schema };
|
|
482
629
|
}
|
|
@@ -513,1382 +660,2040 @@ var ResponseBuilder = class {
|
|
|
513
660
|
}
|
|
514
661
|
};
|
|
515
662
|
|
|
516
|
-
// src/
|
|
517
|
-
var
|
|
663
|
+
// src/schema-builder.ts
|
|
664
|
+
var SchemaBuilder = class {
|
|
518
665
|
/**
|
|
519
|
-
*
|
|
666
|
+
* Merge multiple schemas into one
|
|
520
667
|
*/
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (!document.openapi) {
|
|
525
|
-
errors.push({
|
|
526
|
-
message: "Missing required field: openapi",
|
|
527
|
-
path: "/openapi",
|
|
528
|
-
code: "MISSING_OPENAPI_VERSION"
|
|
529
|
-
});
|
|
530
|
-
} else if (!this.isValidOpenAPIVersion(document.openapi)) {
|
|
531
|
-
errors.push({
|
|
532
|
-
message: `Unsupported OpenAPI version: ${document.openapi}. Expected 3.0.x or 3.1.x`,
|
|
533
|
-
path: "/openapi",
|
|
534
|
-
code: "INVALID_OPENAPI_VERSION"
|
|
535
|
-
});
|
|
668
|
+
static merge(schemas) {
|
|
669
|
+
if (schemas.length === 0) {
|
|
670
|
+
return { type: "object" };
|
|
536
671
|
}
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
672
|
+
if (schemas.length === 1) {
|
|
673
|
+
return schemas[0];
|
|
674
|
+
}
|
|
675
|
+
const merged = {
|
|
676
|
+
type: "object",
|
|
677
|
+
properties: {},
|
|
678
|
+
required: []
|
|
679
|
+
};
|
|
680
|
+
const allRequired = /* @__PURE__ */ new Set();
|
|
681
|
+
for (const schema of schemas) {
|
|
682
|
+
if (schema.properties) {
|
|
683
|
+
merged.properties = {
|
|
684
|
+
...merged.properties,
|
|
685
|
+
...schema.properties
|
|
686
|
+
};
|
|
550
687
|
}
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
message: "Missing required field: info.version",
|
|
554
|
-
path: "/info/version",
|
|
555
|
-
code: "MISSING_VERSION"
|
|
556
|
-
});
|
|
688
|
+
if (schema.required) {
|
|
689
|
+
schema.required.forEach((field) => allRequired.add(field));
|
|
557
690
|
}
|
|
558
691
|
}
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
message: "No paths defined in OpenAPI document",
|
|
562
|
-
path: "/paths",
|
|
563
|
-
code: "NO_PATHS"
|
|
564
|
-
});
|
|
565
|
-
} else {
|
|
566
|
-
this.validatePaths(document.paths, errors, warnings);
|
|
692
|
+
if (allRequired.size > 0) {
|
|
693
|
+
merged.required = Array.from(allRequired);
|
|
567
694
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
695
|
+
return merged;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Create a union schema (oneOf)
|
|
699
|
+
*/
|
|
700
|
+
static union(schemas) {
|
|
701
|
+
if (schemas.length === 0) {
|
|
702
|
+
return {};
|
|
574
703
|
}
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
message: "Security requirements defined but no security schemes found",
|
|
578
|
-
path: "/security",
|
|
579
|
-
code: "NO_SECURITY_SCHEMES"
|
|
580
|
-
});
|
|
704
|
+
if (schemas.length === 1) {
|
|
705
|
+
return schemas[0];
|
|
581
706
|
}
|
|
582
707
|
return {
|
|
583
|
-
|
|
584
|
-
errors: errors.length > 0 ? errors : void 0,
|
|
585
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
708
|
+
oneOf: schemas
|
|
586
709
|
};
|
|
587
710
|
}
|
|
588
711
|
/**
|
|
589
|
-
*
|
|
712
|
+
* Deep clone a schema
|
|
590
713
|
*/
|
|
591
|
-
|
|
592
|
-
return
|
|
714
|
+
static clone(schema) {
|
|
715
|
+
return JSON.parse(JSON.stringify(schema));
|
|
593
716
|
}
|
|
594
717
|
/**
|
|
595
|
-
*
|
|
718
|
+
* Remove $ref from schema (assumes already dereferenced)
|
|
596
719
|
*/
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
hasOperations = true;
|
|
613
|
-
this.validateOperation(operation, path, method, errors, warnings);
|
|
720
|
+
static removeRefs(schema) {
|
|
721
|
+
const cloned = this.clone(schema);
|
|
722
|
+
this.removeRefsRecursive(cloned);
|
|
723
|
+
return cloned;
|
|
724
|
+
}
|
|
725
|
+
static removeRefsRecursive(obj) {
|
|
726
|
+
if (!obj || typeof obj !== "object") return;
|
|
727
|
+
if (obj.$ref) {
|
|
728
|
+
delete obj.$ref;
|
|
729
|
+
}
|
|
730
|
+
for (const key in obj) {
|
|
731
|
+
if (key in obj) {
|
|
732
|
+
const value = obj[key];
|
|
733
|
+
if (value && typeof value === "object") {
|
|
734
|
+
this.removeRefsRecursive(value);
|
|
614
735
|
}
|
|
615
736
|
}
|
|
616
|
-
if (!hasOperations && !pathItem.$ref) {
|
|
617
|
-
warnings.push({
|
|
618
|
-
message: `Path has no operations: ${path}`,
|
|
619
|
-
path: `/paths/${path}`,
|
|
620
|
-
code: "NO_OPERATIONS"
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
737
|
}
|
|
624
738
|
}
|
|
625
739
|
/**
|
|
626
|
-
*
|
|
740
|
+
* Add description to schema
|
|
627
741
|
*/
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
path: `${basePath}/operationId`,
|
|
634
|
-
code: "NO_OPERATION_ID"
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
if (!operation.responses || Object.keys(operation.responses).length === 0) {
|
|
638
|
-
errors.push({
|
|
639
|
-
message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
|
|
640
|
-
path: `${basePath}/responses`,
|
|
641
|
-
code: "NO_RESPONSES"
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
if (operation.parameters) {
|
|
645
|
-
this.validateParameters(operation.parameters, path, method, errors, warnings);
|
|
646
|
-
}
|
|
647
|
-
const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
|
|
648
|
-
const definedPathParams = new Set(
|
|
649
|
-
operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
|
|
650
|
-
);
|
|
651
|
-
for (const param of pathParams) {
|
|
652
|
-
if (!definedPathParams.has(param)) {
|
|
653
|
-
errors.push({
|
|
654
|
-
message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
|
|
655
|
-
path: `${basePath}/parameters`,
|
|
656
|
-
code: "MISSING_PATH_PARAMETER"
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
}
|
|
742
|
+
static withDescription(schema, description) {
|
|
743
|
+
return {
|
|
744
|
+
...schema,
|
|
745
|
+
description
|
|
746
|
+
};
|
|
660
747
|
}
|
|
661
748
|
/**
|
|
662
|
-
*
|
|
749
|
+
* Add example to schema
|
|
663
750
|
*/
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
errors.push({
|
|
671
|
-
message: "Parameter missing name",
|
|
672
|
-
path: `${paramPath}/name`,
|
|
673
|
-
code: "MISSING_PARAMETER_NAME"
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
if (!param.in) {
|
|
677
|
-
errors.push({
|
|
678
|
-
message: 'Parameter missing "in" field',
|
|
679
|
-
path: `${paramPath}/in`,
|
|
680
|
-
code: "MISSING_PARAMETER_IN"
|
|
681
|
-
});
|
|
682
|
-
} else if (!["path", "query", "header", "cookie"].includes(param.in)) {
|
|
683
|
-
errors.push({
|
|
684
|
-
message: `Invalid parameter location: ${param.in}`,
|
|
685
|
-
path: `${paramPath}/in`,
|
|
686
|
-
code: "INVALID_PARAMETER_IN"
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
if (param.in === "path" && !param.required) {
|
|
690
|
-
errors.push({
|
|
691
|
-
message: `Path parameter '${param.name}' must be required`,
|
|
692
|
-
path: `${paramPath}/required`,
|
|
693
|
-
code: "PATH_PARAMETER_NOT_REQUIRED"
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
if (!param.schema && !param.content) {
|
|
697
|
-
errors.push({
|
|
698
|
-
message: `Parameter '${param.name}' missing schema or content`,
|
|
699
|
-
path: `${paramPath}`,
|
|
700
|
-
code: "MISSING_PARAMETER_SCHEMA"
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
751
|
+
static withExample(schema, example) {
|
|
752
|
+
const existingExamples = Array.isArray(schema.examples) ? schema.examples : [];
|
|
753
|
+
return {
|
|
754
|
+
...schema,
|
|
755
|
+
examples: [...existingExamples, example]
|
|
756
|
+
};
|
|
704
757
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.context = context;
|
|
714
|
-
if (Error.captureStackTrace) {
|
|
715
|
-
Error.captureStackTrace(this, this.constructor);
|
|
716
|
-
}
|
|
758
|
+
/**
|
|
759
|
+
* Add default value to schema
|
|
760
|
+
*/
|
|
761
|
+
static withDefault(schema, defaultValue) {
|
|
762
|
+
return {
|
|
763
|
+
...schema,
|
|
764
|
+
default: defaultValue
|
|
765
|
+
};
|
|
717
766
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
767
|
+
/**
|
|
768
|
+
* Add format to schema
|
|
769
|
+
*/
|
|
770
|
+
static withFormat(schema, format) {
|
|
771
|
+
return {
|
|
772
|
+
...schema,
|
|
773
|
+
format
|
|
774
|
+
};
|
|
722
775
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
776
|
+
/**
|
|
777
|
+
* Add pattern to schema
|
|
778
|
+
*/
|
|
779
|
+
static withPattern(schema, pattern) {
|
|
780
|
+
return {
|
|
781
|
+
...schema,
|
|
782
|
+
pattern
|
|
783
|
+
};
|
|
727
784
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Add enum to schema
|
|
787
|
+
*/
|
|
788
|
+
static withEnum(schema, values) {
|
|
789
|
+
return {
|
|
790
|
+
...schema,
|
|
791
|
+
enum: values
|
|
792
|
+
};
|
|
732
793
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
794
|
+
/**
|
|
795
|
+
* Add minimum/maximum constraints
|
|
796
|
+
*/
|
|
797
|
+
static withRange(schema, min, max, options = {}) {
|
|
798
|
+
const result = { ...schema };
|
|
799
|
+
if (min !== void 0) {
|
|
800
|
+
if (options.exclusive) {
|
|
801
|
+
result.exclusiveMinimum = min;
|
|
802
|
+
} else {
|
|
803
|
+
result.minimum = min;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (max !== void 0) {
|
|
807
|
+
if (options.exclusive) {
|
|
808
|
+
result.exclusiveMaximum = max;
|
|
809
|
+
} else {
|
|
810
|
+
result.maximum = max;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
739
814
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
815
|
+
/**
|
|
816
|
+
* Add minLength/maxLength constraints
|
|
817
|
+
*/
|
|
818
|
+
static withLength(schema, minLength, maxLength) {
|
|
819
|
+
const result = { ...schema };
|
|
820
|
+
if (minLength !== void 0) {
|
|
821
|
+
result.minLength = minLength;
|
|
822
|
+
}
|
|
823
|
+
if (maxLength !== void 0) {
|
|
824
|
+
result.maxLength = maxLength;
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
744
827
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
828
|
+
/**
|
|
829
|
+
* Create object schema
|
|
830
|
+
*/
|
|
831
|
+
static object(properties, required) {
|
|
832
|
+
return {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties,
|
|
835
|
+
...required && required.length > 0 && { required },
|
|
836
|
+
additionalProperties: false
|
|
837
|
+
};
|
|
749
838
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}),
|
|
760
|
-
"date-time": (schema) => ({
|
|
761
|
-
...schema,
|
|
762
|
-
description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
|
|
763
|
-
}),
|
|
764
|
-
date: (schema) => ({
|
|
765
|
-
...schema,
|
|
766
|
-
pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
|
|
767
|
-
description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
|
|
768
|
-
}),
|
|
769
|
-
time: (schema) => ({
|
|
770
|
-
...schema,
|
|
771
|
-
pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
|
|
772
|
-
description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
|
|
773
|
-
}),
|
|
774
|
-
email: (schema) => ({
|
|
775
|
-
...schema,
|
|
776
|
-
description: schema.description || "Email address (RFC 5322)"
|
|
777
|
-
}),
|
|
778
|
-
uri: (schema) => ({
|
|
779
|
-
...schema,
|
|
780
|
-
description: schema.description || "URI (RFC 3986)"
|
|
781
|
-
}),
|
|
782
|
-
"uri-reference": (schema) => ({
|
|
783
|
-
...schema,
|
|
784
|
-
description: schema.description || "URI reference (RFC 3986)"
|
|
785
|
-
}),
|
|
786
|
-
hostname: (schema) => ({
|
|
787
|
-
...schema,
|
|
788
|
-
description: schema.description || "Internet hostname (RFC 1123)"
|
|
789
|
-
}),
|
|
790
|
-
ipv4: (schema) => ({
|
|
791
|
-
...schema,
|
|
792
|
-
pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
|
|
793
|
-
description: schema.description || "IPv4 address"
|
|
794
|
-
}),
|
|
795
|
-
ipv6: (schema) => ({
|
|
796
|
-
...schema,
|
|
797
|
-
description: schema.description || "IPv6 address (RFC 4291)"
|
|
798
|
-
}),
|
|
799
|
-
// Integer formats
|
|
800
|
-
int32: (schema) => ({
|
|
801
|
-
...schema,
|
|
802
|
-
minimum: schema.minimum ?? -2147483648,
|
|
803
|
-
maximum: schema.maximum ?? 2147483647
|
|
804
|
-
}),
|
|
805
|
-
int64: (schema) => ({
|
|
806
|
-
...schema,
|
|
807
|
-
minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
|
|
808
|
-
maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
809
|
-
}),
|
|
810
|
-
// Binary/encoding formats
|
|
811
|
-
byte: (schema) => ({
|
|
812
|
-
...schema,
|
|
813
|
-
pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
|
|
814
|
-
description: schema.description || "Base64-encoded string (RFC 4648)"
|
|
815
|
-
}),
|
|
816
|
-
binary: (schema) => ({
|
|
817
|
-
...schema,
|
|
818
|
-
description: schema.description || "Binary data"
|
|
819
|
-
}),
|
|
820
|
-
// Sensitive data formats
|
|
821
|
-
password: (schema) => ({
|
|
822
|
-
...schema,
|
|
823
|
-
description: schema.description || "Password (sensitive, UI should mask input)"
|
|
824
|
-
})
|
|
825
|
-
};
|
|
826
|
-
function resolveSchemaFormats(schema, resolvers) {
|
|
827
|
-
if (!schema || typeof schema !== "object") return schema;
|
|
828
|
-
let result = { ...schema };
|
|
829
|
-
const format = result["format"];
|
|
830
|
-
if (format && resolvers[format]) {
|
|
831
|
-
result = { ...resolvers[format](result) };
|
|
839
|
+
/**
|
|
840
|
+
* Create array schema
|
|
841
|
+
*/
|
|
842
|
+
static array(items, constraints) {
|
|
843
|
+
return {
|
|
844
|
+
type: "array",
|
|
845
|
+
items,
|
|
846
|
+
...constraints
|
|
847
|
+
};
|
|
832
848
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
849
|
+
/**
|
|
850
|
+
* Create string schema
|
|
851
|
+
*/
|
|
852
|
+
static string(constraints) {
|
|
853
|
+
return {
|
|
854
|
+
type: "string",
|
|
855
|
+
...constraints
|
|
856
|
+
};
|
|
839
857
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
858
|
+
/**
|
|
859
|
+
* Create number schema
|
|
860
|
+
*/
|
|
861
|
+
static number(constraints) {
|
|
862
|
+
return {
|
|
863
|
+
type: "number",
|
|
864
|
+
...constraints
|
|
865
|
+
};
|
|
846
866
|
}
|
|
847
|
-
|
|
848
|
-
|
|
867
|
+
/**
|
|
868
|
+
* Create integer schema
|
|
869
|
+
*/
|
|
870
|
+
static integer(constraints) {
|
|
871
|
+
return {
|
|
872
|
+
type: "integer",
|
|
873
|
+
...constraints
|
|
874
|
+
};
|
|
849
875
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
876
|
+
/**
|
|
877
|
+
* Create boolean schema
|
|
878
|
+
*/
|
|
879
|
+
static boolean() {
|
|
880
|
+
return {
|
|
881
|
+
type: "boolean"
|
|
882
|
+
};
|
|
854
883
|
}
|
|
855
|
-
|
|
856
|
-
|
|
884
|
+
/**
|
|
885
|
+
* Create null schema
|
|
886
|
+
*/
|
|
887
|
+
static null() {
|
|
888
|
+
return {
|
|
889
|
+
type: "null"
|
|
890
|
+
};
|
|
857
891
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
+
/**
|
|
893
|
+
* Flatten nested oneOf/anyOf/allOf schemas
|
|
894
|
+
*/
|
|
895
|
+
static flatten(schema, maxDepth = 10) {
|
|
896
|
+
if (maxDepth <= 0) return schema;
|
|
897
|
+
const cloned = this.clone(schema);
|
|
898
|
+
if (cloned.oneOf) {
|
|
899
|
+
const flattened = cloned.oneOf.flatMap((s) => {
|
|
900
|
+
const sub = this.flatten(s, maxDepth - 1);
|
|
901
|
+
return sub.oneOf ? sub.oneOf : [sub];
|
|
902
|
+
});
|
|
903
|
+
cloned.oneOf = flattened;
|
|
904
|
+
}
|
|
905
|
+
if (cloned.anyOf) {
|
|
906
|
+
const flattened = cloned.anyOf.flatMap((s) => {
|
|
907
|
+
const sub = this.flatten(s, maxDepth - 1);
|
|
908
|
+
return sub.anyOf ? sub.anyOf : [sub];
|
|
909
|
+
});
|
|
910
|
+
cloned.anyOf = flattened;
|
|
911
|
+
}
|
|
912
|
+
if (cloned.allOf) {
|
|
913
|
+
const flattened = cloned.allOf.flatMap((s) => {
|
|
914
|
+
const sub = this.flatten(s, maxDepth - 1);
|
|
915
|
+
return sub.allOf ? sub.allOf : [sub];
|
|
916
|
+
});
|
|
917
|
+
cloned.allOf = flattened;
|
|
918
|
+
}
|
|
919
|
+
return cloned;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Truncate a schema tree to a maximum nesting depth.
|
|
923
|
+
*
|
|
924
|
+
* The root sits at depth 0; descending into `properties` values, `items`,
|
|
925
|
+
* `additionalProperties`, composition members (`allOf`/`anyOf`/`oneOf`), or
|
|
926
|
+
* `not` increments the depth. Nodes at `maxDepth` keep their scalar keywords
|
|
927
|
+
* (type, description, format, ...) but have their child schemas stripped and
|
|
928
|
+
* a truncation note appended to the description.
|
|
929
|
+
*/
|
|
930
|
+
static truncateDepth(schema, maxDepth) {
|
|
931
|
+
const bound = Number.isFinite(maxDepth) ? Math.max(0, Math.floor(maxDepth)) : 10;
|
|
932
|
+
return this.truncateDepthRecursive(schema, 0, bound);
|
|
933
|
+
}
|
|
934
|
+
/** Keys whose value is a map of schemas (JSON Schema 2020-12) */
|
|
935
|
+
static TRUNCATE_MAP_KEYS = [
|
|
936
|
+
"properties",
|
|
937
|
+
"patternProperties",
|
|
938
|
+
"$defs",
|
|
939
|
+
"definitions",
|
|
940
|
+
"dependentSchemas"
|
|
941
|
+
];
|
|
942
|
+
/** Keys whose value is a single schema (or, for `items`, a tuple array) */
|
|
943
|
+
static TRUNCATE_SCHEMA_KEYS = [
|
|
944
|
+
"items",
|
|
945
|
+
"additionalProperties",
|
|
946
|
+
"not",
|
|
947
|
+
"if",
|
|
948
|
+
"then",
|
|
949
|
+
"else",
|
|
950
|
+
"propertyNames",
|
|
951
|
+
"contains",
|
|
952
|
+
"contentSchema",
|
|
953
|
+
"unevaluatedProperties",
|
|
954
|
+
"unevaluatedItems"
|
|
955
|
+
];
|
|
956
|
+
/** Keys whose value is an array of schemas */
|
|
957
|
+
static TRUNCATE_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
958
|
+
// Copy-on-walk: never mutates the input, only copies nodes that have schema
|
|
959
|
+
// children, and — because the walk is depth-bounded — terminates even on
|
|
960
|
+
// circular schema graphs (which `clone()`'s JSON round-trip would reject).
|
|
961
|
+
static truncateDepthRecursive(node, depth, maxDepth) {
|
|
962
|
+
if (!node || typeof node !== "object") return node;
|
|
963
|
+
const record = node;
|
|
964
|
+
const childKeys = [...this.TRUNCATE_MAP_KEYS, ...this.TRUNCATE_SCHEMA_KEYS, ...this.TRUNCATE_LIST_KEYS];
|
|
965
|
+
const hasChildren = childKeys.some((key) => {
|
|
966
|
+
const value = record[key];
|
|
967
|
+
return value !== null && typeof value === "object";
|
|
968
|
+
});
|
|
969
|
+
if (!hasChildren) return node;
|
|
970
|
+
const copy = { ...node };
|
|
971
|
+
const copyRecord = copy;
|
|
972
|
+
if (depth >= maxDepth) {
|
|
973
|
+
for (const key of childKeys) {
|
|
974
|
+
const value = copyRecord[key];
|
|
975
|
+
if (value !== null && typeof value === "object") {
|
|
976
|
+
delete copyRecord[key];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
delete copyRecord["required"];
|
|
980
|
+
const note = "[Truncated: nested schema exceeds maxSchemaDepth]";
|
|
981
|
+
copy.description = copy.description ? `${copy.description} ${note}` : note;
|
|
982
|
+
return copy;
|
|
983
|
+
}
|
|
984
|
+
for (const key of this.TRUNCATE_MAP_KEYS) {
|
|
985
|
+
const value = copyRecord[key];
|
|
986
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
987
|
+
const mapped = {};
|
|
988
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
989
|
+
mapped[name] = this.truncateDepthRecursive(sub, depth + 1, maxDepth);
|
|
990
|
+
}
|
|
991
|
+
copyRecord[key] = mapped;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
for (const key of this.TRUNCATE_SCHEMA_KEYS) {
|
|
995
|
+
const value = copyRecord[key];
|
|
996
|
+
if (value !== null && typeof value === "object") {
|
|
997
|
+
copyRecord[key] = Array.isArray(value) ? value.map((item) => this.truncateDepthRecursive(item, depth + 1, maxDepth)) : this.truncateDepthRecursive(value, depth + 1, maxDepth);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
for (const key of this.TRUNCATE_LIST_KEYS) {
|
|
1001
|
+
const value = copyRecord[key];
|
|
1002
|
+
if (Array.isArray(value)) {
|
|
1003
|
+
copyRecord[key] = value.map((member) => this.truncateDepthRecursive(member, depth + 1, maxDepth));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return copy;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Simplify schema by removing unnecessary fields
|
|
1010
|
+
*/
|
|
1011
|
+
static simplify(schema) {
|
|
1012
|
+
const cloned = this.clone(schema);
|
|
1013
|
+
if (Array.isArray(cloned.required) && cloned.required.length === 0) {
|
|
1014
|
+
delete cloned.required;
|
|
1015
|
+
}
|
|
1016
|
+
if (cloned.properties && Object.keys(cloned.properties).length === 0) {
|
|
1017
|
+
delete cloned.properties;
|
|
1018
|
+
}
|
|
1019
|
+
if (Array.isArray(cloned.examples) && cloned.examples.length === 0) {
|
|
1020
|
+
delete cloned.examples;
|
|
1021
|
+
}
|
|
1022
|
+
if (cloned.title && cloned.description && cloned.title === cloned.description) {
|
|
1023
|
+
delete cloned.title;
|
|
1024
|
+
}
|
|
1025
|
+
return cloned;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// src/annotations.ts
|
|
1030
|
+
function inferAnnotationsFromMethod(method) {
|
|
1031
|
+
switch (method) {
|
|
1032
|
+
case "get":
|
|
1033
|
+
case "head":
|
|
1034
|
+
case "options":
|
|
1035
|
+
case "trace":
|
|
1036
|
+
return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
1037
|
+
case "put":
|
|
1038
|
+
case "delete":
|
|
1039
|
+
return { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
|
|
1040
|
+
case "post":
|
|
1041
|
+
case "patch":
|
|
1042
|
+
return { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
892
1043
|
}
|
|
893
|
-
return null;
|
|
894
1044
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (!
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
1045
|
+
var ANNOTATION_KEYS = ["title", "readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"];
|
|
1046
|
+
function pickAnnotations(raw) {
|
|
1047
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1048
|
+
const result = {};
|
|
1049
|
+
for (const key of ANNOTATION_KEYS) {
|
|
1050
|
+
const value = raw[key];
|
|
1051
|
+
if (key === "title" ? typeof value === "string" : typeof value === "boolean") {
|
|
1052
|
+
result[key] = value;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
901
1056
|
}
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
if (a >= 224) return true;
|
|
914
|
-
return false;
|
|
1057
|
+
function mergeOverrides(base, layer) {
|
|
1058
|
+
return {
|
|
1059
|
+
...base,
|
|
1060
|
+
...layer.disabled !== void 0 && { disabled: layer.disabled },
|
|
1061
|
+
...layer.name !== void 0 && { name: layer.name },
|
|
1062
|
+
...layer.title !== void 0 && { title: layer.title },
|
|
1063
|
+
...layer.description !== void 0 && { description: layer.description },
|
|
1064
|
+
...(base.annotations || layer.annotations) && {
|
|
1065
|
+
annotations: { ...base.annotations, ...layer.annotations }
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
915
1068
|
}
|
|
916
|
-
function
|
|
917
|
-
|
|
918
|
-
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
919
|
-
const zone = h.indexOf("%");
|
|
920
|
-
if (zone !== -1) h = h.slice(0, zone);
|
|
921
|
-
const lower = h.toLowerCase();
|
|
922
|
-
if (lower === "::" || lower === "::0") return true;
|
|
923
|
-
if (lower === "::1") return true;
|
|
924
|
-
if (/^f[cd]/.test(lower)) return true;
|
|
925
|
-
if (/^fe[89a-f]/.test(lower)) return true;
|
|
926
|
-
if (/^ff/.test(lower)) return true;
|
|
927
|
-
return false;
|
|
1069
|
+
function readXMcp(node) {
|
|
1070
|
+
return node["x-mcp"];
|
|
928
1071
|
}
|
|
929
|
-
function
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const o = parseIpv4(mapped);
|
|
935
|
-
if (o) return isBlockedIpv4(o);
|
|
1072
|
+
function parseXMcpEnabled(ext) {
|
|
1073
|
+
if (ext === false) return false;
|
|
1074
|
+
if (ext === true) return true;
|
|
1075
|
+
if (ext && typeof ext === "object" && typeof ext.enabled === "boolean") {
|
|
1076
|
+
return ext.enabled;
|
|
936
1077
|
}
|
|
937
|
-
|
|
938
|
-
if (v4) return isBlockedIpv4(v4);
|
|
939
|
-
if (h.includes(":")) return isBlockedIpv6(h);
|
|
940
|
-
return false;
|
|
941
|
-
}
|
|
942
|
-
function isIpLiteral(hostname) {
|
|
943
|
-
if (hostname.startsWith("[") && hostname.endsWith("]")) return true;
|
|
944
|
-
return parseIpv4(hostname) !== null;
|
|
1078
|
+
return void 0;
|
|
945
1079
|
}
|
|
946
|
-
function
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
if (
|
|
954
|
-
return
|
|
1080
|
+
function resolveExtensionEnabled(document, pathItem, operation) {
|
|
1081
|
+
let enabled = true;
|
|
1082
|
+
const rootSetting = parseXMcpEnabled(readXMcp(document));
|
|
1083
|
+
if (rootSetting !== void 0) enabled = rootSetting;
|
|
1084
|
+
const pathSetting = parseXMcpEnabled(readXMcp(pathItem));
|
|
1085
|
+
if (pathSetting !== void 0) enabled = pathSetting;
|
|
1086
|
+
const operationDisabled = extractExtensionOverrides(operation).disabled;
|
|
1087
|
+
if (operationDisabled !== void 0) enabled = !operationDisabled;
|
|
1088
|
+
return enabled;
|
|
955
1089
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
parsed = new URL(url);
|
|
971
|
-
} catch {
|
|
972
|
-
throw new SsrfError(`Invalid spec URL: ${url}`, { url });
|
|
1090
|
+
function extractExtensionOverrides(operation) {
|
|
1091
|
+
const op = operation;
|
|
1092
|
+
let result = {};
|
|
1093
|
+
const speakeasy = op["x-speakeasy-mcp"];
|
|
1094
|
+
if (speakeasy && typeof speakeasy === "object") {
|
|
1095
|
+
const ext = speakeasy;
|
|
1096
|
+
result = mergeOverrides(result, {
|
|
1097
|
+
disabled: typeof ext["disabled"] === "boolean" ? ext["disabled"] : void 0,
|
|
1098
|
+
name: typeof ext["name"] === "string" ? ext["name"] : void 0,
|
|
1099
|
+
title: typeof ext["title"] === "string" ? ext["title"] : void 0,
|
|
1100
|
+
description: typeof ext["description"] === "string" ? ext["description"] : void 0,
|
|
1101
|
+
// Speakeasy's top-level `title` is the tool title, not an annotation slot
|
|
1102
|
+
annotations: pickAnnotations({ ...ext, title: void 0 })
|
|
1103
|
+
});
|
|
973
1104
|
}
|
|
974
|
-
const
|
|
975
|
-
if (
|
|
976
|
-
|
|
1105
|
+
const xMcp = op["x-mcp"];
|
|
1106
|
+
if (xMcp === false) {
|
|
1107
|
+
result = mergeOverrides(result, { disabled: true });
|
|
1108
|
+
} else if (xMcp === true) {
|
|
1109
|
+
result = mergeOverrides(result, { disabled: false });
|
|
1110
|
+
} else if (xMcp && typeof xMcp === "object") {
|
|
1111
|
+
const ext = xMcp;
|
|
1112
|
+
result = mergeOverrides(result, {
|
|
1113
|
+
disabled: typeof ext["enabled"] === "boolean" ? !ext["enabled"] : void 0,
|
|
1114
|
+
name: typeof ext["name"] === "string" ? ext["name"] : void 0,
|
|
1115
|
+
title: typeof ext["title"] === "string" ? ext["title"] : void 0,
|
|
1116
|
+
description: typeof ext["description"] === "string" ? ext["description"] : void 0,
|
|
1117
|
+
annotations: pickAnnotations(ext["annotations"])
|
|
1118
|
+
});
|
|
977
1119
|
}
|
|
978
|
-
const
|
|
979
|
-
if (
|
|
980
|
-
|
|
1120
|
+
const frontmcp = op["x-frontmcp"];
|
|
1121
|
+
if (frontmcp && typeof frontmcp === "object" && frontmcp.annotations) {
|
|
1122
|
+
const annotations = pickAnnotations(frontmcp.annotations);
|
|
1123
|
+
result = mergeOverrides(result, {
|
|
1124
|
+
annotations,
|
|
1125
|
+
title: typeof frontmcp.annotations.title === "string" ? frontmcp.annotations.title : void 0
|
|
1126
|
+
});
|
|
981
1127
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1128
|
+
return result;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/client-targets.ts
|
|
1132
|
+
var MAP_KEYS = ["properties", "patternProperties", "dependentSchemas"];
|
|
1133
|
+
var SCHEMA_KEYS = [
|
|
1134
|
+
"items",
|
|
1135
|
+
"additionalProperties",
|
|
1136
|
+
"not",
|
|
1137
|
+
"if",
|
|
1138
|
+
"then",
|
|
1139
|
+
"else",
|
|
1140
|
+
"propertyNames",
|
|
1141
|
+
"contains",
|
|
1142
|
+
"contentSchema",
|
|
1143
|
+
"unevaluatedItems",
|
|
1144
|
+
"unevaluatedProperties"
|
|
1145
|
+
];
|
|
1146
|
+
var LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
1147
|
+
function isSchemaObject(value) {
|
|
1148
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1149
|
+
}
|
|
1150
|
+
function walkSchema(node, visit) {
|
|
1151
|
+
if (!isSchemaObject(node)) return node;
|
|
1152
|
+
const visited = visit({ ...node });
|
|
1153
|
+
for (const key of MAP_KEYS) {
|
|
1154
|
+
const value = visited[key];
|
|
1155
|
+
if (isSchemaObject(value)) {
|
|
1156
|
+
const mapped = {};
|
|
1157
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
1158
|
+
mapped[name] = walkSchema(sub, visit);
|
|
1159
|
+
}
|
|
1160
|
+
visited[key] = mapped;
|
|
985
1161
|
}
|
|
986
|
-
return [];
|
|
987
|
-
}
|
|
988
|
-
if (isBlockedHostname(hostname, ssrf)) {
|
|
989
|
-
throw new SsrfError(`Host "${hostname}" maps to a blocked internal address`, { url });
|
|
990
|
-
}
|
|
991
|
-
if (isIpLiteral(hostname)) {
|
|
992
|
-
return [];
|
|
993
1162
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (
|
|
999
|
-
|
|
1163
|
+
for (const key of SCHEMA_KEYS) {
|
|
1164
|
+
const value = visited[key];
|
|
1165
|
+
if (Array.isArray(value)) {
|
|
1166
|
+
visited[key] = value.map((item) => walkSchema(item, visit));
|
|
1167
|
+
} else if (isSchemaObject(value)) {
|
|
1168
|
+
visited[key] = walkSchema(value, visit);
|
|
1000
1169
|
}
|
|
1001
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1002
|
-
throw new SsrfError(`Host "${hostname}" could not be resolved for SSRF validation: ${message}`, { url });
|
|
1003
1170
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
if (isBlockedAddress(address)) {
|
|
1009
|
-
throw new SsrfError(`Host "${hostname}" resolves to blocked address ${address}`, { url });
|
|
1171
|
+
for (const key of LIST_KEYS) {
|
|
1172
|
+
const value = visited[key];
|
|
1173
|
+
if (Array.isArray(value)) {
|
|
1174
|
+
visited[key] = value.map((member) => walkSchema(member, visit));
|
|
1010
1175
|
}
|
|
1011
1176
|
}
|
|
1012
|
-
return
|
|
1013
|
-
}
|
|
1014
|
-
var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
1015
|
-
async function loadNodeHttpModules() {
|
|
1016
|
-
try {
|
|
1017
|
-
const [http, https] = await Promise.all([import("node:http"), import("node:https")]);
|
|
1018
|
-
return { http, https };
|
|
1019
|
-
} catch {
|
|
1020
|
-
return null;
|
|
1021
|
-
}
|
|
1177
|
+
return visited;
|
|
1022
1178
|
}
|
|
1023
|
-
function
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1179
|
+
function inlineLocalRefs(schema) {
|
|
1180
|
+
if (!isSchemaObject(schema)) return schema;
|
|
1181
|
+
const root = schema;
|
|
1182
|
+
const resolvePointer = (pointer) => {
|
|
1183
|
+
const parts = pointer.replace(/^#\/?/, "").split("/").filter((part) => part.length > 0).map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
1184
|
+
let current = root;
|
|
1185
|
+
for (const part of parts) {
|
|
1186
|
+
if (!isSchemaObject(current)) return void 0;
|
|
1187
|
+
current = current[part];
|
|
1188
|
+
}
|
|
1189
|
+
return current;
|
|
1190
|
+
};
|
|
1191
|
+
const inline = (node, seenPointers) => {
|
|
1192
|
+
if (!isSchemaObject(node)) return node;
|
|
1193
|
+
const record = node;
|
|
1194
|
+
const ref = record["$ref"];
|
|
1195
|
+
if (typeof ref === "string" && !ref.startsWith("#")) {
|
|
1196
|
+
const { $ref: _external, ...siblings } = record;
|
|
1197
|
+
return inline(
|
|
1198
|
+
{ description: `[External $ref ${ref} removed for client compatibility]`, ...siblings },
|
|
1199
|
+
seenPointers
|
|
1034
1200
|
);
|
|
1035
|
-
} else {
|
|
1036
|
-
done(null, pinned[0].address, pinned[0].family);
|
|
1037
1201
|
}
|
|
1202
|
+
if (typeof ref === "string") {
|
|
1203
|
+
const { $ref: _ref, ...siblings } = record;
|
|
1204
|
+
if (seenPointers.has(ref)) {
|
|
1205
|
+
return { description: "[Circular $ref removed for client compatibility]", ...siblings };
|
|
1206
|
+
}
|
|
1207
|
+
const resolved = resolvePointer(ref);
|
|
1208
|
+
if (!isSchemaObject(resolved)) {
|
|
1209
|
+
return { description: `[Unresolvable $ref ${ref} removed for client compatibility]`, ...siblings };
|
|
1210
|
+
}
|
|
1211
|
+
const inlined = inline(resolved, /* @__PURE__ */ new Set([...seenPointers, ref]));
|
|
1212
|
+
if (!isSchemaObject(inlined)) return inlined;
|
|
1213
|
+
return { ...inlined, ...siblings };
|
|
1214
|
+
}
|
|
1215
|
+
const copy = { ...record };
|
|
1216
|
+
delete copy["$defs"];
|
|
1217
|
+
delete copy["definitions"];
|
|
1218
|
+
for (const key of MAP_KEYS) {
|
|
1219
|
+
const value = copy[key];
|
|
1220
|
+
if (isSchemaObject(value)) {
|
|
1221
|
+
const mapped = {};
|
|
1222
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
1223
|
+
mapped[name] = inline(sub, seenPointers);
|
|
1224
|
+
}
|
|
1225
|
+
copy[key] = mapped;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
for (const key of SCHEMA_KEYS) {
|
|
1229
|
+
const value = copy[key];
|
|
1230
|
+
if (Array.isArray(value)) {
|
|
1231
|
+
copy[key] = value.map((item) => inline(item, seenPointers));
|
|
1232
|
+
} else if (isSchemaObject(value)) {
|
|
1233
|
+
copy[key] = inline(value, seenPointers);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
for (const key of LIST_KEYS) {
|
|
1237
|
+
const value = copy[key];
|
|
1238
|
+
if (Array.isArray(value)) {
|
|
1239
|
+
copy[key] = value.map((member) => inline(member, seenPointers));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return copy;
|
|
1038
1243
|
};
|
|
1244
|
+
return inline(schema, /* @__PURE__ */ new Set());
|
|
1039
1245
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
headers: { ...headers, "accept-encoding": "identity" }
|
|
1049
|
-
};
|
|
1050
|
-
if (pinned.length > 0) {
|
|
1051
|
-
requestOptions["lookup"] = makePinnedLookup(pinned);
|
|
1052
|
-
}
|
|
1053
|
-
const request = lib.request(url, requestOptions, (response) => {
|
|
1054
|
-
const chunks = [];
|
|
1055
|
-
let received = 0;
|
|
1056
|
-
response.on("data", (chunk) => {
|
|
1057
|
-
received += chunk.length;
|
|
1058
|
-
if (received > limit) {
|
|
1059
|
-
request.destroy();
|
|
1060
|
-
reject(new SsrfError(`Response body exceeds ${limit} bytes`, { url }));
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
chunks.push(chunk);
|
|
1064
|
-
});
|
|
1065
|
-
response.on("end", () => {
|
|
1066
|
-
const status = response.statusCode;
|
|
1067
|
-
const responseHeaders = new Headers();
|
|
1068
|
-
const entries = Object.entries(response.headers);
|
|
1069
|
-
for (const [key, value] of entries) {
|
|
1070
|
-
if (Array.isArray(value)) {
|
|
1071
|
-
for (const item of value) responseHeaders.append(key, item);
|
|
1072
|
-
} else {
|
|
1073
|
-
responseHeaders.append(key, value);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
const body = NULL_BODY_STATUS.has(status) ? null : Buffer.concat(chunks);
|
|
1077
|
-
resolve(new Response(body, { status, statusText: response.statusMessage, headers: responseHeaders }));
|
|
1078
|
-
});
|
|
1079
|
-
response.on("error", reject);
|
|
1080
|
-
});
|
|
1081
|
-
request.on("error", reject);
|
|
1082
|
-
request.end();
|
|
1246
|
+
function ensureArrayItems(schema) {
|
|
1247
|
+
return walkSchema(schema, (node) => {
|
|
1248
|
+
const type = node["type"];
|
|
1249
|
+
const isArray = type === "array" || Array.isArray(type) && type.includes("array");
|
|
1250
|
+
if (isArray && node["items"] === void 0) {
|
|
1251
|
+
return { ...node, items: {} };
|
|
1252
|
+
}
|
|
1253
|
+
return node;
|
|
1083
1254
|
});
|
|
1084
1255
|
}
|
|
1085
|
-
function
|
|
1086
|
-
|
|
1256
|
+
function mergeAllOf(node) {
|
|
1257
|
+
const members = node["allOf"];
|
|
1258
|
+
const merged = {};
|
|
1259
|
+
const properties = {};
|
|
1260
|
+
const required = /* @__PURE__ */ new Set();
|
|
1261
|
+
for (const rawMember of members) {
|
|
1262
|
+
if (!isSchemaObject(rawMember)) continue;
|
|
1263
|
+
const member = Array.isArray(rawMember["allOf"]) ? mergeAllOf(rawMember) : rawMember;
|
|
1264
|
+
const { properties: memberProps, required: memberRequired, ...scalars } = member;
|
|
1265
|
+
Object.assign(merged, scalars);
|
|
1266
|
+
if (isSchemaObject(memberProps)) Object.assign(properties, memberProps);
|
|
1267
|
+
if (Array.isArray(memberRequired)) memberRequired.forEach((field) => required.add(String(field)));
|
|
1268
|
+
}
|
|
1269
|
+
const { allOf: _allOf, properties: ownProps, required: ownRequired, ...rest } = node;
|
|
1270
|
+
Object.assign(merged, rest);
|
|
1271
|
+
if (isSchemaObject(ownProps)) Object.assign(properties, ownProps);
|
|
1272
|
+
if (Array.isArray(ownRequired)) ownRequired.forEach((field) => required.add(String(field)));
|
|
1273
|
+
if (Object.keys(properties).length > 0) merged["properties"] = properties;
|
|
1274
|
+
if (required.size > 0) merged["required"] = [...required];
|
|
1275
|
+
return merged;
|
|
1087
1276
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1277
|
+
function nullableWrapperMember(node) {
|
|
1278
|
+
const anyOf = node["anyOf"];
|
|
1279
|
+
if (!Array.isArray(anyOf) || anyOf.length !== 2) return void 0;
|
|
1280
|
+
const nullIndex = anyOf.findIndex((m) => isSchemaObject(m) && m["type"] === "null");
|
|
1281
|
+
if (nullIndex === -1) return void 0;
|
|
1282
|
+
const other = anyOf[1 - nullIndex];
|
|
1283
|
+
return isSchemaObject(other) ? other : void 0;
|
|
1284
|
+
}
|
|
1285
|
+
function describeVariants(members) {
|
|
1286
|
+
return members.map((member, index) => {
|
|
1287
|
+
if (!isSchemaObject(member)) return `variant ${index + 1}`;
|
|
1288
|
+
const record = member;
|
|
1289
|
+
return typeof record["title"] === "string" && record["title"] || typeof record["description"] === "string" && record["description"] || typeof record["type"] === "string" && `type ${record["type"]}` || `variant ${index + 1}`;
|
|
1290
|
+
}).join("; ");
|
|
1291
|
+
}
|
|
1292
|
+
function collapseRootCompositions(schema) {
|
|
1293
|
+
if (!isSchemaObject(schema)) return schema;
|
|
1294
|
+
const node = { ...schema };
|
|
1295
|
+
if (Array.isArray(node["allOf"])) {
|
|
1296
|
+
return collapseRootCompositions(mergeAllOf(node));
|
|
1297
|
+
}
|
|
1298
|
+
const nullableMember = nullableWrapperMember(node);
|
|
1299
|
+
if (nullableMember) {
|
|
1300
|
+
const { anyOf: _anyOf, ...rest } = node;
|
|
1301
|
+
const merged = { ...nullableMember, ...rest };
|
|
1302
|
+
const note = "May be null.";
|
|
1303
|
+
merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
|
|
1304
|
+
return merged;
|
|
1091
1305
|
}
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
return
|
|
1306
|
+
for (const key of ["oneOf", "anyOf"]) {
|
|
1307
|
+
const members = node[key];
|
|
1308
|
+
if (Array.isArray(members)) {
|
|
1309
|
+
const { [key]: _members, ...rest } = node;
|
|
1310
|
+
return {
|
|
1311
|
+
...rest,
|
|
1312
|
+
description: `${typeof rest["description"] === "string" ? `${rest["description"]} ` : ""}Accepts one of ${members.length} variants: ${describeVariants(members)}.`,
|
|
1313
|
+
"x-variants": members
|
|
1314
|
+
};
|
|
1097
1315
|
}
|
|
1098
|
-
throw new SsrfError("No fetch implementation available to load OpenAPI spec from URL", { url });
|
|
1099
1316
|
}
|
|
1100
|
-
return
|
|
1317
|
+
return node;
|
|
1101
1318
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1319
|
+
function collapseNestedUnions(schema) {
|
|
1320
|
+
return walkSchema(schema, (node) => {
|
|
1321
|
+
let current = node;
|
|
1322
|
+
for (; ; ) {
|
|
1323
|
+
if (Array.isArray(current["allOf"])) {
|
|
1324
|
+
current = mergeAllOf(current);
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const type = current["type"];
|
|
1328
|
+
if (Array.isArray(type)) {
|
|
1329
|
+
const nonNull = type.filter((t) => t !== "null");
|
|
1330
|
+
const notes = [];
|
|
1331
|
+
if (nonNull.length > 1) notes.push(`Alternative types accepted: ${nonNull.slice(1).join(", ")}.`);
|
|
1332
|
+
if (nonNull.length !== type.length) notes.push("May be null.");
|
|
1333
|
+
current = { ...current, type: nonNull[0] ?? "null" };
|
|
1334
|
+
if (notes.length > 0) {
|
|
1335
|
+
const joined = notes.join(" ");
|
|
1336
|
+
current["description"] = current["description"] ? `${current["description"]} ${joined}` : joined;
|
|
1337
|
+
}
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const nullableMember = nullableWrapperMember(current);
|
|
1341
|
+
if (nullableMember) {
|
|
1342
|
+
const { anyOf: _anyOf, ...rest } = current;
|
|
1343
|
+
const merged = { ...nullableMember, ...rest };
|
|
1344
|
+
const note = "May be null.";
|
|
1345
|
+
merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
|
|
1346
|
+
current = merged;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
let collapsedUnion = false;
|
|
1350
|
+
for (const key of ["oneOf", "anyOf"]) {
|
|
1351
|
+
const members = current[key];
|
|
1352
|
+
if (Array.isArray(members) && members.length > 0 && isSchemaObject(members[0])) {
|
|
1353
|
+
const { [key]: _members, ...rest } = current;
|
|
1354
|
+
const first = { ...members[0] };
|
|
1355
|
+
const note = members.length > 1 ? `${members.length - 1} alternative schema variant(s) omitted for client compatibility: ${describeVariants(
|
|
1356
|
+
members.slice(1)
|
|
1357
|
+
)}.` : void 0;
|
|
1358
|
+
const merged = { ...first, ...rest };
|
|
1359
|
+
if (note) {
|
|
1360
|
+
merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
|
|
1361
|
+
}
|
|
1362
|
+
current = merged;
|
|
1363
|
+
collapsedUnion = true;
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (collapsedUnion) continue;
|
|
1368
|
+
return current;
|
|
1115
1369
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
var GEMINI_SUPPORTED_FORMATS = /* @__PURE__ */ new Set(["date-time", "enum"]);
|
|
1373
|
+
var GEMINI_NUMERIC_FORMATS = /* @__PURE__ */ new Set(["int32", "int64", "float", "double"]);
|
|
1374
|
+
function isNumericNode(node) {
|
|
1375
|
+
const type = node["type"];
|
|
1376
|
+
return type === "integer" || type === "number" || Array.isArray(type) && (type.includes("integer") || type.includes("number"));
|
|
1377
|
+
}
|
|
1378
|
+
function demoteFormats(schema, supported = GEMINI_SUPPORTED_FORMATS) {
|
|
1379
|
+
return walkSchema(schema, (node) => {
|
|
1380
|
+
const format = node["format"];
|
|
1381
|
+
if (typeof format !== "string" || supported.has(format)) return node;
|
|
1382
|
+
if (GEMINI_NUMERIC_FORMATS.has(format) && isNumericNode(node)) return node;
|
|
1383
|
+
const { format: _format, ...rest } = node;
|
|
1384
|
+
const note = `(format: ${format})`;
|
|
1385
|
+
rest["description"] = rest["description"] ? `${rest["description"]} ${note}` : note;
|
|
1386
|
+
return rest;
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
function isObjectNode(node) {
|
|
1390
|
+
const type = node["type"];
|
|
1391
|
+
return type === "object" || Array.isArray(type) && type.includes("object") || type === void 0 && isSchemaObject(node["properties"]);
|
|
1392
|
+
}
|
|
1393
|
+
function enforceClosedObjects(schema) {
|
|
1394
|
+
return walkSchema(schema, (node) => {
|
|
1395
|
+
if (isObjectNode(node) && (node["additionalProperties"] === void 0 || node["additionalProperties"] === true)) {
|
|
1396
|
+
return { ...node, additionalProperties: false };
|
|
1120
1397
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1398
|
+
return node;
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
function requireAllProperties(schema) {
|
|
1402
|
+
return walkSchema(schema, (node) => {
|
|
1403
|
+
if (!isObjectNode(node) || !isSchemaObject(node["properties"])) return node;
|
|
1404
|
+
const properties = node["properties"];
|
|
1405
|
+
const originallyRequired = new Set(Array.isArray(node["required"]) ? node["required"].map(String) : []);
|
|
1406
|
+
const rewritten = {};
|
|
1407
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
1408
|
+
if (originallyRequired.has(name) || !isSchemaObject(propSchema) || propSchema["const"] !== void 0) {
|
|
1409
|
+
rewritten[name] = propSchema;
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const prop = propSchema;
|
|
1413
|
+
const withNullEnum = (next) => {
|
|
1414
|
+
const enumValues = next["enum"];
|
|
1415
|
+
if (Array.isArray(enumValues) && !enumValues.includes(null)) {
|
|
1416
|
+
return { ...next, enum: [...enumValues, null] };
|
|
1417
|
+
}
|
|
1418
|
+
return next;
|
|
1419
|
+
};
|
|
1420
|
+
const type = prop["type"];
|
|
1421
|
+
if (typeof type === "string" && type !== "null") {
|
|
1422
|
+
rewritten[name] = withNullEnum({ ...prop, type: [type, "null"] });
|
|
1423
|
+
} else if (Array.isArray(type) && !type.includes("null")) {
|
|
1424
|
+
rewritten[name] = withNullEnum({ ...prop, type: [...type, "null"] });
|
|
1425
|
+
} else {
|
|
1426
|
+
rewritten[name] = withNullEnum(prop);
|
|
1427
|
+
}
|
|
1124
1428
|
}
|
|
1125
|
-
|
|
1429
|
+
return { ...node, properties: rewritten, required: Object.keys(properties) };
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
function applyClientTarget(schema, target) {
|
|
1433
|
+
let result = inlineLocalRefs(schema);
|
|
1434
|
+
result = ensureArrayItems(result);
|
|
1435
|
+
if (target === "gemini") {
|
|
1436
|
+
result = collapseNestedUnions(result);
|
|
1437
|
+
result = demoteFormats(result);
|
|
1438
|
+
return result;
|
|
1126
1439
|
}
|
|
1127
|
-
|
|
1440
|
+
result = collapseRootCompositions(result);
|
|
1441
|
+
if (target === "openai") {
|
|
1442
|
+
result = enforceClosedObjects(result);
|
|
1443
|
+
result = requireAllProperties(result);
|
|
1444
|
+
}
|
|
1445
|
+
return result;
|
|
1128
1446
|
}
|
|
1129
1447
|
|
|
1130
|
-
// src/
|
|
1131
|
-
var
|
|
1132
|
-
document;
|
|
1133
|
-
dereferencedDocument;
|
|
1134
|
-
options;
|
|
1135
|
-
/**
|
|
1136
|
-
* Private constructor - use static factory methods to create instances
|
|
1137
|
-
*/
|
|
1138
|
-
constructor(document, options = {}) {
|
|
1139
|
-
this.document = document;
|
|
1140
|
-
this.options = {
|
|
1141
|
-
dereference: options.dereference ?? true,
|
|
1142
|
-
baseUrl: options.baseUrl ?? "",
|
|
1143
|
-
headers: options.headers ?? {},
|
|
1144
|
-
timeout: options.timeout ?? 3e4,
|
|
1145
|
-
validate: options.validate ?? true,
|
|
1146
|
-
followRedirects: options.followRedirects ?? true,
|
|
1147
|
-
refResolution: options.refResolution ?? {}
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1448
|
+
// src/validator.ts
|
|
1449
|
+
var Validator = class {
|
|
1150
1450
|
/**
|
|
1151
|
-
*
|
|
1451
|
+
* Validate an OpenAPI document
|
|
1152
1452
|
*/
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1453
|
+
async validate(document) {
|
|
1454
|
+
const errors = [];
|
|
1455
|
+
const warnings = [];
|
|
1456
|
+
if (!document.openapi) {
|
|
1457
|
+
errors.push({
|
|
1458
|
+
message: "Missing required field: openapi",
|
|
1459
|
+
path: "/openapi",
|
|
1460
|
+
code: "MISSING_OPENAPI_VERSION"
|
|
1160
1461
|
});
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1462
|
+
} else if (!this.isValidOpenAPIVersion(document.openapi)) {
|
|
1463
|
+
errors.push({
|
|
1464
|
+
message: `Unsupported OpenAPI version: ${document.openapi}. Expected 3.0.x or 3.1.x`,
|
|
1465
|
+
path: "/openapi",
|
|
1466
|
+
code: "INVALID_OPENAPI_VERSION"
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
if (!document.info) {
|
|
1470
|
+
errors.push({
|
|
1471
|
+
message: "Missing required field: info",
|
|
1472
|
+
path: "/info",
|
|
1473
|
+
code: "MISSING_INFO"
|
|
1474
|
+
});
|
|
1475
|
+
} else {
|
|
1476
|
+
if (!document.info.title) {
|
|
1477
|
+
errors.push({
|
|
1478
|
+
message: "Missing required field: info.title",
|
|
1479
|
+
path: "/info/title",
|
|
1480
|
+
code: "MISSING_TITLE"
|
|
1165
1481
|
});
|
|
1166
1482
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
document = JSON.parse(text);
|
|
1174
|
-
}
|
|
1175
|
-
return new _OpenAPIToolGenerator(document, options);
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
if (error instanceof LoadError) {
|
|
1178
|
-
throw error;
|
|
1483
|
+
if (!document.info.version) {
|
|
1484
|
+
errors.push({
|
|
1485
|
+
message: "Missing required field: info.version",
|
|
1486
|
+
path: "/info/version",
|
|
1487
|
+
code: "MISSING_VERSION"
|
|
1488
|
+
});
|
|
1179
1489
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1490
|
+
}
|
|
1491
|
+
if (!document.paths || Object.keys(document.paths).length === 0) {
|
|
1492
|
+
warnings.push({
|
|
1493
|
+
message: "No paths defined in OpenAPI document",
|
|
1494
|
+
path: "/paths",
|
|
1495
|
+
code: "NO_PATHS"
|
|
1184
1496
|
});
|
|
1497
|
+
} else {
|
|
1498
|
+
this.validatePaths(document.paths, errors, warnings);
|
|
1185
1499
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
try {
|
|
1192
|
-
const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
|
|
1193
|
-
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
1194
|
-
const content = await fs.readFile(absolutePath, "utf-8");
|
|
1195
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
1196
|
-
let document;
|
|
1197
|
-
if (ext === ".yaml" || ext === ".yml") {
|
|
1198
|
-
document = yaml.parse(content);
|
|
1199
|
-
} else if (ext === ".json") {
|
|
1200
|
-
document = JSON.parse(content);
|
|
1201
|
-
} else {
|
|
1202
|
-
try {
|
|
1203
|
-
document = JSON.parse(content);
|
|
1204
|
-
} catch {
|
|
1205
|
-
document = yaml.parse(content);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
return new _OpenAPIToolGenerator(document, options);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1211
|
-
throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
|
|
1212
|
-
filePath,
|
|
1213
|
-
originalError: error
|
|
1500
|
+
if (!document.servers || document.servers.length === 0) {
|
|
1501
|
+
warnings.push({
|
|
1502
|
+
message: "No servers defined. You may need to provide a baseUrl option.",
|
|
1503
|
+
path: "/servers",
|
|
1504
|
+
code: "NO_SERVERS"
|
|
1214
1505
|
});
|
|
1215
1506
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
try {
|
|
1222
|
-
const document = yaml.parse(yamlString);
|
|
1223
|
-
return new _OpenAPIToolGenerator(document, options);
|
|
1224
|
-
} catch (error) {
|
|
1225
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1226
|
-
throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
|
|
1227
|
-
originalError: error
|
|
1507
|
+
if (document.security && !document.components?.securitySchemes) {
|
|
1508
|
+
warnings.push({
|
|
1509
|
+
message: "Security requirements defined but no security schemes found",
|
|
1510
|
+
path: "/security",
|
|
1511
|
+
code: "NO_SECURITY_SCHEMES"
|
|
1228
1512
|
});
|
|
1229
1513
|
}
|
|
1514
|
+
return {
|
|
1515
|
+
valid: errors.length === 0,
|
|
1516
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
1517
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
1518
|
+
};
|
|
1230
1519
|
}
|
|
1231
1520
|
/**
|
|
1232
|
-
*
|
|
1521
|
+
* Check if OpenAPI version is valid
|
|
1233
1522
|
*/
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
return new _OpenAPIToolGenerator(document, options);
|
|
1523
|
+
isValidOpenAPIVersion(version) {
|
|
1524
|
+
return /^3\.[01]\.\d+$/.test(version);
|
|
1237
1525
|
}
|
|
1238
1526
|
/**
|
|
1239
|
-
*
|
|
1527
|
+
* Validate paths
|
|
1240
1528
|
*/
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
}
|
|
1251
|
-
// NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
|
|
1252
|
-
// in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
|
|
1253
|
-
// shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
|
|
1254
|
-
// augmented there with DNS resolution (closing the DNS-name-to-internal bypass)
|
|
1255
|
-
// and per-hop redirect re-validation (`safeFetch`).
|
|
1256
|
-
/**
|
|
1257
|
-
* Build $RefParser options based on refResolution configuration.
|
|
1258
|
-
* Defaults: allow http/https, block file://, block internal IPs.
|
|
1259
|
-
*/
|
|
1260
|
-
buildRefParserOptions() {
|
|
1261
|
-
const raw = this.options.refResolution;
|
|
1262
|
-
const refOpts = {
|
|
1263
|
-
allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
|
|
1264
|
-
allowedHosts: raw.allowedHosts ?? [],
|
|
1265
|
-
blockedHosts: raw.blockedHosts ?? [],
|
|
1266
|
-
allowInternalIPs: raw.allowInternalIPs ?? false
|
|
1267
|
-
};
|
|
1268
|
-
const allowedProtocols = new Set(refOpts.allowedProtocols);
|
|
1269
|
-
const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
|
|
1270
|
-
if (allowedProtocols.size === 0) {
|
|
1271
|
-
return { resolve: { external: false } };
|
|
1272
|
-
}
|
|
1273
|
-
const resolveConfig = {
|
|
1274
|
-
external: true,
|
|
1275
|
-
file: allowedProtocols.has("file") ? void 0 : false
|
|
1276
|
-
};
|
|
1277
|
-
if (hasNetworkProtocol) {
|
|
1278
|
-
const hasHostAllowlist = refOpts.allowedHosts.length > 0;
|
|
1279
|
-
const hostAllowSet = new Set(refOpts.allowedHosts);
|
|
1280
|
-
resolveConfig["http"] = {
|
|
1281
|
-
// SECURITY: never auto-follow HTTP redirects when resolving external
|
|
1282
|
-
// `$ref`s. `canRead` validates only the INITIAL URL; the resolver's
|
|
1283
|
-
// default redirect-following (up to 5 hops) re-fetches the `Location`
|
|
1284
|
-
// target WITHOUT re-invoking `canRead`, so an allowlisted host could
|
|
1285
|
-
// 302 → `http://169.254.169.254/...` and smuggle a blocked target past
|
|
1286
|
-
// the allow/deny lists. `redirects: 0` refuses the first redirect, and
|
|
1287
|
-
// our custom `read` (below) additionally refuses redirects itself.
|
|
1288
|
-
redirects: 0,
|
|
1289
|
-
// Synchronous gate: protocol, host allow-list, and literal/known
|
|
1290
|
-
// internal hosts. DNS names that *resolve* to internal addresses pass
|
|
1291
|
-
// here (canRead cannot be async) and are caught in `read` via DNS
|
|
1292
|
-
// resolution — closing the `127.0.0.1.nip.io` bypass for `$ref`s too.
|
|
1293
|
-
canRead: (file) => {
|
|
1294
|
-
try {
|
|
1295
|
-
const parsed = new URL(file.url);
|
|
1296
|
-
const protocol = parsed.protocol.replace(":", "");
|
|
1297
|
-
if (!allowedProtocols.has(protocol)) {
|
|
1298
|
-
return false;
|
|
1299
|
-
}
|
|
1300
|
-
if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
|
|
1301
|
-
return false;
|
|
1302
|
-
}
|
|
1303
|
-
if (isBlockedHostname(parsed.hostname, refOpts)) {
|
|
1304
|
-
return false;
|
|
1305
|
-
}
|
|
1306
|
-
return true;
|
|
1307
|
-
} catch {
|
|
1308
|
-
return false;
|
|
1309
|
-
}
|
|
1310
|
-
},
|
|
1311
|
-
// SSRF-safe fetch: resolves DNS and rejects names that map to internal
|
|
1312
|
-
// addresses, and refuses redirects. NOTE: deliberately does NOT forward
|
|
1313
|
-
// `this.options.headers` (the spec-load credentials) to third-party
|
|
1314
|
-
// `$ref` hosts — that would leak the spec's auth token cross-origin.
|
|
1315
|
-
read: async (file) => {
|
|
1316
|
-
const response = await safeFetch(file.url, {
|
|
1317
|
-
timeoutMs: this.options.timeout,
|
|
1318
|
-
followRedirects: false,
|
|
1319
|
-
ssrf: refOpts
|
|
1320
|
-
});
|
|
1321
|
-
if (!response.ok) {
|
|
1322
|
-
throw new LoadError(
|
|
1323
|
-
`Failed to resolve external $ref "${file.url}": ${response.status} ${response.statusText}`,
|
|
1324
|
-
{ url: file.url, status: response.status }
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1327
|
-
return response.text();
|
|
1328
|
-
}
|
|
1329
|
-
};
|
|
1330
|
-
} else {
|
|
1331
|
-
resolveConfig["http"] = false;
|
|
1332
|
-
}
|
|
1333
|
-
return { resolve: resolveConfig };
|
|
1334
|
-
}
|
|
1335
|
-
/**
|
|
1336
|
-
* Does the document contain any EXTERNAL `$ref` (a ref that is not a local
|
|
1337
|
-
* JSON-pointer beginning with `#`)? Only external refs require the full
|
|
1338
|
-
* `$RefParser` (file/http resolvers, which pull Node builtins). A document
|
|
1339
|
-
* with only internal refs can be dereferenced with the runtime-agnostic
|
|
1340
|
-
* resolver below — so it works on V8 isolates (Cloudflare Workers) too.
|
|
1341
|
-
*/
|
|
1342
|
-
static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
|
|
1343
|
-
if (node === null || typeof node !== "object") return false;
|
|
1344
|
-
if (seen.has(node)) return false;
|
|
1345
|
-
seen.add(node);
|
|
1346
|
-
if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
|
|
1347
|
-
const ref = node.$ref;
|
|
1348
|
-
if (typeof ref === "string" && !ref.startsWith("#")) return true;
|
|
1349
|
-
return Object.values(node).some(
|
|
1350
|
-
(v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
|
|
1351
|
-
);
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
|
|
1355
|
-
* free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
|
|
1356
|
-
* shared reference instead of recursing forever (same contract as `$RefParser`).
|
|
1357
|
-
*/
|
|
1358
|
-
static dereferenceInternal(root) {
|
|
1359
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1360
|
-
const resolvePointer = (ptr) => {
|
|
1361
|
-
const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
1362
|
-
let cur = root;
|
|
1363
|
-
for (const p of parts) cur = cur?.[p];
|
|
1364
|
-
return cur;
|
|
1365
|
-
};
|
|
1366
|
-
const walk = (node) => {
|
|
1367
|
-
if (node === null || typeof node !== "object") return node;
|
|
1368
|
-
if (Array.isArray(node)) return node.map(walk);
|
|
1369
|
-
const ref = node.$ref;
|
|
1370
|
-
if (typeof ref === "string" && ref.startsWith("#")) {
|
|
1371
|
-
const cached = cache.get(ref);
|
|
1372
|
-
if (cached !== void 0) return cached;
|
|
1373
|
-
const placeholder = {};
|
|
1374
|
-
cache.set(ref, placeholder);
|
|
1375
|
-
const resolved = walk(resolvePointer(ref));
|
|
1376
|
-
if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
|
|
1377
|
-
return placeholder;
|
|
1378
|
-
}
|
|
1379
|
-
const out = {};
|
|
1380
|
-
for (const [k, v] of Object.entries(node)) out[k] = walk(v);
|
|
1381
|
-
return out;
|
|
1382
|
-
};
|
|
1383
|
-
return walk(root);
|
|
1384
|
-
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Initialize the generator (dereference if needed, then validate)
|
|
1387
|
-
*/
|
|
1388
|
-
async initialize() {
|
|
1389
|
-
if (this.options.dereference && !this.dereferencedDocument) {
|
|
1390
|
-
const cloned = JSON.parse(JSON.stringify(this.document));
|
|
1391
|
-
if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
|
|
1392
|
-
this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
|
|
1393
|
-
} else {
|
|
1394
|
-
try {
|
|
1395
|
-
const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
|
|
1396
|
-
const refParserOptions = this.buildRefParserOptions();
|
|
1397
|
-
this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
|
|
1398
|
-
} catch (error) {
|
|
1399
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1400
|
-
throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
|
|
1401
|
-
originalError: error
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
if (this.options.validate) {
|
|
1407
|
-
const validator = new Validator();
|
|
1408
|
-
const documentToValidate = this.dereferencedDocument ?? this.document;
|
|
1409
|
-
const result = await validator.validate(documentToValidate);
|
|
1410
|
-
if (!result.valid) {
|
|
1411
|
-
throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
|
|
1529
|
+
validatePaths(paths, errors, warnings) {
|
|
1530
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
1531
|
+
if (!pathItem) continue;
|
|
1532
|
+
if (!path.startsWith("/")) {
|
|
1533
|
+
errors.push({
|
|
1534
|
+
message: `Path must start with '/': ${path}`,
|
|
1535
|
+
path: `/paths/${path}`,
|
|
1536
|
+
code: "INVALID_PATH_FORMAT"
|
|
1537
|
+
});
|
|
1412
1538
|
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Generate all tools from the OpenAPI specification
|
|
1417
|
-
*/
|
|
1418
|
-
async generateTools(options = {}) {
|
|
1419
|
-
await this.initialize();
|
|
1420
|
-
const document = this.getDocument();
|
|
1421
|
-
const tools = [];
|
|
1422
|
-
if (!document.paths) {
|
|
1423
|
-
return tools;
|
|
1424
|
-
}
|
|
1425
|
-
for (const [pathStr, pathItem] of Object.entries(document.paths)) {
|
|
1426
|
-
if (!pathItem || "$ref" in pathItem) continue;
|
|
1427
1539
|
const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
1540
|
+
let hasOperations = false;
|
|
1428
1541
|
for (const method of methods) {
|
|
1429
1542
|
const operation = pathItem[method];
|
|
1430
|
-
if (
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
}
|
|
1434
|
-
try {
|
|
1435
|
-
const tool = await this.generateTool(pathStr, method, options);
|
|
1436
|
-
tools.push(tool);
|
|
1437
|
-
} catch (error) {
|
|
1438
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1439
|
-
console.warn(`Failed to generate tool for ${method.toUpperCase()} ${pathStr}:`, errorMessage);
|
|
1543
|
+
if (operation) {
|
|
1544
|
+
hasOperations = true;
|
|
1545
|
+
this.validateOperation(operation, path, method, errors, warnings);
|
|
1440
1546
|
}
|
|
1441
1547
|
}
|
|
1548
|
+
if (!hasOperations && !pathItem.$ref) {
|
|
1549
|
+
warnings.push({
|
|
1550
|
+
message: `Path has no operations: ${path}`,
|
|
1551
|
+
path: `/paths/${path}`,
|
|
1552
|
+
code: "NO_OPERATIONS"
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1442
1555
|
}
|
|
1443
|
-
return tools;
|
|
1444
1556
|
}
|
|
1445
1557
|
/**
|
|
1446
|
-
*
|
|
1558
|
+
* Validate an operation
|
|
1447
1559
|
*/
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
if (!operation) {
|
|
1457
|
-
throw new Error(`Operation not found: ${method.toUpperCase()} ${pathStr}`);
|
|
1560
|
+
validateOperation(operation, path, method, errors, warnings) {
|
|
1561
|
+
const basePath = `/paths/${path}/${method}`;
|
|
1562
|
+
if (!operation.operationId) {
|
|
1563
|
+
warnings.push({
|
|
1564
|
+
message: `Operation missing operationId: ${method.toUpperCase()} ${path}`,
|
|
1565
|
+
path: `${basePath}/operationId`,
|
|
1566
|
+
code: "NO_OPERATION_ID"
|
|
1567
|
+
});
|
|
1458
1568
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
);
|
|
1569
|
+
if (!operation.responses || Object.keys(operation.responses).length === 0) {
|
|
1570
|
+
errors.push({
|
|
1571
|
+
message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
|
|
1572
|
+
path: `${basePath}/responses`,
|
|
1573
|
+
code: "NO_RESPONSES"
|
|
1574
|
+
});
|
|
1465
1575
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
if (securitySpec) {
|
|
1469
|
-
securityRequirements = this.extractSecurityRequirements(securitySpec, document);
|
|
1576
|
+
if (operation.parameters) {
|
|
1577
|
+
this.validateParameters(operation.parameters, path, method, errors, warnings);
|
|
1470
1578
|
}
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
securityRequirements,
|
|
1475
|
-
options.includeSecurityInInput
|
|
1579
|
+
const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
|
|
1580
|
+
const definedPathParams = new Set(
|
|
1581
|
+
operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
|
|
1476
1582
|
);
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
}
|
|
1486
|
-
const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
|
|
1487
|
-
const resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
|
|
1488
|
-
const resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
|
|
1489
|
-
return {
|
|
1490
|
-
name,
|
|
1491
|
-
description,
|
|
1492
|
-
inputSchema: resolvedInputSchema,
|
|
1493
|
-
outputSchema: resolvedOutputSchema,
|
|
1494
|
-
mapper,
|
|
1495
|
-
metadata
|
|
1496
|
-
};
|
|
1583
|
+
for (const param of pathParams) {
|
|
1584
|
+
if (!definedPathParams.has(param)) {
|
|
1585
|
+
errors.push({
|
|
1586
|
+
message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
|
|
1587
|
+
path: `${basePath}/parameters`,
|
|
1588
|
+
code: "MISSING_PATH_PARAMETER"
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1497
1592
|
}
|
|
1498
1593
|
/**
|
|
1499
|
-
*
|
|
1594
|
+
* Validate parameters
|
|
1500
1595
|
*/
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (!
|
|
1507
|
-
|
|
1596
|
+
validateParameters(parameters, path, method, errors, warnings) {
|
|
1597
|
+
const basePath = `/paths/${path}/${method}/parameters`;
|
|
1598
|
+
for (let i = 0; i < parameters.length; i++) {
|
|
1599
|
+
const param = parameters[i];
|
|
1600
|
+
const paramPath = `${basePath}/${i}`;
|
|
1601
|
+
if (!param.name) {
|
|
1602
|
+
errors.push({
|
|
1603
|
+
message: "Parameter missing name",
|
|
1604
|
+
path: `${paramPath}/name`,
|
|
1605
|
+
code: "MISSING_PARAMETER_NAME"
|
|
1606
|
+
});
|
|
1508
1607
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1608
|
+
if (!param.in) {
|
|
1609
|
+
errors.push({
|
|
1610
|
+
message: 'Parameter missing "in" field',
|
|
1611
|
+
path: `${paramPath}/in`,
|
|
1612
|
+
code: "MISSING_PARAMETER_IN"
|
|
1613
|
+
});
|
|
1614
|
+
} else if (!["path", "query", "header", "cookie"].includes(param.in)) {
|
|
1615
|
+
errors.push({
|
|
1616
|
+
message: `Invalid parameter location: ${param.in}`,
|
|
1617
|
+
path: `${paramPath}/in`,
|
|
1618
|
+
code: "INVALID_PARAMETER_IN"
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
if (param.in === "path" && !param.required) {
|
|
1622
|
+
errors.push({
|
|
1623
|
+
message: `Path parameter '${param.name}' must be required`,
|
|
1624
|
+
path: `${paramPath}/required`,
|
|
1625
|
+
code: "PATH_PARAMETER_NOT_REQUIRED"
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if (!param.schema && !param.content) {
|
|
1629
|
+
errors.push({
|
|
1630
|
+
message: `Parameter '${param.name}' missing schema or content`,
|
|
1631
|
+
path: `${paramPath}`,
|
|
1632
|
+
code: "MISSING_PARAMETER_SCHEMA"
|
|
1633
|
+
});
|
|
1513
1634
|
}
|
|
1514
1635
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// src/errors.ts
|
|
1640
|
+
var OpenAPIToolError = class extends Error {
|
|
1641
|
+
context;
|
|
1642
|
+
constructor(message, context) {
|
|
1643
|
+
super(message);
|
|
1644
|
+
this.name = this.constructor.name;
|
|
1645
|
+
this.context = context;
|
|
1646
|
+
if (Error.captureStackTrace) {
|
|
1647
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1521
1648
|
}
|
|
1522
|
-
return true;
|
|
1523
1649
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1650
|
+
};
|
|
1651
|
+
var LoadError = class extends OpenAPIToolError {
|
|
1652
|
+
constructor(message, context) {
|
|
1653
|
+
super(message, context);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
var SsrfError = class extends LoadError {
|
|
1657
|
+
constructor(message, context) {
|
|
1658
|
+
super(message, context);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
var ParseError = class extends OpenAPIToolError {
|
|
1662
|
+
constructor(message, context) {
|
|
1663
|
+
super(message, context);
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
var ValidationError = class extends OpenAPIToolError {
|
|
1667
|
+
errors;
|
|
1668
|
+
constructor(message, context) {
|
|
1669
|
+
super(message, context);
|
|
1670
|
+
this.errors = context?.["errors"];
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
var GenerationError = class extends OpenAPIToolError {
|
|
1674
|
+
constructor(message, context) {
|
|
1675
|
+
super(message, context);
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var RequestBuildError = class extends OpenAPIToolError {
|
|
1679
|
+
constructor(message, context) {
|
|
1680
|
+
super(message, context);
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
var SchemaError = class extends OpenAPIToolError {
|
|
1684
|
+
constructor(message, context) {
|
|
1685
|
+
super(message, context);
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
// src/format-resolver.ts
|
|
1690
|
+
var BUILTIN_FORMAT_RESOLVERS = {
|
|
1691
|
+
// String formats
|
|
1692
|
+
uuid: (schema) => ({
|
|
1693
|
+
...schema,
|
|
1694
|
+
pattern: schema.pattern ?? "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
|
1695
|
+
description: schema.description || "UUID string (RFC 4122)"
|
|
1696
|
+
}),
|
|
1697
|
+
"date-time": (schema) => ({
|
|
1698
|
+
...schema,
|
|
1699
|
+
description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
|
|
1700
|
+
}),
|
|
1701
|
+
date: (schema) => ({
|
|
1702
|
+
...schema,
|
|
1703
|
+
pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
|
|
1704
|
+
description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
|
|
1705
|
+
}),
|
|
1706
|
+
time: (schema) => ({
|
|
1707
|
+
...schema,
|
|
1708
|
+
pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
|
|
1709
|
+
description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
|
|
1710
|
+
}),
|
|
1711
|
+
email: (schema) => ({
|
|
1712
|
+
...schema,
|
|
1713
|
+
description: schema.description || "Email address (RFC 5322)"
|
|
1714
|
+
}),
|
|
1715
|
+
uri: (schema) => ({
|
|
1716
|
+
...schema,
|
|
1717
|
+
description: schema.description || "URI (RFC 3986)"
|
|
1718
|
+
}),
|
|
1719
|
+
"uri-reference": (schema) => ({
|
|
1720
|
+
...schema,
|
|
1721
|
+
description: schema.description || "URI reference (RFC 3986)"
|
|
1722
|
+
}),
|
|
1723
|
+
hostname: (schema) => ({
|
|
1724
|
+
...schema,
|
|
1725
|
+
description: schema.description || "Internet hostname (RFC 1123)"
|
|
1726
|
+
}),
|
|
1727
|
+
ipv4: (schema) => ({
|
|
1728
|
+
...schema,
|
|
1729
|
+
pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
|
|
1730
|
+
description: schema.description || "IPv4 address"
|
|
1731
|
+
}),
|
|
1732
|
+
ipv6: (schema) => ({
|
|
1733
|
+
...schema,
|
|
1734
|
+
description: schema.description || "IPv6 address (RFC 4291)"
|
|
1735
|
+
}),
|
|
1736
|
+
// Integer formats
|
|
1737
|
+
int32: (schema) => ({
|
|
1738
|
+
...schema,
|
|
1739
|
+
minimum: schema.minimum ?? -2147483648,
|
|
1740
|
+
maximum: schema.maximum ?? 2147483647
|
|
1741
|
+
}),
|
|
1742
|
+
int64: (schema) => ({
|
|
1743
|
+
...schema,
|
|
1744
|
+
minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
|
|
1745
|
+
maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
1746
|
+
}),
|
|
1747
|
+
// Binary/encoding formats
|
|
1748
|
+
byte: (schema) => ({
|
|
1749
|
+
...schema,
|
|
1750
|
+
pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
|
|
1751
|
+
description: schema.description || "Base64-encoded string (RFC 4648)"
|
|
1752
|
+
}),
|
|
1753
|
+
binary: (schema) => ({
|
|
1754
|
+
...schema,
|
|
1755
|
+
description: schema.description || "Binary data"
|
|
1756
|
+
}),
|
|
1757
|
+
// Sensitive data formats
|
|
1758
|
+
password: (schema) => ({
|
|
1759
|
+
...schema,
|
|
1760
|
+
description: schema.description || "Password (sensitive, UI should mask input)"
|
|
1761
|
+
})
|
|
1762
|
+
};
|
|
1763
|
+
function resolveSchemaFormats(schema, resolvers) {
|
|
1764
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
1765
|
+
let result = { ...schema };
|
|
1766
|
+
const format = result["format"];
|
|
1767
|
+
if (format && resolvers[format]) {
|
|
1768
|
+
result = { ...resolvers[format](result) };
|
|
1769
|
+
}
|
|
1770
|
+
if (result["properties"] && typeof result["properties"] === "object") {
|
|
1771
|
+
const props = {};
|
|
1772
|
+
for (const [key, value] of Object.entries(result["properties"])) {
|
|
1773
|
+
props[key] = resolveSchemaFormats(value, resolvers);
|
|
1774
|
+
}
|
|
1775
|
+
result["properties"] = props;
|
|
1776
|
+
}
|
|
1777
|
+
if (result["items"]) {
|
|
1778
|
+
if (Array.isArray(result["items"])) {
|
|
1779
|
+
result["items"] = result["items"].map((item) => resolveSchemaFormats(item, resolvers));
|
|
1780
|
+
} else {
|
|
1781
|
+
result["items"] = resolveSchemaFormats(result["items"], resolvers);
|
|
1530
1782
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1783
|
+
}
|
|
1784
|
+
if (result["additionalProperties"] && typeof result["additionalProperties"] === "object") {
|
|
1785
|
+
result["additionalProperties"] = resolveSchemaFormats(result["additionalProperties"], resolvers);
|
|
1786
|
+
}
|
|
1787
|
+
for (const key of ["allOf", "anyOf", "oneOf"]) {
|
|
1788
|
+
if (result[key] && Array.isArray(result[key])) {
|
|
1789
|
+
result[key] = result[key].map((s) => resolveSchemaFormats(s, resolvers));
|
|
1533
1790
|
}
|
|
1534
|
-
const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
1535
|
-
return `${method}_${sanitized}`;
|
|
1536
1791
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1792
|
+
if (result["not"] && typeof result["not"] === "object") {
|
|
1793
|
+
result["not"] = resolveSchemaFormats(result["not"], resolvers);
|
|
1794
|
+
}
|
|
1795
|
+
return result;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// src/ssrf.ts
|
|
1799
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
1800
|
+
"localhost",
|
|
1801
|
+
"localhost.localdomain",
|
|
1802
|
+
"ip6-localhost",
|
|
1803
|
+
"ip6-loopback",
|
|
1804
|
+
"metadata",
|
|
1805
|
+
"metadata.google.internal",
|
|
1806
|
+
"metadata.goog"
|
|
1807
|
+
]);
|
|
1808
|
+
function normalizeSsrfOptions(refResolution) {
|
|
1809
|
+
return {
|
|
1810
|
+
allowedHosts: refResolution?.allowedHosts ?? [],
|
|
1811
|
+
blockedHosts: refResolution?.blockedHosts ?? [],
|
|
1812
|
+
allowInternalIPs: refResolution?.allowInternalIPs ?? false
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
function decodeIpv4MappedIpv6(hostname) {
|
|
1816
|
+
let h = hostname;
|
|
1817
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
1818
|
+
const lower = h.toLowerCase();
|
|
1819
|
+
const marker = lower.lastIndexOf("::ffff:");
|
|
1820
|
+
if (marker === -1) return null;
|
|
1821
|
+
const tail = lower.slice(marker + "::ffff:".length);
|
|
1822
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(tail)) return tail;
|
|
1823
|
+
const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
1824
|
+
if (hex) {
|
|
1825
|
+
const hi = parseInt(hex[1], 16);
|
|
1826
|
+
const lo = parseInt(hex[2], 16);
|
|
1827
|
+
if (Number.isNaN(hi) || Number.isNaN(lo)) return null;
|
|
1828
|
+
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
function parseIpv4(host) {
|
|
1833
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
1834
|
+
if (!m) return null;
|
|
1835
|
+
const octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
1836
|
+
if (octets.some((n) => n > 255)) return null;
|
|
1837
|
+
return octets;
|
|
1838
|
+
}
|
|
1839
|
+
function isBlockedIpv4(octets) {
|
|
1840
|
+
const [a, b, c] = octets;
|
|
1841
|
+
if (a === 0) return true;
|
|
1842
|
+
if (a === 10) return true;
|
|
1843
|
+
if (a === 127) return true;
|
|
1844
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
1845
|
+
if (a === 169 && b === 254) return true;
|
|
1846
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1847
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
1848
|
+
if (a === 192 && b === 168) return true;
|
|
1849
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
1850
|
+
if (a >= 224) return true;
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
function isBlockedIpv6(host) {
|
|
1854
|
+
let h = host;
|
|
1855
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
1856
|
+
const zone = h.indexOf("%");
|
|
1857
|
+
if (zone !== -1) h = h.slice(0, zone);
|
|
1858
|
+
const lower = h.toLowerCase();
|
|
1859
|
+
if (lower === "::" || lower === "::0") return true;
|
|
1860
|
+
if (lower === "::1") return true;
|
|
1861
|
+
if (/^f[cd]/.test(lower)) return true;
|
|
1862
|
+
if (/^fe[89a-f]/.test(lower)) return true;
|
|
1863
|
+
if (/^ff/.test(lower)) return true;
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
function isBlockedAddress(host) {
|
|
1867
|
+
let h = host;
|
|
1868
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
1869
|
+
const mapped = decodeIpv4MappedIpv6(host);
|
|
1870
|
+
if (mapped) {
|
|
1871
|
+
const o = parseIpv4(mapped);
|
|
1872
|
+
if (o) return isBlockedIpv4(o);
|
|
1873
|
+
}
|
|
1874
|
+
const v4 = parseIpv4(h);
|
|
1875
|
+
if (v4) return isBlockedIpv4(v4);
|
|
1876
|
+
if (h.includes(":")) return isBlockedIpv6(h);
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
function isIpLiteral(hostname) {
|
|
1880
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) return true;
|
|
1881
|
+
return parseIpv4(hostname) !== null;
|
|
1882
|
+
}
|
|
1883
|
+
function isBlockedHostname(hostname, ssrf) {
|
|
1884
|
+
if (ssrf.allowInternalIPs) {
|
|
1885
|
+
return ssrf.blockedHosts.includes(hostname);
|
|
1886
|
+
}
|
|
1887
|
+
if (ssrf.blockedHosts.includes(hostname)) return true;
|
|
1888
|
+
const lower = hostname.toLowerCase();
|
|
1889
|
+
const stripped = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
|
1890
|
+
if (BLOCKED_HOSTNAMES.has(lower) || BLOCKED_HOSTNAMES.has(stripped)) return true;
|
|
1891
|
+
return isBlockedAddress(hostname);
|
|
1892
|
+
}
|
|
1893
|
+
var SsrfResolverUnavailableError = class extends Error {
|
|
1894
|
+
};
|
|
1895
|
+
var defaultLookup = async (hostname) => {
|
|
1896
|
+
let dns;
|
|
1897
|
+
try {
|
|
1898
|
+
dns = await import("node:dns");
|
|
1899
|
+
} catch {
|
|
1900
|
+
throw new SsrfResolverUnavailableError("DNS resolution is unavailable on this runtime");
|
|
1901
|
+
}
|
|
1902
|
+
return dns.promises.lookup(hostname, { all: true });
|
|
1903
|
+
};
|
|
1904
|
+
async function assertUrlSafe(url, ssrf, lookup = defaultLookup) {
|
|
1905
|
+
let parsed;
|
|
1906
|
+
try {
|
|
1907
|
+
parsed = new URL(url);
|
|
1908
|
+
} catch {
|
|
1909
|
+
throw new SsrfError(`Invalid spec URL: ${url}`, { url });
|
|
1910
|
+
}
|
|
1911
|
+
const protocol = parsed.protocol.replace(/:$/, "");
|
|
1912
|
+
if (protocol !== "http" && protocol !== "https") {
|
|
1913
|
+
throw new SsrfError(`Protocol "${protocol}" is not allowed for network spec loading (only http/https)`, { url });
|
|
1914
|
+
}
|
|
1915
|
+
const hostname = parsed.hostname;
|
|
1916
|
+
if (ssrf.allowedHosts.length > 0 && !ssrf.allowedHosts.includes(hostname)) {
|
|
1917
|
+
throw new SsrfError(`Host "${hostname}" is not in the allowed-hosts list`, { url });
|
|
1918
|
+
}
|
|
1919
|
+
if (ssrf.allowInternalIPs) {
|
|
1920
|
+
if (ssrf.blockedHosts.includes(hostname)) {
|
|
1921
|
+
throw new SsrfError(`Host "${hostname}" is blocked`, { url });
|
|
1922
|
+
}
|
|
1923
|
+
return [];
|
|
1924
|
+
}
|
|
1925
|
+
if (isBlockedHostname(hostname, ssrf)) {
|
|
1926
|
+
throw new SsrfError(`Host "${hostname}" maps to a blocked internal address`, { url });
|
|
1927
|
+
}
|
|
1928
|
+
if (isIpLiteral(hostname)) {
|
|
1929
|
+
return [];
|
|
1930
|
+
}
|
|
1931
|
+
let addresses;
|
|
1932
|
+
try {
|
|
1933
|
+
addresses = await lookup(hostname);
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (error instanceof SsrfResolverUnavailableError) {
|
|
1936
|
+
return [];
|
|
1937
|
+
}
|
|
1938
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1939
|
+
throw new SsrfError(`Host "${hostname}" could not be resolved for SSRF validation: ${message}`, { url });
|
|
1940
|
+
}
|
|
1941
|
+
if (addresses.length === 0) {
|
|
1942
|
+
throw new SsrfError(`Host "${hostname}" did not resolve to any address`, { url });
|
|
1943
|
+
}
|
|
1944
|
+
for (const { address } of addresses) {
|
|
1945
|
+
if (isBlockedAddress(address)) {
|
|
1946
|
+
throw new SsrfError(`Host "${hostname}" resolves to blocked address ${address}`, { url });
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return addresses;
|
|
1950
|
+
}
|
|
1951
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
1952
|
+
async function loadNodeHttpModules() {
|
|
1953
|
+
try {
|
|
1954
|
+
const [http, https] = await Promise.all([import("node:http"), import("node:https")]);
|
|
1955
|
+
return { http, https };
|
|
1956
|
+
} catch {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function pickHttpModule(protocol, modules) {
|
|
1961
|
+
return protocol === "https:" ? modules.https : modules.http;
|
|
1962
|
+
}
|
|
1963
|
+
function makePinnedLookup(pinned) {
|
|
1964
|
+
return (_hostname, options, callback) => {
|
|
1965
|
+
const done = typeof options === "function" ? options : callback;
|
|
1966
|
+
const wantsAll = typeof options === "object" && options !== null && options.all === true;
|
|
1967
|
+
if (wantsAll) {
|
|
1968
|
+
done(
|
|
1969
|
+
null,
|
|
1970
|
+
pinned.map(({ address, family }) => ({ address, family }))
|
|
1554
1971
|
);
|
|
1972
|
+
} else {
|
|
1973
|
+
done(null, pinned[0].address, pinned[0].family);
|
|
1555
1974
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
var NULL_BODY_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
1978
|
+
function nodePinnedTransport(modules) {
|
|
1979
|
+
return (url, { headers, signal, pinned, maxBytes }) => new Promise((resolve, reject) => {
|
|
1980
|
+
const limit = maxBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
1981
|
+
const lib = pickHttpModule(new URL(url).protocol, modules);
|
|
1982
|
+
const requestOptions = {
|
|
1983
|
+
method: "GET",
|
|
1984
|
+
signal,
|
|
1985
|
+
headers: { ...headers, "accept-encoding": "identity" }
|
|
1986
|
+
};
|
|
1987
|
+
if (pinned.length > 0) {
|
|
1988
|
+
requestOptions["lookup"] = makePinnedLookup(pinned);
|
|
1565
1989
|
}
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1990
|
+
const request = lib.request(url, requestOptions, (response) => {
|
|
1991
|
+
const chunks = [];
|
|
1992
|
+
let received = 0;
|
|
1993
|
+
response.on("data", (chunk) => {
|
|
1994
|
+
received += chunk.length;
|
|
1995
|
+
if (received > limit) {
|
|
1996
|
+
request.destroy();
|
|
1997
|
+
reject(new SsrfError(`Response body exceeds ${limit} bytes`, { url }));
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
chunks.push(chunk);
|
|
2001
|
+
});
|
|
2002
|
+
response.on("end", () => {
|
|
2003
|
+
const status = response.statusCode;
|
|
2004
|
+
const responseHeaders = new Headers();
|
|
2005
|
+
const entries = Object.entries(response.headers);
|
|
2006
|
+
for (const [key, value] of entries) {
|
|
2007
|
+
if (Array.isArray(value)) {
|
|
2008
|
+
for (const item of value) responseHeaders.append(key, item);
|
|
2009
|
+
} else {
|
|
2010
|
+
responseHeaders.append(key, value);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
const body = NULL_BODY_STATUS.has(status) ? null : Buffer.concat(chunks);
|
|
2014
|
+
resolve(new Response(body, { status, statusText: response.statusMessage, headers: responseHeaders }));
|
|
2015
|
+
});
|
|
2016
|
+
response.on("error", reject);
|
|
2017
|
+
});
|
|
2018
|
+
request.on("error", reject);
|
|
2019
|
+
request.end();
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
function fetchTransport(fetchImpl) {
|
|
2023
|
+
return (url, { headers, signal }) => fetchImpl(url, { headers, signal, redirect: "manual" });
|
|
2024
|
+
}
|
|
2025
|
+
async function selectTransport(opts, url) {
|
|
2026
|
+
if (opts.fetchImpl) {
|
|
2027
|
+
return fetchTransport(opts.fetchImpl);
|
|
2028
|
+
}
|
|
2029
|
+
const modules = await loadNodeHttpModules();
|
|
2030
|
+
if (!modules) {
|
|
2031
|
+
const platformFetch = globalThis.fetch;
|
|
2032
|
+
if (typeof platformFetch === "function") {
|
|
2033
|
+
return fetchTransport(platformFetch);
|
|
1574
2034
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
2035
|
+
throw new SsrfError("No fetch implementation available to load OpenAPI spec from URL", { url });
|
|
2036
|
+
}
|
|
2037
|
+
return nodePinnedTransport(modules);
|
|
2038
|
+
}
|
|
2039
|
+
async function safeFetch(url, opts) {
|
|
2040
|
+
const { headers, timeoutMs = 3e4, followRedirects = true, maxRedirects = 5, ssrf, lookup } = opts;
|
|
2041
|
+
const transport = await selectTransport(opts, url);
|
|
2042
|
+
let current = url;
|
|
2043
|
+
for (let hop = 0; hop <= maxRedirects; hop++) {
|
|
2044
|
+
const pinned = await assertUrlSafe(current, ssrf, lookup);
|
|
2045
|
+
const controller = new AbortController();
|
|
2046
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2047
|
+
let response;
|
|
2048
|
+
try {
|
|
2049
|
+
response = await transport(current, { headers, signal: controller.signal, pinned, maxBytes: opts.maxResponseBytes });
|
|
2050
|
+
} finally {
|
|
2051
|
+
clearTimeout(timer);
|
|
1577
2052
|
}
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
2053
|
+
const status = typeof response.status === "number" ? response.status : 0;
|
|
2054
|
+
const isRedirect = status >= 300 && status < 400 && status !== 304;
|
|
2055
|
+
if (!isRedirect || !followRedirects) {
|
|
2056
|
+
return response;
|
|
1581
2057
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
* Extract security requirements
|
|
1586
|
-
*/
|
|
1587
|
-
extractSecurityRequirements(security, document) {
|
|
1588
|
-
if (!security || !document.components?.securitySchemes) {
|
|
1589
|
-
return [];
|
|
2058
|
+
const location = response.headers?.get?.("location") ?? void 0;
|
|
2059
|
+
if (!location) {
|
|
2060
|
+
return response;
|
|
1590
2061
|
}
|
|
1591
|
-
|
|
1592
|
-
(req) => Object.entries(req).map(([scheme, scopes]) => {
|
|
1593
|
-
const securityScheme = document.components.securitySchemes[scheme];
|
|
1594
|
-
if (isReferenceObject(securityScheme)) {
|
|
1595
|
-
return { scheme, type: "http", scopes };
|
|
1596
|
-
}
|
|
1597
|
-
const apiKeyIn = "in" in securityScheme ? securityScheme.in : void 0;
|
|
1598
|
-
const result = {
|
|
1599
|
-
scheme,
|
|
1600
|
-
type: securityScheme.type,
|
|
1601
|
-
scopes,
|
|
1602
|
-
name: "name" in securityScheme ? securityScheme.name : void 0,
|
|
1603
|
-
in: apiKeyIn && (apiKeyIn === "query" || apiKeyIn === "header" || apiKeyIn === "cookie") ? apiKeyIn : void 0
|
|
1604
|
-
};
|
|
1605
|
-
if (securityScheme.type === "http") {
|
|
1606
|
-
result.httpScheme = "scheme" in securityScheme ? securityScheme.scheme : void 0;
|
|
1607
|
-
result.bearerFormat = "bearerFormat" in securityScheme ? securityScheme.bearerFormat : void 0;
|
|
1608
|
-
}
|
|
1609
|
-
result.description = "description" in securityScheme ? securityScheme.description : void 0;
|
|
1610
|
-
return result;
|
|
1611
|
-
})
|
|
1612
|
-
);
|
|
2062
|
+
current = new URL(location, current).toString();
|
|
1613
2063
|
}
|
|
1614
|
-
};
|
|
2064
|
+
throw new SsrfError(`Too many redirects while loading OpenAPI spec (max ${maxRedirects})`, { url });
|
|
2065
|
+
}
|
|
1615
2066
|
|
|
1616
|
-
// src/
|
|
1617
|
-
var
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
2067
|
+
// src/generator.ts
|
|
2068
|
+
var MCP_MAX_TOOL_NAME_LENGTH = 128;
|
|
2069
|
+
var DEFAULT_MAX_TOOL_NAME_LENGTH = 64;
|
|
2070
|
+
var MAX_NAME_DEDUP_ATTEMPTS = 256;
|
|
2071
|
+
function applySecureDefaults(options) {
|
|
2072
|
+
if (!options.secureDefaults) return options;
|
|
2073
|
+
return {
|
|
2074
|
+
...options,
|
|
2075
|
+
followRedirects: options.followRedirects ?? false,
|
|
2076
|
+
// Merge PER KEY: a user tightening one refResolution knob (e.g.
|
|
2077
|
+
// blockedHosts) must not silently discard the preset's external-$ref
|
|
2078
|
+
// lockdown. A DEFINED allowedProtocols still wins — but an explicitly
|
|
2079
|
+
// undefined one (programmatic option building) must not defeat the
|
|
2080
|
+
// preset via object spread copying undefined-valued keys.
|
|
2081
|
+
refResolution: {
|
|
2082
|
+
...options.refResolution,
|
|
2083
|
+
allowedProtocols: options.refResolution?.allowedProtocols ?? []
|
|
1627
2084
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
if (
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
}
|
|
1641
|
-
if (schema.required) {
|
|
1642
|
-
schema.required.forEach((field) => allRequired.add(field));
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
function globToRegExp(glob) {
|
|
2088
|
+
let pattern = "^";
|
|
2089
|
+
for (let i = 0; i < glob.length; i++) {
|
|
2090
|
+
const char = glob[i];
|
|
2091
|
+
if (char === "*") {
|
|
2092
|
+
if (glob[i + 1] === "*") {
|
|
2093
|
+
pattern += ".*";
|
|
2094
|
+
i++;
|
|
2095
|
+
} else {
|
|
2096
|
+
pattern += "[^/]*";
|
|
1643
2097
|
}
|
|
2098
|
+
} else if (char === "?") {
|
|
2099
|
+
pattern += "[^/]";
|
|
2100
|
+
} else {
|
|
2101
|
+
pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1644
2102
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
2103
|
+
}
|
|
2104
|
+
return new RegExp(`${pattern}$`);
|
|
2105
|
+
}
|
|
2106
|
+
function matchesAnyGlob(path, globs) {
|
|
2107
|
+
return globs.some((glob) => globToRegExp(glob).test(path));
|
|
2108
|
+
}
|
|
2109
|
+
function fnv1aHex(input) {
|
|
2110
|
+
let hash = 2166136261;
|
|
2111
|
+
for (let i = 0; i < input.length; i++) {
|
|
2112
|
+
hash ^= input.charCodeAt(i);
|
|
2113
|
+
hash = Math.imul(hash, 16777619);
|
|
2114
|
+
}
|
|
2115
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2116
|
+
}
|
|
2117
|
+
function normalizeToolName(raw, maxLength, fallbackSeed) {
|
|
2118
|
+
let hashSeed = raw;
|
|
2119
|
+
let name = raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
2120
|
+
if (name.length === 0) {
|
|
2121
|
+
hashSeed = fallbackSeed;
|
|
2122
|
+
name = `tool_${fnv1aHex(fallbackSeed)}`;
|
|
2123
|
+
}
|
|
2124
|
+
const cap = Math.min(Math.max(1, maxLength), MCP_MAX_TOOL_NAME_LENGTH);
|
|
2125
|
+
if (name.length > cap) {
|
|
2126
|
+
if (cap >= 13) {
|
|
2127
|
+
name = `${name.slice(0, cap - 9)}_${fnv1aHex(hashSeed)}`;
|
|
2128
|
+
} else {
|
|
2129
|
+
name = fnv1aHex(hashSeed).slice(0, cap);
|
|
1647
2130
|
}
|
|
1648
|
-
return merged;
|
|
1649
2131
|
}
|
|
2132
|
+
return name;
|
|
2133
|
+
}
|
|
2134
|
+
var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
2135
|
+
document;
|
|
2136
|
+
dereferencedDocument;
|
|
2137
|
+
options;
|
|
1650
2138
|
/**
|
|
1651
|
-
*
|
|
2139
|
+
* Private constructor - use static factory methods to create instances
|
|
1652
2140
|
*/
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2141
|
+
constructor(document, rawOptions = {}) {
|
|
2142
|
+
this.document = document;
|
|
2143
|
+
const options = applySecureDefaults(rawOptions);
|
|
2144
|
+
this.options = {
|
|
2145
|
+
dereference: options.dereference ?? true,
|
|
2146
|
+
baseUrl: options.baseUrl ?? "",
|
|
2147
|
+
headers: options.headers ?? {},
|
|
2148
|
+
timeout: options.timeout ?? 3e4,
|
|
2149
|
+
validate: options.validate ?? true,
|
|
2150
|
+
followRedirects: options.followRedirects ?? true,
|
|
2151
|
+
refResolution: options.refResolution ?? {},
|
|
2152
|
+
secureDefaults: options.secureDefaults ?? false
|
|
1662
2153
|
};
|
|
1663
2154
|
}
|
|
1664
2155
|
/**
|
|
1665
|
-
*
|
|
2156
|
+
* Create generator from a URL
|
|
1666
2157
|
*/
|
|
1667
|
-
static
|
|
1668
|
-
|
|
2158
|
+
static async fromURL(url, rawOptions = {}) {
|
|
2159
|
+
const options = applySecureDefaults(rawOptions);
|
|
2160
|
+
try {
|
|
2161
|
+
const response = await safeFetch(url, {
|
|
2162
|
+
headers: options.headers,
|
|
2163
|
+
timeoutMs: options.timeout ?? 3e4,
|
|
2164
|
+
followRedirects: options.followRedirects ?? true,
|
|
2165
|
+
ssrf: normalizeSsrfOptions(options.refResolution)
|
|
2166
|
+
});
|
|
2167
|
+
if (!response.ok) {
|
|
2168
|
+
throw new LoadError(`Failed to fetch OpenAPI spec from URL: ${response.status} ${response.statusText}`, {
|
|
2169
|
+
url,
|
|
2170
|
+
status: response.status
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2174
|
+
const text = await response.text();
|
|
2175
|
+
let document;
|
|
2176
|
+
if (contentType.includes("yaml") || contentType.includes("yml") || url.match(/\.ya?ml$/i)) {
|
|
2177
|
+
document = yaml.parse(text);
|
|
2178
|
+
} else {
|
|
2179
|
+
document = JSON.parse(text);
|
|
2180
|
+
}
|
|
2181
|
+
return new _OpenAPIToolGenerator(document, options);
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
if (error instanceof LoadError) {
|
|
2184
|
+
throw error;
|
|
2185
|
+
}
|
|
2186
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2187
|
+
throw new LoadError(`Failed to load OpenAPI spec from URL: ${errorMessage}`, {
|
|
2188
|
+
url,
|
|
2189
|
+
originalError: error
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
1669
2192
|
}
|
|
1670
2193
|
/**
|
|
1671
|
-
*
|
|
2194
|
+
* Create generator from a file path
|
|
1672
2195
|
*/
|
|
1673
|
-
static
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2196
|
+
static async fromFile(filePath, options = {}) {
|
|
2197
|
+
try {
|
|
2198
|
+
const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
|
|
2199
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
2200
|
+
const content = await fs.readFile(absolutePath, "utf-8");
|
|
2201
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2202
|
+
let document;
|
|
2203
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
2204
|
+
document = yaml.parse(content);
|
|
2205
|
+
} else if (ext === ".json") {
|
|
2206
|
+
document = JSON.parse(content);
|
|
2207
|
+
} else {
|
|
2208
|
+
try {
|
|
2209
|
+
document = JSON.parse(content);
|
|
2210
|
+
} catch {
|
|
2211
|
+
document = yaml.parse(content);
|
|
1688
2212
|
}
|
|
1689
2213
|
}
|
|
2214
|
+
return new _OpenAPIToolGenerator(document, options);
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2217
|
+
throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
|
|
2218
|
+
filePath,
|
|
2219
|
+
originalError: error
|
|
2220
|
+
});
|
|
1690
2221
|
}
|
|
1691
2222
|
}
|
|
1692
2223
|
/**
|
|
1693
|
-
*
|
|
2224
|
+
* Create generator from a YAML string
|
|
1694
2225
|
*/
|
|
1695
|
-
static
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
}
|
|
2226
|
+
static async fromYAML(yamlString, options = {}) {
|
|
2227
|
+
try {
|
|
2228
|
+
const document = yaml.parse(yamlString);
|
|
2229
|
+
return new _OpenAPIToolGenerator(document, options);
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2232
|
+
throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
|
|
2233
|
+
originalError: error
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
1700
2236
|
}
|
|
1701
2237
|
/**
|
|
1702
|
-
*
|
|
2238
|
+
* Create generator from a JSON object
|
|
1703
2239
|
*/
|
|
1704
|
-
static
|
|
1705
|
-
const
|
|
1706
|
-
return
|
|
1707
|
-
...schema,
|
|
1708
|
-
examples: [...existingExamples, example]
|
|
1709
|
-
};
|
|
2240
|
+
static async fromJSON(json, options = {}) {
|
|
2241
|
+
const document = JSON.parse(JSON.stringify(json));
|
|
2242
|
+
return new _OpenAPIToolGenerator(document, options);
|
|
1710
2243
|
}
|
|
1711
2244
|
/**
|
|
1712
|
-
*
|
|
2245
|
+
* Get the OpenAPI document
|
|
1713
2246
|
*/
|
|
1714
|
-
|
|
1715
|
-
return
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2247
|
+
getDocument() {
|
|
2248
|
+
return this.dereferencedDocument ?? this.document;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Validate the OpenAPI document
|
|
2252
|
+
*/
|
|
2253
|
+
async validate() {
|
|
2254
|
+
const validator = new Validator();
|
|
2255
|
+
return validator.validate(this.document);
|
|
1719
2256
|
}
|
|
2257
|
+
// NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
|
|
2258
|
+
// in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
|
|
2259
|
+
// shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
|
|
2260
|
+
// augmented there with DNS resolution (closing the DNS-name-to-internal bypass)
|
|
2261
|
+
// and per-hop redirect re-validation (`safeFetch`).
|
|
1720
2262
|
/**
|
|
1721
|
-
*
|
|
2263
|
+
* Build $RefParser options based on refResolution configuration.
|
|
2264
|
+
* Defaults: allow http/https, block file://, block internal IPs.
|
|
1722
2265
|
*/
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
2266
|
+
buildRefParserOptions() {
|
|
2267
|
+
const raw = this.options.refResolution;
|
|
2268
|
+
const refOpts = {
|
|
2269
|
+
allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
|
|
2270
|
+
allowedHosts: raw.allowedHosts ?? [],
|
|
2271
|
+
blockedHosts: raw.blockedHosts ?? [],
|
|
2272
|
+
allowInternalIPs: raw.allowInternalIPs ?? false
|
|
2273
|
+
};
|
|
2274
|
+
const allowedProtocols = new Set(refOpts.allowedProtocols);
|
|
2275
|
+
const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
|
|
2276
|
+
if (allowedProtocols.size === 0) {
|
|
2277
|
+
return { resolve: { external: false } };
|
|
2278
|
+
}
|
|
2279
|
+
const resolveConfig = {
|
|
2280
|
+
external: true,
|
|
2281
|
+
file: allowedProtocols.has("file") ? void 0 : false
|
|
1727
2282
|
};
|
|
2283
|
+
if (hasNetworkProtocol) {
|
|
2284
|
+
const hasHostAllowlist = refOpts.allowedHosts.length > 0;
|
|
2285
|
+
const hostAllowSet = new Set(refOpts.allowedHosts);
|
|
2286
|
+
resolveConfig["http"] = {
|
|
2287
|
+
// SECURITY: never auto-follow HTTP redirects when resolving external
|
|
2288
|
+
// `$ref`s. `canRead` validates only the INITIAL URL; the resolver's
|
|
2289
|
+
// default redirect-following (up to 5 hops) re-fetches the `Location`
|
|
2290
|
+
// target WITHOUT re-invoking `canRead`, so an allowlisted host could
|
|
2291
|
+
// 302 → `http://169.254.169.254/...` and smuggle a blocked target past
|
|
2292
|
+
// the allow/deny lists. `redirects: 0` refuses the first redirect, and
|
|
2293
|
+
// our custom `read` (below) additionally refuses redirects itself.
|
|
2294
|
+
redirects: 0,
|
|
2295
|
+
// Synchronous gate: protocol, host allow-list, and literal/known
|
|
2296
|
+
// internal hosts. DNS names that *resolve* to internal addresses pass
|
|
2297
|
+
// here (canRead cannot be async) and are caught in `read` via DNS
|
|
2298
|
+
// resolution — closing the `127.0.0.1.nip.io` bypass for `$ref`s too.
|
|
2299
|
+
canRead: (file) => {
|
|
2300
|
+
try {
|
|
2301
|
+
const parsed = new URL(file.url);
|
|
2302
|
+
const protocol = parsed.protocol.replace(":", "");
|
|
2303
|
+
if (!allowedProtocols.has(protocol)) {
|
|
2304
|
+
return false;
|
|
2305
|
+
}
|
|
2306
|
+
if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
|
|
2307
|
+
return false;
|
|
2308
|
+
}
|
|
2309
|
+
if (isBlockedHostname(parsed.hostname, refOpts)) {
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
return true;
|
|
2313
|
+
} catch {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
},
|
|
2317
|
+
// SSRF-safe fetch: resolves DNS and rejects names that map to internal
|
|
2318
|
+
// addresses, and refuses redirects. NOTE: deliberately does NOT forward
|
|
2319
|
+
// `this.options.headers` (the spec-load credentials) to third-party
|
|
2320
|
+
// `$ref` hosts — that would leak the spec's auth token cross-origin.
|
|
2321
|
+
read: async (file) => {
|
|
2322
|
+
const response = await safeFetch(file.url, {
|
|
2323
|
+
timeoutMs: this.options.timeout,
|
|
2324
|
+
followRedirects: false,
|
|
2325
|
+
ssrf: refOpts
|
|
2326
|
+
});
|
|
2327
|
+
if (!response.ok) {
|
|
2328
|
+
throw new LoadError(
|
|
2329
|
+
`Failed to resolve external $ref "${file.url}": ${response.status} ${response.statusText}`,
|
|
2330
|
+
{ url: file.url, status: response.status }
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
return response.text();
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
} else {
|
|
2337
|
+
resolveConfig["http"] = false;
|
|
2338
|
+
}
|
|
2339
|
+
return { resolve: resolveConfig };
|
|
1728
2340
|
}
|
|
1729
2341
|
/**
|
|
1730
|
-
*
|
|
2342
|
+
* Does the document contain any EXTERNAL `$ref` (a ref that is not a local
|
|
2343
|
+
* JSON-pointer beginning with `#`)? Only external refs require the full
|
|
2344
|
+
* `$RefParser` (file/http resolvers, which pull Node builtins). A document
|
|
2345
|
+
* with only internal refs can be dereferenced with the runtime-agnostic
|
|
2346
|
+
* resolver below — so it works on V8 isolates (Cloudflare Workers) too.
|
|
1731
2347
|
*/
|
|
1732
|
-
static
|
|
1733
|
-
return
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2348
|
+
static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
|
|
2349
|
+
if (node === null || typeof node !== "object") return false;
|
|
2350
|
+
if (seen.has(node)) return false;
|
|
2351
|
+
seen.add(node);
|
|
2352
|
+
if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
|
|
2353
|
+
const ref = node.$ref;
|
|
2354
|
+
if (typeof ref === "string" && !ref.startsWith("#")) return true;
|
|
2355
|
+
return Object.values(node).some(
|
|
2356
|
+
(v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
|
|
2357
|
+
);
|
|
1737
2358
|
}
|
|
1738
2359
|
/**
|
|
1739
|
-
*
|
|
2360
|
+
* Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
|
|
2361
|
+
* free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
|
|
2362
|
+
* shared reference instead of recursing forever (same contract as `$RefParser`).
|
|
1740
2363
|
*/
|
|
1741
|
-
static
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2364
|
+
static dereferenceInternal(root) {
|
|
2365
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2366
|
+
const resolvePointer = (ptr) => {
|
|
2367
|
+
const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
2368
|
+
let cur = root;
|
|
2369
|
+
for (const p of parts) cur = cur?.[p];
|
|
2370
|
+
return cur;
|
|
2371
|
+
};
|
|
2372
|
+
const walk = (node) => {
|
|
2373
|
+
if (node === null || typeof node !== "object") return node;
|
|
2374
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
2375
|
+
const ref = node.$ref;
|
|
2376
|
+
if (typeof ref === "string" && ref.startsWith("#")) {
|
|
2377
|
+
const cached = cache.get(ref);
|
|
2378
|
+
if (cached !== void 0) return cached;
|
|
2379
|
+
const placeholder = {};
|
|
2380
|
+
cache.set(ref, placeholder);
|
|
2381
|
+
const resolved = walk(resolvePointer(ref));
|
|
2382
|
+
if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
|
|
2383
|
+
return placeholder;
|
|
2384
|
+
}
|
|
2385
|
+
const out = {};
|
|
2386
|
+
for (const [k, v] of Object.entries(node)) out[k] = walk(v);
|
|
2387
|
+
return out;
|
|
1745
2388
|
};
|
|
2389
|
+
return walk(root);
|
|
1746
2390
|
}
|
|
1747
2391
|
/**
|
|
1748
|
-
*
|
|
2392
|
+
* Initialize the generator (dereference if needed, then validate)
|
|
1749
2393
|
*/
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
if (
|
|
1754
|
-
|
|
2394
|
+
async initialize() {
|
|
2395
|
+
if (this.options.dereference && !this.dereferencedDocument) {
|
|
2396
|
+
const cloned = JSON.parse(JSON.stringify(this.document));
|
|
2397
|
+
if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
|
|
2398
|
+
this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
|
|
1755
2399
|
} else {
|
|
1756
|
-
|
|
2400
|
+
try {
|
|
2401
|
+
const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
|
|
2402
|
+
const refParserOptions = this.buildRefParserOptions();
|
|
2403
|
+
this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2406
|
+
throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
|
|
2407
|
+
originalError: error
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
1757
2410
|
}
|
|
1758
2411
|
}
|
|
1759
|
-
if (
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2412
|
+
if (this.options.validate) {
|
|
2413
|
+
const validator = new Validator();
|
|
2414
|
+
const documentToValidate = this.dereferencedDocument ?? this.document;
|
|
2415
|
+
const result = await validator.validate(documentToValidate);
|
|
2416
|
+
if (!result.valid) {
|
|
2417
|
+
throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
|
|
1764
2418
|
}
|
|
1765
2419
|
}
|
|
1766
|
-
return result;
|
|
1767
2420
|
}
|
|
1768
2421
|
/**
|
|
1769
|
-
*
|
|
2422
|
+
* Generate all tools from the OpenAPI specification
|
|
1770
2423
|
*/
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2424
|
+
async generateTools(options = {}) {
|
|
2425
|
+
await this.initialize();
|
|
2426
|
+
const document = this.getDocument();
|
|
2427
|
+
const tools = [];
|
|
2428
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
2429
|
+
if (!document.paths) {
|
|
2430
|
+
return tools;
|
|
1775
2431
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
2432
|
+
const sortedPaths = Object.entries(document.paths).sort(([a], [b]) => a < b ? -1 : 1);
|
|
2433
|
+
for (const [pathStr, pathItem] of sortedPaths) {
|
|
2434
|
+
if (!pathItem || "$ref" in pathItem) continue;
|
|
2435
|
+
const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
2436
|
+
for (const method of methods) {
|
|
2437
|
+
const operation = pathItem[method];
|
|
2438
|
+
if (!operation) continue;
|
|
2439
|
+
if (!this.shouldIncludeOperation(operation, pathStr, method, options, document, pathItem)) {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
try {
|
|
2443
|
+
let tool = await this.generateTool(pathStr, method, options);
|
|
2444
|
+
if (usedNames.has(tool.name)) {
|
|
2445
|
+
const maxLength = options.maxToolNameLength ?? DEFAULT_MAX_TOOL_NAME_LENGTH;
|
|
2446
|
+
let seed = `${method} ${pathStr}`;
|
|
2447
|
+
let deduped = normalizeToolName(`${tool.name}_${fnv1aHex(seed)}`, maxLength, seed);
|
|
2448
|
+
let attempts = 1;
|
|
2449
|
+
while (usedNames.has(deduped)) {
|
|
2450
|
+
if (attempts >= MAX_NAME_DEDUP_ATTEMPTS) {
|
|
2451
|
+
throw new GenerationError(
|
|
2452
|
+
`Unable to find a unique tool name for "${tool.name}" (${method.toUpperCase()} ${pathStr}) within ${MAX_NAME_DEDUP_ATTEMPTS} attempts \u2014 the name space under maxToolNameLength=${maxLength} is exhausted. Increase maxToolNameLength or rename the operation.`,
|
|
2453
|
+
{ name: tool.name, method, path: pathStr, maxToolNameLength: maxLength }
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
seed += "#";
|
|
2457
|
+
deduped = normalizeToolName(`${tool.name}_${fnv1aHex(seed)}`, maxLength, seed);
|
|
2458
|
+
attempts++;
|
|
2459
|
+
}
|
|
2460
|
+
tool = { ...tool, name: deduped };
|
|
2461
|
+
}
|
|
2462
|
+
usedNames.add(tool.name);
|
|
2463
|
+
tools.push(tool);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2466
|
+
console.warn(`Failed to generate tool for ${method.toUpperCase()} ${pathStr}:`, errorMessage);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
1778
2469
|
}
|
|
1779
|
-
return
|
|
1780
|
-
}
|
|
1781
|
-
/**
|
|
1782
|
-
* Create object schema
|
|
1783
|
-
*/
|
|
1784
|
-
static object(properties, required) {
|
|
1785
|
-
return {
|
|
1786
|
-
type: "object",
|
|
1787
|
-
properties,
|
|
1788
|
-
...required && required.length > 0 && { required },
|
|
1789
|
-
additionalProperties: false
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
|
-
/**
|
|
1793
|
-
* Create array schema
|
|
1794
|
-
*/
|
|
1795
|
-
static array(items, constraints) {
|
|
1796
|
-
return {
|
|
1797
|
-
type: "array",
|
|
1798
|
-
items,
|
|
1799
|
-
...constraints
|
|
1800
|
-
};
|
|
2470
|
+
return tools;
|
|
1801
2471
|
}
|
|
1802
2472
|
/**
|
|
1803
|
-
*
|
|
2473
|
+
* Generate a specific tool for a path and method
|
|
1804
2474
|
*/
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2475
|
+
async generateTool(pathStr, method, options = {}) {
|
|
2476
|
+
await this.initialize();
|
|
2477
|
+
const document = this.getDocument();
|
|
2478
|
+
if (!document.paths) {
|
|
2479
|
+
throw new Error("No paths defined in OpenAPI document");
|
|
2480
|
+
}
|
|
2481
|
+
const pathItem = document.paths[pathStr];
|
|
2482
|
+
const operation = pathItem?.[method.toLowerCase()];
|
|
2483
|
+
if (!operation) {
|
|
2484
|
+
throw new Error(`Operation not found: ${method.toUpperCase()} ${pathStr}`);
|
|
2485
|
+
}
|
|
2486
|
+
const parameterResolver = new ParameterResolver(options.namingStrategy, {
|
|
2487
|
+
includeExamples: options.includeExamples
|
|
2488
|
+
});
|
|
2489
|
+
let pathParameters = void 0;
|
|
2490
|
+
if (pathItem.parameters) {
|
|
2491
|
+
pathParameters = pathItem.parameters.filter(
|
|
2492
|
+
(p) => !isReferenceObject(p)
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
let securityRequirements = void 0;
|
|
2496
|
+
const securitySpec = operation.security ?? document.security;
|
|
2497
|
+
if (securitySpec) {
|
|
2498
|
+
securityRequirements = this.extractSecurityRequirements(securitySpec, document);
|
|
2499
|
+
}
|
|
2500
|
+
const { inputSchema, mapper } = parameterResolver.resolve(
|
|
2501
|
+
operation,
|
|
2502
|
+
pathParameters,
|
|
2503
|
+
securityRequirements,
|
|
2504
|
+
options.includeSecurityInInput
|
|
2505
|
+
);
|
|
2506
|
+
const responseBuilder = new ResponseBuilder(options);
|
|
2507
|
+
const outputSchema = responseBuilder.build(operation.responses);
|
|
2508
|
+
const overrides = extractExtensionOverrides(operation);
|
|
2509
|
+
const name = this.generateToolName(pathStr, method, overrides.name ?? operation.operationId, options);
|
|
2510
|
+
const description = overrides.description ?? (operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`);
|
|
2511
|
+
const title = overrides.title ?? operation.summary;
|
|
2512
|
+
const inferred = options.inferAnnotations !== false ? inferAnnotationsFromMethod(method.toLowerCase()) : void 0;
|
|
2513
|
+
const annotations = inferred || overrides.annotations ? { ...inferred, ...overrides.annotations } : void 0;
|
|
2514
|
+
const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
|
|
2515
|
+
const formatResolvers = {
|
|
2516
|
+
...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
|
|
2517
|
+
...options.formatResolvers
|
|
1809
2518
|
};
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2519
|
+
const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
|
|
2520
|
+
let resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
|
|
2521
|
+
let resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
|
|
2522
|
+
const maxSchemaDepth = Math.max(1, options.maxSchemaDepth ?? 10);
|
|
2523
|
+
resolvedInputSchema = SchemaBuilder.truncateDepth(resolvedInputSchema, maxSchemaDepth);
|
|
2524
|
+
if (resolvedOutputSchema) {
|
|
2525
|
+
resolvedOutputSchema = SchemaBuilder.truncateDepth(resolvedOutputSchema, maxSchemaDepth);
|
|
2526
|
+
}
|
|
2527
|
+
if (options.target) {
|
|
2528
|
+
resolvedInputSchema = applyClientTarget(resolvedInputSchema, options.target);
|
|
2529
|
+
if (resolvedOutputSchema) {
|
|
2530
|
+
resolvedOutputSchema = applyClientTarget(resolvedOutputSchema, options.target);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
1815
2533
|
return {
|
|
1816
|
-
|
|
1817
|
-
...
|
|
2534
|
+
name,
|
|
2535
|
+
...title !== void 0 && { title },
|
|
2536
|
+
description,
|
|
2537
|
+
...annotations && { annotations },
|
|
2538
|
+
inputSchema: resolvedInputSchema,
|
|
2539
|
+
outputSchema: resolvedOutputSchema,
|
|
2540
|
+
mapper,
|
|
2541
|
+
metadata
|
|
1818
2542
|
};
|
|
1819
2543
|
}
|
|
1820
2544
|
/**
|
|
1821
|
-
*
|
|
2545
|
+
* Check if an operation should be included
|
|
1822
2546
|
*/
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2547
|
+
shouldIncludeOperation(operation, path, method, options, document, pathItem) {
|
|
2548
|
+
if (!resolveExtensionEnabled(document, pathItem, operation)) {
|
|
2549
|
+
return false;
|
|
2550
|
+
}
|
|
2551
|
+
if (operation.deprecated && !options.includeDeprecated) {
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2554
|
+
const lowerMethod = method.toLowerCase();
|
|
2555
|
+
if (options.includeMethods && !options.includeMethods.includes(lowerMethod)) {
|
|
2556
|
+
return false;
|
|
2557
|
+
}
|
|
2558
|
+
if (options.excludeMethods?.includes(lowerMethod)) {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
if (options.includePaths && !matchesAnyGlob(path, options.includePaths)) {
|
|
2562
|
+
return false;
|
|
2563
|
+
}
|
|
2564
|
+
if (options.excludePaths && matchesAnyGlob(path, options.excludePaths)) {
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
const tags = operation.tags ?? [];
|
|
2568
|
+
if (options.includeTags && !tags.some((tag) => options.includeTags.includes(tag))) {
|
|
2569
|
+
return false;
|
|
2570
|
+
}
|
|
2571
|
+
if (options.excludeTags && tags.some((tag) => options.excludeTags.includes(tag))) {
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
if (options.includeOperations && operation.operationId) {
|
|
2575
|
+
if (!options.includeOperations.includes(operation.operationId)) {
|
|
2576
|
+
return false;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
if (options.excludeOperations && operation.operationId) {
|
|
2580
|
+
if (options.excludeOperations.includes(operation.operationId)) {
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (options.readOnlyOnly) {
|
|
2585
|
+
const effective = {
|
|
2586
|
+
...inferAnnotationsFromMethod(lowerMethod),
|
|
2587
|
+
...extractExtensionOverrides(operation).annotations
|
|
2588
|
+
};
|
|
2589
|
+
if (effective.readOnlyHint !== true) {
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (options.filterFn) {
|
|
2594
|
+
return options.filterFn({
|
|
2595
|
+
...operation,
|
|
2596
|
+
path,
|
|
2597
|
+
method
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
return true;
|
|
1828
2601
|
}
|
|
1829
2602
|
/**
|
|
1830
|
-
*
|
|
2603
|
+
* Generate a tool name
|
|
1831
2604
|
*/
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2605
|
+
generateToolName(path, method, operationId, options = {}) {
|
|
2606
|
+
let rawName;
|
|
2607
|
+
if (options.namingStrategy?.toolNameGenerator) {
|
|
2608
|
+
rawName = options.namingStrategy.toolNameGenerator(path, method, operationId);
|
|
2609
|
+
} else if (operationId) {
|
|
2610
|
+
rawName = operationId;
|
|
2611
|
+
} else {
|
|
2612
|
+
const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
2613
|
+
rawName = `${method}_${sanitized}`;
|
|
2614
|
+
}
|
|
2615
|
+
return normalizeToolName(
|
|
2616
|
+
rawName,
|
|
2617
|
+
options.maxToolNameLength ?? DEFAULT_MAX_TOOL_NAME_LENGTH,
|
|
2618
|
+
`${method} ${path}`
|
|
2619
|
+
);
|
|
1836
2620
|
}
|
|
1837
2621
|
/**
|
|
1838
|
-
*
|
|
2622
|
+
* Extract metadata from operation
|
|
1839
2623
|
*/
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2624
|
+
extractMetadata(path, method, operation, document, outputSchema) {
|
|
2625
|
+
const metadata = {
|
|
2626
|
+
path,
|
|
2627
|
+
method,
|
|
2628
|
+
operationId: operation.operationId,
|
|
2629
|
+
operationSummary: operation.summary,
|
|
2630
|
+
operationDescription: operation.description,
|
|
2631
|
+
tags: operation.tags,
|
|
2632
|
+
deprecated: operation.deprecated
|
|
1843
2633
|
};
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
if (maxDepth <= 0) return schema;
|
|
1850
|
-
const cloned = this.clone(schema);
|
|
1851
|
-
if (cloned.oneOf) {
|
|
1852
|
-
const flattened = cloned.oneOf.flatMap((s) => {
|
|
1853
|
-
const sub = this.flatten(s, maxDepth - 1);
|
|
1854
|
-
return sub.oneOf ? sub.oneOf : [sub];
|
|
1855
|
-
});
|
|
1856
|
-
cloned.oneOf = flattened;
|
|
2634
|
+
if (operation.security || document.security) {
|
|
2635
|
+
metadata.security = this.extractSecurityRequirements(
|
|
2636
|
+
operation.security ?? document.security,
|
|
2637
|
+
document
|
|
2638
|
+
);
|
|
1857
2639
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2640
|
+
const servers = operation.servers ?? document.servers;
|
|
2641
|
+
if (servers) {
|
|
2642
|
+
metadata.servers = servers.map((server) => ({
|
|
2643
|
+
url: this.options.baseUrl || server.url,
|
|
2644
|
+
description: server.description,
|
|
2645
|
+
variables: server.variables
|
|
2646
|
+
}));
|
|
2647
|
+
} else if (this.options.baseUrl) {
|
|
2648
|
+
metadata.servers = [{ url: this.options.baseUrl }];
|
|
1864
2649
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2650
|
+
const schemaObj = outputSchema;
|
|
2651
|
+
if (schemaObj && Array.isArray(schemaObj["oneOf"])) {
|
|
2652
|
+
const codes = schemaObj["oneOf"].map((schema) => schema["x-status-code"]).filter((code) => code !== void 0 && code !== null);
|
|
2653
|
+
if (codes.length > 0) {
|
|
2654
|
+
metadata.responseStatusCodes = codes;
|
|
2655
|
+
}
|
|
2656
|
+
} else if (schemaObj && schemaObj["x-status-code"] !== void 0 && schemaObj["x-status-code"] !== null) {
|
|
2657
|
+
metadata.responseStatusCodes = [schemaObj["x-status-code"]];
|
|
1871
2658
|
}
|
|
1872
|
-
|
|
2659
|
+
if (operation.externalDocs) {
|
|
2660
|
+
metadata.externalDocs = operation.externalDocs;
|
|
2661
|
+
}
|
|
2662
|
+
const operationWithExt = operation;
|
|
2663
|
+
if (operationWithExt["x-frontmcp"]) {
|
|
2664
|
+
metadata.frontmcp = operationWithExt["x-frontmcp"];
|
|
2665
|
+
}
|
|
2666
|
+
return metadata;
|
|
1873
2667
|
}
|
|
1874
2668
|
/**
|
|
1875
|
-
*
|
|
2669
|
+
* Extract security requirements
|
|
1876
2670
|
*/
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
delete cloned.required;
|
|
1881
|
-
}
|
|
1882
|
-
if (cloned.properties && Object.keys(cloned.properties).length === 0) {
|
|
1883
|
-
delete cloned.properties;
|
|
1884
|
-
}
|
|
1885
|
-
if (Array.isArray(cloned.examples) && cloned.examples.length === 0) {
|
|
1886
|
-
delete cloned.examples;
|
|
1887
|
-
}
|
|
1888
|
-
if (cloned.title && cloned.description && cloned.title === cloned.description) {
|
|
1889
|
-
delete cloned.title;
|
|
2671
|
+
extractSecurityRequirements(security, document) {
|
|
2672
|
+
if (!security || !document.components?.securitySchemes) {
|
|
2673
|
+
return [];
|
|
1890
2674
|
}
|
|
1891
|
-
return
|
|
2675
|
+
return security.flatMap(
|
|
2676
|
+
(req) => Object.entries(req).map(([scheme, scopes]) => {
|
|
2677
|
+
const securityScheme = document.components.securitySchemes[scheme];
|
|
2678
|
+
if (isReferenceObject(securityScheme)) {
|
|
2679
|
+
return { scheme, type: "http", scopes };
|
|
2680
|
+
}
|
|
2681
|
+
const apiKeyIn = "in" in securityScheme ? securityScheme.in : void 0;
|
|
2682
|
+
const result = {
|
|
2683
|
+
scheme,
|
|
2684
|
+
type: securityScheme.type,
|
|
2685
|
+
scopes,
|
|
2686
|
+
name: "name" in securityScheme ? securityScheme.name : void 0,
|
|
2687
|
+
in: apiKeyIn && (apiKeyIn === "query" || apiKeyIn === "header" || apiKeyIn === "cookie") ? apiKeyIn : void 0
|
|
2688
|
+
};
|
|
2689
|
+
if (securityScheme.type === "http") {
|
|
2690
|
+
result.httpScheme = "scheme" in securityScheme ? securityScheme.scheme : void 0;
|
|
2691
|
+
result.bearerFormat = "bearerFormat" in securityScheme ? securityScheme.bearerFormat : void 0;
|
|
2692
|
+
}
|
|
2693
|
+
result.description = "description" in securityScheme ? securityScheme.description : void 0;
|
|
2694
|
+
return result;
|
|
2695
|
+
})
|
|
2696
|
+
);
|
|
1892
2697
|
}
|
|
1893
2698
|
};
|
|
1894
2699
|
|
|
@@ -2135,6 +2940,351 @@ function createSecurityContext(auth) {
|
|
|
2135
2940
|
customResolver: auth.customResolver
|
|
2136
2941
|
};
|
|
2137
2942
|
}
|
|
2943
|
+
|
|
2944
|
+
// src/request-builder.ts
|
|
2945
|
+
var RESERVED_DECODE = {
|
|
2946
|
+
"%3A": ":",
|
|
2947
|
+
"%2F": "/",
|
|
2948
|
+
"%3F": "?",
|
|
2949
|
+
"%23": "#",
|
|
2950
|
+
"%5B": "[",
|
|
2951
|
+
"%5D": "]",
|
|
2952
|
+
"%40": "@",
|
|
2953
|
+
"%24": "$",
|
|
2954
|
+
"%26": "&",
|
|
2955
|
+
"%2B": "+",
|
|
2956
|
+
"%2C": ",",
|
|
2957
|
+
"%3B": ";",
|
|
2958
|
+
"%3D": "="
|
|
2959
|
+
};
|
|
2960
|
+
function encodeValue(value, allowReserved) {
|
|
2961
|
+
const encoded = encodeURIComponent(value);
|
|
2962
|
+
if (!allowReserved) return encoded;
|
|
2963
|
+
return encoded.replace(/%3A|%2F|%3F|%23|%5B|%5D|%40|%24|%26|%2B|%2C|%3B|%3D/gi, (m) => RESERVED_DECODE[m.toUpperCase()]);
|
|
2964
|
+
}
|
|
2965
|
+
function isPlainObject(value) {
|
|
2966
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2967
|
+
}
|
|
2968
|
+
function primitiveString(value, paramName, location) {
|
|
2969
|
+
if (value === null || value === void 0 || typeof value === "object") {
|
|
2970
|
+
throw new RequestBuildError(
|
|
2971
|
+
`${location} parameter '${paramName}' must serialize to a primitive; received ${value === null ? "null" : Array.isArray(value) ? "an array" : typeof value}`,
|
|
2972
|
+
{ param: paramName, location }
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2975
|
+
return String(value);
|
|
2976
|
+
}
|
|
2977
|
+
function serializePathValue(mapper, value) {
|
|
2978
|
+
const style = mapper.style ?? "simple";
|
|
2979
|
+
const explode = mapper.explode ?? false;
|
|
2980
|
+
const name = mapper.key;
|
|
2981
|
+
const enc = (v) => encodeValue(primitiveString(v, name, "path"));
|
|
2982
|
+
if (Array.isArray(value)) {
|
|
2983
|
+
if (style === "label") {
|
|
2984
|
+
return `.${value.map(enc).join(explode ? "." : ",")}`;
|
|
2985
|
+
}
|
|
2986
|
+
if (style === "matrix") {
|
|
2987
|
+
return explode ? value.map((v) => `;${name}=${enc(v)}`).join("") : `;${name}=${value.map(enc).join(",")}`;
|
|
2988
|
+
}
|
|
2989
|
+
return value.map(enc).join(",");
|
|
2990
|
+
}
|
|
2991
|
+
if (isPlainObject(value)) {
|
|
2992
|
+
const entries = Object.entries(value);
|
|
2993
|
+
if (style === "label") {
|
|
2994
|
+
return explode ? entries.map(([k, v]) => `.${encodeValue(k)}=${enc(v)}`).join("") : `.${entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",")}`;
|
|
2995
|
+
}
|
|
2996
|
+
if (style === "matrix") {
|
|
2997
|
+
return explode ? entries.map(([k, v]) => `;${encodeValue(k)}=${enc(v)}`).join("") : `;${name}=${entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",")}`;
|
|
2998
|
+
}
|
|
2999
|
+
return explode ? entries.map(([k, v]) => `${encodeValue(k)}=${enc(v)}`).join(",") : entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",");
|
|
3000
|
+
}
|
|
3001
|
+
const core = enc(value);
|
|
3002
|
+
if (style === "label") return `.${core}`;
|
|
3003
|
+
if (style === "matrix") return `;${name}=${core}`;
|
|
3004
|
+
return core;
|
|
3005
|
+
}
|
|
3006
|
+
function serializeQueryPairs(mapper, value) {
|
|
3007
|
+
const style = mapper.style ?? "form";
|
|
3008
|
+
const explode = mapper.explode ?? style === "form";
|
|
3009
|
+
const name = mapper.key;
|
|
3010
|
+
const str = (v) => primitiveString(v, name, "query");
|
|
3011
|
+
if (Array.isArray(value)) {
|
|
3012
|
+
if ((style === "deepObject" ? mapper.explode ?? true : explode) || value.length === 0) {
|
|
3013
|
+
return value.map((v) => [name, str(v)]);
|
|
3014
|
+
}
|
|
3015
|
+
const delimiter = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
|
|
3016
|
+
return [[name, value.map(str).join(delimiter)]];
|
|
3017
|
+
}
|
|
3018
|
+
if (isPlainObject(value)) {
|
|
3019
|
+
if (style === "deepObject") {
|
|
3020
|
+
const pairs = [];
|
|
3021
|
+
const walk = (prefix, node) => {
|
|
3022
|
+
for (const [k, v] of Object.entries(node)) {
|
|
3023
|
+
if (v === void 0) continue;
|
|
3024
|
+
if (isPlainObject(v)) {
|
|
3025
|
+
walk(`${prefix}[${k}]`, v);
|
|
3026
|
+
} else if (Array.isArray(v)) {
|
|
3027
|
+
for (const item of v) pairs.push([`${prefix}[${k}]`, str(item)]);
|
|
3028
|
+
} else {
|
|
3029
|
+
pairs.push([`${prefix}[${k}]`, str(v)]);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
walk(name, value);
|
|
3034
|
+
return pairs;
|
|
3035
|
+
}
|
|
3036
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0);
|
|
3037
|
+
if (explode) {
|
|
3038
|
+
return entries.map(([k, v]) => [k, str(v)]);
|
|
3039
|
+
}
|
|
3040
|
+
return [[name, entries.map(([k, v]) => `${k},${str(v)}`).join(",")]];
|
|
3041
|
+
}
|
|
3042
|
+
return [[name, str(value)]];
|
|
3043
|
+
}
|
|
3044
|
+
function serializeHeaderValue(mapper, value) {
|
|
3045
|
+
const explode = mapper.explode ?? false;
|
|
3046
|
+
const name = mapper.key;
|
|
3047
|
+
const str = (v) => primitiveString(v, name, "header");
|
|
3048
|
+
if (Array.isArray(value)) {
|
|
3049
|
+
return value.map(str).join(",");
|
|
3050
|
+
}
|
|
3051
|
+
if (isPlainObject(value)) {
|
|
3052
|
+
const entries = Object.entries(value);
|
|
3053
|
+
return explode ? entries.map(([k, v]) => `${k}=${str(v)}`).join(",") : entries.map(([k, v]) => `${k},${str(v)}`).join(",");
|
|
3054
|
+
}
|
|
3055
|
+
return str(value);
|
|
3056
|
+
}
|
|
3057
|
+
function assertHeaderSafe(name, value) {
|
|
3058
|
+
if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
|
|
3059
|
+
throw new RequestBuildError(`Invalid header name '${name}' (RFC 7230 token required)`, { header: name });
|
|
3060
|
+
}
|
|
3061
|
+
if (/[\r\n\x00]/.test(value)) {
|
|
3062
|
+
throw new RequestBuildError(`Header '${name}' value contains control characters (possible header injection)`, {
|
|
3063
|
+
header: name
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
function assertCookieName(name) {
|
|
3068
|
+
if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
|
|
3069
|
+
throw new RequestBuildError(`Invalid cookie name '${name}' (RFC 6265 token required)`, { cookie: name });
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
function assertCookieValue(name, value) {
|
|
3073
|
+
if (/[\x00-\x1f\x7f\s";\\]/.test(value)) {
|
|
3074
|
+
throw new RequestBuildError(
|
|
3075
|
+
`Cookie '${name}' value contains characters that break the Cookie header (RFC 6265 cookie-octet violation)`,
|
|
3076
|
+
{ cookie: name }
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
function formatSecurityValue(mapper, value) {
|
|
3081
|
+
const security = mapper.security;
|
|
3082
|
+
if (security.type === "http") {
|
|
3083
|
+
const scheme = (security.httpScheme ?? "bearer").toLowerCase();
|
|
3084
|
+
if (scheme !== "bearer" && scheme !== "basic") {
|
|
3085
|
+
return value;
|
|
3086
|
+
}
|
|
3087
|
+
const prefix = scheme.charAt(0).toUpperCase() + scheme.slice(1);
|
|
3088
|
+
return value.toLowerCase().startsWith(`${scheme} `) ? value : `${prefix} ${value}`;
|
|
3089
|
+
}
|
|
3090
|
+
if (security.type === "oauth2" || security.type === "openIdConnect") {
|
|
3091
|
+
return value.toLowerCase().startsWith("bearer ") ? value : `Bearer ${value}`;
|
|
3092
|
+
}
|
|
3093
|
+
return value;
|
|
3094
|
+
}
|
|
3095
|
+
function resolveServerUrl(tool) {
|
|
3096
|
+
const server = tool.metadata.servers?.[0];
|
|
3097
|
+
if (!server) return "";
|
|
3098
|
+
let url = server.url;
|
|
3099
|
+
if (server.variables) {
|
|
3100
|
+
for (const [name, variable] of Object.entries(server.variables)) {
|
|
3101
|
+
if (variable && typeof variable.default === "string") {
|
|
3102
|
+
url = url.replaceAll(`{${name}}`, variable.default);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return url;
|
|
3107
|
+
}
|
|
3108
|
+
var JSON_CONTENT = /^application\/(.+\+)?json$/i;
|
|
3109
|
+
function buildHttpRequest(tool, input, options = {}) {
|
|
3110
|
+
const rawBase = options.baseUrl ?? resolveServerUrl(tool);
|
|
3111
|
+
if (rawBase.includes("{")) {
|
|
3112
|
+
throw new RequestBuildError(
|
|
3113
|
+
`Base URL '${rawBase}' contains unresolved server template variables (no default value in the spec); pass an explicit baseUrl`,
|
|
3114
|
+
{ baseUrl: rawBase }
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
if (rawBase !== "" && !/^https?:\/\//i.test(rawBase)) {
|
|
3118
|
+
throw new RequestBuildError(`Base URL must be http(s) or empty; received '${rawBase}'`, { baseUrl: rawBase });
|
|
3119
|
+
}
|
|
3120
|
+
let base = rawBase;
|
|
3121
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
3122
|
+
let path = tool.metadata.path;
|
|
3123
|
+
const queryPairs = [];
|
|
3124
|
+
const query = {};
|
|
3125
|
+
const headers = {};
|
|
3126
|
+
const cookies = {};
|
|
3127
|
+
let rawBody;
|
|
3128
|
+
let bodyObject;
|
|
3129
|
+
let contentType;
|
|
3130
|
+
let hasBody = false;
|
|
3131
|
+
let binaryBody = false;
|
|
3132
|
+
for (const mapper of tool.mapper) {
|
|
3133
|
+
const value = input[mapper.inputKey];
|
|
3134
|
+
if (mapper.security) {
|
|
3135
|
+
if (value === void 0 || value === null) continue;
|
|
3136
|
+
const formatted = formatSecurityValue(mapper, String(value));
|
|
3137
|
+
if (mapper.type === "header") {
|
|
3138
|
+
assertHeaderSafe(mapper.key, formatted);
|
|
3139
|
+
headers[mapper.key] = formatted;
|
|
3140
|
+
} else if (mapper.type === "query") {
|
|
3141
|
+
queryPairs.push([mapper.key, formatted]);
|
|
3142
|
+
} else {
|
|
3143
|
+
assertCookieName(mapper.key);
|
|
3144
|
+
cookies[mapper.key] = formatted;
|
|
3145
|
+
}
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
if (value === void 0 || value === null && mapper.type !== "body") {
|
|
3149
|
+
if (mapper.required) {
|
|
3150
|
+
throw new RequestBuildError(
|
|
3151
|
+
`Required ${mapper.type} parameter '${mapper.key}' (input key '${mapper.inputKey}') is missing`,
|
|
3152
|
+
{ param: mapper.key, inputKey: mapper.inputKey, location: mapper.type }
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
continue;
|
|
3156
|
+
}
|
|
3157
|
+
switch (mapper.type) {
|
|
3158
|
+
case "path":
|
|
3159
|
+
path = path.replaceAll(`{${mapper.key}}`, serializePathValue(mapper, value));
|
|
3160
|
+
break;
|
|
3161
|
+
case "query":
|
|
3162
|
+
for (const [k, v] of serializeQueryPairs(mapper, value)) {
|
|
3163
|
+
queryPairs.push([k, v, mapper.allowReserved]);
|
|
3164
|
+
}
|
|
3165
|
+
break;
|
|
3166
|
+
case "header": {
|
|
3167
|
+
const headerValue = serializeHeaderValue(mapper, value);
|
|
3168
|
+
assertHeaderSafe(mapper.key, headerValue);
|
|
3169
|
+
headers[mapper.key] = headerValue;
|
|
3170
|
+
break;
|
|
3171
|
+
}
|
|
3172
|
+
case "cookie": {
|
|
3173
|
+
assertCookieName(mapper.key);
|
|
3174
|
+
cookies[mapper.key] = Array.isArray(value) ? value.map((v) => primitiveString(v, mapper.key, "cookie")).join(",") : primitiveString(value, mapper.key, "cookie");
|
|
3175
|
+
break;
|
|
3176
|
+
}
|
|
3177
|
+
case "body":
|
|
3178
|
+
hasBody = true;
|
|
3179
|
+
contentType = contentType ?? mapper.serialization?.contentType ?? "application/json";
|
|
3180
|
+
if (mapper.serialization?.binary) binaryBody = true;
|
|
3181
|
+
if (mapper.wholeBody) {
|
|
3182
|
+
rawBody = value;
|
|
3183
|
+
} else {
|
|
3184
|
+
if (bodyObject === void 0) bodyObject = {};
|
|
3185
|
+
bodyObject[mapper.key] = value;
|
|
3186
|
+
}
|
|
3187
|
+
break;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
if (path.includes("{")) {
|
|
3191
|
+
throw new RequestBuildError(`Unresolved path parameters remain in '${path}'`, { path });
|
|
3192
|
+
}
|
|
3193
|
+
if (bodyObject !== void 0) rawBody = bodyObject;
|
|
3194
|
+
const queryString = queryPairs.map(([k, v, allowReserved]) => {
|
|
3195
|
+
query[k] = query[k] ?? [];
|
|
3196
|
+
query[k].push(v);
|
|
3197
|
+
const encodedKey = encodeURIComponent(k).replace(/%5B/gi, "[").replace(/%5D/gi, "]");
|
|
3198
|
+
return `${encodedKey}=${encodeValue(v, allowReserved)}`;
|
|
3199
|
+
}).join("&");
|
|
3200
|
+
const cookieEntries = Object.entries(cookies);
|
|
3201
|
+
if (cookieEntries.length > 0) {
|
|
3202
|
+
for (const [k, v] of cookieEntries) assertCookieValue(k, v);
|
|
3203
|
+
headers["Cookie"] = cookieEntries.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
3204
|
+
}
|
|
3205
|
+
const contentTypeKey = Object.keys(headers).find((h) => h.toLowerCase() === "content-type") ?? "content-type";
|
|
3206
|
+
const hasExplicitContentType = contentTypeKey in headers;
|
|
3207
|
+
let body;
|
|
3208
|
+
if (hasBody && rawBody !== void 0) {
|
|
3209
|
+
const ct = contentType;
|
|
3210
|
+
if (binaryBody) {
|
|
3211
|
+
body = rawBody;
|
|
3212
|
+
if (!hasExplicitContentType) headers[contentTypeKey] = ct;
|
|
3213
|
+
} else if (ct.toLowerCase() === "application/x-www-form-urlencoded") {
|
|
3214
|
+
const params = new URLSearchParams();
|
|
3215
|
+
if (!isPlainObject(rawBody)) {
|
|
3216
|
+
throw new RequestBuildError(`form-urlencoded bodies must be objects; received ${typeof rawBody}`, {
|
|
3217
|
+
contentType: ct
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
for (const [k, v] of Object.entries(rawBody)) {
|
|
3221
|
+
if (v === void 0) continue;
|
|
3222
|
+
if (Array.isArray(v)) {
|
|
3223
|
+
for (const item of v) params.append(k, primitiveString(item, k, "body"));
|
|
3224
|
+
} else {
|
|
3225
|
+
params.append(k, isPlainObject(v) ? JSON.stringify(v) : String(v));
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
body = params.toString();
|
|
3229
|
+
headers[contentTypeKey] = ct;
|
|
3230
|
+
} else if (ct.toLowerCase() === "multipart/form-data") {
|
|
3231
|
+
if (typeof FormData === "undefined") {
|
|
3232
|
+
throw new RequestBuildError("multipart/form-data requires a FormData implementation in this runtime", {});
|
|
3233
|
+
}
|
|
3234
|
+
const form = new FormData();
|
|
3235
|
+
if (!isPlainObject(rawBody)) {
|
|
3236
|
+
throw new RequestBuildError(`multipart bodies must be objects; received ${typeof rawBody}`, {
|
|
3237
|
+
contentType: ct
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
for (const [k, v] of Object.entries(rawBody)) {
|
|
3241
|
+
if (v === void 0) continue;
|
|
3242
|
+
if (typeof Blob !== "undefined" && v instanceof Blob) {
|
|
3243
|
+
form.append(k, v);
|
|
3244
|
+
} else if (v instanceof Uint8Array) {
|
|
3245
|
+
form.append(k, new Blob([v]));
|
|
3246
|
+
} else if (isPlainObject(v) || Array.isArray(v)) {
|
|
3247
|
+
form.append(k, JSON.stringify(v));
|
|
3248
|
+
} else {
|
|
3249
|
+
form.append(k, String(v));
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
body = form;
|
|
3253
|
+
if (hasExplicitContentType) delete headers[contentTypeKey];
|
|
3254
|
+
} else if (JSON_CONTENT.test(ct)) {
|
|
3255
|
+
body = JSON.stringify(rawBody);
|
|
3256
|
+
headers[contentTypeKey] = ct;
|
|
3257
|
+
} else {
|
|
3258
|
+
body = isPlainObject(rawBody) || Array.isArray(rawBody) ? JSON.stringify(rawBody) : String(rawBody);
|
|
3259
|
+
headers[contentTypeKey] = ct;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
url: `${base}${path}${queryString ? `?${queryString}` : ""}`,
|
|
3264
|
+
method: tool.metadata.method.toUpperCase(),
|
|
3265
|
+
headers,
|
|
3266
|
+
query,
|
|
3267
|
+
cookies,
|
|
3268
|
+
contentType,
|
|
3269
|
+
body,
|
|
3270
|
+
rawBody
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// src/sdk.ts
|
|
3275
|
+
function toSdkTool(tool, wrapper) {
|
|
3276
|
+
const wrapSchema = wrapper?.fromJsonSchema ?? ((schema) => schema);
|
|
3277
|
+
return [
|
|
3278
|
+
tool.name,
|
|
3279
|
+
{
|
|
3280
|
+
...tool.title !== void 0 && { title: tool.title },
|
|
3281
|
+
description: tool.description,
|
|
3282
|
+
inputSchema: wrapSchema(tool.inputSchema),
|
|
3283
|
+
...tool.outputSchema !== void 0 && { outputSchema: wrapSchema(tool.outputSchema) },
|
|
3284
|
+
...tool.annotations !== void 0 && { annotations: tool.annotations }
|
|
3285
|
+
}
|
|
3286
|
+
];
|
|
3287
|
+
}
|
|
2138
3288
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2139
3289
|
0 && (module.exports = {
|
|
2140
3290
|
BLOCKED_HOSTNAMES,
|
|
@@ -2145,6 +3295,7 @@ function createSecurityContext(auth) {
|
|
|
2145
3295
|
OpenAPIToolGenerator,
|
|
2146
3296
|
ParameterResolver,
|
|
2147
3297
|
ParseError,
|
|
3298
|
+
RequestBuildError,
|
|
2148
3299
|
ResponseBuilder,
|
|
2149
3300
|
SchemaBuilder,
|
|
2150
3301
|
SchemaError,
|
|
@@ -2152,15 +3303,28 @@ function createSecurityContext(auth) {
|
|
|
2152
3303
|
SsrfError,
|
|
2153
3304
|
ValidationError,
|
|
2154
3305
|
Validator,
|
|
3306
|
+
applyClientTarget,
|
|
2155
3307
|
assertUrlSafe,
|
|
3308
|
+
buildHttpRequest,
|
|
3309
|
+
collapseNestedUnions,
|
|
3310
|
+
collapseRootCompositions,
|
|
2156
3311
|
createSecurityContext,
|
|
2157
3312
|
decodeIpv4MappedIpv6,
|
|
2158
3313
|
defaultLookup,
|
|
3314
|
+
demoteFormats,
|
|
3315
|
+
enforceClosedObjects,
|
|
3316
|
+
ensureArrayItems,
|
|
3317
|
+
extractExtensionOverrides,
|
|
3318
|
+
inferAnnotationsFromMethod,
|
|
3319
|
+
inlineLocalRefs,
|
|
2159
3320
|
isBlockedAddress,
|
|
2160
3321
|
isBlockedHostname,
|
|
2161
3322
|
isReferenceObject,
|
|
2162
3323
|
normalizeSsrfOptions,
|
|
3324
|
+
requireAllProperties,
|
|
3325
|
+
resolveExtensionEnabled,
|
|
2163
3326
|
resolveSchemaFormats,
|
|
2164
3327
|
safeFetch,
|
|
2165
|
-
toJsonSchema
|
|
3328
|
+
toJsonSchema,
|
|
3329
|
+
toSdkTool
|
|
2166
3330
|
});
|