@scalar/json-magic 0.13.3 → 0.13.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @scalar/json-magic
2
2
 
3
+ ## 0.13.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#9817](https://github.com/scalar/scalar/pull/9817): Resolve relative `externalValue` URLs on example objects against the document origin, so external request and response examples load even when referenced with a relative path
8
+ - [#9666](https://github.com/scalar/scalar/pull/9666): Only normalize OpenAPI Reference Objects during bundling, never Schema Objects. `normalizeRefs` used to strip every sibling except `$ref` on any node outside `components/schemas`, which also hit inline schemas. In JSON Schema 2020-12 a `$ref` may legally carry sibling keywords — for example a `$defs`/`$dynamicAnchor` binding that specializes a generic template like `Paginated<T>` — and such schemas appear inline anywhere a schema is allowed (a response's `content.<media>.schema`, an `allOf` branch, …). Dropping those siblings discarded the binding, leaving `$dynamicRef` to resolve to the template's empty fallback and rendering an empty array (for example the `data` array of `GET /planets` in the Scalar Galaxy). Reference Objects are still normalized as before. A new `@scalar/helpers/openapi/is-schema-path` helper detects schema positions.
9
+ - [#10112](https://github.com/scalar/scalar/pull/10112): Pin guarded requests to validated public IP addresses and reject custom transports that bypass the connection checks.
10
+ - [#10140](https://github.com/scalar/scalar/pull/10140): Replace redundant type assertions with compiler-checked annotations, typed accumulators, and existing guards across helpers, API conversion, request handling, and schema rendering.
11
+
12
+ Narrow DOM elements and caught errors before accessing their properties. Correct header lookup to include missing values and handle them during PowerShell snippet generation.
13
+
14
+ Validate release-note provider responses, represent unresolved references and absent groups in helper return types, and require narrowing merged object values. Preserve AsyncAPI broker credentials separately from HTTP authentication schemes.
15
+
16
+ ## 0.13.4
17
+
18
+ ### Patch Changes
19
+
20
+ - [#10079](https://github.com/scalar/scalar/pull/10079): Harden the mock server against SSRF and local file disclosure through OpenAPI `$ref`s. External `$ref` resolution now refuses to fetch private, loopback, link-local, and metadata addresses, and confines local file reads to the document's directory. The `fetchUrls` and `readFiles` bundling plugins gain opt-in `blockPrivateNetworks` and `basePath` options, so other callers keep their current behavior unless they opt in.
21
+
3
22
  ## 0.13.3
4
23
 
5
24
  ## 0.13.2
@@ -124,13 +124,17 @@ export type LoaderPlugin = {
124
124
  *
125
125
  * - `path`: The JSON pointer path (as an array of strings) from the document root to the current node.
126
126
  * - `resolutionCache`: A cache for storing promises of resolved references.
127
+ * - `origin`: The origin (URL or file path) of the document this node lives in. Lifecycle plugins can
128
+ * use it to resolve relative references (for example an example's `externalValue`) into absolute URLs.
127
129
  */
128
130
  type NodeProcessContext = {
129
131
  path: readonly string[];
132
+ referencedFromPath?: readonly string[];
130
133
  resolutionCache: Map<string, Promise<Readonly<ResolveResult>>>;
131
134
  parentNode: UnknownObject | null;
132
135
  rootNode: UnknownObject;
133
136
  loaders: LoaderPlugin[];
137
+ origin: string;
134
138
  };
135
139
  /**
136
140
  * A plugin type for lifecycle hooks, allowing custom logic to be injected into the bundler's process.
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/bundle/bundle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/bundle/bundle.ts"],"names":[],"mappings":"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"}
@@ -349,7 +349,7 @@ export async function bundle(input, config) {
349
349
  }
350
350
  }
351
351
  };
352
- const bundler = async (root, origin = defaultOrigin, isChunkParent = false, depth = 0, currentPath = [], parent = null) => {
352
+ const bundler = async (root, origin = defaultOrigin, isChunkParent = false, depth = 0, currentPath = [], parent = null, referencedFromPath) => {
353
353
  // If a maximum depth is set in the config, stop bundling when the current depth reaches or exceeds it
354
354
  if (config.depth !== undefined && depth > config.depth) {
355
355
  return;
@@ -364,15 +364,19 @@ export async function bundle(input, config) {
364
364
  }
365
365
  // Mark this node as processed before continuing
366
366
  processedNodes.add(root);
367
+ // A node with its own `$id` establishes a new base for resolving relative references within it.
368
+ // Fall back to the origin inherited from the parent document otherwise.
369
+ const id = getId(root);
367
370
  const context = {
368
371
  path: currentPath,
372
+ referencedFromPath,
369
373
  resolutionCache: cache,
370
374
  parentNode: parent,
371
375
  rootNode: documentRoot,
372
376
  loaders: loaderPlugins,
377
+ origin: id ?? origin,
373
378
  };
374
379
  await executeHooks('onBeforeNodeProcess', root, context);
375
- const id = getId(root);
376
380
  if (hasRef(root)) {
377
381
  const ref = root['$ref'];
378
382
  const isChunk = '$global' in root && typeof root['$global'] === 'boolean' && root['$global'];
@@ -393,7 +397,7 @@ export async function bundle(input, config) {
393
397
  // referenced by this local reference to ensure the partial bundle is complete.
394
398
  // This includes not just the direct reference but also all its dependencies,
395
399
  // creating a complete and self-contained partial bundle.
396
- await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent);
400
+ await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent, referencedFromPath);
397
401
  }
398
402
  await executeHooks('onAfterNodeProcess', root, context);
399
403
  return;
@@ -436,11 +440,9 @@ export async function bundle(input, config) {
436
440
  // to handle any nested references it may contain. We pass the resolvedPath as the new origin
437
441
  // to ensure any relative references within this content are resolved correctly relative to
438
442
  // their new location in the bundled document.
439
- await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1, [
440
- config.externalDocumentsKey,
441
- compressedPath,
442
- documentRoot[config.externalDocumentsMappingsKey],
443
- ]);
443
+ await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1, [config.externalDocumentsKey, compressedPath], null,
444
+ // Keep the referring document path across external hops, replacing the storage wrapper.
445
+ referencedFromPath ? [...referencedFromPath, ...currentPath.slice(2)] : currentPath);
444
446
  // Store the mapping between hashed keys and original URLs in x-ext-urls
445
447
  // This allows tracking which external URLs were bundled and their corresponding locations
446
448
  setValueAtPath(documentRoot, `/${config.externalDocumentsMappingsKey}/${escapeJsonPointer(compressedPath)}`, relativePath);
@@ -478,7 +480,7 @@ export async function bundle(input, config) {
478
480
  if (key === config.externalDocumentsKey || key === config.externalDocumentsMappingsKey) {
479
481
  continue;
480
482
  }
481
- await bundler(root[key], id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root);
483
+ await bundler(root[key], id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root, referencedFromPath);
482
484
  }
483
485
  await executeHooks('onAfterNodeProcess', root, context);
484
486
  };
@@ -1,3 +1,4 @@
1
+ export { resolveReferencePath } from '../helpers/resolve-reference-path.js';
1
2
  export type { LifecyclePlugin, LoaderPlugin, Plugin, ResolveResult } from './bundle.js';
2
3
  export { bundle, extensions, isLocalRef, prefixInternalRef, prefixInternalRefRecursive, resolveAndCopyReferences, } from './bundle.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bundle/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EACL,MAAM,EACN,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bundle/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAEvE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EACL,MAAM,EACN,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,UAAU,CAAA"}
@@ -1 +1,2 @@
1
+ export { resolveReferencePath } from '../helpers/resolve-reference-path.js';
1
2
  export { bundle, extensions, isLocalRef, prefixInternalRef, prefixInternalRefRecursive, resolveAndCopyReferences, } from './bundle.js';
@@ -1,4 +1,4 @@
1
- export { fetchUrls } from './fetch-urls/index.js';
1
+ export { fetchUrls } from './fetch-urls/browser.js';
2
2
  export { parseJson } from './parse-json/index.js';
3
3
  export { parseYaml } from './parse-yaml/index.js';
4
4
  //# sourceMappingURL=browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../src/bundle/plugins/browser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../src/bundle/plugins/browser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA"}
@@ -1,4 +1,4 @@
1
1
  // biome-ignore lint/performance/noBarrelFile: entrypoint
2
- export { fetchUrls } from './fetch-urls/index.js';
2
+ export { fetchUrls } from './fetch-urls/browser.js';
3
3
  export { parseJson } from './parse-json/index.js';
4
4
  export { parseYaml } from './parse-yaml/index.js';
@@ -0,0 +1,16 @@
1
+ import type { LoaderPlugin } from '../../../bundle/index.js';
2
+ type FetchConfig = Partial<{
3
+ headers: {
4
+ headers: HeadersInit;
5
+ domains: string[];
6
+ }[];
7
+ fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>;
8
+ }>;
9
+ /**
10
+ * Creates a browser-safe plugin for handling remote URL references.
11
+ */
12
+ export declare const fetchUrls: (config?: FetchConfig & Partial<{
13
+ limit: number | null;
14
+ }>) => LoaderPlugin;
15
+ export {};
16
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/browser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,UAAU,CAAA;AAI3D,KAAK,WAAW,GAAG,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAA;IACtD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3F,CAAC,CAAA;AA+CF;;GAEG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,WAAW,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,KAAG,YAQpF,CAAA"}
@@ -0,0 +1,51 @@
1
+ import { createLimiter } from '@scalar/helpers/general/create-limiter';
2
+ import { isHttpUrl } from '../../../helpers/is-http-url.js';
3
+ import { normalize } from '../../../helpers/normalize.js';
4
+ /**
5
+ * Safely checks for host from a URL.
6
+ * Needed because we cannot create a URL from a relative remote URL such as examples/openapi.json.
7
+ */
8
+ const getHost = (url) => {
9
+ try {
10
+ return new URL(url).host;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ };
16
+ /**
17
+ * Fetches and normalizes data from a remote URL in a browser environment.
18
+ */
19
+ const fetchUrl = async (url, limiter, config) => {
20
+ try {
21
+ const host = getHost(url);
22
+ const headers = config?.headers?.find((a) => a.domains.find((d) => d === host) !== undefined)?.headers;
23
+ const exec = config?.fetch ?? fetch;
24
+ const result = await limiter(() => exec(url, { headers }));
25
+ if (result.ok) {
26
+ const body = await result.text();
27
+ return { ok: true, data: normalize(body), raw: body };
28
+ }
29
+ const contentType = result.headers.get('Content-Type') ?? '';
30
+ if (['text/html', 'application/xml'].includes(contentType)) {
31
+ console.warn(`[WARN] We only support JSON/YAML formats, received ${contentType}`);
32
+ }
33
+ console.warn(`[WARN] Fetch failed with status ${result.status} ${result.statusText} for URL: ${url}`);
34
+ return { ok: false };
35
+ }
36
+ catch {
37
+ console.warn(`[WARN] Failed to parse JSON/YAML from URL: ${url}`);
38
+ return { ok: false };
39
+ }
40
+ };
41
+ /**
42
+ * Creates a browser-safe plugin for handling remote URL references.
43
+ */
44
+ export const fetchUrls = (config) => {
45
+ const limiter = config?.limit ? createLimiter(config.limit) : (fn) => fn();
46
+ return {
47
+ type: 'loader',
48
+ validate: isHttpUrl,
49
+ exec: (value) => fetchUrl(value, limiter, config),
50
+ };
51
+ };
@@ -0,0 +1,3 @@
1
+ /** Fetches through a connection pinned to validated addresses while preserving the host and TLS name. */
2
+ export declare const fetchPublicUrl: (url: string, headers?: HeadersInit) => Promise<Response>;
3
+ //# sourceMappingURL=fetch-public-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-public-url.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/fetch-public-url.ts"],"names":[],"mappings":"AAIA,yGAAyG;AACzG,eAAO,MAAM,cAAc,GAAU,KAAK,MAAM,EAAE,UAAU,WAAW,KAAG,OAAO,CAAC,QAAQ,CAwCzF,CAAA"}
@@ -0,0 +1,42 @@
1
+ import { Agent, fetch } from 'undici';
2
+ import { resolvePublicHost } from './is-blocked-host.js';
3
+ /** Fetches through a connection pinned to validated addresses while preserving the host and TLS name. */
4
+ export const fetchPublicUrl = async (url, headers) => {
5
+ const target = new URL(url);
6
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') {
7
+ throw new Error('Only HTTP and HTTPS URLs are supported');
8
+ }
9
+ const addresses = await resolvePublicHost(target.hostname);
10
+ if (!addresses) {
11
+ throw new Error('Refused to fetch a private or internal address');
12
+ }
13
+ // Never resolve the hostname again when connecting. A later DNS answer could point at a private IP.
14
+ const dispatcher = new Agent({
15
+ connect: {
16
+ lookup: (_hostname, options, callback) => {
17
+ if (options.all) {
18
+ callback(null, addresses);
19
+ }
20
+ else {
21
+ callback(null, addresses[0].address, addresses[0].family);
22
+ }
23
+ },
24
+ },
25
+ });
26
+ try {
27
+ const response = await fetch(target, {
28
+ headers: Object.fromEntries(new Headers(headers)),
29
+ redirect: 'error',
30
+ dispatcher,
31
+ });
32
+ const body = !response.ok || [204, 205].includes(response.status) ? null : await response.arrayBuffer();
33
+ return new Response(body, {
34
+ status: response.status,
35
+ statusText: response.statusText,
36
+ headers: Object.fromEntries(response.headers),
37
+ });
38
+ }
39
+ finally {
40
+ await dispatcher.destroy();
41
+ }
42
+ };
@@ -4,7 +4,14 @@ type FetchConfig = Partial<{
4
4
  headers: HeadersInit;
5
5
  domains: string[];
6
6
  }[];
7
+ /** Custom transports cannot be combined with blockPrivateNetworks. */
7
8
  fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>;
9
+ /**
10
+ * When true, refuse to fetch URLs that resolve to a private, loopback, link-local, or otherwise
11
+ * internal address. Only enforced in Node, where DNS resolution is available. Off by default so
12
+ * existing callers keep working unchanged.
13
+ */
14
+ blockPrivateNetworks: boolean;
8
15
  }>;
9
16
  /**
10
17
  * Fetches and normalizes data from a remote URL
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAI3D,KAAK,WAAW,GAAG,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAA;IACtD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3F,CAAC,CAAA;AAcF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAChD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GAAG,YAAY,CAShG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAI3D,KAAK,WAAW,GAAG,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAA;IACtD,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1F;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAA;CAC9B,CAAC,CAAA;AAcF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAChD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CA8CxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GAAG,YAAY,CAShG"}
@@ -30,12 +30,19 @@ const getHost = (url) => {
30
30
  export async function fetchUrl(url, limiter, config) {
31
31
  try {
32
32
  const host = getHost(url);
33
- // Get the headers that match the domain
34
33
  const headers = config?.headers?.find((a) => a.domains.find((d) => d === host) !== undefined)?.headers;
35
- const exec = config?.fetch ?? fetch;
36
- const result = await limiter(() => exec(url, {
37
- headers,
38
- }));
34
+ const guarded = config?.blockPrivateNetworks && typeof window === 'undefined';
35
+ const result = await limiter(async () => {
36
+ if (guarded) {
37
+ // A custom fetch can ignore the pinned connection and resolve the host again.
38
+ if (config?.fetch) {
39
+ throw new Error('Custom fetch cannot be combined with private network blocking');
40
+ }
41
+ const { fetchPublicUrl } = await import('./fetch-public-url.js');
42
+ return fetchPublicUrl(url, headers);
43
+ }
44
+ return (config?.fetch ?? fetch)(url, { headers });
45
+ });
39
46
  if (result.ok) {
40
47
  const body = await result.text();
41
48
  return {
@@ -0,0 +1,18 @@
1
+ import type { LookupAddress } from 'node:dns';
2
+ /**
3
+ * Reports whether a hostname resolves to a private, loopback, link-local, or otherwise internal
4
+ * address that a bundled `$ref` should not be allowed to reach.
5
+ *
6
+ * Literal IPs are checked directly. Hostnames are resolved via DNS and every returned address is
7
+ * checked, so a name that points at an internal IP is rejected. A host that cannot be resolved is
8
+ * treated as blocked.
9
+ *
10
+ * This lives in a Node-only module because it depends on `node:dns` and `node:net`. Load it with a
11
+ * dynamic import so the browser build of the fetch plugin stays free of Node built-ins.
12
+ *
13
+ * @param hostname - The hostname or IP (brackets around an IPv6 literal are tolerated).
14
+ */
15
+ export declare const isBlockedHost: (hostname: string) => Promise<boolean>;
16
+ /** Resolves a host once and returns only validated public addresses, or null when blocked. */
17
+ export declare const resolvePublicHost: (hostname: string) => Promise<LookupAddress[] | null>;
18
+ //# sourceMappingURL=is-blocked-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-blocked-host.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/is-blocked-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAqE7C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAAiD,CAAA;AAEvH,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,aAAa,EAAE,GAAG,IAAI,CAWxF,CAAA"}
@@ -0,0 +1,81 @@
1
+ import { lookup } from 'node:dns/promises';
2
+ import { BlockList, isIP, isIPv4 } from 'node:net';
3
+ /**
4
+ * Address ranges that must never be reachable through a bundled `$ref`.
5
+ *
6
+ * Covers loopback, private, link-local (including the cloud metadata endpoint 169.254.169.254),
7
+ * carrier-grade NAT, and their IPv6 equivalents. The IPv6 transition ranges (6to4, NAT64, Teredo,
8
+ * IPv4-compatible) are blocked wholesale so an address like 64:ff9b::a9fe:a9fe cannot smuggle a
9
+ * private IPv4 destination past the checks. Those ranges are deprecated or rarely used for real API
10
+ * hosting, so blocking them outright is a safe trade.
11
+ */
12
+ const blockList = new BlockList();
13
+ blockList.addAddress('0.0.0.0', 'ipv4');
14
+ blockList.addSubnet('127.0.0.0', 8, 'ipv4');
15
+ blockList.addSubnet('10.0.0.0', 8, 'ipv4');
16
+ blockList.addSubnet('172.16.0.0', 12, 'ipv4');
17
+ blockList.addSubnet('192.168.0.0', 16, 'ipv4');
18
+ blockList.addSubnet('169.254.0.0', 16, 'ipv4');
19
+ blockList.addSubnet('100.64.0.0', 10, 'ipv4');
20
+ blockList.addSubnet('::', 96, 'ipv6'); // unspecified, loopback (::1), and deprecated IPv4-compatible
21
+ blockList.addSubnet('fe80::', 10, 'ipv6'); // link-local
22
+ blockList.addSubnet('fc00::', 7, 'ipv6'); // unique local
23
+ blockList.addSubnet('2002::', 16, 'ipv6'); // 6to4
24
+ blockList.addSubnet('64:ff9b::', 96, 'ipv6'); // NAT64
25
+ blockList.addSubnet('64:ff9b:1::', 48, 'ipv6'); // local NAT64 translation prefixes
26
+ blockList.addSubnet('2001::', 32, 'ipv6'); // Teredo
27
+ /**
28
+ * Checks whether a single IP address falls inside a blocked range.
29
+ */
30
+ const ipIsBlocked = (ip) => {
31
+ const family = isIP(ip);
32
+ if (family === 0) {
33
+ // Not a valid IP, block to be safe
34
+ return true;
35
+ }
36
+ if (blockList.check(ip, family === 4 ? 'ipv4' : 'ipv6')) {
37
+ return true;
38
+ }
39
+ // Normalize an IPv4-mapped IPv6 address (::ffff:x.x.x.x, in dotted or hex form) and re-check it
40
+ // against the IPv4 rules, since it targets the embedded IPv4 destination.
41
+ const dotted = ip.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
42
+ if (dotted && isIPv4(dotted[1]) && blockList.check(dotted[1], 'ipv4')) {
43
+ return true;
44
+ }
45
+ const hex = ip.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
46
+ if (hex) {
47
+ const high = Number.parseInt(hex[1], 16);
48
+ const low = Number.parseInt(hex[2], 16);
49
+ const mapped = `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
50
+ if (blockList.check(mapped, 'ipv4')) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ };
56
+ /**
57
+ * Reports whether a hostname resolves to a private, loopback, link-local, or otherwise internal
58
+ * address that a bundled `$ref` should not be allowed to reach.
59
+ *
60
+ * Literal IPs are checked directly. Hostnames are resolved via DNS and every returned address is
61
+ * checked, so a name that points at an internal IP is rejected. A host that cannot be resolved is
62
+ * treated as blocked.
63
+ *
64
+ * This lives in a Node-only module because it depends on `node:dns` and `node:net`. Load it with a
65
+ * dynamic import so the browser build of the fetch plugin stays free of Node built-ins.
66
+ *
67
+ * @param hostname - The hostname or IP (brackets around an IPv6 literal are tolerated).
68
+ */
69
+ export const isBlockedHost = async (hostname) => (await resolvePublicHost(hostname)) === null;
70
+ /** Resolves a host once and returns only validated public addresses, or null when blocked. */
71
+ export const resolvePublicHost = async (hostname) => {
72
+ const host = hostname.replace(/^\[/, '').replace(/\]$/, '');
73
+ try {
74
+ const family = isIP(host);
75
+ const addresses = family ? [{ address: host, family }] : await lookup(host, { all: true });
76
+ return addresses.length === 0 || addresses.some(({ address }) => ipIsBlocked(address)) ? null : addresses;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ };
@@ -1,7 +1,8 @@
1
1
  import type { LoaderPlugin, ResolveResult } from '../../../bundle/index.js';
2
2
  /**
3
3
  * Reads and normalizes data from a local file
4
- * @param path - The file path to read from
4
+ * @param filePath - The file path to read from
5
+ * @param basePath - When set, reads are confined to this directory so a `$ref` cannot escape it
5
6
  * @returns A promise that resolves to either the normalized data or an error result
6
7
  * @example
7
8
  * ```ts
@@ -13,17 +14,20 @@ import type { LoaderPlugin, ResolveResult } from '../../../bundle/index.js';
13
14
  * }
14
15
  * ```
15
16
  */
16
- export declare function readFile(path: string): Promise<ResolveResult>;
17
+ export declare function readFile(filePath: string, basePath?: string): Promise<ResolveResult>;
17
18
  /**
18
19
  * Creates a plugin for handling local file references.
19
20
  * This plugin validates and reads data from local filesystem paths.
20
21
  *
21
22
  * @returns A plugin object with validate and exec functions
23
+ * @param config - Optional settings. `basePath` confines reads to a directory.
22
24
  * @example
23
25
  * const filePlugin = readFiles()
24
26
  * if (filePlugin.validate('./local-schema.json')) {
25
27
  * const result = await filePlugin.exec('./local-schema.json')
26
28
  * }
27
29
  */
28
- export declare function readFiles(): LoaderPlugin;
30
+ export declare function readFiles(config?: {
31
+ basePath?: string;
32
+ }): LoaderPlugin;
29
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/read-files/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAI3D;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAoBnE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAMxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/read-files/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AA8B3D;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAMtE"}
@@ -1,8 +1,32 @@
1
+ import path from 'pathe';
1
2
  import { isFilePath } from '../../../helpers/is-file-path.js';
2
3
  import { normalize } from '../../../helpers/normalize.js';
4
+ /**
5
+ * Resolves a path to its real location, following symlinks. Falls back to a lexical resolve when the
6
+ * path does not exist yet, so a missing file is still confined (the read fails afterwards anyway).
7
+ */
8
+ const realpathOrResolve = async (fs, target) => {
9
+ try {
10
+ return await fs.realpath(path.resolve(target));
11
+ }
12
+ catch {
13
+ return path.resolve(target);
14
+ }
15
+ };
16
+ /**
17
+ * Reports whether a file escapes the allowed base directory. Symlinks are dereferenced first, so a
18
+ * link inside the base directory cannot point at a file outside it.
19
+ */
20
+ const isOutsideBasePath = async (fs, filePath, basePath) => {
21
+ const realBase = await realpathOrResolve(fs, basePath);
22
+ const realTarget = await realpathOrResolve(fs, filePath);
23
+ const relative = path.relative(realBase, realTarget);
24
+ return relative === '..' || relative.startsWith('../') || path.isAbsolute(relative);
25
+ };
3
26
  /**
4
27
  * Reads and normalizes data from a local file
5
- * @param path - The file path to read from
28
+ * @param filePath - The file path to read from
29
+ * @param basePath - When set, reads are confined to this directory so a `$ref` cannot escape it
6
30
  * @returns A promise that resolves to either the normalized data or an error result
7
31
  * @example
8
32
  * ```ts
@@ -14,13 +38,21 @@ import { normalize } from '../../../helpers/normalize.js';
14
38
  * }
15
39
  * ```
16
40
  */
17
- export async function readFile(path) {
41
+ export async function readFile(filePath, basePath) {
18
42
  const fs = typeof window === 'undefined' ? await import('node:fs/promises') : undefined;
19
43
  if (fs === undefined) {
20
44
  throw 'Can not use readFiles plugin outside of a node environment';
21
45
  }
46
+ // Confine reads to basePath when provided, so a `$ref` like `../../../../etc/passwd` (or a symlink
47
+ // pointing outside the directory) cannot read files outside the document's directory.
48
+ if (basePath !== undefined && (await isOutsideBasePath(fs, filePath, basePath))) {
49
+ console.warn(`[WARN] Refused to read a file outside the allowed directory: ${filePath}`);
50
+ return {
51
+ ok: false,
52
+ };
53
+ }
22
54
  try {
23
- const fileContents = await fs.readFile(path, { encoding: 'utf-8' });
55
+ const fileContents = await fs.readFile(filePath, { encoding: 'utf-8' });
24
56
  return {
25
57
  ok: true,
26
58
  data: normalize(fileContents),
@@ -38,16 +70,17 @@ export async function readFile(path) {
38
70
  * This plugin validates and reads data from local filesystem paths.
39
71
  *
40
72
  * @returns A plugin object with validate and exec functions
73
+ * @param config - Optional settings. `basePath` confines reads to a directory.
41
74
  * @example
42
75
  * const filePlugin = readFiles()
43
76
  * if (filePlugin.validate('./local-schema.json')) {
44
77
  * const result = await filePlugin.exec('./local-schema.json')
45
78
  * }
46
79
  */
47
- export function readFiles() {
80
+ export function readFiles(config) {
48
81
  return {
49
82
  type: 'loader',
50
83
  validate: isFilePath,
51
- exec: readFile,
84
+ exec: (value) => readFile(value, config?.basePath),
52
85
  };
53
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/diff/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,eAAe,GAAI,GAAG,OAAO,EAAE,GAAG,OAAO,KAAG,OAoCxD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAsB3G,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,YAY7C,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/diff/utils.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,eAAe,GAAI,GAAG,OAAO,EAAE,GAAG,OAAO,KAAG,OAoCxD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAsB3G,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,YAY7C,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { isObjectLike } from '@scalar/helpers/object/is-object';
1
2
  import { isPollutionKey } from '@scalar/helpers/object/prevent-pollution';
2
3
  /**
3
4
  * Deep check for objects for collisions
@@ -96,7 +97,7 @@ export const mergeObjects = (a, b) => {
96
97
  else {
97
98
  const aValue = a[key];
98
99
  const bValue = b[key];
99
- if (typeof aValue === 'object' && aValue !== null && typeof bValue === 'object' && bValue !== null) {
100
+ if (isObjectLike(aValue) && isObjectLike(bValue)) {
100
101
  a[key] = mergeObjects(aValue, bValue);
101
102
  }
102
103
  }
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/scalar/scalar.git",
11
11
  "directory": "packages/json-magic"
12
12
  },
13
- "version": "0.13.3",
13
+ "version": "0.13.5",
14
14
  "engines": {
15
15
  "node": ">=22"
16
16
  },
@@ -108,8 +108,9 @@
108
108
  ],
109
109
  "dependencies": {
110
110
  "pathe": "^2.0.3",
111
+ "undici": "7.24.4",
111
112
  "yaml": "^2.9.0",
112
- "@scalar/helpers": "0.11.2"
113
+ "@scalar/helpers": "0.12.0"
113
114
  },
114
115
  "devDependencies": {
115
116
  "fastify": "^5.11.2",