@scalar/json-magic 0.13.4 → 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,18 @@
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
+
3
16
  ## 0.13.4
4
17
 
5
18
  ### Patch Changes
@@ -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';
@@ -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,6 +4,7 @@ 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>;
8
9
  /**
9
10
  * When true, refuse to fetch URLs that resolve to a private, loopback, link-local, or otherwise
@@ -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;IAC1F;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAA;CAC9B,CAAC,CAAA;AAyBF;;;;;;;;;;;;;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,CAkExB;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"}
@@ -13,17 +13,6 @@ const getHost = (url) => {
13
13
  return null;
14
14
  }
15
15
  };
16
- /**
17
- * Safely reads the hostname (without port) from a URL, for the private-address check.
18
- */
19
- const getHostname = (url) => {
20
- try {
21
- return new URL(url).hostname;
22
- }
23
- catch {
24
- return null;
25
- }
26
- };
27
16
  /**
28
17
  * Fetches and normalizes data from a remote URL
29
18
  * @param url - The URL to fetch data from
@@ -40,32 +29,20 @@ const getHostname = (url) => {
40
29
  */
41
30
  export async function fetchUrl(url, limiter, config) {
42
31
  try {
43
- // SSRF guard: optionally refuse to fetch internal or private targets before making the request.
44
- // Only runs in Node, since the browser build has no DNS access and its own network isolation.
45
- if (config?.blockPrivateNetworks && typeof window === 'undefined') {
46
- const hostname = getHostname(url);
47
- if (hostname) {
48
- const { isBlockedHost } = await import('./is-blocked-host.js');
49
- if (await isBlockedHost(hostname)) {
50
- console.warn(`[WARN] Refused to fetch a private or internal address: ${url}`);
51
- return {
52
- ok: false,
53
- };
54
- }
55
- }
56
- }
57
32
  const host = getHost(url);
58
- // Get the headers that match the domain
59
33
  const headers = config?.headers?.find((a) => a.domains.find((d) => d === host) !== undefined)?.headers;
60
- const exec = config?.fetch ?? fetch;
61
- // Under the SSRF guard, do not follow redirects. Otherwise a public URL that passes the host
62
- // check above could redirect to an internal target (for example the metadata endpoint) that
63
- // the redirect would reach without being re-validated.
64
- const redirect = config?.blockPrivateNetworks && typeof window === 'undefined' ? 'error' : undefined;
65
- const result = await limiter(() => exec(url, {
66
- headers,
67
- redirect,
68
- }));
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
+ });
69
46
  if (result.ok) {
70
47
  const body = await result.text();
71
48
  return {
@@ -1,3 +1,4 @@
1
+ import type { LookupAddress } from 'node:dns';
1
2
  /**
2
3
  * Reports whether a hostname resolves to a private, loopback, link-local, or otherwise internal
3
4
  * address that a bundled `$ref` should not be allowed to reach.
@@ -12,4 +13,6 @@
12
13
  * @param hostname - The hostname or IP (brackets around an IPv6 literal are tolerated).
13
14
  */
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>;
15
18
  //# sourceMappingURL=is-blocked-host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"is-blocked-host.d.ts","sourceRoot":"","sources":["../../../../src/bundle/plugins/fetch-urls/is-blocked-host.ts"],"names":[],"mappings":"AAmEA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAerE,CAAA"}
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"}
@@ -22,6 +22,7 @@ blockList.addSubnet('fe80::', 10, 'ipv6'); // link-local
22
22
  blockList.addSubnet('fc00::', 7, 'ipv6'); // unique local
23
23
  blockList.addSubnet('2002::', 16, 'ipv6'); // 6to4
24
24
  blockList.addSubnet('64:ff9b::', 96, 'ipv6'); // NAT64
25
+ blockList.addSubnet('64:ff9b:1::', 48, 'ipv6'); // local NAT64 translation prefixes
25
26
  blockList.addSubnet('2001::', 32, 'ipv6'); // Teredo
26
27
  /**
27
28
  * Checks whether a single IP address falls inside a blocked range.
@@ -65,17 +66,16 @@ const ipIsBlocked = (ip) => {
65
66
  *
66
67
  * @param hostname - The hostname or IP (brackets around an IPv6 literal are tolerated).
67
68
  */
68
- export const isBlockedHost = async (hostname) => {
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) => {
69
72
  const host = hostname.replace(/^\[/, '').replace(/\]$/, '');
70
- if (isIP(host)) {
71
- return ipIsBlocked(host);
72
- }
73
73
  try {
74
- const addresses = await lookup(host, { all: true });
75
- return addresses.some(({ address }) => ipIsBlocked(address));
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;
76
77
  }
77
78
  catch {
78
- // Block when the host cannot be resolved
79
- return true;
79
+ return null;
80
80
  }
81
81
  };
@@ -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.4",
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.3"
113
+ "@scalar/helpers": "0.12.0"
113
114
  },
114
115
  "devDependencies": {
115
116
  "fastify": "^5.11.2",