@prauga/flexdoc-backend 2.9.0 → 2.9.9

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.
Files changed (42) hide show
  1. package/dist/framework-adapters.d.ts +13 -0
  2. package/dist/framework-adapters.js +42 -9
  3. package/dist/framework-adapters.js.map +1 -1
  4. package/dist/framework-adapters.test.js +8 -0
  5. package/dist/framework-adapters.test.js.map +1 -1
  6. package/dist/hono-adapter.d.ts +3 -0
  7. package/dist/hono-adapter.js +65 -7
  8. package/dist/hono-adapter.js.map +1 -1
  9. package/dist/hono-adapter.test.js +20 -6
  10. package/dist/hono-adapter.test.js.map +1 -1
  11. package/dist/host-execution-route.d.ts +29 -0
  12. package/dist/host-execution-route.js +212 -0
  13. package/dist/host-execution-route.js.map +1 -0
  14. package/dist/host-execution-route.test.d.ts +1 -0
  15. package/dist/host-execution-route.test.js +290 -0
  16. package/dist/host-execution-route.test.js.map +1 -0
  17. package/dist/host-execution.d.ts +168 -0
  18. package/dist/host-execution.js +768 -0
  19. package/dist/host-execution.js.map +1 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +9 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/interfaces.d.ts +31 -0
  24. package/dist/page-cache.d.ts +7 -0
  25. package/dist/page-cache.js +35 -0
  26. package/dist/page-cache.js.map +1 -0
  27. package/dist/page-cache.test.d.ts +1 -0
  28. package/dist/page-cache.test.js +43 -0
  29. package/dist/page-cache.test.js.map +1 -0
  30. package/dist/renderer/flexdoc.standalone.css +1 -1
  31. package/dist/renderer/flexdoc.standalone.js +69 -53
  32. package/dist/setup.js +46 -9
  33. package/dist/setup.js.map +1 -1
  34. package/dist/setup.test.js +30 -1
  35. package/dist/setup.test.js.map +1 -1
  36. package/dist/template.d.ts +1 -0
  37. package/dist/template.js +11 -1
  38. package/dist/template.js.map +1 -1
  39. package/dist/template.test.js +25 -0
  40. package/dist/template.test.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +2 -2
@@ -0,0 +1,768 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HostExecutionBadRequestError = exports.HostExecutionUnsupportedError = exports.HostExecutionForbiddenError = void 0;
37
+ exports.createHostExecutionState = createHostExecutionState;
38
+ exports.publicHostExecutionOptions = publicHostExecutionOptions;
39
+ exports.ensureHostExecutionSession = ensureHostExecutionSession;
40
+ exports.isCookieDomainAllowed = isCookieDomainAllowed;
41
+ exports.publicCookiesForSession = publicCookiesForSession;
42
+ exports.clearCookiesForSession = clearCookiesForSession;
43
+ exports.allowedHostExecutionOrigins = allowedHostExecutionOrigins;
44
+ exports.assertHostExecutionUrlAllowed = assertHostExecutionUrlAllowed;
45
+ exports.assertHostExecutionResolvedAddressAllowed = assertHostExecutionResolvedAddressAllowed;
46
+ exports.executeHostRequest = executeHostRequest;
47
+ const crypto = __importStar(require("crypto"));
48
+ const dns = __importStar(require("dns"));
49
+ const http = __importStar(require("http"));
50
+ const https = __importStar(require("https"));
51
+ const net = __importStar(require("net"));
52
+ const tldts_1 = require("tldts");
53
+ class HostExecutionForbiddenError extends Error {
54
+ }
55
+ exports.HostExecutionForbiddenError = HostExecutionForbiddenError;
56
+ class HostExecutionUnsupportedError extends Error {
57
+ }
58
+ exports.HostExecutionUnsupportedError = HostExecutionUnsupportedError;
59
+ class HostExecutionBadRequestError extends Error {
60
+ }
61
+ exports.HostExecutionBadRequestError = HostExecutionBadRequestError;
62
+ const HOP_BY_HOP = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'host', 'content-length', 'set-cookie']);
63
+ const METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
64
+ const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
65
+ const MAX_REDIRECTS = 5;
66
+ const MAX_SESSION_JARS = 1000;
67
+ const SESSION_COOKIE = '__flexdoc_session';
68
+ function createHostExecutionState(value) {
69
+ const enabled = value === true || (typeof value === 'object' && value !== null && value.enabled !== false);
70
+ const options = typeof value === 'object' ? value : {};
71
+ const certificates = new Map((options.clientCertificates || []).map((certificate) => [certificate.id, { ...certificate }]));
72
+ const capabilities = enabled
73
+ ? ['cookies', 'digest', 'hawk', 'oauth1', 'awsv4', ...(certificates.size ? ['clientCertificates'] : [])]
74
+ : [];
75
+ return {
76
+ enabled,
77
+ options,
78
+ capabilities,
79
+ certificates,
80
+ sessionSecret: crypto.randomBytes(32),
81
+ jars: new Map(),
82
+ };
83
+ }
84
+ function publicHostExecutionOptions(state, rendererBasePath) {
85
+ if (!state.enabled)
86
+ return undefined;
87
+ return {
88
+ available: true,
89
+ endpoint: `${rendererBasePath}/execute`,
90
+ cookiesEndpoint: `${rendererBasePath}/cookies`,
91
+ capabilities: [...state.capabilities],
92
+ ...(state.certificates.size ? { clientCertificates: [...state.certificates.values()].map(({ id, name }) => ({ id, name })) } : {}),
93
+ };
94
+ }
95
+ function base64Url(input) {
96
+ return input.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
97
+ }
98
+ function signSession(state, id) {
99
+ return base64Url(crypto.createHmac('sha256', state.sessionSecret).update(id).digest());
100
+ }
101
+ function parseCookieHeader(value) {
102
+ const out = new Map();
103
+ for (const part of (value || '').split(';')) {
104
+ const index = part.indexOf('=');
105
+ if (index <= 0)
106
+ continue;
107
+ out.set(part.slice(0, index).trim(), part.slice(index + 1).trim());
108
+ }
109
+ return out;
110
+ }
111
+ function allocateSessionJar(state, id) {
112
+ while (state.jars.size >= MAX_SESSION_JARS) {
113
+ const oldest = state.jars.keys().next().value;
114
+ if (!oldest)
115
+ break;
116
+ state.jars.delete(oldest);
117
+ }
118
+ state.jars.set(id, []);
119
+ }
120
+ function ensureHostExecutionSession(state, cookieHeader) {
121
+ const raw = parseCookieHeader(cookieHeader).get(SESSION_COOKIE);
122
+ if (raw) {
123
+ const separator = raw.lastIndexOf('.');
124
+ if (separator > 0) {
125
+ const id = raw.slice(0, separator);
126
+ const signature = raw.slice(separator + 1);
127
+ const expected = signSession(state, id);
128
+ const a = Buffer.from(signature);
129
+ const b = Buffer.from(expected);
130
+ if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
131
+ if (!state.jars.has(id))
132
+ allocateSessionJar(state, id);
133
+ return { sessionId: id };
134
+ }
135
+ }
136
+ }
137
+ const sessionId = base64Url(crypto.randomBytes(24));
138
+ const signed = `${sessionId}.${signSession(state, sessionId)}`;
139
+ allocateSessionJar(state, sessionId);
140
+ return {
141
+ sessionId,
142
+ setCookie: `${SESSION_COOKIE}=${signed}; Path=/; HttpOnly; SameSite=Strict`,
143
+ };
144
+ }
145
+ function cleanExpired(cookies) {
146
+ const now = Date.now();
147
+ return cookies.filter((cookie) => cookie.expiresAt === undefined || cookie.expiresAt > now);
148
+ }
149
+ function domainMatches(hostname, cookie) {
150
+ const host = hostname.toLowerCase();
151
+ const domain = cookie.domain.toLowerCase().replace(/^\./, '');
152
+ return cookie.hostOnly ? host === domain : host === domain || host.endsWith(`.${domain}`);
153
+ }
154
+ function isCookieDomainAllowed(responseHostname, candidateDomain) {
155
+ const responseHost = responseHostname.toLowerCase().replace(/^\[|\]$/g, '');
156
+ const domain = candidateDomain.toLowerCase().replace(/^\./, '');
157
+ if (!domain || domain.endsWith('.') || net.isIP(responseHost) || net.isIP(domain))
158
+ return false;
159
+ if (responseHost !== domain && !responseHost.endsWith(`.${domain}`))
160
+ return false;
161
+ const publicSuffix = (0, tldts_1.getPublicSuffix)(domain, { allowPrivateDomains: true, extractHostname: false });
162
+ return !publicSuffix || publicSuffix.toLowerCase() !== domain;
163
+ }
164
+ function cookiesForUrl(state, sessionId, url) {
165
+ const existing = cleanExpired(state.jars.get(sessionId) || []);
166
+ state.jars.set(sessionId, existing);
167
+ return existing.filter((cookie) => domainMatches(url.hostname, cookie) && url.pathname.startsWith(cookie.path) && (!cookie.secure || url.protocol === 'https:'));
168
+ }
169
+ function parseSetCookie(value, url) {
170
+ const parts = value.split(';').map((part) => part.trim());
171
+ const pair = parts.shift();
172
+ if (!pair)
173
+ return undefined;
174
+ const equals = pair.indexOf('=');
175
+ if (equals <= 0)
176
+ return undefined;
177
+ const cookie = {
178
+ name: pair.slice(0, equals),
179
+ value: pair.slice(equals + 1),
180
+ domain: url.hostname,
181
+ path: '/',
182
+ hostOnly: true,
183
+ secure: false,
184
+ httpOnly: false,
185
+ };
186
+ for (const attribute of parts) {
187
+ const index = attribute.indexOf('=');
188
+ const key = (index < 0 ? attribute : attribute.slice(0, index)).trim().toLowerCase();
189
+ const raw = index < 0 ? '' : attribute.slice(index + 1).trim();
190
+ if (key === 'domain' && raw) {
191
+ const domain = raw.replace(/^\./, '').toLowerCase();
192
+ if (!isCookieDomainAllowed(url.hostname, domain))
193
+ return undefined;
194
+ cookie.domain = domain;
195
+ cookie.hostOnly = false;
196
+ }
197
+ else if (key === 'path' && raw)
198
+ cookie.path = raw.startsWith('/') ? raw : '/';
199
+ else if (key === 'secure')
200
+ cookie.secure = true;
201
+ else if (key === 'httponly')
202
+ cookie.httpOnly = true;
203
+ else if (key === 'max-age' && /^-?\d+$/.test(raw))
204
+ cookie.expiresAt = Date.now() + Number(raw) * 1000;
205
+ else if (key === 'expires') {
206
+ const parsed = Date.parse(raw);
207
+ if (Number.isFinite(parsed))
208
+ cookie.expiresAt = parsed;
209
+ }
210
+ }
211
+ return cookie;
212
+ }
213
+ function storeSetCookies(state, sessionId, url, values) {
214
+ let jar = cleanExpired(state.jars.get(sessionId) || []);
215
+ for (const value of values) {
216
+ const cookie = parseSetCookie(value, url);
217
+ if (!cookie)
218
+ continue;
219
+ jar = jar.filter((existing) => !(existing.name === cookie.name && existing.domain === cookie.domain && existing.path === cookie.path));
220
+ if (cookie.expiresAt === undefined || cookie.expiresAt > Date.now())
221
+ jar.push(cookie);
222
+ }
223
+ if (jar.length > 100)
224
+ jar = jar.slice(jar.length - 100);
225
+ state.jars.set(sessionId, jar);
226
+ }
227
+ function publicCookiesForSession(state, sessionId) {
228
+ const jar = cleanExpired(state.jars.get(sessionId) || []);
229
+ state.jars.set(sessionId, jar);
230
+ return jar.map((cookie) => ({ name: cookie.name, value: cookie.value, domain: cookie.domain, path: cookie.path, httpOnly: cookie.httpOnly }));
231
+ }
232
+ function clearCookiesForSession(state, sessionId) {
233
+ state.jars.set(sessionId, []);
234
+ }
235
+ function normalizeServerUrl(server) {
236
+ if (!server || typeof server.url !== 'string')
237
+ return undefined;
238
+ let value = server.url;
239
+ const variables = server.variables && typeof server.variables === 'object' ? server.variables : {};
240
+ value = value.replace(/\{([^{}]+)\}/g, (match, name) => {
241
+ const candidate = variables[name];
242
+ return candidate && typeof candidate.default !== 'undefined' ? String(candidate.default) : match;
243
+ });
244
+ return value;
245
+ }
246
+ function collectOpenApiServerUrls(spec) {
247
+ const values = [];
248
+ const addServers = (servers) => {
249
+ if (!Array.isArray(servers))
250
+ return;
251
+ for (const server of servers) {
252
+ const value = normalizeServerUrl(server);
253
+ if (value)
254
+ values.push(value);
255
+ }
256
+ };
257
+ addServers(spec?.servers);
258
+ if (spec?.paths && typeof spec.paths === 'object') {
259
+ for (const pathItem of Object.values(spec.paths)) {
260
+ addServers(pathItem?.servers);
261
+ for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'])
262
+ addServers(pathItem?.[method]?.servers);
263
+ }
264
+ }
265
+ return values;
266
+ }
267
+ function normalizedOrigin(value, base) {
268
+ try {
269
+ const url = base ? new URL(value, base) : new URL(value);
270
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.origin : undefined;
271
+ }
272
+ catch {
273
+ return undefined;
274
+ }
275
+ }
276
+ function allowedHostExecutionOrigins(state, spec, docsOrigin) {
277
+ const configured = state.options.allowedOrigins?.filter(Boolean);
278
+ const result = new Set();
279
+ if (configured?.length) {
280
+ for (const value of configured) {
281
+ const origin = normalizedOrigin(value);
282
+ if (origin)
283
+ result.add(origin);
284
+ }
285
+ return result;
286
+ }
287
+ const servers = collectOpenApiServerUrls(spec);
288
+ const values = servers.length ? servers : ['/'];
289
+ for (const value of values) {
290
+ const origin = normalizedOrigin(value, docsOrigin);
291
+ if (origin)
292
+ result.add(origin);
293
+ }
294
+ return result;
295
+ }
296
+ function isMetadataAddress(hostname) {
297
+ const value = hostname.replace(/^\[|\]$/g, '').toLowerCase();
298
+ if (value === '169.254.169.254' || value === 'metadata.google.internal' || value === 'metadata.google')
299
+ return true;
300
+ if (value.startsWith('::ffff:') && net.isIPv4(value.slice(7)))
301
+ return isMetadataAddress(value.slice(7));
302
+ if (net.isIPv4(value)) {
303
+ const [a, b] = value.split('.').map(Number);
304
+ return a === 169 && b === 254;
305
+ }
306
+ return value === 'fe80::a9fe:a9fe' || value.startsWith('fe80:');
307
+ }
308
+ function assertHostExecutionUrlAllowed(url, allowedOrigins) {
309
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
310
+ throw new HostExecutionForbiddenError(`Host execution only allows HTTP(S) URLs.`);
311
+ if (url.username || url.password)
312
+ throw new HostExecutionForbiddenError('Host execution URLs cannot contain embedded credentials.');
313
+ if (isMetadataAddress(url.hostname))
314
+ throw new HostExecutionForbiddenError('Host execution blocks link-local and cloud metadata endpoints.');
315
+ if (!allowedOrigins.has(url.origin))
316
+ throw new HostExecutionForbiddenError(`Origin ${url.origin} is not allowed for host execution.`);
317
+ }
318
+ function assertHostExecutionResolvedAddressAllowed(address) {
319
+ if (isMetadataAddress(address))
320
+ throw new HostExecutionForbiddenError('Host execution blocks DNS resolutions to link-local and cloud metadata endpoints.');
321
+ }
322
+ function enabledEntries(entries) {
323
+ return (entries || []).flatMap((entry) => entry.enabled === false || !String(entry.key || '').trim() ? [] : [{ key: String(entry.key || ''), value: String(entry.value || '') }]);
324
+ }
325
+ function setHeader(headers, name, value) {
326
+ for (let index = headers.length - 1; index >= 0; index -= 1)
327
+ if (headers[index][0].toLowerCase() === name.toLowerCase())
328
+ headers.splice(index, 1);
329
+ headers.push([name, value]);
330
+ }
331
+ function getHeader(headers, name) {
332
+ return headers.find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
333
+ }
334
+ function deleteHeader(headers, name) {
335
+ for (let index = headers.length - 1; index >= 0; index -= 1)
336
+ if (headers[index][0].toLowerCase() === name.toLowerCase())
337
+ headers.splice(index, 1);
338
+ }
339
+ function sanitizeHeaders(entries) {
340
+ return enabledEntries(entries).flatMap(({ key, value }) => {
341
+ const normalized = key.trim().toLowerCase();
342
+ if (HOP_BY_HOP.has(normalized) || normalized.startsWith('proxy-') || normalized.startsWith('sec-') || normalized === 'origin' || normalized === 'referer')
343
+ return [];
344
+ return [[key.trim(), value]];
345
+ });
346
+ }
347
+ function appendQuery(url, entries) {
348
+ for (const entry of entries)
349
+ url.searchParams.append(entry.key, entry.value);
350
+ }
351
+ function inferBodyMode(draft) {
352
+ if (draft.bodyMode)
353
+ return draft.bodyMode;
354
+ if (draft.binary?.fileName)
355
+ return 'binary';
356
+ if (draft.formData?.length)
357
+ return 'formdata';
358
+ if (draft.urlencoded?.length)
359
+ return 'urlencoded';
360
+ if (draft.graphql && (draft.graphql.query || draft.graphql.variables))
361
+ return 'graphql';
362
+ if (!draft.body)
363
+ return 'none';
364
+ return (draft.contentType || '').toLowerCase().includes('json') ? 'json' : 'raw';
365
+ }
366
+ function quoteMultipart(value) {
367
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/[\r\n]/g, '');
368
+ }
369
+ function multipartBody(draft, files) {
370
+ const boundary = `----flexdoc-${crypto.randomBytes(12).toString('hex')}`;
371
+ const chunks = [];
372
+ const push = (value) => chunks.push(Buffer.isBuffer(value) ? value : Buffer.from(value, 'utf8'));
373
+ (draft.formData || []).forEach((entry, index) => {
374
+ if (entry.enabled === false || !String(entry.key || '').trim())
375
+ return;
376
+ const key = quoteMultipart(String(entry.key));
377
+ push(`--${boundary}\r\n`);
378
+ if (entry.type === 'file') {
379
+ const file = files?.get(index);
380
+ if (!file)
381
+ throw new HostExecutionBadRequestError(`File field "${entry.key}" needs an uploaded file part.`);
382
+ push(`Content-Disposition: form-data; name="${key}"; filename="${quoteMultipart(file.name || entry.fileName || 'upload.bin')}"\r\n`);
383
+ push(`Content-Type: ${file.contentType || entry.contentType || 'application/octet-stream'}\r\n\r\n`);
384
+ push(file.data);
385
+ push('\r\n');
386
+ }
387
+ else {
388
+ push(`Content-Disposition: form-data; name="${key}"\r\n\r\n${String(entry.value || '')}\r\n`);
389
+ }
390
+ });
391
+ push(`--${boundary}--\r\n`);
392
+ return { body: Buffer.concat(chunks), contentType: `multipart/form-data; boundary=${boundary}` };
393
+ }
394
+ function prepareBody(draft, envelope) {
395
+ const mode = inferBodyMode(draft);
396
+ if (mode === 'none')
397
+ return {};
398
+ if (mode === 'binary') {
399
+ if (!envelope.bodyBase64)
400
+ throw new HostExecutionBadRequestError('Binary host execution requires bodyBase64.');
401
+ return { body: Buffer.from(envelope.bodyBase64, 'base64'), contentType: draft.contentType || draft.binary?.contentType || 'application/octet-stream' };
402
+ }
403
+ if (mode === 'formdata')
404
+ return multipartBody(draft, envelope.formDataFiles);
405
+ if (mode === 'urlencoded') {
406
+ const body = new URLSearchParams(enabledEntries(draft.urlencoded).map(({ key, value }) => [key, value])).toString();
407
+ return { body: Buffer.from(body), contentType: draft.contentType || 'application/x-www-form-urlencoded' };
408
+ }
409
+ if (mode === 'graphql') {
410
+ let variables = {};
411
+ const raw = String(draft.graphql?.variables || '').trim();
412
+ if (raw) {
413
+ try {
414
+ variables = JSON.parse(raw);
415
+ }
416
+ catch {
417
+ throw new HostExecutionBadRequestError('GraphQL variables must be valid JSON.');
418
+ }
419
+ }
420
+ return { body: Buffer.from(JSON.stringify({ query: draft.graphql?.query || '', variables })), contentType: draft.contentType || 'application/json' };
421
+ }
422
+ return { body: Buffer.from(String(draft.body || ''), 'utf8'), contentType: draft.contentType || (mode === 'json' ? 'application/json' : undefined) };
423
+ }
424
+ function percentEncode(value) {
425
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
426
+ }
427
+ function oauth1Header(auth, method, url, headers, body) {
428
+ const signatureMethod = auth.signatureMethod || 'HMAC-SHA1';
429
+ const oauth = {
430
+ oauth_consumer_key: auth.consumerKey || '',
431
+ oauth_nonce: base64Url(crypto.randomBytes(18)),
432
+ oauth_signature_method: signatureMethod,
433
+ oauth_timestamp: String(Math.floor(Date.now() / 1000)),
434
+ oauth_version: '1.0',
435
+ };
436
+ if (auth.token)
437
+ oauth.oauth_token = auth.token;
438
+ const params = [];
439
+ url.searchParams.forEach((value, key) => params.push([key, value]));
440
+ for (const [key, value] of Object.entries(oauth))
441
+ params.push([key, value]);
442
+ const contentType = (getHeader(headers, 'Content-Type') || '').split(';')[0].trim().toLowerCase();
443
+ if (contentType === 'application/x-www-form-urlencoded' && body)
444
+ new URLSearchParams(body.toString('utf8')).forEach((value, key) => params.push([key, value]));
445
+ params.sort((a, b) => percentEncode(a[0]).localeCompare(percentEncode(b[0])) || percentEncode(a[1]).localeCompare(percentEncode(b[1])));
446
+ const normalized = params.map(([key, value]) => `${percentEncode(key)}=${percentEncode(value)}`).join('&');
447
+ const baseUrl = `${url.protocol}//${url.host}${url.pathname}`;
448
+ const baseString = [method.toUpperCase(), percentEncode(baseUrl), percentEncode(normalized)].join('&');
449
+ const signingKey = `${percentEncode(auth.consumerSecret || '')}&${percentEncode(auth.tokenSecret || '')}`;
450
+ oauth.oauth_signature = signatureMethod === 'PLAINTEXT'
451
+ ? signingKey
452
+ : crypto.createHmac(signatureMethod === 'HMAC-SHA256' ? 'sha256' : 'sha1', signingKey).update(baseString).digest('base64');
453
+ const fields = Object.entries(oauth).map(([key, value]) => `${percentEncode(key)}="${percentEncode(value)}"`);
454
+ if (auth.realm)
455
+ fields.unshift(`realm="${percentEncode(auth.realm)}"`);
456
+ return `OAuth ${fields.join(', ')}`;
457
+ }
458
+ function hawkHeader(auth, method, url) {
459
+ const algorithm = auth.algorithm || 'sha256';
460
+ const ts = String(Math.floor(Date.now() / 1000));
461
+ const nonce = base64Url(crypto.randomBytes(8));
462
+ const port = url.port || (url.protocol === 'https:' ? '443' : '80');
463
+ const ext = auth.ext || '';
464
+ const normalized = `hawk.1.header\n${ts}\n${nonce}\n${method.toUpperCase()}\n${url.pathname}${url.search}\n${url.hostname.toLowerCase()}\n${port}\n\n${ext}\n\n\n`;
465
+ const mac = crypto.createHmac(algorithm, auth.key || '').update(normalized).digest('base64');
466
+ return `Hawk id="${String(auth.id || '').replace(/"/g, '\\"')}", ts="${ts}", nonce="${nonce}", mac="${mac}"${ext ? `, ext="${ext.replace(/"/g, '\\"')}"` : ''}`;
467
+ }
468
+ function sha256(value) {
469
+ return crypto.createHash('sha256').update(value).digest('hex');
470
+ }
471
+ function awsV4Headers(auth, method, url, headers, body) {
472
+ if (!auth.accessKey || !auth.secretKey || !auth.region || !auth.service)
473
+ throw new HostExecutionBadRequestError('AWS Signature V4 requires access key, secret key, region, and service.');
474
+ const now = new Date();
475
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
476
+ const date = amzDate.slice(0, 8);
477
+ const payloadHash = sha256(body || Buffer.alloc(0));
478
+ setHeader(headers, 'Host', url.host);
479
+ setHeader(headers, 'X-Amz-Date', amzDate);
480
+ setHeader(headers, 'X-Amz-Content-Sha256', payloadHash);
481
+ if (auth.sessionToken)
482
+ setHeader(headers, 'X-Amz-Security-Token', auth.sessionToken);
483
+ const canonicalHeaders = [...headers]
484
+ .map(([key, value]) => [key.trim().toLowerCase(), value.trim().replace(/\s+/g, ' ')])
485
+ .sort((a, b) => a[0].localeCompare(b[0]));
486
+ const deduped = new Map();
487
+ for (const [key, value] of canonicalHeaders)
488
+ deduped.set(key, deduped.has(key) ? `${deduped.get(key)},${value}` : value);
489
+ const signedHeaders = [...deduped.keys()].join(';');
490
+ const canonicalHeaderText = [...deduped.entries()].map(([key, value]) => `${key}:${value}\n`).join('');
491
+ const canonicalQuery = [...url.searchParams.entries()]
492
+ .map(([key, value]) => [percentEncode(key), percentEncode(value)])
493
+ .sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]))
494
+ .map(([key, value]) => `${key}=${value}`).join('&');
495
+ const canonicalUri = url.pathname.split('/').map((part) => percentEncode(decodeURIComponent(part))).join('/') || '/';
496
+ const canonicalRequest = `${method.toUpperCase()}\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaderText}\n${signedHeaders}\n${payloadHash}`;
497
+ const scope = `${date}/${auth.region}/${auth.service}/aws4_request`;
498
+ const stringToSign = `AWS4-HMAC-SHA256\n${amzDate}\n${scope}\n${sha256(canonicalRequest)}`;
499
+ const hmac = (key, data) => crypto.createHmac('sha256', key).update(data).digest();
500
+ const dateKey = hmac(`AWS4${auth.secretKey}`, date);
501
+ const regionKey = hmac(dateKey, auth.region);
502
+ const serviceKey = hmac(regionKey, auth.service);
503
+ const signingKey = hmac(serviceKey, 'aws4_request');
504
+ const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
505
+ setHeader(headers, 'Authorization', `AWS4-HMAC-SHA256 Credential=${auth.accessKey}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`);
506
+ deleteHeader(headers, 'Host');
507
+ }
508
+ function parseDigestChallenge(value) {
509
+ if (!/^\s*Digest\s+/i.test(value))
510
+ throw new HostExecutionUnsupportedError('Server did not return a Digest challenge.');
511
+ const result = {};
512
+ const raw = value.replace(/^\s*Digest\s+/i, '');
513
+ const pattern = /(\w+)=(?:"([^"]*)"|([^,\s]+))/g;
514
+ let match;
515
+ while ((match = pattern.exec(raw)))
516
+ result[match[1].toLowerCase()] = match[2] ?? match[3] ?? '';
517
+ return result;
518
+ }
519
+ function digestHeader(auth, challengeValue, method, url) {
520
+ const challenge = parseDigestChallenge(challengeValue);
521
+ const realm = challenge.realm || '';
522
+ const nonce = challenge.nonce;
523
+ if (!nonce)
524
+ throw new HostExecutionUnsupportedError('Digest challenge is missing a nonce.');
525
+ const algorithmRaw = (challenge.algorithm || 'MD5').toUpperCase();
526
+ const session = algorithmRaw.endsWith('-SESS');
527
+ const algorithm = algorithmRaw.replace(/-SESS$/, '');
528
+ const hashName = algorithm === 'SHA-256' ? 'sha256' : algorithm === 'MD5' ? 'md5' : undefined;
529
+ if (!hashName)
530
+ throw new HostExecutionUnsupportedError(`Digest algorithm ${algorithmRaw} is not supported.`);
531
+ const hash = (value) => crypto.createHash(hashName).update(value).digest('hex');
532
+ const username = auth.username || '';
533
+ const password = auth.password || '';
534
+ const uri = `${url.pathname}${url.search}` || '/';
535
+ const cnonce = crypto.randomBytes(12).toString('hex');
536
+ const nc = '00000001';
537
+ const qopValues = (challenge.qop || '').split(',').map((value) => value.trim().toLowerCase()).filter(Boolean);
538
+ const qop = qopValues.includes('auth') ? 'auth' : qopValues[0];
539
+ if (qop && qop !== 'auth')
540
+ throw new HostExecutionUnsupportedError(`Digest qop ${qop} is not supported.`);
541
+ let ha1 = hash(`${username}:${realm}:${password}`);
542
+ if (session)
543
+ ha1 = hash(`${ha1}:${nonce}:${cnonce}`);
544
+ const ha2 = hash(`${method.toUpperCase()}:${uri}`);
545
+ const response = qop ? hash(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`) : hash(`${ha1}:${nonce}:${ha2}`);
546
+ const parts = [
547
+ `username="${username.replace(/"/g, '\\"')}"`,
548
+ `realm="${realm.replace(/"/g, '\\"')}"`,
549
+ `nonce="${nonce.replace(/"/g, '\\"')}"`,
550
+ `uri="${uri.replace(/"/g, '\\"')}"`,
551
+ `response="${response}"`,
552
+ ];
553
+ if (challenge.algorithm)
554
+ parts.push(`algorithm=${challenge.algorithm}`);
555
+ if (challenge.opaque)
556
+ parts.push(`opaque="${challenge.opaque.replace(/"/g, '\\"')}"`);
557
+ if (qop)
558
+ parts.push(`qop=${qop}`, `nc=${nc}`, `cnonce="${cnonce}"`);
559
+ return `Digest ${parts.join(', ')}`;
560
+ }
561
+ function headersForNode(entries) {
562
+ const result = {};
563
+ for (const [key, value] of entries) {
564
+ const existing = result[key];
565
+ if (existing === undefined)
566
+ result[key] = value;
567
+ else if (Array.isArray(existing))
568
+ existing.push(value);
569
+ else
570
+ result[key] = [String(existing), value];
571
+ }
572
+ return result;
573
+ }
574
+ const safeLookup = ((hostname, options, callback) => {
575
+ dns.lookup(hostname, options, (error, address, family) => {
576
+ if (error)
577
+ return callback(error, address, family);
578
+ try {
579
+ const addresses = Array.isArray(address) ? address.map((entry) => entry.address) : [address];
580
+ for (const resolved of addresses)
581
+ assertHostExecutionResolvedAddressAllowed(String(resolved));
582
+ }
583
+ catch (lookupError) {
584
+ return callback(lookupError);
585
+ }
586
+ return callback(null, address, family);
587
+ });
588
+ });
589
+ function requestOnce(url, method, headers, body, timeoutMs, agent) {
590
+ return new Promise((resolve, reject) => {
591
+ const client = url.protocol === 'https:' ? https : http;
592
+ const started = Date.now();
593
+ const request = client.request(url, {
594
+ method,
595
+ headers: headersForNode(headers),
596
+ lookup: safeLookup,
597
+ ...(url.protocol === 'https:' && agent ? { agent } : {}),
598
+ }, (response) => {
599
+ const chunks = [];
600
+ let size = 0;
601
+ response.on('data', (chunk) => {
602
+ const buffer = Buffer.from(chunk);
603
+ size += buffer.length;
604
+ if (size > MAX_RESPONSE_BYTES) {
605
+ request.destroy(new Error('Host response exceeded the 10 MiB safety limit.'));
606
+ return;
607
+ }
608
+ chunks.push(buffer);
609
+ });
610
+ response.on('end', () => {
611
+ const rawHeaders = [];
612
+ for (let index = 0; index < response.rawHeaders.length; index += 2)
613
+ rawHeaders.push([response.rawHeaders[index], response.rawHeaders[index + 1] || '']);
614
+ resolve({
615
+ status: response.statusCode || 0,
616
+ statusText: response.statusMessage || '',
617
+ headers: rawHeaders,
618
+ body: Buffer.concat(chunks),
619
+ setCookies: response.headers['set-cookie'] || [],
620
+ location: response.headers.location,
621
+ });
622
+ });
623
+ response.on('error', reject);
624
+ });
625
+ request.on('error', reject);
626
+ request.setTimeout(timeoutMs, () => request.destroy(new Error(`Host request timed out after ${timeoutMs} ms.`)));
627
+ if (body && !['GET', 'HEAD'].includes(method))
628
+ request.write(body);
629
+ request.end();
630
+ void started;
631
+ });
632
+ }
633
+ function responseHeader(headers, name) {
634
+ return headers.find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
635
+ }
636
+ function applyCookieHeader(headers, jarCookies, explicitCookieHeader, apiKeyCookie) {
637
+ const values = new Map();
638
+ for (const cookie of jarCookies)
639
+ values.set(cookie.name, cookie.value);
640
+ for (const [key, value] of parseCookieHeader(explicitCookieHeader))
641
+ values.set(key, value);
642
+ if (apiKeyCookie?.key)
643
+ values.set(apiKeyCookie.key, apiKeyCookie.value);
644
+ deleteHeader(headers, 'Cookie');
645
+ if (values.size)
646
+ setHeader(headers, 'Cookie', [...values.entries()].map(([key, value]) => `${key}=${value}`).join('; '));
647
+ }
648
+ function applySimpleAuth(auth, url, headers) {
649
+ if (!auth || auth.type === 'none' || auth.type === 'inherit')
650
+ return {};
651
+ if (auth.type === 'bearer' && auth.token)
652
+ setHeader(headers, 'Authorization', `Bearer ${auth.token}`);
653
+ else if (auth.type === 'oauth2' && auth.accessToken)
654
+ setHeader(headers, 'Authorization', `Bearer ${auth.accessToken}`);
655
+ else if (auth.type === 'basic')
656
+ setHeader(headers, 'Authorization', `Basic ${Buffer.from(`${auth.username || ''}:${auth.password || ''}`, 'utf8').toString('base64')}`);
657
+ else if (auth.type === 'apiKey' && auth.key) {
658
+ if (auth.in === 'query')
659
+ url.searchParams.append(auth.key, auth.value || '');
660
+ else if (auth.in === 'header')
661
+ setHeader(headers, auth.key, auth.value || '');
662
+ else
663
+ return { apiKeyCookie: { key: auth.key, value: auth.value || '' } };
664
+ }
665
+ return {};
666
+ }
667
+ function applyAdvancedAuth(auth, method, url, headers, body) {
668
+ if (!auth)
669
+ return;
670
+ if (auth.type === 'hawk')
671
+ setHeader(headers, 'Authorization', hawkHeader(auth, method, url));
672
+ else if (auth.type === 'oauth1')
673
+ setHeader(headers, 'Authorization', oauth1Header(auth, method, url, headers, body));
674
+ else if (auth.type === 'awsv4')
675
+ awsV4Headers(auth, method, url, headers, body);
676
+ else if (auth.type === 'ntlm')
677
+ throw new HostExecutionUnsupportedError('NTLM/Negotiate host execution is not implemented by the Node adapter.');
678
+ }
679
+ async function sendPrepared(state, envelope, spec, sessionId, docsOrigin) {
680
+ const draft = envelope.request || {};
681
+ const method = String(draft.method || 'GET').trim().toUpperCase();
682
+ if (!METHODS.has(method))
683
+ throw new HostExecutionBadRequestError(`Unsupported HTTP method ${method}.`);
684
+ if (!draft.url)
685
+ throw new HostExecutionBadRequestError('Request URL is required.');
686
+ const initialUrl = new URL(String(draft.url));
687
+ appendQuery(initialUrl, enabledEntries(draft.query));
688
+ const allowedOrigins = allowedHostExecutionOrigins(state, spec, docsOrigin);
689
+ const preparedBody = prepareBody(draft, envelope);
690
+ const timeoutMs = Math.min(Math.max(Number(envelope.timeoutMs || 30000), 100), 120000);
691
+ const certificate = envelope.certificateId ? state.certificates.get(envelope.certificateId) : undefined;
692
+ if (envelope.certificateId && !certificate)
693
+ throw new HostExecutionBadRequestError('Unknown client certificate id.');
694
+ const agent = certificate ? new https.Agent({ cert: certificate.cert, key: certificate.key, passphrase: certificate.passphrase }) : undefined;
695
+ const executeAt = async (target, nextMethod, redirectCount) => {
696
+ assertHostExecutionUrlAllowed(target, allowedOrigins);
697
+ const headers = sanitizeHeaders(draft.headers);
698
+ if (preparedBody.contentType && inferBodyMode(draft) === 'formdata')
699
+ deleteHeader(headers, 'Content-Type');
700
+ if (preparedBody.contentType && !getHeader(headers, 'Content-Type'))
701
+ setHeader(headers, 'Content-Type', preparedBody.contentType);
702
+ const { apiKeyCookie } = applySimpleAuth(draft.auth, target, headers);
703
+ const explicitCookieHeader = getHeader(headers, 'Cookie');
704
+ applyCookieHeader(headers, envelope.cookieJar === 'session' ? cookiesForUrl(state, sessionId, target) : [], explicitCookieHeader, apiKeyCookie);
705
+ let intercepted = { method: nextMethod, url: target.toString(), headers: [...headers], body: preparedBody.body };
706
+ if (state.options.interceptor)
707
+ intercepted = await state.options.interceptor(intercepted);
708
+ const interceptedUrl = new URL(intercepted.url);
709
+ assertHostExecutionUrlAllowed(interceptedUrl, allowedOrigins);
710
+ const finalMethod = String(intercepted.method || nextMethod).toUpperCase();
711
+ const finalHeaders = intercepted.headers.map(([key, value]) => [String(key), String(value)]);
712
+ const finalBody = intercepted.body;
713
+ applyAdvancedAuth(draft.auth, finalMethod, interceptedUrl, finalHeaders, finalBody);
714
+ const started = Date.now();
715
+ let raw;
716
+ if (draft.auth?.type === 'digest') {
717
+ deleteHeader(finalHeaders, 'Authorization');
718
+ const challenge = await requestOnce(interceptedUrl, finalMethod, finalHeaders, finalBody, timeoutMs, agent);
719
+ if (envelope.cookieJar === 'session' && challenge.setCookies.length)
720
+ storeSetCookies(state, sessionId, interceptedUrl, challenge.setCookies);
721
+ if (challenge.status !== 401)
722
+ raw = challenge;
723
+ else {
724
+ const digest = responseHeader(challenge.headers, 'WWW-Authenticate');
725
+ if (!digest)
726
+ throw new HostExecutionUnsupportedError('Server did not return a Digest challenge.');
727
+ setHeader(finalHeaders, 'Authorization', digestHeader(draft.auth, digest, finalMethod, interceptedUrl));
728
+ applyCookieHeader(finalHeaders, envelope.cookieJar === 'session' ? cookiesForUrl(state, sessionId, interceptedUrl) : [], explicitCookieHeader, apiKeyCookie);
729
+ raw = await requestOnce(interceptedUrl, finalMethod, finalHeaders, finalBody, timeoutMs, agent);
730
+ }
731
+ }
732
+ else {
733
+ raw = await requestOnce(interceptedUrl, finalMethod, finalHeaders, finalBody, timeoutMs, agent);
734
+ }
735
+ if (envelope.cookieJar === 'session' && raw.setCookies.length)
736
+ storeSetCookies(state, sessionId, interceptedUrl, raw.setCookies);
737
+ if (raw.status >= 300 && raw.status < 400 && raw.location) {
738
+ if (redirectCount >= MAX_REDIRECTS)
739
+ throw new HostExecutionForbiddenError('Host execution stopped after too many redirects.');
740
+ const redirectUrl = new URL(raw.location, interceptedUrl);
741
+ if (redirectUrl.origin !== interceptedUrl.origin)
742
+ throw new HostExecutionForbiddenError('Host execution does not follow cross-origin redirects.');
743
+ assertHostExecutionUrlAllowed(redirectUrl, allowedOrigins);
744
+ const redirectMethod = raw.status === 303 ? 'GET' : finalMethod;
745
+ return executeAt(redirectUrl, redirectMethod, redirectCount + 1);
746
+ }
747
+ return {
748
+ status: raw.status,
749
+ statusText: raw.statusText,
750
+ headers: raw.headers,
751
+ body: raw.body.toString('utf8'),
752
+ responseTime: Date.now() - started,
753
+ ...(envelope.cookieJar === 'session' ? { cookies: publicCookiesForSession(state, sessionId) } : {}),
754
+ };
755
+ };
756
+ try {
757
+ return await executeAt(initialUrl, method, 0);
758
+ }
759
+ finally {
760
+ agent?.destroy();
761
+ }
762
+ }
763
+ async function executeHostRequest(state, envelope, context) {
764
+ if (!state.enabled)
765
+ throw new HostExecutionUnsupportedError('Host execution is disabled on this documentation server.');
766
+ return sendPrepared(state, envelope, context.spec, context.sessionId, context.docsOrigin);
767
+ }
768
+ //# sourceMappingURL=host-execution.js.map