🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@amritk/resolve-refs

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/resolve-refs - npm Package Compare versions

Comparing version
0.4.0
to
0.4.1
+69
-7
dist/is-private-host.js

@@ -70,2 +70,60 @@ /**

};
/**
* Expands a hex IPv6 literal (no dotted-IPv4 tail — that is handled separately)
* into its eight 16-bit groups, resolving `::` zero-compression. Returns null if
* the string is not a well-formed hex IPv6 address.
*/
const expandHexIpv6 = (host) => {
const halves = host.split('::');
if (halves.length > 2)
return null;
const toGroups = (part) => {
if (part === '')
return [];
const groups = [];
for (const g of part.split(':')) {
if (!/^[0-9a-f]{1,4}$/.test(g))
return null;
groups.push(Number.parseInt(g, 16));
}
return groups;
};
const head = toGroups(halves[0]);
if (head === null)
return null;
if (halves.length === 1)
return head.length === 8 ? head : null;
const tail = toGroups(halves[1]);
if (tail === null)
return null;
const fill = 8 - head.length - tail.length;
if (fill < 0)
return null;
return [...head, ...new Array(fill).fill(0), ...tail];
};
/**
* When an expanded IPv6 address embeds an IPv4 address in its low 32 bits,
* returns that IPv4 as a 32-bit number; otherwise null. Covers every embedding
* the WHATWG URL parser can hand us as bare hex — the `ffff:`-only regex this
* replaced let `::7f00:1` (`::127.0.0.1`) and `::a9fe:a9fe` (`::169.254.169.254`)
* through, since those normalize away the `ffff:` marker:
* - `::/96` compatible (`::X:Y`, also covers `::`/`::1`),
* - IPv4-mapped (`::ffff:X:Y`),
* - IPv4-translated (`::ffff:0:X:Y`),
* - NAT64 (`64:ff9b::/96`).
*/
const ipv4EmbeddedInIpv6 = (g) => {
const [a, b, c, d, e, f, hi, lo] = g;
const embedded = (hi * 0x10000 + lo) >>> 0;
const zeros4 = a === 0 && b === 0 && c === 0 && d === 0;
if (zeros4 && e === 0 && f === 0)
return embedded; // ::/96 compatible
if (zeros4 && e === 0 && f === 0xffff)
return embedded; // ::ffff:X:Y mapped
if (zeros4 && e === 0xffff && f === 0)
return embedded; // ::ffff:0:X:Y translated
if (a === 0x64 && b === 0xff9b && c === 0 && d === 0 && e === 0 && f === 0)
return embedded; // 64:ff9b::/96 NAT64
return null;
};
export const isPrivateHost = (hostname) => {

@@ -90,4 +148,4 @@ // Strip IPv6 brackets and any trailing dot(s): `localhost.` is the FQDN-root

return true; // fc00::/7 unique-local
// IPv4-mapped/compatible, dotted (`::ffff:127.0.0.1`) — the URL parser
// rewrites this to hex, but a direct caller may pass the dotted form.
// IPv4-mapped/compatible, dotted (`::ffff:127.0.0.1`, `::127.0.0.1`) — the URL
// parser rewrites these to hex, but a direct caller may pass the dotted form.
const dotted = /:((?:\d{1,3}\.){3}\d{1,3})$/.exec(host);

@@ -99,7 +157,11 @@ if (dotted?.[1]) {

}
// IPv4-mapped, hex (`::ffff:7f00:1`) — what `new URL()` produces.
const hex = /:ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
if (hex?.[1] && hex[2]) {
const hi = Number.parseInt(hex[1], 16);
return isPrivateIpv4((hi >> 8) & 0xff, hi & 0xff);
// Every hex IPv4-in-IPv6 embedding `new URL()` can produce (mapped,
// compatible, translated, NAT64) — via a full expansion rather than a
// single-form regex, which missed `::7f00:1` / `::a9fe:a9fe`. This also
// catches the fully-expanded loopback `0:0:0:0:0:0:0:1`.
const groups = expandHexIpv6(host);
if (groups) {
const embedded = ipv4EmbeddedInIpv6(groups);
if (embedded !== null)
return isPrivateIpv4((embedded >>> 24) & 0xff, (embedded >>> 16) & 0xff);
}

@@ -106,0 +168,0 @@ return false;

+1
-1
{
"name": "@amritk/resolve-refs",
"version": "0.4.0",
"version": "0.4.1",
"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",

+26
-18

@@ -100,25 +100,33 @@ # @amritk/resolve-refs

`resolveRefs` is single-pass: every unique `$ref` string is resolved exactly
once and memoized, with a sentinel that breaks cycles. The `bench/` suite isolates
what that memoization buys by pitting it against a naive inliner that re-resolves
each ref every time it is encountered — same inlined output, the cache is the only
difference. Representative numbers (Bun 1.3, Linux x64 — your hardware will
differ, run `bun run bench` yourself):
`resolveRefs` memoizes: every unique `$ref` string is resolved once per scope,
with a sentinel that breaks cycles by keeping the reference node in place. But it
is no longer *only* a memoized inliner — before resolving anything it walks the
whole document once to build a resource registry (`$id`/`$anchor` scoping), keeps
recursive cycles intact, and records a diagnostic for every external ref. The
`bench/` suite pits it against a bare naive inliner that does none of that — no
registry, no scoping, no diagnostics — and re-resolves each ref on every
encounter, so the gap is the production resolver's *total* per-call cost against
the cheapest thing that produces the same inlined shape. Both are asserted to
produce byte-identical output before either is timed. Representative numbers
(Bun 1.3, Linux x64 — your hardware will differ, run `bun run bench` yourself):
| schema | cached | naive | speedup |
|:---|---:|---:|---:|
| reuse-heavy (50 refs → 1 def) | ~35k ops/s | ~6k ops/s | **~5.8×** |
| chain (40 `$ref` → `$ref` links) | ~5k ops/s | ~0.6k ops/s | **~8.2×** |
| cyclic tree | ~122k ops/s | ~117k ops/s | ~1.05× |
| wide-distinct (60 defs, each used once) | ~3.3k ops/s | ~6.1k ops/s | ~0.54× |
| chain (40 `$ref` → `$ref` links) | ~2.6k ops/s | ~0.8k ops/s | **~3.2×** |
| reuse-heavy (50 refs → 1 def) | ~4.5k ops/s | ~8k ops/s | ~0.5× |
| cyclic tree | ~27k ops/s | ~74k ops/s | ~0.37× |
| wide-distinct (60 defs, each used once) | ~2.3k ops/s | ~7.3k ops/s | ~0.32× |
The cache earns its keep exactly where you'd expect: a `$def` referenced from
many sites, or a long indirection chain, is resolved once instead of re-walked
every time. On a document where every ref is distinct — so the cache never hits —
its bookkeeping is pure overhead and the naive walk is faster; that `wide-distinct`
row is kept in the table precisely to show the trade honestly. Real API schemas
lean heavily on shared `$def`s, which is the case the resolver is tuned for.
Memoization overtakes the naive walk only on the **chain** shape, where a long
indirection path is expensive to re-resolve and the cache collapses it to one
pass. On every other shape here the fixed cost of the up-front registry walk (paid
once per call, no matter how the refs reuse) outweighs what memoization saves, and
the bare inliner is faster — these are small schemas where that one document walk
dominates. The takeaway is practical: the resolver's per-call floor is a full
document traversal, so in a hot loop resolve a document **once** and reuse the
result rather than re-resolving. The `reuse-heavy`, `cyclic`, and `wide-distinct`
rows are kept in the table precisely to show that trade honestly rather than
cherry-picking the one shape the cache wins.
Opting into `trackOrigins` (which records where each inlined value came from) adds
roughly **5–20%** on top. Both strategies are asserted to produce byte-identical
output before either is timed.
roughly **0–20%** on top, within run-to-run noise on these small schemas.