@amritk/resolve-refs
Advanced tools
@@ -169,11 +169,13 @@ import { readFileSync } from 'node:fs'; | ||
| if (typeof obj['$ref'] === 'string') { | ||
| // A `$ref` object's siblings are ignored on resolution, so we do not recurse | ||
| // into them — mirroring resolveAt's behaviour. | ||
| const { filePart } = splitRef(obj['$ref']); | ||
| if (filePart !== '') | ||
| out.add(filePart); | ||
| return out; | ||
| } | ||
| for (const key of Object.keys(obj)) | ||
| // Recurse into every key — including a `$ref` node's siblings, which apply | ||
| // alongside the referenced schema (2020-12) and may carry their own refs. | ||
| for (const key of Object.keys(obj)) { | ||
| if (key === '$ref') | ||
| continue; | ||
| collectRefTargets(obj[key], out); | ||
| } | ||
| return out; | ||
@@ -223,20 +225,36 @@ }; | ||
| const cacheKey = `${targetLocation}#${pointer}`; | ||
| let resolved; | ||
| if (refCache.has(cacheKey)) { | ||
| const cached = refCache.get(cacheKey); | ||
| return cached === CYCLE ? {} : cached; | ||
| resolved = cached === CYCLE ? {} : cached; | ||
| } | ||
| refCache.set(cacheKey, CYCLE); | ||
| const target = pointer ? getByPointer(targetRoot, pointer) : targetRoot; | ||
| const resolved = resolveAt(target, targetLocation, docCache, refCache, origins); | ||
| refCache.set(cacheKey, resolved); | ||
| // Stamp the inlined node with where it was defined so a consumer can map a | ||
| // resolved-tree node back to its source document/path. Only objects/arrays are | ||
| // stamped (primitives can't key the map). First-write-wins: resolution recurses | ||
| // to the deepest `$ref` before returning, so the *definition* site stamps first; | ||
| // an outer ref that merely points (transitively) at the same object must not | ||
| // overwrite it with an intermediate location. | ||
| if (origins && resolved !== null && typeof resolved === 'object' && !origins.has(resolved)) { | ||
| origins.set(resolved, { location: targetLocation, pointer: pointerToPath(pointer) }); | ||
| else { | ||
| refCache.set(cacheKey, CYCLE); | ||
| const target = pointer ? getByPointer(targetRoot, pointer) : targetRoot; | ||
| resolved = resolveAt(target, targetLocation, docCache, refCache, origins); | ||
| refCache.set(cacheKey, resolved); | ||
| // Stamp the inlined node with where it was defined so a consumer can map a | ||
| // resolved-tree node back to its source document/path. Only objects/arrays are | ||
| // stamped (primitives can't key the map). First-write-wins: resolution recurses | ||
| // to the deepest `$ref` before returning, so the *definition* site stamps first; | ||
| // an outer ref that merely points (transitively) at the same object must not | ||
| // overwrite it with an intermediate location. | ||
| if (origins && resolved !== null && typeof resolved === 'object' && !origins.has(resolved)) { | ||
| origins.set(resolved, { location: targetLocation, pointer: pointerToPath(pointer) }); | ||
| } | ||
| } | ||
| return resolved; | ||
| // Keywords sibling to `$ref` apply alongside the referenced schema (2020-12), | ||
| // so preserve them by combining both in an `allOf` rather than dropping them. | ||
| const siblingKeys = Object.keys(obj).filter((key) => key !== '$ref'); | ||
| if (siblingKeys.length === 0) | ||
| return resolved; | ||
| const siblings = {}; | ||
| for (const key of siblingKeys) | ||
| siblings[key] = resolveAt(obj[key], baseLocation, docCache, refCache, origins); | ||
| const existingAllOf = Array.isArray(siblings['allOf']) ? siblings['allOf'] : []; | ||
| const merged = { ...siblings, allOf: [...existingAllOf, resolved] }; | ||
| // Stamp the wrapper too, so origin lookups resolve for a `$ref`-with-siblings node. | ||
| if (origins && !origins.has(merged)) | ||
| origins.set(merged, { location: targetLocation, pointer: pointerToPath(pointer) }); | ||
| return merged; | ||
| } | ||
@@ -243,0 +261,0 @@ const result = {}; |
+30
-10
@@ -22,16 +22,36 @@ import { getByPointer, pointerToPath } from './get-by-pointer.js'; | ||
| return obj; // external ref — leave as-is | ||
| let target; | ||
| if (cache.has(ref)) { | ||
| const cached = cache.get(ref); | ||
| return cached === CYCLE ? {} : cached; | ||
| target = cached === CYCLE ? {} : cached; | ||
| } | ||
| cache.set(ref, CYCLE); | ||
| const resolved = resolveInternal(getByPointer(root, ref.slice(1)), root, cache, origins); | ||
| cache.set(ref, resolved); | ||
| // Stamp the inlined node with the path it was defined at (see resolveAt). | ||
| // First-write-wins so the deepest definition stamps before any outer ref that | ||
| // transitively points at the same object. Primitives can't key the map. | ||
| if (origins && resolved !== null && typeof resolved === 'object' && !origins.has(resolved)) { | ||
| origins.set(resolved, { location: '', pointer: pointerToPath(ref.slice(1)) }); | ||
| else { | ||
| cache.set(ref, CYCLE); | ||
| target = resolveInternal(getByPointer(root, ref.slice(1)), root, cache, origins); | ||
| cache.set(ref, target); | ||
| // Stamp the inlined node with the path it was defined at (see resolveAt). | ||
| // First-write-wins so the deepest definition stamps before any outer ref that | ||
| // transitively points at the same object. Primitives can't key the map. | ||
| if (origins && target !== null && typeof target === 'object' && !origins.has(target)) { | ||
| origins.set(target, { location: '', pointer: pointerToPath(ref.slice(1)) }); | ||
| } | ||
| } | ||
| return resolved; | ||
| // Per JSON Schema 2020-12, keywords sibling to `$ref` are *not* ignored: they | ||
| // apply alongside the referenced schema. Preserve them by combining both in an | ||
| // `allOf` (so a constraint present on both sides is never silently dropped) | ||
| // rather than returning the bare target. The cache always stores the | ||
| // sibling-free target so each occurrence applies its own siblings. | ||
| const siblingKeys = Object.keys(obj).filter((key) => key !== '$ref'); | ||
| if (siblingKeys.length === 0) | ||
| return target; | ||
| const siblings = {}; | ||
| for (const key of siblingKeys) | ||
| siblings[key] = resolveInternal(obj[key], root, cache, origins); | ||
| const existingAllOf = Array.isArray(siblings['allOf']) ? siblings['allOf'] : []; | ||
| const merged = { ...siblings, allOf: [...existingAllOf, target] }; | ||
| // Stamp the wrapper too, so a consumer mapping the resolved node back to its | ||
| // origin finds one for a `$ref`-with-siblings node (not only the inner target). | ||
| if (origins && !origins.has(merged)) | ||
| origins.set(merged, { location: '', pointer: pointerToPath(ref.slice(1)) }); | ||
| return merged; | ||
| } | ||
@@ -38,0 +58,0 @@ const result = {}; |
+1
-1
| { | ||
| "name": "@amritk/resolve-refs", | ||
| "version": "0.2.1", | ||
| "version": "0.2.2", | ||
| "description": "Resolve and inline JSON Schema / OpenAPI $refs — internal, cross-file, and remote — with session caching and a default-deny SSRF guard.", | ||
@@ -5,0 +5,0 @@ "module": "./dist/index.js", |
32945
7.39%655
6.16%