@mintlify/prebuild 1.0.1190 → 1.0.1191
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/dist/prebuild/update/ConfigUpdater.d.ts +90 -0
- package/dist/prebuild/update/docsConfig/generateSdkDivisions.d.ts +30 -0
- package/dist/prebuild/update/docsConfig/generateSdkDivisions.js +304 -0
- package/dist/prebuild/update/docsConfig/index.d.ts +2 -0
- package/dist/prebuild/update/docsConfig/index.js +13 -2
- package/dist/prebuild/update/docsConfig/loadRemoteGraphqlSdl.d.ts +5 -2
- package/dist/prebuild/update/docsConfig/loadRemoteGraphqlSdl.js +32 -19
- package/dist/prebuild/update/index.d.ts +90 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { generateSdkReference } from '@mintlify/scraping';
|
|
2
|
+
import { Unzip, UnzipInflate } from 'fflate';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { getGeneratedRouteKey } from './generatedRouteCollisions.js';
|
|
7
|
+
import { loadRemoteFile } from './loadRemoteGraphqlSdl.js';
|
|
8
|
+
const DEFAULT_OUTPUT_DIR = 'sdk-reference';
|
|
9
|
+
const REMOTE_ARTIFACT_LABEL = 'SDK artifact';
|
|
10
|
+
const MAX_REMOTE_SDK_ARTIFACT_BYTES = 50 * 1024 * 1024;
|
|
11
|
+
const MAX_EXTRACTED_SDK_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
12
|
+
const REMOTE_SDK_ARTIFACT_TIMEOUT_MS = 30_000;
|
|
13
|
+
export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReference) {
|
|
14
|
+
const references = new Map();
|
|
15
|
+
const pagesByRouteKey = new Map();
|
|
16
|
+
function load(config) {
|
|
17
|
+
const key = `${config.format}:${config.source}`;
|
|
18
|
+
let pending = references.get(key);
|
|
19
|
+
if (!pending) {
|
|
20
|
+
pending = loadReference(config);
|
|
21
|
+
references.set(key, pending);
|
|
22
|
+
}
|
|
23
|
+
return pending;
|
|
24
|
+
}
|
|
25
|
+
async function processNode(value) {
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return Promise.all(value.map(processNode));
|
|
28
|
+
if (typeof value !== 'object' || value === null)
|
|
29
|
+
return value;
|
|
30
|
+
const node = value;
|
|
31
|
+
if ('tab' in node && 'sdk' in node) {
|
|
32
|
+
const { sdk, ...tab } = node;
|
|
33
|
+
const config = sdk;
|
|
34
|
+
const directory = config.directory ?? DEFAULT_OUTPUT_DIR;
|
|
35
|
+
const reference = await load(config);
|
|
36
|
+
const pageBySlug = new Map(reference.pages.map((page) => [page.slug, page]));
|
|
37
|
+
const referenceSlugs = new Set(reference.pages.map((page) => page.slug));
|
|
38
|
+
const groups = reference.groups.map((group) => ({
|
|
39
|
+
group: group.group,
|
|
40
|
+
pages: group.pages.map((slug) => path.posix.join(directory, slug)),
|
|
41
|
+
}));
|
|
42
|
+
for (const page of reference.pages) {
|
|
43
|
+
assertSafeSlug(page.slug, config);
|
|
44
|
+
const slug = path.posix.join(directory, page.slug);
|
|
45
|
+
const generatedPage = {
|
|
46
|
+
slug,
|
|
47
|
+
title: page.title,
|
|
48
|
+
...(page.description ? { description: page.description } : {}),
|
|
49
|
+
...(page.tag ? { tag: page.tag } : {}),
|
|
50
|
+
content: prefixInternalLinks(stripLeadingDescription(page.content, page.description), directory, referenceSlugs),
|
|
51
|
+
format: config.format,
|
|
52
|
+
source: config.source,
|
|
53
|
+
};
|
|
54
|
+
const routeKey = getGeneratedRouteKey(slug);
|
|
55
|
+
const existingPage = pagesByRouteKey.get(routeKey);
|
|
56
|
+
if (existingPage &&
|
|
57
|
+
(existingPage.slug !== slug ||
|
|
58
|
+
existingPage.format !== generatedPage.format ||
|
|
59
|
+
existingPage.source !== generatedPage.source)) {
|
|
60
|
+
throw new Error(`Multiple SDK references generate colliding routes "${existingPage.slug}" and "${slug}"`);
|
|
61
|
+
}
|
|
62
|
+
pagesByRouteKey.set(routeKey, generatedPage);
|
|
63
|
+
}
|
|
64
|
+
const orphanedSlugs = reference.pages
|
|
65
|
+
.map((page) => page.slug)
|
|
66
|
+
.filter((slug) => !reference.groups.some((group) => group.pages.includes(slug)));
|
|
67
|
+
if (orphanedSlugs.length > 0 && pageBySlug.size > 0) {
|
|
68
|
+
groups.push({
|
|
69
|
+
group: 'Reference',
|
|
70
|
+
pages: orphanedSlugs.map((slug) => path.posix.join(directory, slug)),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { ...tab, groups: [...(tab.groups ?? []), ...groups] };
|
|
74
|
+
}
|
|
75
|
+
return Object.fromEntries(await Promise.all(Object.entries(node).map(async ([key, entry]) => [key, await processNode(entry)])));
|
|
76
|
+
}
|
|
77
|
+
const navigation = (await processNode(docsConfig.navigation));
|
|
78
|
+
const generatedPages = [...pagesByRouteKey.values()].sort((left, right) => left.slug.localeCompare(right.slug));
|
|
79
|
+
const pagesAcc = Object.fromEntries(generatedPages.map((page) => [
|
|
80
|
+
page.slug,
|
|
81
|
+
{
|
|
82
|
+
href: `/${page.slug}`,
|
|
83
|
+
title: page.title,
|
|
84
|
+
...(page.description ? { description: page.description } : {}),
|
|
85
|
+
...(page.tag ? { tag: page.tag } : {}),
|
|
86
|
+
},
|
|
87
|
+
]));
|
|
88
|
+
return {
|
|
89
|
+
newDocsConfig: { ...docsConfig, navigation },
|
|
90
|
+
pagesAcc,
|
|
91
|
+
generatedPages,
|
|
92
|
+
generatedRoutes: generatedPages.map(({ slug }) => slug),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export const isRemoteSdkSource = (source) => {
|
|
96
|
+
try {
|
|
97
|
+
return new URL(source).protocol === 'https:';
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function assertSafeArchiveEntry(name, source) {
|
|
104
|
+
const segments = name.split('/');
|
|
105
|
+
if (name.includes('\\') ||
|
|
106
|
+
path.posix.isAbsolute(name) ||
|
|
107
|
+
path.win32.isAbsolute(name) ||
|
|
108
|
+
segments.some((segment) => segment === '.' || segment === '..')) {
|
|
109
|
+
throw new Error(`SDK artifact ${source} contains an unsafe archive entry "${name}"`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function unzipWithSizeCap(bytes, source) {
|
|
113
|
+
const entries = new Map();
|
|
114
|
+
let extractedBytes = 0;
|
|
115
|
+
let failure;
|
|
116
|
+
const overflow = () => new Error(`SDK artifact ${source} exceeds the ${MAX_EXTRACTED_SDK_ARTIFACT_BYTES} byte extracted size limit`);
|
|
117
|
+
const unzip = new Unzip((file) => {
|
|
118
|
+
try {
|
|
119
|
+
assertSafeArchiveEntry(file.name, source);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
failure ??= error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (file.name.endsWith('/'))
|
|
126
|
+
return;
|
|
127
|
+
const chunks = [];
|
|
128
|
+
file.ondata = (error, data, final) => {
|
|
129
|
+
if (failure)
|
|
130
|
+
return;
|
|
131
|
+
if (error) {
|
|
132
|
+
failure = error;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
extractedBytes += data.byteLength;
|
|
136
|
+
if (extractedBytes > MAX_EXTRACTED_SDK_ARTIFACT_BYTES) {
|
|
137
|
+
failure = overflow();
|
|
138
|
+
file.terminate();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
chunks.push(data);
|
|
142
|
+
if (final) {
|
|
143
|
+
const merged = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));
|
|
144
|
+
let offset = 0;
|
|
145
|
+
for (const chunk of chunks) {
|
|
146
|
+
merged.set(chunk, offset);
|
|
147
|
+
offset += chunk.byteLength;
|
|
148
|
+
}
|
|
149
|
+
entries.set(file.name, merged);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
file.start();
|
|
153
|
+
});
|
|
154
|
+
unzip.register(UnzipInflate);
|
|
155
|
+
unzip.push(bytes, true);
|
|
156
|
+
if (failure)
|
|
157
|
+
throw failure;
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
async function extractRemoteArtifact(buffer, source, tempDir) {
|
|
161
|
+
const bytes = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
162
|
+
if (bytes[0] !== 0x50 || bytes[1] !== 0x4b) {
|
|
163
|
+
const basename = path.posix.basename(new URL(source).pathname) || 'artifact';
|
|
164
|
+
const filePath = path.join(tempDir, basename.replace(/[^\w.-]+/g, '-'));
|
|
165
|
+
await fse.writeFile(filePath, buffer);
|
|
166
|
+
return filePath;
|
|
167
|
+
}
|
|
168
|
+
const entries = unzipWithSizeCap(bytes, source);
|
|
169
|
+
for (const [name, data] of entries) {
|
|
170
|
+
await fse.outputFile(path.join(tempDir, name), Buffer.from(data));
|
|
171
|
+
}
|
|
172
|
+
let root = tempDir;
|
|
173
|
+
for (;;) {
|
|
174
|
+
const rootEntries = await fse.readdir(root, { withFileTypes: true });
|
|
175
|
+
if (rootEntries.length !== 1 || !rootEntries[0])
|
|
176
|
+
return root;
|
|
177
|
+
const only = path.join(root, rootEntries[0].name);
|
|
178
|
+
if (rootEntries[0].isFile())
|
|
179
|
+
return only;
|
|
180
|
+
root = only;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function loadRemoteSdkReference(config, remoteOptions) {
|
|
184
|
+
const buffer = await loadRemoteFile(config.source, {
|
|
185
|
+
label: REMOTE_ARTIFACT_LABEL,
|
|
186
|
+
maxBytes: MAX_REMOTE_SDK_ARTIFACT_BYTES,
|
|
187
|
+
timeoutMs: REMOTE_SDK_ARTIFACT_TIMEOUT_MS,
|
|
188
|
+
...remoteOptions,
|
|
189
|
+
});
|
|
190
|
+
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mintlify-sdk-'));
|
|
191
|
+
try {
|
|
192
|
+
const artifactPath = await extractRemoteArtifact(buffer, config.source, tempDir);
|
|
193
|
+
return await generateSdkReference(config.format, artifactPath);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await fse.remove(tempDir);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export async function loadSdkReferenceSource(config, contentDirectoryPath, remoteOptions) {
|
|
200
|
+
const { format, source } = config;
|
|
201
|
+
if (isRemoteSdkSource(source)) {
|
|
202
|
+
return loadRemoteSdkReference(config, remoteOptions);
|
|
203
|
+
}
|
|
204
|
+
if (path.isAbsolute(source) || path.win32.isAbsolute(source)) {
|
|
205
|
+
throw new Error(`Unable to load SDK reference ${source}: path must be relative`);
|
|
206
|
+
}
|
|
207
|
+
const contentRoot = await fse.realpath(contentDirectoryPath);
|
|
208
|
+
const candidatePath = path.resolve(contentRoot, source);
|
|
209
|
+
const assertContained = (target, label) => {
|
|
210
|
+
const relativePath = path.relative(contentRoot, target);
|
|
211
|
+
if (relativePath === '..' ||
|
|
212
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
213
|
+
path.isAbsolute(relativePath)) {
|
|
214
|
+
throw new Error(`Unable to load SDK reference ${source}: ${label}`);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
assertContained(candidatePath, 'path escapes the content directory');
|
|
218
|
+
const realSourcePath = await fse.realpath(candidatePath);
|
|
219
|
+
assertContained(realSourcePath, 'path resolves outside the content directory');
|
|
220
|
+
return generateSdkReference(format, realSourcePath);
|
|
221
|
+
}
|
|
222
|
+
function prefixInternalLinks(content, directory, slugs) {
|
|
223
|
+
return content.replace(/\]\(\/([^)#\s]+)((?:#[^)\s]*)?)\)/g, (match, target, fragment) => slugs.has(target) ? `](/${path.posix.join(directory, target)}${fragment})` : match);
|
|
224
|
+
}
|
|
225
|
+
function assertSafeSlug(slug, config) {
|
|
226
|
+
const segments = slug.split('/');
|
|
227
|
+
if (!slug ||
|
|
228
|
+
slug.includes('\\') ||
|
|
229
|
+
path.posix.isAbsolute(slug) ||
|
|
230
|
+
segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
231
|
+
throw new Error(`SDK reference ${config.format}:${config.source} produced an unsafe page slug "${slug}"`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function normalizeProse(text) {
|
|
235
|
+
return text
|
|
236
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
237
|
+
.replace(/[`*_\\]/g, '')
|
|
238
|
+
.replace(/\s+/g, ' ')
|
|
239
|
+
.trim();
|
|
240
|
+
}
|
|
241
|
+
function stripLeadingDescription(content, description) {
|
|
242
|
+
if (!description)
|
|
243
|
+
return content;
|
|
244
|
+
const [firstChunk = '', ...rest] = content.split('\n\n');
|
|
245
|
+
if (/^[#<>`-]/.test(firstChunk.trim()))
|
|
246
|
+
return content;
|
|
247
|
+
const matches = normalizeProse(firstChunk) === normalizeProse(description);
|
|
248
|
+
return matches ? rest.join('\n\n').trim() : content;
|
|
249
|
+
}
|
|
250
|
+
function frontmatter(page) {
|
|
251
|
+
const fields = [
|
|
252
|
+
`title: ${JSON.stringify(page.title)}`,
|
|
253
|
+
...(page.description ? [`description: ${JSON.stringify(page.description)}`] : []),
|
|
254
|
+
...(page.tag ? [`tag: ${JSON.stringify(page.tag)}`] : []),
|
|
255
|
+
];
|
|
256
|
+
return `---\n${fields.join('\n')}\n---\n\n`;
|
|
257
|
+
}
|
|
258
|
+
function getContainedSdkPagePath(targetDir, page) {
|
|
259
|
+
const propsDirectory = path.resolve(targetDir ?? '', 'src', '_props');
|
|
260
|
+
const pagePath = path.resolve(propsDirectory, `${page}.mdx`);
|
|
261
|
+
const relativePath = path.relative(propsDirectory, pagePath);
|
|
262
|
+
if (relativePath.startsWith(`..${path.sep}`) ||
|
|
263
|
+
relativePath === '..' ||
|
|
264
|
+
path.isAbsolute(relativePath)) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
return pagePath;
|
|
268
|
+
}
|
|
269
|
+
export async function writeSdkArtifacts(result, targetDir) {
|
|
270
|
+
const propsDirectory = path.resolve(targetDir ?? '', 'src', '_props');
|
|
271
|
+
const manifestPath = path.join(propsDirectory, 'sdk-data.json');
|
|
272
|
+
let previousPages = [];
|
|
273
|
+
try {
|
|
274
|
+
const previousManifest = JSON.parse(await fse.readFile(manifestPath, 'utf8'));
|
|
275
|
+
if (Array.isArray(previousManifest.pages)) {
|
|
276
|
+
previousPages = previousManifest.pages.filter((page) => typeof page === 'string');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { }
|
|
280
|
+
await Promise.all(previousPages.map(async (page) => {
|
|
281
|
+
const pagePath = getContainedSdkPagePath(targetDir, page);
|
|
282
|
+
if (!pagePath)
|
|
283
|
+
return;
|
|
284
|
+
try {
|
|
285
|
+
await fse.remove(pagePath);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
console.warn(`Failed to remove stale SDK reference page ${page}: ${error instanceof Error ? error.message : String(error)}`);
|
|
289
|
+
}
|
|
290
|
+
}));
|
|
291
|
+
await fse.remove(manifestPath);
|
|
292
|
+
if (result.generatedPages.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
await fse.outputFile(manifestPath, JSON.stringify({ pages: result.generatedRoutes }), {
|
|
295
|
+
flag: 'w',
|
|
296
|
+
});
|
|
297
|
+
await Promise.all(result.generatedPages.map((page) => {
|
|
298
|
+
const pagePath = getContainedSdkPagePath(targetDir, page.slug);
|
|
299
|
+
if (!pagePath) {
|
|
300
|
+
throw new Error(`SDK reference page "${page.slug}" resolves outside the output directory`);
|
|
301
|
+
}
|
|
302
|
+
return fse.outputFile(pagePath, `${frontmatter(page)}${page.content}\n`, { flag: 'w' });
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
@@ -24,6 +24,8 @@ export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFile
|
|
|
24
24
|
}>;
|
|
25
25
|
export { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
|
|
26
26
|
export { generateOpenApiFromDocsConfig } from './generateOpenApiFromDocsConfig.js';
|
|
27
|
+
export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
|
|
28
|
+
export type { GeneratedSdkPage, GenerateSdkDivisionsResult, SdkReferenceLoader, } from './generateSdkDivisions.js';
|
|
27
29
|
export { getOpenApiFilesFromConfig } from '../read/getOpenApiFilesFromConfig.js';
|
|
28
30
|
export { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
|
|
29
31
|
export { generateAsyncApiFromDocsConfig } from './generateAsyncApiFromDocsConfig.js';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getConfigPath } from '../../../utils.js';
|
|
2
2
|
import { DocsConfigUpdater } from '../ConfigUpdater.js';
|
|
3
3
|
import { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
|
|
4
|
+
import { assertNoGeneratedRouteCollisions } from './generatedRouteCollisions.js';
|
|
4
5
|
import { generateGraphqlDivisionsFromDocsConfig, loadGraphqlSdlSource, writeGraphqlArtifacts, } from './generateGraphqlDivisions.js';
|
|
5
6
|
import { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
|
|
7
|
+
import { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
|
|
6
8
|
import { getCustomLanguages } from './getCustomLanguages.js';
|
|
7
9
|
const NOT_CORRECT_PATH_ERROR = 'must be run in a directory where a docs.json file exists.';
|
|
8
10
|
export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, }) {
|
|
@@ -30,16 +32,24 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
|
|
|
30
32
|
flushWrites: async () => { },
|
|
31
33
|
};
|
|
32
34
|
const { newDocsConfig: docsConfigWithAsyncApiPages, pagesAcc: pagesAccWithAsyncApiPages, asyncApiFiles: newAsyncApiFiles, flushWrites: flushAsyncApiWrites, } = await generateAsyncApiDivisions(docsConfigWithOpenApiPages, asyncApiFiles, undefined, localSchema, pagesAccWithGraphqlPages, true, true);
|
|
35
|
+
const sdkResult = await generateSdkDivisionsFromDocsConfig(docsConfigWithAsyncApiPages, (config) => loadSdkReferenceSource(config, contentDirectoryPath));
|
|
36
|
+
const { newDocsConfig: docsConfigWithSdkPages, pagesAcc: pagesAccWithSdkPages } = sdkResult;
|
|
37
|
+
const sdkRoutes = { label: 'SDK', pages: pagesAccWithSdkPages };
|
|
38
|
+
assertNoGeneratedRouteCollisions({ label: 'GraphQL', pages: pagesAccWithGraphqlPages }, sdkRoutes);
|
|
39
|
+
assertNoGeneratedRouteCollisions({ label: 'OpenAPI', pages: pagesAccWithOpenApiPages }, sdkRoutes);
|
|
40
|
+
assertNoGeneratedRouteCollisions({ label: 'AsyncAPI', pages: pagesAccWithAsyncApiPages }, sdkRoutes);
|
|
33
41
|
const pagesAcc = {
|
|
34
42
|
...pagesAccWithOpenApiPages,
|
|
35
43
|
...pagesAccWithAsyncApiPages,
|
|
36
44
|
...pagesAccWithGraphqlPages,
|
|
45
|
+
...pagesAccWithSdkPages,
|
|
37
46
|
};
|
|
38
47
|
await Promise.all([flushOpenApiWrites(), flushAsyncApiWrites()]);
|
|
39
48
|
await writeGraphqlArtifacts(graphqlResult);
|
|
40
|
-
await
|
|
49
|
+
await writeSdkArtifacts(sdkResult);
|
|
50
|
+
await DocsConfigUpdater.writeConfigFile(docsConfigWithSdkPages);
|
|
41
51
|
return {
|
|
42
|
-
docsConfig:
|
|
52
|
+
docsConfig: docsConfigWithSdkPages,
|
|
43
53
|
pagesAcc,
|
|
44
54
|
newOpenApiFiles,
|
|
45
55
|
newAsyncApiFiles,
|
|
@@ -48,6 +58,7 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
|
|
|
48
58
|
}
|
|
49
59
|
export { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
|
|
50
60
|
export { generateOpenApiFromDocsConfig } from './generateOpenApiFromDocsConfig.js';
|
|
61
|
+
export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
|
|
51
62
|
export { getOpenApiFilesFromConfig } from '../read/getOpenApiFilesFromConfig.js';
|
|
52
63
|
export { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
|
|
53
64
|
export { generateAsyncApiFromDocsConfig } from './generateAsyncApiFromDocsConfig.js';
|
|
@@ -9,10 +9,13 @@ type ResolveHostname = (hostname: string) => Promise<ResolvedAddress[]>;
|
|
|
9
9
|
type HttpsRequest = typeof request;
|
|
10
10
|
export declare const isGlobalIpAddress: (address: string) => boolean;
|
|
11
11
|
export declare function createPinnedLookup(resolved: ResolvedAddress): LookupFunction;
|
|
12
|
-
export
|
|
12
|
+
export type LoadRemoteFileOptions = {
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
maxBytes?: number;
|
|
15
15
|
resolveHostname?: ResolveHostname;
|
|
16
16
|
requestImpl?: HttpsRequest;
|
|
17
|
-
|
|
17
|
+
label?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function loadRemoteGraphqlSdl(source: string, options?: LoadRemoteFileOptions): Promise<string>;
|
|
20
|
+
export declare function loadRemoteFile(source: string, options?: LoadRemoteFileOptions): Promise<Buffer>;
|
|
18
21
|
export {};
|
|
@@ -4,6 +4,7 @@ import { BlockList, isIP } from 'node:net';
|
|
|
4
4
|
const MAX_REMOTE_REDIRECTS = 5;
|
|
5
5
|
export const MAX_GRAPHQL_SCHEMA_BYTES = 5 * 1024 * 1024;
|
|
6
6
|
const REMOTE_SCHEMA_TIMEOUT_MS = 10_000;
|
|
7
|
+
const DEFAULT_LABEL = 'GraphQL schema';
|
|
7
8
|
const blockedAddresses = new BlockList();
|
|
8
9
|
for (const [network, prefix] of [
|
|
9
10
|
['0.0.0.0', 8],
|
|
@@ -66,17 +67,25 @@ async function defaultResolveHostname(hostname) {
|
|
|
66
67
|
return (await lookup(hostname, { all: true }));
|
|
67
68
|
}
|
|
68
69
|
export function createPinnedLookup(resolved) {
|
|
69
|
-
return ((_hostname,
|
|
70
|
-
|
|
70
|
+
return ((_hostname, options, callback) => {
|
|
71
|
+
const withAll = typeof options === 'object' && options?.all;
|
|
72
|
+
if (withAll) {
|
|
73
|
+
callback(null, [
|
|
74
|
+
resolved,
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
callback(null, resolved.address, resolved.family);
|
|
79
|
+
}
|
|
71
80
|
});
|
|
72
81
|
}
|
|
73
|
-
async function resolveAndValidate(url, resolveHostname) {
|
|
82
|
+
async function resolveAndValidate(url, resolveHostname, label) {
|
|
74
83
|
if (url.protocol !== 'https:') {
|
|
75
|
-
throw new Error(
|
|
84
|
+
throw new Error(`${label} redirects must use HTTPS: ${url.href}`);
|
|
76
85
|
}
|
|
77
86
|
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
78
87
|
if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
|
|
79
|
-
throw new Error(
|
|
88
|
+
throw new Error(`${label} host must be public: ${hostname}`);
|
|
80
89
|
}
|
|
81
90
|
let addresses;
|
|
82
91
|
try {
|
|
@@ -84,15 +93,15 @@ async function resolveAndValidate(url, resolveHostname) {
|
|
|
84
93
|
}
|
|
85
94
|
catch (error) {
|
|
86
95
|
const detail = error instanceof Error ? `: ${error.message}` : '';
|
|
87
|
-
throw new Error(`Unable to resolve
|
|
96
|
+
throw new Error(`Unable to resolve ${label} host ${hostname}${detail}`);
|
|
88
97
|
}
|
|
89
98
|
// Reject ambiguous split-horizon answers so every address associated with the host is safe.
|
|
90
99
|
if (addresses.length === 0 || addresses.some(({ address }) => !isGlobalIpAddress(address))) {
|
|
91
|
-
throw new Error(
|
|
100
|
+
throw new Error(`${label} host must resolve only to public addresses: ${hostname}`);
|
|
92
101
|
}
|
|
93
102
|
return addresses[0];
|
|
94
103
|
}
|
|
95
|
-
function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
|
|
104
|
+
function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes, label) {
|
|
96
105
|
return new Promise((resolve, reject) => {
|
|
97
106
|
let responseRef;
|
|
98
107
|
let requestRef;
|
|
@@ -125,7 +134,7 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
|
|
|
125
134
|
const contentLength = Number(response.headers['content-length']);
|
|
126
135
|
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
127
136
|
response.destroy();
|
|
128
|
-
finish(reject, new Error(
|
|
137
|
+
finish(reject, new Error(`${label} ${url.href} exceeds the ${maxBytes} byte limit`));
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
131
140
|
response.on('data', (chunk) => {
|
|
@@ -133,7 +142,7 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
|
|
|
133
142
|
totalBytes += bytes.byteLength;
|
|
134
143
|
if (totalBytes > maxBytes) {
|
|
135
144
|
response.destroy();
|
|
136
|
-
finish(reject, new Error(
|
|
145
|
+
finish(reject, new Error(`${label} ${url.href} exceeds the ${maxBytes} byte limit`));
|
|
137
146
|
return;
|
|
138
147
|
}
|
|
139
148
|
chunks.push(new Uint8Array(bytes));
|
|
@@ -143,7 +152,7 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
|
|
|
143
152
|
statusCode: response.statusCode ?? 0,
|
|
144
153
|
statusMessage: response.statusMessage ?? '',
|
|
145
154
|
headers: response.headers,
|
|
146
|
-
body: Buffer.concat(chunks)
|
|
155
|
+
body: Buffer.concat(chunks),
|
|
147
156
|
});
|
|
148
157
|
});
|
|
149
158
|
response.on('error', (error) => finish(reject, error));
|
|
@@ -158,10 +167,14 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
|
|
|
158
167
|
});
|
|
159
168
|
}
|
|
160
169
|
export async function loadRemoteGraphqlSdl(source, options = {}) {
|
|
170
|
+
return (await loadRemoteFile(source, options)).toString('utf8');
|
|
171
|
+
}
|
|
172
|
+
export async function loadRemoteFile(source, options = {}) {
|
|
161
173
|
const timeoutMs = options.timeoutMs ?? REMOTE_SCHEMA_TIMEOUT_MS;
|
|
162
174
|
const maxBytes = options.maxBytes ?? MAX_GRAPHQL_SCHEMA_BYTES;
|
|
163
175
|
const resolveHostname = options.resolveHostname ?? defaultResolveHostname;
|
|
164
176
|
const requestImpl = options.requestImpl ?? request;
|
|
177
|
+
const label = options.label ?? DEFAULT_LABEL;
|
|
165
178
|
const deadline = Date.now() + timeoutMs;
|
|
166
179
|
let currentUrl = new URL(source);
|
|
167
180
|
const withTimeout = async (pending, remainingMs) => {
|
|
@@ -182,36 +195,36 @@ export async function loadRemoteGraphqlSdl(source, options = {}) {
|
|
|
182
195
|
for (let redirects = 0; redirects <= MAX_REMOTE_REDIRECTS; redirects++) {
|
|
183
196
|
let remainingMs = deadline - Date.now();
|
|
184
197
|
if (remainingMs <= 0)
|
|
185
|
-
throw new Error(`Timed out loading
|
|
198
|
+
throw new Error(`Timed out loading ${label} ${source}`);
|
|
186
199
|
let response;
|
|
187
200
|
try {
|
|
188
|
-
const resolved = await withTimeout(resolveAndValidate(currentUrl, resolveHostname), remainingMs);
|
|
201
|
+
const resolved = await withTimeout(resolveAndValidate(currentUrl, resolveHostname, label), remainingMs);
|
|
189
202
|
remainingMs = deadline - Date.now();
|
|
190
203
|
if (remainingMs <= 0)
|
|
191
204
|
throw new Error('timeout');
|
|
192
|
-
response = await requestPinned(currentUrl, resolved, requestImpl, remainingMs, maxBytes);
|
|
205
|
+
response = await requestPinned(currentUrl, resolved, requestImpl, remainingMs, maxBytes, label);
|
|
193
206
|
}
|
|
194
207
|
catch (error) {
|
|
195
208
|
if (Date.now() >= deadline || (error instanceof Error && error.message === 'timeout')) {
|
|
196
|
-
throw new Error(`Timed out loading
|
|
209
|
+
throw new Error(`Timed out loading ${label} ${source}`);
|
|
197
210
|
}
|
|
198
211
|
throw error;
|
|
199
212
|
}
|
|
200
213
|
if (response.statusCode >= 300 && response.statusCode < 400) {
|
|
201
214
|
const location = response.headers.location;
|
|
202
215
|
if (typeof location !== 'string') {
|
|
203
|
-
throw new Error(
|
|
216
|
+
throw new Error(`${label} redirect from ${currentUrl.href} has no location`);
|
|
204
217
|
}
|
|
205
218
|
if (redirects === MAX_REMOTE_REDIRECTS) {
|
|
206
|
-
throw new Error(`Too many redirects loading
|
|
219
|
+
throw new Error(`Too many redirects loading ${label} ${source}`);
|
|
207
220
|
}
|
|
208
221
|
currentUrl = new URL(location, currentUrl);
|
|
209
222
|
continue;
|
|
210
223
|
}
|
|
211
224
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
212
|
-
throw new Error(`Unable to load
|
|
225
|
+
throw new Error(`Unable to load ${label} ${currentUrl.href}: ${response.statusCode} ${response.statusMessage}`);
|
|
213
226
|
}
|
|
214
227
|
return response.body;
|
|
215
228
|
}
|
|
216
|
-
throw new Error(`Too many redirects loading
|
|
229
|
+
throw new Error(`Too many redirects loading ${label} ${source}`);
|
|
217
230
|
}
|