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