nexus-ai-pro 2.20.2 → 2.22.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/API_STABILITY.md +93 -0
- package/CHANGELOG.md +208 -1
- package/README.md +11 -6
- package/SECURITY.md +16 -2
- package/dist/adapter-kit/gather.d.ts +76 -0
- package/dist/adapter-kit/gather.js +147 -0
- package/dist/adapter-kit/index.d.ts +1 -0
- package/dist/adapter-kit/index.js +1 -0
- package/dist/adapter-kit/record.d.ts +3 -1
- package/dist/adapter-kit/record.js +20 -5
- package/dist/agent/openapi.d.ts +94 -8
- package/dist/agent/openapi.js +528 -32
- package/dist/cli/adapters.d.ts +4 -3
- package/dist/cli/adapters.js +71 -2
- package/dist/cli/doctor.js +3 -0
- package/dist/cli.js +3 -1
- package/dist/core/config-builder.d.ts +1 -0
- package/dist/core/config-builder.js +1 -0
- package/dist/core/defaults.d.ts +34 -0
- package/dist/core/defaults.js +39 -0
- package/dist/core/secure-stream.js +4 -1
- package/dist/decisions/completion.d.ts +10 -7
- package/dist/decisions/completion.js +82 -8
- package/dist/decisions/core.d.ts +88 -43
- package/dist/decisions/core.js +115 -28
- package/dist/decisions/errors.d.ts +25 -0
- package/dist/decisions/errors.js +12 -0
- package/dist/decisions/index.d.ts +6 -5
- package/dist/decisions/index.js +2 -1
- package/dist/decisions/integrations.d.ts +14 -5
- package/dist/decisions/integrations.js +16 -1
- package/dist/decisions/mock.d.ts +5 -3
- package/dist/decisions/mock.js +4 -1
- package/dist/decisions/pictures.d.ts +43 -0
- package/dist/decisions/pictures.js +103 -0
- package/dist/decisions/typesafe.d.ts +33 -5
- package/dist/decisions/typesafe.js +121 -8
- package/dist/doctor/index.d.ts +6 -9
- package/dist/doctor/index.js +41 -0
- package/dist/embeddings/adapters.d.ts +1 -1
- package/dist/embeddings/adapters.js +3 -3
- package/dist/embeddings/batch-google.d.ts +27 -8
- package/dist/embeddings/batch-google.js +112 -36
- package/dist/embeddings/batch-mock.js +1 -1
- package/dist/embeddings/batch.js +10 -4
- package/dist/embeddings/engine.js +14 -13
- package/dist/embeddings/index.d.ts +1 -1
- package/dist/embeddings/inputs.d.ts +26 -3
- package/dist/embeddings/inputs.js +84 -5
- package/dist/embeddings/mock.js +1 -1
- package/dist/embeddings/models.js +39 -4
- package/dist/loaders/csv.d.ts +23 -1
- package/dist/loaders/csv.js +125 -72
- package/dist/loaders/index.d.ts +7 -2
- package/dist/loaders/input.d.ts +6 -0
- package/dist/loaders/input.js +42 -1
- package/dist/loaders/json.d.ts +14 -3
- package/dist/loaders/json.js +171 -20
- package/dist/loaders/web.d.ts +55 -0
- package/dist/loaders/web.js +20 -1
- package/dist/mcp/client.d.ts +54 -5
- package/dist/mcp/client.js +171 -20
- package/dist/mcp/http.d.ts +45 -0
- package/dist/mcp/http.js +233 -0
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/protocol.d.ts +107 -0
- package/dist/mcp/sampling.d.ts +41 -0
- package/dist/mcp/sampling.js +52 -0
- package/dist/mcp/server.d.ts +96 -14
- package/dist/mcp/server.js +274 -59
- package/dist/models/data.js +20 -1
- package/dist/operations/adapters.d.ts +1 -0
- package/dist/operations/adapters.js +3 -1
- package/dist/protocols/a2a-grpc.js +10 -3
- package/dist/protocols/a2a-signing.d.ts +65 -0
- package/dist/protocols/a2a-signing.js +133 -0
- package/dist/protocols/a2a.d.ts +14 -0
- package/dist/protocols/a2a.js +60 -5
- package/dist/providers/cohere.js +16 -2
- package/dist/providers/google.js +7 -1
- package/dist/providers/ollama.d.ts +1 -1
- package/dist/providers/ollama.js +35 -27
- package/dist/providers/openai.js +19 -2
- package/dist/rag/pinecone.d.ts +63 -2
- package/dist/rag/pinecone.js +80 -0
- package/dist/realtime/conversation.js +18 -0
- package/dist/realtime/gemini-live.transport.d.ts +28 -3
- package/dist/realtime/gemini-live.transport.js +170 -32
- package/dist/realtime/session.d.ts +2 -0
- package/dist/realtime/session.js +19 -4
- package/dist/realtime/types.d.ts +35 -3
- package/dist/security/output-guard.js +4 -1
- package/dist/server/auth.d.ts +263 -2
- package/dist/server/auth.js +432 -5
- package/dist/server/deployments.d.ts +3 -2
- package/dist/server/deployments.js +6 -1
- package/dist/telephony/index.d.ts +1 -1
- package/dist/telephony/manager.d.ts +8 -1
- package/dist/telephony/manager.js +13 -0
- package/dist/telephony/providers/telnyx.d.ts +18 -5
- package/dist/telephony/providers/telnyx.js +44 -7
- package/dist/telephony/providers/twilio.d.ts +3 -1
- package/dist/telephony/providers/twilio.js +16 -0
- package/dist/tracing/stores.d.ts +2 -0
- package/dist/tracing/stores.js +1 -0
- package/dist/tracing/tracer.d.ts +5 -2
- package/dist/tracing/tracer.js +6 -3
- package/dist/types/embeddings.d.ts +47 -5
- package/dist/types/messages.d.ts +7 -0
- package/dist/types/principal.d.ts +1 -1
- package/dist/types/response.d.ts +5 -0
- package/dist/types/telephony.d.ts +38 -0
- package/dist/types/tracing.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/voice/providers/google.d.ts +44 -0
- package/dist/voice/providers/google.js +215 -0
- package/dist-cjs/adapter-kit/gather.d.ts +76 -0
- package/dist-cjs/adapter-kit/gather.js +152 -0
- package/dist-cjs/adapter-kit/index.d.ts +1 -0
- package/dist-cjs/adapter-kit/index.js +4 -1
- package/dist-cjs/adapter-kit/record.d.ts +3 -1
- package/dist-cjs/adapter-kit/record.js +20 -5
- package/dist-cjs/agent/openapi.d.ts +94 -8
- package/dist-cjs/agent/openapi.js +530 -32
- package/dist-cjs/cli/adapters.d.ts +4 -3
- package/dist-cjs/cli/adapters.js +70 -1
- package/dist-cjs/cli/doctor.js +3 -0
- package/dist-cjs/core/config-builder.d.ts +1 -0
- package/dist-cjs/core/config-builder.js +5 -1
- package/dist-cjs/core/defaults.d.ts +34 -0
- package/dist-cjs/core/defaults.js +44 -0
- package/dist-cjs/core/secure-stream.js +4 -1
- package/dist-cjs/decisions/completion.d.ts +10 -7
- package/dist-cjs/decisions/completion.js +81 -7
- package/dist-cjs/decisions/core.d.ts +88 -43
- package/dist-cjs/decisions/core.js +134 -46
- package/dist-cjs/decisions/errors.d.ts +25 -0
- package/dist-cjs/decisions/errors.js +16 -0
- package/dist-cjs/decisions/index.d.ts +6 -5
- package/dist-cjs/decisions/index.js +5 -1
- package/dist-cjs/decisions/integrations.d.ts +14 -5
- package/dist-cjs/decisions/integrations.js +16 -1
- package/dist-cjs/decisions/mock.d.ts +5 -3
- package/dist-cjs/decisions/mock.js +4 -1
- package/dist-cjs/decisions/pictures.d.ts +43 -0
- package/dist-cjs/decisions/pictures.js +109 -0
- package/dist-cjs/decisions/typesafe.d.ts +33 -5
- package/dist-cjs/decisions/typesafe.js +126 -13
- package/dist-cjs/doctor/index.d.ts +6 -9
- package/dist-cjs/doctor/index.js +41 -0
- package/dist-cjs/embeddings/adapters.d.ts +1 -1
- package/dist-cjs/embeddings/adapters.js +3 -3
- package/dist-cjs/embeddings/batch-google.d.ts +27 -8
- package/dist-cjs/embeddings/batch-google.js +112 -36
- package/dist-cjs/embeddings/batch-mock.js +1 -1
- package/dist-cjs/embeddings/batch.js +9 -3
- package/dist-cjs/embeddings/engine.js +13 -12
- package/dist-cjs/embeddings/index.d.ts +1 -1
- package/dist-cjs/embeddings/inputs.d.ts +26 -3
- package/dist-cjs/embeddings/inputs.js +89 -5
- package/dist-cjs/embeddings/mock.js +1 -1
- package/dist-cjs/embeddings/models.js +39 -4
- package/dist-cjs/loaders/csv.d.ts +23 -1
- package/dist-cjs/loaders/csv.js +126 -71
- package/dist-cjs/loaders/index.d.ts +7 -2
- package/dist-cjs/loaders/input.d.ts +6 -0
- package/dist-cjs/loaders/input.js +43 -1
- package/dist-cjs/loaders/json.d.ts +14 -3
- package/dist-cjs/loaders/json.js +170 -19
- package/dist-cjs/loaders/web.d.ts +55 -0
- package/dist-cjs/loaders/web.js +21 -1
- package/dist-cjs/mcp/client.d.ts +54 -5
- package/dist-cjs/mcp/client.js +171 -20
- package/dist-cjs/mcp/http.d.ts +45 -0
- package/dist-cjs/mcp/http.js +236 -0
- package/dist-cjs/mcp/index.d.ts +4 -2
- package/dist-cjs/mcp/index.js +5 -1
- package/dist-cjs/mcp/protocol.d.ts +107 -0
- package/dist-cjs/mcp/sampling.d.ts +41 -0
- package/dist-cjs/mcp/sampling.js +55 -0
- package/dist-cjs/mcp/server.d.ts +96 -14
- package/dist-cjs/mcp/server.js +273 -58
- package/dist-cjs/models/data.js +20 -1
- package/dist-cjs/operations/adapters.d.ts +1 -0
- package/dist-cjs/operations/adapters.js +3 -1
- package/dist-cjs/protocols/a2a-grpc.js +10 -3
- package/dist-cjs/protocols/a2a-signing.d.ts +65 -0
- package/dist-cjs/protocols/a2a-signing.js +140 -0
- package/dist-cjs/protocols/a2a.d.ts +14 -0
- package/dist-cjs/protocols/a2a.js +65 -6
- package/dist-cjs/providers/cohere.js +16 -2
- package/dist-cjs/providers/google.js +7 -1
- package/dist-cjs/providers/ollama.d.ts +1 -1
- package/dist-cjs/providers/ollama.js +35 -27
- package/dist-cjs/providers/openai.js +19 -2
- package/dist-cjs/rag/pinecone.d.ts +63 -2
- package/dist-cjs/rag/pinecone.js +81 -0
- package/dist-cjs/realtime/conversation.js +18 -0
- package/dist-cjs/realtime/gemini-live.transport.d.ts +28 -3
- package/dist-cjs/realtime/gemini-live.transport.js +170 -32
- package/dist-cjs/realtime/session.d.ts +2 -0
- package/dist-cjs/realtime/session.js +19 -4
- package/dist-cjs/realtime/types.d.ts +35 -3
- package/dist-cjs/security/output-guard.js +4 -1
- package/dist-cjs/server/auth.d.ts +263 -2
- package/dist-cjs/server/auth.js +437 -5
- package/dist-cjs/server/deployments.d.ts +3 -2
- package/dist-cjs/server/deployments.js +6 -1
- package/dist-cjs/telephony/index.d.ts +1 -1
- package/dist-cjs/telephony/manager.d.ts +8 -1
- package/dist-cjs/telephony/manager.js +13 -0
- package/dist-cjs/telephony/providers/telnyx.d.ts +18 -5
- package/dist-cjs/telephony/providers/telnyx.js +44 -7
- package/dist-cjs/telephony/providers/twilio.d.ts +3 -1
- package/dist-cjs/telephony/providers/twilio.js +16 -0
- package/dist-cjs/tracing/stores.d.ts +2 -0
- package/dist-cjs/tracing/stores.js +1 -0
- package/dist-cjs/tracing/tracer.d.ts +5 -2
- package/dist-cjs/tracing/tracer.js +6 -3
- package/dist-cjs/types/embeddings.d.ts +47 -5
- package/dist-cjs/types/messages.d.ts +7 -0
- package/dist-cjs/types/principal.d.ts +1 -1
- package/dist-cjs/types/response.d.ts +5 -0
- package/dist-cjs/types/telephony.d.ts +38 -0
- package/dist-cjs/types/tracing.d.ts +6 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/dist-cjs/voice/providers/google.d.ts +44 -0
- package/dist-cjs/voice/providers/google.js +252 -0
- package/llms.txt +1 -1
- package/package.json +17 -1
package/dist/agent/openapi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeBase64, encodeBase64 } from '../utils/base64.js';
|
|
1
2
|
import { linkSignals } from '../utils/signals.js';
|
|
2
3
|
export class OpenApiError extends Error {
|
|
3
4
|
code;
|
|
@@ -12,6 +13,16 @@ export class OpenApiError extends Error {
|
|
|
12
13
|
const METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
13
14
|
const IGNORED_HEADERS = new Set(['accept', 'content-type', 'authorization']);
|
|
14
15
|
const OPENAPI_ONLY = new Set(['nullable', 'example', 'xml', 'externalDocs', 'discriminator']);
|
|
16
|
+
const FILE_SCHEMA = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
description: 'A file, its bytes as base64',
|
|
19
|
+
properties: {
|
|
20
|
+
base64: { type: 'string', description: 'The bytes, base64-encoded' },
|
|
21
|
+
filename: { type: 'string' },
|
|
22
|
+
mimeType: { type: 'string' },
|
|
23
|
+
},
|
|
24
|
+
required: ['base64'],
|
|
25
|
+
};
|
|
15
26
|
export function openApiTools(document, options = {}) {
|
|
16
27
|
const doc = parseDocument(document);
|
|
17
28
|
const apiName = options.name ?? (slug(doc.info?.title ?? '') || 'api');
|
|
@@ -19,8 +30,11 @@ export function openApiTools(document, options = {}) {
|
|
|
19
30
|
const tools = [];
|
|
20
31
|
const operations = [];
|
|
21
32
|
const results = {};
|
|
22
|
-
const skipped = [];
|
|
23
33
|
const used = new Set();
|
|
34
|
+
const schemes = isRecord(doc.components?.securitySchemes)
|
|
35
|
+
? doc.components.securitySchemes
|
|
36
|
+
: {};
|
|
37
|
+
const tokens = new Map();
|
|
24
38
|
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
25
39
|
const pathItem = deref(doc, item);
|
|
26
40
|
for (const method of METHODS) {
|
|
@@ -37,17 +51,13 @@ export function openApiTools(document, options = {}) {
|
|
|
37
51
|
...(typeof raw.summary === 'string' ? { summary: raw.summary } : {}),
|
|
38
52
|
tags: Array.isArray(raw.tags) ? raw.tags.map(String) : [],
|
|
39
53
|
access: method === 'delete' ? 'destructive' : ['get', 'head', 'options', 'trace'].includes(method) ? 'read' : 'write',
|
|
54
|
+
security: securityOf(raw.security ?? doc.security),
|
|
40
55
|
};
|
|
41
56
|
if (options.include && !options.include(operation)) {
|
|
42
57
|
used.delete(name);
|
|
43
58
|
continue;
|
|
44
59
|
}
|
|
45
60
|
const built = buildOperation(doc, pathItem, raw);
|
|
46
|
-
if ('skip' in built) {
|
|
47
|
-
used.delete(name);
|
|
48
|
-
skipped.push({ method: operation.method, path, reason: built.skip });
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
61
|
operations.push(operation);
|
|
52
62
|
if (built.result)
|
|
53
63
|
results[name] = built.result;
|
|
@@ -59,11 +69,23 @@ export function openApiTools(document, options = {}) {
|
|
|
59
69
|
`api.${operation.access}:${apiName}/${name}`,
|
|
60
70
|
...(baseUrl ? [`network:${new URL(baseUrl).host}`] : []),
|
|
61
71
|
],
|
|
62
|
-
...(baseUrl
|
|
72
|
+
...(baseUrl
|
|
73
|
+
? {
|
|
74
|
+
execute: caller(operation, built.placements, built.body, baseUrl, options, (headers, url, context) => authorize(operation, schemes, options, context, headers, url, baseUrl, tokens)),
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
name: apiName,
|
|
82
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
83
|
+
tools,
|
|
84
|
+
operations,
|
|
85
|
+
results,
|
|
86
|
+
skipped: [],
|
|
87
|
+
webhooks: webhooksOf(doc),
|
|
88
|
+
};
|
|
67
89
|
}
|
|
68
90
|
export async function loadOpenApiTools(url, options = {}) {
|
|
69
91
|
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
@@ -72,7 +94,18 @@ export async function loadOpenApiTools(url, options = {}) {
|
|
|
72
94
|
});
|
|
73
95
|
if (!response.ok)
|
|
74
96
|
throw new OpenApiError(`The OpenAPI document at ${url} answered ${response.status}`, 'OPENAPI_REQUEST', response.status);
|
|
75
|
-
|
|
97
|
+
const doc = parseDocument(await readCapped(response, options.maxBytes ?? 5_000_000, `The OpenAPI document at ${url}`));
|
|
98
|
+
const bundled = hasOutsideRef(doc) ? await bundleOpenApiDocument(doc, { ...options, documentUrl: url }) : doc;
|
|
99
|
+
return openApiTools(bundled, { documentUrl: url, ...options });
|
|
100
|
+
}
|
|
101
|
+
function hasOutsideRef(value) {
|
|
102
|
+
if (Array.isArray(value))
|
|
103
|
+
return value.some(hasOutsideRef);
|
|
104
|
+
if (!isRecord(value))
|
|
105
|
+
return false;
|
|
106
|
+
if (typeof value.$ref === 'string' && !value.$ref.startsWith('#'))
|
|
107
|
+
return true;
|
|
108
|
+
return Object.values(value).some(hasOutsideRef);
|
|
76
109
|
}
|
|
77
110
|
function parseDocument(document) {
|
|
78
111
|
let doc = document;
|
|
@@ -86,6 +119,8 @@ function parseDocument(document) {
|
|
|
86
119
|
}
|
|
87
120
|
if (!isRecord(doc))
|
|
88
121
|
throw new OpenApiError('An OpenAPI document is an object', 'OPENAPI_INVALID');
|
|
122
|
+
if (doc.swagger === '2.0')
|
|
123
|
+
return swaggerToOpenApi(doc);
|
|
89
124
|
if (typeof doc.swagger === 'string') {
|
|
90
125
|
throw new OpenApiError(`Swagger ${doc.swagger} is not read; convert the document to OpenAPI 3`, 'OPENAPI_UNSUPPORTED');
|
|
91
126
|
}
|
|
@@ -124,7 +159,7 @@ function deref(doc, value, seen = []) {
|
|
|
124
159
|
}
|
|
125
160
|
function resolve(doc, ref) {
|
|
126
161
|
if (!ref.startsWith('#')) {
|
|
127
|
-
throw new OpenApiError(`${ref} is outside the document
|
|
162
|
+
throw new OpenApiError(`${ref} is outside the document: read it with loadOpenApiTools(), or bundleOpenApiDocument() first`, 'OPENAPI_UNSUPPORTED');
|
|
128
163
|
}
|
|
129
164
|
let node = doc;
|
|
130
165
|
for (const part of ref.slice(1).split('/').slice(1)) {
|
|
@@ -223,39 +258,56 @@ function buildOperation(doc, pathItem, raw) {
|
|
|
223
258
|
required.push(arg);
|
|
224
259
|
placements.push({ arg, name, in: place });
|
|
225
260
|
}
|
|
226
|
-
let
|
|
261
|
+
let bodyOf;
|
|
227
262
|
const body = deref(doc, raw.requestBody);
|
|
228
|
-
if (isRecord(body) && isRecord(body.content)) {
|
|
263
|
+
if (isRecord(body) && isRecord(body.content) && Object.keys(body.content).length) {
|
|
229
264
|
const types = Object.keys(body.content);
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
265
|
+
const find = (pattern) => types.find((type) => pattern.test(type));
|
|
266
|
+
const chosen = [
|
|
267
|
+
['json', find(/^application\/(?:[\w.+-]+\+)?json\b/i)],
|
|
268
|
+
['form', find(/^application\/x-www-form-urlencoded\b/i)],
|
|
269
|
+
['multipart', find(/^multipart\/form-data\b/i)],
|
|
270
|
+
['text', find(/^text\/|^application\/(?:[\w.+-]+\+)?xml\b/i)],
|
|
271
|
+
].find((candidate) => candidate[1] !== undefined) ?? ['binary', types[0]];
|
|
272
|
+
bodyOf = { type: chosen[0], mediaType: chosen[1] };
|
|
273
|
+
const media = body.content[bodyOf.mediaType];
|
|
237
274
|
const schema = schemaOf(doc, isRecord(media) ? media.schema : undefined, 'readOnly');
|
|
238
275
|
const fields = isRecord(schema.properties) ? schema.properties : undefined;
|
|
239
|
-
const
|
|
276
|
+
const fielded = bodyOf.type === 'json' || bodyOf.type === 'form' || bodyOf.type === 'multipart';
|
|
277
|
+
const flat = fielded &&
|
|
278
|
+
fields &&
|
|
240
279
|
(schema.type === 'object' || schema.type === undefined) &&
|
|
241
280
|
!schema.allOf &&
|
|
242
281
|
!schema.anyOf &&
|
|
243
282
|
!schema.oneOf &&
|
|
244
283
|
Object.keys(fields).every((field) => !(field in properties));
|
|
245
|
-
if (flat) {
|
|
284
|
+
if (flat && fields) {
|
|
246
285
|
const fieldsRequired = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
|
|
247
286
|
for (const [field, fieldSchema] of Object.entries(fields)) {
|
|
248
|
-
|
|
287
|
+
const file = bodyOf.type === 'multipart' && isFile(fieldSchema);
|
|
288
|
+
properties[field] = file
|
|
289
|
+
? {
|
|
290
|
+
...FILE_SCHEMA,
|
|
291
|
+
...(isRecord(fieldSchema) && typeof fieldSchema.description === 'string'
|
|
292
|
+
? { description: fieldSchema.description }
|
|
293
|
+
: {}),
|
|
294
|
+
}
|
|
295
|
+
: fieldSchema;
|
|
249
296
|
if (body.required === true && fieldsRequired.has(field))
|
|
250
297
|
required.push(field);
|
|
251
|
-
placements.push({ arg: field, name: field, in: 'body-field' });
|
|
298
|
+
placements.push({ arg: field, name: field, in: 'body-field', ...(file ? { file: true } : {}) });
|
|
252
299
|
}
|
|
253
300
|
}
|
|
254
301
|
else {
|
|
255
302
|
const arg = 'body' in properties ? 'requestBody' : 'body';
|
|
303
|
+
const shape = bodyOf.type === 'binary'
|
|
304
|
+
? FILE_SCHEMA
|
|
305
|
+
: bodyOf.type === 'text'
|
|
306
|
+
? { type: 'string', ...(typeof schema.description === 'string' ? { description: schema.description } : {}) }
|
|
307
|
+
: schema;
|
|
256
308
|
properties[arg] = {
|
|
257
|
-
...
|
|
258
|
-
...(typeof body.description === 'string' && !
|
|
309
|
+
...shape,
|
|
310
|
+
...(typeof body.description === 'string' && !shape.description ? { description: body.description } : {}),
|
|
259
311
|
};
|
|
260
312
|
if (body.required === true)
|
|
261
313
|
required.push(arg);
|
|
@@ -285,16 +337,77 @@ function buildOperation(doc, pathItem, raw) {
|
|
|
285
337
|
additionalProperties: false,
|
|
286
338
|
},
|
|
287
339
|
placements,
|
|
288
|
-
...(
|
|
340
|
+
...(bodyOf ? { body: bodyOf } : {}),
|
|
289
341
|
...(result ? { result } : {}),
|
|
290
342
|
};
|
|
291
343
|
}
|
|
344
|
+
function isFile(schema) {
|
|
345
|
+
if (!isRecord(schema))
|
|
346
|
+
return false;
|
|
347
|
+
return (schema.format === 'binary' || (typeof schema.contentMediaType === 'string' && schema.contentEncoding === undefined));
|
|
348
|
+
}
|
|
349
|
+
function securityOf(value) {
|
|
350
|
+
if (!Array.isArray(value))
|
|
351
|
+
return [];
|
|
352
|
+
return value
|
|
353
|
+
.filter(isRecord)
|
|
354
|
+
.map((alternative) => Object.fromEntries(Object.entries(alternative).map(([name, scopes]) => [name, Array.isArray(scopes) ? scopes.map(String) : []])));
|
|
355
|
+
}
|
|
356
|
+
function webhooksOf(doc) {
|
|
357
|
+
const found = [];
|
|
358
|
+
const bodyOf = (operation) => {
|
|
359
|
+
const body = deref(doc, operation.requestBody);
|
|
360
|
+
const content = isRecord(body) && isRecord(body.content) ? body.content : undefined;
|
|
361
|
+
const json = content && Object.keys(content).find((type) => /json/i.test(type));
|
|
362
|
+
const media = json && content ? content[json] : undefined;
|
|
363
|
+
return isRecord(media) && media.schema !== undefined ? schemaOf(doc, media.schema, 'readOnly') : undefined;
|
|
364
|
+
};
|
|
365
|
+
const add = (name, source, item, expression) => {
|
|
366
|
+
const pathItem = deref(doc, item);
|
|
367
|
+
if (!isRecord(pathItem))
|
|
368
|
+
return;
|
|
369
|
+
for (const method of METHODS) {
|
|
370
|
+
const operation = pathItem[method];
|
|
371
|
+
if (!isRecord(operation))
|
|
372
|
+
continue;
|
|
373
|
+
const body = bodyOf(operation);
|
|
374
|
+
found.push({
|
|
375
|
+
name,
|
|
376
|
+
source,
|
|
377
|
+
method: method.toUpperCase(),
|
|
378
|
+
...(expression ? { expression } : {}),
|
|
379
|
+
...(body ? { body } : {}),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
for (const [name, item] of Object.entries(isRecord(doc.webhooks) ? doc.webhooks : {}))
|
|
384
|
+
add(name, 'webhook', item);
|
|
385
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
386
|
+
const pathItem = deref(doc, item);
|
|
387
|
+
if (!isRecord(pathItem))
|
|
388
|
+
continue;
|
|
389
|
+
for (const method of METHODS) {
|
|
390
|
+
const operation = pathItem[method];
|
|
391
|
+
if (!isRecord(operation) || !isRecord(operation.callbacks))
|
|
392
|
+
continue;
|
|
393
|
+
const owner = typeof operation.operationId === 'string' ? operation.operationId : `${method}_${path}`;
|
|
394
|
+
for (const [callback, raw] of Object.entries(operation.callbacks)) {
|
|
395
|
+
const expressions = deref(doc, raw);
|
|
396
|
+
if (!isRecord(expressions))
|
|
397
|
+
continue;
|
|
398
|
+
for (const [expression, callbackItem] of Object.entries(expressions))
|
|
399
|
+
add(`${owner}.${callback}`, 'callback', callbackItem, expression);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return found;
|
|
404
|
+
}
|
|
292
405
|
function describe(raw, operation) {
|
|
293
406
|
const text = [raw.summary, raw.description].filter((part) => typeof part === 'string' && part.trim() !== '');
|
|
294
407
|
const said = text.length ? text.join('\n\n') : `${operation.method} ${operation.path}`;
|
|
295
408
|
return said.length > 1_024 ? `${said.slice(0, 1_021)}...` : said;
|
|
296
409
|
}
|
|
297
|
-
function caller(operation, placements,
|
|
410
|
+
function caller(operation, placements, bodyOf, baseUrl, options, authorizing) {
|
|
298
411
|
const base = new URL(`${baseUrl}/`);
|
|
299
412
|
return async (args, context) => {
|
|
300
413
|
context?.signal?.throwIfAborted();
|
|
@@ -333,13 +446,41 @@ function caller(operation, placements, bodyType, baseUrl, options) {
|
|
|
333
446
|
const own = typeof options.headers === 'function' ? await options.headers({ operation, context }) : options.headers;
|
|
334
447
|
for (const [name, value] of Object.entries(own ?? {}))
|
|
335
448
|
headers[name.toLowerCase()] = value;
|
|
449
|
+
await authorizing(headers, url, context);
|
|
336
450
|
let payload;
|
|
337
|
-
|
|
451
|
+
const type = bodyOf?.type ?? 'json';
|
|
452
|
+
if (body !== undefined && type === 'form') {
|
|
338
453
|
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
339
454
|
payload = new URLSearchParams(Object.entries(body).map(([key, value]) => [key, text(value)])).toString();
|
|
340
455
|
}
|
|
456
|
+
else if (body !== undefined && type === 'multipart') {
|
|
457
|
+
const form = new FormData();
|
|
458
|
+
const files = new Set(placements.filter((item) => item.file).map((item) => item.name));
|
|
459
|
+
for (const [key, value] of Object.entries(body)) {
|
|
460
|
+
if (files.has(key)) {
|
|
461
|
+
const file = fileOf(value, `${operation.name}'s "${key}"`);
|
|
462
|
+
form.append(key, new Blob([file.bytes], { type: file.mimeType ?? 'application/octet-stream' }), file.filename ?? key);
|
|
463
|
+
}
|
|
464
|
+
else
|
|
465
|
+
form.append(key, text(value));
|
|
466
|
+
}
|
|
467
|
+
payload = form;
|
|
468
|
+
}
|
|
469
|
+
else if (body !== undefined && type === 'text') {
|
|
470
|
+
headers['content-type'] = bodyOf?.mediaType ?? 'text/plain';
|
|
471
|
+
payload = text(body);
|
|
472
|
+
}
|
|
473
|
+
else if (body !== undefined && type === 'binary') {
|
|
474
|
+
const file = fileOf(body, `${operation.name}'s body`);
|
|
475
|
+
headers['content-type'] =
|
|
476
|
+
file.mimeType ??
|
|
477
|
+
(bodyOf?.mediaType.includes('*')
|
|
478
|
+
? 'application/octet-stream'
|
|
479
|
+
: (bodyOf?.mediaType ?? 'application/octet-stream'));
|
|
480
|
+
payload = file.bytes;
|
|
481
|
+
}
|
|
341
482
|
else if (body !== undefined) {
|
|
342
|
-
headers['content-type'] = 'application/json';
|
|
483
|
+
headers['content-type'] = bodyOf?.mediaType ?? 'application/json';
|
|
343
484
|
payload = JSON.stringify(body);
|
|
344
485
|
}
|
|
345
486
|
const linked = linkSignals([context?.signal, AbortSignal.timeout(options.timeoutMs ?? 30_000)]);
|
|
@@ -351,7 +492,7 @@ function caller(operation, placements, bodyType, baseUrl, options) {
|
|
|
351
492
|
redirect: 'manual',
|
|
352
493
|
signal: linked.signal,
|
|
353
494
|
});
|
|
354
|
-
const answer = await readCapped(response, options.maxResponseBytes ?? 1_000_000, operation);
|
|
495
|
+
const answer = await readCapped(response, options.maxResponseBytes ?? 1_000_000, operation.name);
|
|
355
496
|
if (response.status >= 300 && response.status < 400) {
|
|
356
497
|
throw new OpenApiError(`${operation.name} was redirected to ${response.headers.get('location') ?? 'another place'}, which is not followed`, 'OPENAPI_REQUEST', response.status);
|
|
357
498
|
}
|
|
@@ -380,7 +521,105 @@ function caller(operation, placements, bodyType, baseUrl, options) {
|
|
|
380
521
|
}
|
|
381
522
|
};
|
|
382
523
|
}
|
|
383
|
-
|
|
524
|
+
function fileOf(value, what) {
|
|
525
|
+
const given = typeof value === 'string' ? { base64: value } : isRecord(value) ? value : {};
|
|
526
|
+
if (typeof given.base64 !== 'string')
|
|
527
|
+
throw new OpenApiError(`${what} is a file: give its base64`, 'OPENAPI_INVALID');
|
|
528
|
+
if (!/^[A-Za-z0-9+/=\s]*$/.test(given.base64))
|
|
529
|
+
throw new OpenApiError(`${what} is not base64`, 'OPENAPI_INVALID');
|
|
530
|
+
return {
|
|
531
|
+
bytes: decodeBase64(given.base64),
|
|
532
|
+
...(typeof given.filename === 'string' ? { filename: given.filename } : {}),
|
|
533
|
+
...(typeof given.mimeType === 'string' ? { mimeType: given.mimeType } : {}),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async function authorize(operation, schemes, options, context, headers, url, baseUrl, tokens) {
|
|
537
|
+
const credentials = options.credentials;
|
|
538
|
+
if (!credentials || operation.security.length === 0)
|
|
539
|
+
return;
|
|
540
|
+
const has = (name) => Object.hasOwn(credentials, name) && isRecord(schemes[name]);
|
|
541
|
+
const chosen = operation.security.find((alternative) => Object.keys(alternative).every(has));
|
|
542
|
+
if (!chosen) {
|
|
543
|
+
const asked = operation.security.map((alternative) => Object.keys(alternative).join(' and ')).join(', or ');
|
|
544
|
+
throw new OpenApiError(`${operation.name} needs credentials for ${asked}`, 'OPENAPI_INVALID');
|
|
545
|
+
}
|
|
546
|
+
for (const [name, scopes] of Object.entries(chosen)) {
|
|
547
|
+
const scheme = schemes[name];
|
|
548
|
+
const given = credentials[name];
|
|
549
|
+
const credential = typeof given === 'function' ? await given({ operation, context }) : given;
|
|
550
|
+
const fields = (isRecord(credential) ? credential : {});
|
|
551
|
+
const token = () => {
|
|
552
|
+
if (typeof credential === 'string')
|
|
553
|
+
return credential;
|
|
554
|
+
if (typeof fields.token === 'string')
|
|
555
|
+
return fields.token;
|
|
556
|
+
throw new OpenApiError(`The credential for ${name} is a token, as text or { token }`, 'OPENAPI_INVALID');
|
|
557
|
+
};
|
|
558
|
+
if (scheme.type === 'apiKey') {
|
|
559
|
+
const key = String(scheme.name ?? '');
|
|
560
|
+
if (scheme.in === 'query')
|
|
561
|
+
url.searchParams.set(key, token());
|
|
562
|
+
else if (scheme.in === 'cookie') {
|
|
563
|
+
const cookie = `${key}=${encodeURIComponent(token())}`;
|
|
564
|
+
headers.cookie = headers.cookie ? `${headers.cookie}; ${cookie}` : cookie;
|
|
565
|
+
}
|
|
566
|
+
else
|
|
567
|
+
headers[key.toLowerCase()] = token();
|
|
568
|
+
}
|
|
569
|
+
else if (scheme.type === 'http') {
|
|
570
|
+
const kind = String(scheme.scheme ?? 'bearer').toLowerCase();
|
|
571
|
+
if (kind === 'basic') {
|
|
572
|
+
if (typeof fields.username !== 'string' || typeof fields.password !== 'string') {
|
|
573
|
+
throw new OpenApiError(`The credential for ${name} is { username, password }`, 'OPENAPI_INVALID');
|
|
574
|
+
}
|
|
575
|
+
headers.authorization = `Basic ${encodeBase64(new TextEncoder().encode(`${fields.username}:${fields.password}`))}`;
|
|
576
|
+
}
|
|
577
|
+
else
|
|
578
|
+
headers.authorization = `${kind === 'bearer' ? 'Bearer' : String(scheme.scheme)} ${token()}`;
|
|
579
|
+
}
|
|
580
|
+
else if (scheme.type === 'oauth2' || scheme.type === 'openIdConnect') {
|
|
581
|
+
const client = typeof fields.clientId === 'string' && typeof fields.clientSecret === 'string'
|
|
582
|
+
? { clientId: fields.clientId, clientSecret: fields.clientSecret }
|
|
583
|
+
: undefined;
|
|
584
|
+
headers.authorization = `Bearer ${client ? await clientCredentials(name, scheme, client, scopes, baseUrl, options, tokens) : token()}`;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
throw new OpenApiError(`${name} is ${String(scheme.type)}, which the tools do not send`, 'OPENAPI_UNSUPPORTED');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async function clientCredentials(name, scheme, client, scopes, baseUrl, options, tokens) {
|
|
592
|
+
const flow = isRecord(scheme.flows) && isRecord(scheme.flows.clientCredentials) ? scheme.flows.clientCredentials : undefined;
|
|
593
|
+
if (!flow || typeof flow.tokenUrl !== 'string') {
|
|
594
|
+
throw new OpenApiError(`${name} has no client-credentials flow to fetch a token from`, 'OPENAPI_UNSUPPORTED');
|
|
595
|
+
}
|
|
596
|
+
const key = `${name}\u0000${client.clientId}\u0000${[...scopes].sort().join(' ')}`;
|
|
597
|
+
const kept = tokens.get(key);
|
|
598
|
+
if (kept && kept.until > Date.now())
|
|
599
|
+
return kept.token;
|
|
600
|
+
const response = await (options.fetch ?? globalThis.fetch)(new URL(flow.tokenUrl, `${baseUrl}/`).href, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: {
|
|
603
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
604
|
+
accept: 'application/json',
|
|
605
|
+
authorization: `Basic ${encodeBase64(new TextEncoder().encode(`${encodeURIComponent(client.clientId)}:${encodeURIComponent(client.clientSecret)}`))}`,
|
|
606
|
+
},
|
|
607
|
+
body: new URLSearchParams({
|
|
608
|
+
grant_type: 'client_credentials',
|
|
609
|
+
...(scopes.length ? { scope: scopes.join(' ') } : {}),
|
|
610
|
+
}).toString(),
|
|
611
|
+
redirect: 'manual',
|
|
612
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
|
|
613
|
+
});
|
|
614
|
+
const answer = (await response.json().catch(() => ({})));
|
|
615
|
+
if (typeof answer.access_token !== 'string') {
|
|
616
|
+
throw new OpenApiError(`${name}'s token URL answered ${response.status}: ${String(answer.error ?? 'no token')}`, 'OPENAPI_REQUEST', response.status);
|
|
617
|
+
}
|
|
618
|
+
const lifetime = typeof answer.expires_in === 'number' ? answer.expires_in * 1_000 : 3_600_000;
|
|
619
|
+
tokens.set(key, { token: answer.access_token, until: Date.now() + Math.max(0, lifetime - 60_000) });
|
|
620
|
+
return answer.access_token;
|
|
621
|
+
}
|
|
622
|
+
async function readCapped(response, limit, what) {
|
|
384
623
|
if (!response.body)
|
|
385
624
|
return '';
|
|
386
625
|
const reader = response.body.getReader();
|
|
@@ -393,7 +632,7 @@ async function readCapped(response, limit, operation) {
|
|
|
393
632
|
size += value.byteLength;
|
|
394
633
|
if (size > limit) {
|
|
395
634
|
await reader.cancel().catch(() => undefined);
|
|
396
|
-
throw new OpenApiError(`${
|
|
635
|
+
throw new OpenApiError(`${what} answered more than ${limit} bytes`, 'OPENAPI_REQUEST', response.status);
|
|
397
636
|
}
|
|
398
637
|
chunks.push(value);
|
|
399
638
|
}
|
|
@@ -453,3 +692,260 @@ function isRecord(value) {
|
|
|
453
692
|
function messageOf(error) {
|
|
454
693
|
return error instanceof Error ? error.message : String(error);
|
|
455
694
|
}
|
|
695
|
+
export function swaggerToOpenApi(swagger) {
|
|
696
|
+
if (swagger.swagger !== '2.0') {
|
|
697
|
+
throw new OpenApiError(`Swagger ${String(swagger.swagger)} is not 2.0`, 'OPENAPI_UNSUPPORTED');
|
|
698
|
+
}
|
|
699
|
+
const moved = renameRefs(swagger);
|
|
700
|
+
const consumes = stringList(moved.consumes) ?? ['application/json'];
|
|
701
|
+
const produces = stringList(moved.produces) ?? ['application/json'];
|
|
702
|
+
const schemes = stringList(moved.schemes) ?? ['https'];
|
|
703
|
+
const host = typeof moved.host === 'string' ? moved.host : undefined;
|
|
704
|
+
const basePath = typeof moved.basePath === 'string' ? moved.basePath : '';
|
|
705
|
+
const components = {};
|
|
706
|
+
if (isRecord(moved.definitions))
|
|
707
|
+
components.schemas = moved.definitions;
|
|
708
|
+
if (isRecord(moved.parameters)) {
|
|
709
|
+
components.parameters = Object.fromEntries(Object.entries(moved.parameters).map(([name, parameter]) => [
|
|
710
|
+
name,
|
|
711
|
+
isRecord(parameter) ? parameterOf(parameter) : parameter,
|
|
712
|
+
]));
|
|
713
|
+
}
|
|
714
|
+
if (isRecord(moved.responses)) {
|
|
715
|
+
components.responses = Object.fromEntries(Object.entries(moved.responses).map(([name, response]) => [name, responseOf(response, produces)]));
|
|
716
|
+
}
|
|
717
|
+
if (isRecord(moved.securityDefinitions)) {
|
|
718
|
+
components.securitySchemes = Object.fromEntries(Object.entries(moved.securityDefinitions).map(([name, scheme]) => [name, securitySchemeOf(scheme)]));
|
|
719
|
+
}
|
|
720
|
+
const paths = {};
|
|
721
|
+
for (const [path, item] of Object.entries(isRecord(moved.paths) ? moved.paths : {})) {
|
|
722
|
+
if (!isRecord(item))
|
|
723
|
+
continue;
|
|
724
|
+
const converted = {};
|
|
725
|
+
const shared = Array.isArray(item.parameters) ? item.parameters : [];
|
|
726
|
+
for (const [key, value] of Object.entries(item)) {
|
|
727
|
+
if (key === 'parameters') {
|
|
728
|
+
converted.parameters = shared.filter((parameter) => !isBodyLike(moved, parameter)).map(parameterOf);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!METHODS.includes(key) || !isRecord(value)) {
|
|
732
|
+
converted[key] = value;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
converted[key] = operationOf(moved, value, shared, stringList(value.consumes) ?? consumes, stringList(value.produces) ?? produces);
|
|
736
|
+
}
|
|
737
|
+
paths[path] = converted;
|
|
738
|
+
}
|
|
739
|
+
const { swagger: _swagger, host: _host, basePath: _basePath, schemes: _schemes, consumes: _consumes, produces: _produces, definitions: _definitions, parameters: _parameters, responses: _responses, securityDefinitions: _security, paths: _paths, ...rest } = moved;
|
|
740
|
+
return {
|
|
741
|
+
...rest,
|
|
742
|
+
openapi: '3.0.3',
|
|
743
|
+
...(host
|
|
744
|
+
? { servers: [{ url: `${schemes[0]}://${host}${basePath}` }] }
|
|
745
|
+
: basePath
|
|
746
|
+
? { servers: [{ url: basePath }] }
|
|
747
|
+
: {}),
|
|
748
|
+
paths,
|
|
749
|
+
...(Object.keys(components).length ? { components } : {}),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function renameRefs(value) {
|
|
753
|
+
if (Array.isArray(value))
|
|
754
|
+
return value.map(renameRefs);
|
|
755
|
+
if (!isRecord(value))
|
|
756
|
+
return value;
|
|
757
|
+
const out = {};
|
|
758
|
+
for (const [key, item] of Object.entries(value)) {
|
|
759
|
+
out[key] =
|
|
760
|
+
key === '$ref' && typeof item === 'string'
|
|
761
|
+
? item
|
|
762
|
+
.replace(/^#\/definitions\//, '#/components/schemas/')
|
|
763
|
+
.replace(/^#\/parameters\//, '#/components/parameters/')
|
|
764
|
+
.replace(/^#\/responses\//, '#/components/responses/')
|
|
765
|
+
: renameRefs(item);
|
|
766
|
+
}
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
function isBodyLike(doc, parameter) {
|
|
770
|
+
const found = swaggerParameter(doc, parameter);
|
|
771
|
+
return found !== undefined && (found.in === 'body' || found.in === 'formData');
|
|
772
|
+
}
|
|
773
|
+
function swaggerParameter(doc, parameter) {
|
|
774
|
+
if (!isRecord(parameter))
|
|
775
|
+
return undefined;
|
|
776
|
+
if (typeof parameter.$ref !== 'string')
|
|
777
|
+
return parameter;
|
|
778
|
+
const name = parameter.$ref.replace(/^#\/components\/parameters\//, '');
|
|
779
|
+
const shared = isRecord(doc.parameters) ? doc.parameters[name] : undefined;
|
|
780
|
+
return isRecord(shared) ? shared : undefined;
|
|
781
|
+
}
|
|
782
|
+
function parameterOf(parameter) {
|
|
783
|
+
if (!isRecord(parameter) ||
|
|
784
|
+
typeof parameter.$ref === 'string' ||
|
|
785
|
+
parameter.in === 'body' ||
|
|
786
|
+
parameter.in === 'formData') {
|
|
787
|
+
return parameter;
|
|
788
|
+
}
|
|
789
|
+
const { name, in: place, description, required, ...schema } = parameter;
|
|
790
|
+
const { collectionFormat: _format, allowEmptyValue: _empty, ...kept } = schema;
|
|
791
|
+
return {
|
|
792
|
+
name,
|
|
793
|
+
in: place,
|
|
794
|
+
...(description !== undefined ? { description } : {}),
|
|
795
|
+
...(required !== undefined ? { required } : {}),
|
|
796
|
+
schema: kept,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function responseOf(response, produces) {
|
|
800
|
+
if (!isRecord(response) || typeof response.$ref === 'string')
|
|
801
|
+
return response;
|
|
802
|
+
const { schema, examples: _examples, ...rest } = response;
|
|
803
|
+
return {
|
|
804
|
+
...rest,
|
|
805
|
+
description: typeof rest.description === 'string' ? rest.description : '',
|
|
806
|
+
...(schema !== undefined ? { content: { [produces[0] ?? 'application/json']: { schema } } } : {}),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function securitySchemeOf(scheme) {
|
|
810
|
+
if (!isRecord(scheme))
|
|
811
|
+
return scheme;
|
|
812
|
+
if (scheme.type === 'basic')
|
|
813
|
+
return { type: 'http', scheme: 'basic', ...(scheme.description ? { description: scheme.description } : {}) };
|
|
814
|
+
if (scheme.type !== 'oauth2')
|
|
815
|
+
return scheme;
|
|
816
|
+
const flowName = { implicit: 'implicit', password: 'password', application: 'clientCredentials', accessCode: 'authorizationCode' }[String(scheme.flow)] ?? 'clientCredentials';
|
|
817
|
+
return {
|
|
818
|
+
type: 'oauth2',
|
|
819
|
+
...(scheme.description ? { description: scheme.description } : {}),
|
|
820
|
+
flows: {
|
|
821
|
+
[flowName]: {
|
|
822
|
+
...(scheme.authorizationUrl ? { authorizationUrl: scheme.authorizationUrl } : {}),
|
|
823
|
+
...(scheme.tokenUrl ? { tokenUrl: scheme.tokenUrl } : {}),
|
|
824
|
+
scopes: isRecord(scheme.scopes) ? scheme.scopes : {},
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function operationOf(doc, operation, shared, consumes, produces) {
|
|
830
|
+
const own = Array.isArray(operation.parameters) ? operation.parameters : [];
|
|
831
|
+
const all = [...shared, ...own].map((parameter) => swaggerParameter(doc, parameter) ?? parameter);
|
|
832
|
+
const body = all.find((parameter) => isRecord(parameter) && parameter.in === 'body');
|
|
833
|
+
const form = all.filter((parameter) => isRecord(parameter) && parameter.in === 'formData');
|
|
834
|
+
const { parameters: _parameters, consumes: _consumes, produces: _produces, responses, ...rest } = operation;
|
|
835
|
+
const out = {
|
|
836
|
+
...rest,
|
|
837
|
+
...(own.length ? { parameters: own.filter((parameter) => !isBodyLike(doc, parameter)).map(parameterOf) } : {}),
|
|
838
|
+
responses: Object.fromEntries(Object.entries(isRecord(responses) ? responses : {}).map(([code, response]) => [
|
|
839
|
+
code,
|
|
840
|
+
responseOf(response, produces),
|
|
841
|
+
])),
|
|
842
|
+
};
|
|
843
|
+
if (body) {
|
|
844
|
+
out.requestBody = {
|
|
845
|
+
...(body.description ? { description: body.description } : {}),
|
|
846
|
+
...(body.required !== undefined ? { required: body.required } : {}),
|
|
847
|
+
content: {
|
|
848
|
+
[consumes.find((type) => /json/i.test(type)) ?? consumes[0] ?? 'application/json']: {
|
|
849
|
+
schema: body.schema ?? {},
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
else if (form.length) {
|
|
855
|
+
const files = form.some((field) => field.type === 'file');
|
|
856
|
+
const type = files || consumes.some((item) => /multipart\/form-data/i.test(item))
|
|
857
|
+
? 'multipart/form-data'
|
|
858
|
+
: 'application/x-www-form-urlencoded';
|
|
859
|
+
const properties = Object.fromEntries(form.map((field) => {
|
|
860
|
+
const { name: _name, in: _in, required: _required, collectionFormat: _format, ...schema } = field;
|
|
861
|
+
return [String(field.name), field.type === 'file' ? { ...schema, type: 'string', format: 'binary' } : schema];
|
|
862
|
+
}));
|
|
863
|
+
const required = form.filter((field) => field.required === true).map((field) => String(field.name));
|
|
864
|
+
out.requestBody = {
|
|
865
|
+
...(required.length ? { required: true } : {}),
|
|
866
|
+
content: { [type]: { schema: { type: 'object', properties, ...(required.length ? { required } : {}) } } },
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}
|
|
871
|
+
function stringList(value) {
|
|
872
|
+
return Array.isArray(value) && value.length ? value.map(String) : undefined;
|
|
873
|
+
}
|
|
874
|
+
export async function bundleOpenApiDocument(document, options) {
|
|
875
|
+
const root = new URL(options.documentUrl);
|
|
876
|
+
const allowed = (url) => options.references === 'any'
|
|
877
|
+
? url.protocol === 'https:' || url.protocol === 'http:'
|
|
878
|
+
: typeof options.references === 'function'
|
|
879
|
+
? options.references(url.href)
|
|
880
|
+
: url.origin === root.origin;
|
|
881
|
+
const loaded = new Map([[stripHash(root), Promise.resolve(document)]]);
|
|
882
|
+
const load = (url) => {
|
|
883
|
+
const key = stripHash(url);
|
|
884
|
+
let found = loaded.get(key);
|
|
885
|
+
if (!found) {
|
|
886
|
+
if (!allowed(url)) {
|
|
887
|
+
return Promise.reject(new OpenApiError(`${key} is not a document references may name here`, 'OPENAPI_UNSUPPORTED'));
|
|
888
|
+
}
|
|
889
|
+
if (loaded.size >= (options.maxDocuments ?? 50)) {
|
|
890
|
+
return Promise.reject(new OpenApiError(`The document refers to more than ${options.maxDocuments ?? 50} documents`, 'OPENAPI_INVALID'));
|
|
891
|
+
}
|
|
892
|
+
found = fetchDocument(key, options);
|
|
893
|
+
loaded.set(key, found);
|
|
894
|
+
}
|
|
895
|
+
return found;
|
|
896
|
+
};
|
|
897
|
+
const inline = async (value, from, trail) => {
|
|
898
|
+
if (Array.isArray(value))
|
|
899
|
+
return Promise.all(value.map((item) => inline(item, from, trail)));
|
|
900
|
+
if (!isRecord(value))
|
|
901
|
+
return value;
|
|
902
|
+
if (typeof value.$ref === 'string') {
|
|
903
|
+
const target = new URL(value.$ref, from);
|
|
904
|
+
if (stripHash(target) === stripHash(root)) {
|
|
905
|
+
const { $ref: _ref, ...siblings } = value;
|
|
906
|
+
return { $ref: target.hash || '#', ...(await inlineFields(siblings, from, trail)) };
|
|
907
|
+
}
|
|
908
|
+
if (trail.includes(target.href))
|
|
909
|
+
return { description: `The same shape as ${target.hash.split('/').at(-1) || target.pathname}, nested` };
|
|
910
|
+
const named = pointer(await load(target), target.hash, target.href);
|
|
911
|
+
const { $ref: _ref, ...siblings } = value;
|
|
912
|
+
return {
|
|
913
|
+
...(await inline(named, target, [...trail, target.href])),
|
|
914
|
+
...(await inlineFields(siblings, from, trail)),
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return inlineFields(value, from, trail);
|
|
918
|
+
};
|
|
919
|
+
const inlineFields = async (value, from, trail) => Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await inline(item, from, trail)])));
|
|
920
|
+
return (await inline(document, root, []));
|
|
921
|
+
}
|
|
922
|
+
async function fetchDocument(url, options) {
|
|
923
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
924
|
+
headers: { accept: 'application/json' },
|
|
925
|
+
signal: AbortSignal.timeout(30_000),
|
|
926
|
+
});
|
|
927
|
+
if (!response.ok)
|
|
928
|
+
throw new OpenApiError(`${url} answered ${response.status}`, 'OPENAPI_REQUEST', response.status);
|
|
929
|
+
const text = await readCapped(response, options.maxBytes ?? 5_000_000, url);
|
|
930
|
+
try {
|
|
931
|
+
return JSON.parse(text);
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
throw new OpenApiError(`${url} is not JSON; parse a YAML document first`, 'OPENAPI_INVALID');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function pointer(document, hash, where) {
|
|
938
|
+
let node = document;
|
|
939
|
+
for (const part of hash.replace(/^#/, '').split('/').slice(1)) {
|
|
940
|
+
const key = decodeURIComponent(part).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
941
|
+
node = isRecord(node) || Array.isArray(node) ? node[key] : undefined;
|
|
942
|
+
if (node === undefined)
|
|
943
|
+
throw new OpenApiError(`${where} names nothing`, 'OPENAPI_INVALID');
|
|
944
|
+
}
|
|
945
|
+
return node;
|
|
946
|
+
}
|
|
947
|
+
function stripHash(url) {
|
|
948
|
+
const copy = new URL(url.href);
|
|
949
|
+
copy.hash = '';
|
|
950
|
+
return copy.href;
|
|
951
|
+
}
|