@frontmcp/utils 1.8.0 → 1.8.1

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.
@@ -14,7 +14,7 @@
14
14
  * - Error → { name, message, stack }
15
15
  * - Map → plain object
16
16
  * - Set → array
17
- * - Circular references → dropped (undefined)
17
+ * - Circular references → dropped (undefined); a value referenced more than once without a cycle is kept each time
18
18
  *
19
19
  * @param value - Any JavaScript value
20
20
  * @returns JSON-safe version of the value
@@ -32,6 +32,23 @@ __export(browser_env_exports, {
32
32
  supportsAnsi: () => supportsAnsi
33
33
  });
34
34
  module.exports = __toCommonJS(browser_env_exports);
35
+
36
+ // libs/utils/src/env/edge-scope.ts
37
+ function hasEdgeGlobalScope() {
38
+ if (typeof globalThis === "undefined") return false;
39
+ if ("EdgeRuntime" in globalThis) return true;
40
+ if (!("caches" in globalThis) || "window" in globalThis) return false;
41
+ return !isBrowserWorkerScope();
42
+ }
43
+ function isBrowserWorkerScope() {
44
+ const userAgent = typeof navigator === "undefined" ? void 0 : navigator.userAgent;
45
+ if (userAgent === void 0 || userAgent === "Cloudflare-Workers") return false;
46
+ return ["DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope"].some(
47
+ (scopeName) => scopeName in globalThis
48
+ );
49
+ }
50
+
51
+ // libs/utils/src/env/browser-env.ts
35
52
  function getEnv(_key, defaultValue) {
36
53
  return defaultValue;
37
54
  }
@@ -53,9 +70,7 @@ function isDebug() {
53
70
  function setEnv(_key, _value) {
54
71
  }
55
72
  function isEdgeRuntime() {
56
- if (typeof globalThis !== "undefined" && "EdgeRuntime" in globalThis) return true;
57
- if (typeof globalThis !== "undefined" && "caches" in globalThis && !("window" in globalThis)) return true;
58
- return false;
73
+ return hasEdgeGlobalScope();
59
74
  }
60
75
  function isServerless() {
61
76
  return false;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Whether the global scope looks like an edge runtime (Cloudflare Workers, Vercel Edge, Deno Deploy):
3
+ * `EdgeRuntime` is defined, or `caches` is present without `window`. A browser Web Worker also has
4
+ * `caches` and no `window`, and defines its dedicated, shared or service worker scope. Cloudflare's workerd
5
+ * defines `ServiceWorkerGlobalScope` too, so it is told apart by its user agent, or by having no `navigator`.
6
+ */
7
+ export declare function hasEdgeGlobalScope(): boolean;
package/env/node-env.js CHANGED
@@ -32,6 +32,23 @@ __export(node_env_exports, {
32
32
  supportsAnsi: () => supportsAnsi
33
33
  });
34
34
  module.exports = __toCommonJS(node_env_exports);
35
+
36
+ // libs/utils/src/env/edge-scope.ts
37
+ function hasEdgeGlobalScope() {
38
+ if (typeof globalThis === "undefined") return false;
39
+ if ("EdgeRuntime" in globalThis) return true;
40
+ if (!("caches" in globalThis) || "window" in globalThis) return false;
41
+ return !isBrowserWorkerScope();
42
+ }
43
+ function isBrowserWorkerScope() {
44
+ const userAgent = typeof navigator === "undefined" ? void 0 : navigator.userAgent;
45
+ if (userAgent === void 0 || userAgent === "Cloudflare-Workers") return false;
46
+ return ["DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope"].some(
47
+ (scopeName) => scopeName in globalThis
48
+ );
49
+ }
50
+
51
+ // libs/utils/src/env/node-env.ts
35
52
  function getEnv(key, defaultValue) {
36
53
  return process.env[key] ?? defaultValue;
37
54
  }
@@ -55,8 +72,7 @@ function setEnv(key, value) {
55
72
  process.env[key] = value;
56
73
  }
57
74
  function isEdgeRuntime() {
58
- if (typeof globalThis !== "undefined" && "EdgeRuntime" in globalThis) return true;
59
- if (typeof globalThis !== "undefined" && "caches" in globalThis && !("window" in globalThis)) return true;
75
+ if (hasEdgeGlobalScope()) return true;
60
76
  return process.env["EDGE_RUNTIME"] !== void 0 && process.env["VERCEL_ENV"] !== void 0;
61
77
  }
62
78
  function isServerless() {
@@ -1,5 +1,20 @@
1
1
  import { createRequire as __frontmcpCreateRequire } from 'module';
2
2
  const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
3
+ // libs/utils/src/env/edge-scope.ts
4
+ function hasEdgeGlobalScope() {
5
+ if (typeof globalThis === "undefined") return false;
6
+ if ("EdgeRuntime" in globalThis) return true;
7
+ if (!("caches" in globalThis) || "window" in globalThis) return false;
8
+ return !isBrowserWorkerScope();
9
+ }
10
+ function isBrowserWorkerScope() {
11
+ const userAgent = typeof navigator === "undefined" ? void 0 : navigator.userAgent;
12
+ if (userAgent === void 0 || userAgent === "Cloudflare-Workers") return false;
13
+ return ["DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope"].some(
14
+ (scopeName) => scopeName in globalThis
15
+ );
16
+ }
17
+
3
18
  // libs/utils/src/env/browser-env.ts
4
19
  function getEnv(_key, defaultValue) {
5
20
  return defaultValue;
@@ -22,9 +37,7 @@ function isDebug() {
22
37
  function setEnv(_key, _value) {
23
38
  }
24
39
  function isEdgeRuntime() {
25
- if (typeof globalThis !== "undefined" && "EdgeRuntime" in globalThis) return true;
26
- if (typeof globalThis !== "undefined" && "caches" in globalThis && !("window" in globalThis)) return true;
27
- return false;
40
+ return hasEdgeGlobalScope();
28
41
  }
29
42
  function isServerless() {
30
43
  return false;
@@ -1,5 +1,20 @@
1
1
  import { createRequire as __frontmcpCreateRequire } from 'module';
2
2
  const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
3
+ // libs/utils/src/env/edge-scope.ts
4
+ function hasEdgeGlobalScope() {
5
+ if (typeof globalThis === "undefined") return false;
6
+ if ("EdgeRuntime" in globalThis) return true;
7
+ if (!("caches" in globalThis) || "window" in globalThis) return false;
8
+ return !isBrowserWorkerScope();
9
+ }
10
+ function isBrowserWorkerScope() {
11
+ const userAgent = typeof navigator === "undefined" ? void 0 : navigator.userAgent;
12
+ if (userAgent === void 0 || userAgent === "Cloudflare-Workers") return false;
13
+ return ["DedicatedWorkerGlobalScope", "SharedWorkerGlobalScope", "ServiceWorkerGlobalScope"].some(
14
+ (scopeName) => scopeName in globalThis
15
+ );
16
+ }
17
+
3
18
  // libs/utils/src/env/node-env.ts
4
19
  function getEnv(key, defaultValue) {
5
20
  return process.env[key] ?? defaultValue;
@@ -24,8 +39,7 @@ function setEnv(key, value) {
24
39
  process.env[key] = value;
25
40
  }
26
41
  function isEdgeRuntime() {
27
- if (typeof globalThis !== "undefined" && "EdgeRuntime" in globalThis) return true;
28
- if (typeof globalThis !== "undefined" && "caches" in globalThis && !("window" in globalThis)) return true;
42
+ if (hasEdgeGlobalScope()) return true;
29
43
  return process.env["EDGE_RUNTIME"] !== void 0 && process.env["VERCEL_ENV"] !== void 0;
30
44
  }
31
45
  function isServerless() {
package/esm/index.mjs CHANGED
@@ -4378,7 +4378,18 @@ import { pathToFileURL } from "url";
4378
4378
 
4379
4379
  // libs/utils/src/content/content.ts
4380
4380
  function sanitizeToJson(value) {
4381
- const seen = /* @__PURE__ */ new WeakSet();
4381
+ const ancestors = /* @__PURE__ */ new WeakSet();
4382
+ function withinAncestors(container, build) {
4383
+ if (ancestors.has(container)) {
4384
+ return void 0;
4385
+ }
4386
+ ancestors.add(container);
4387
+ try {
4388
+ return build();
4389
+ } finally {
4390
+ ancestors.delete(container);
4391
+ }
4392
+ }
4382
4393
  function sanitize(val) {
4383
4394
  if (typeof val === "function" || typeof val === "symbol") {
4384
4395
  return void 0;
@@ -4397,35 +4408,31 @@ function sanitizeToJson(value) {
4397
4408
  };
4398
4409
  }
4399
4410
  if (val instanceof Map) {
4400
- const obj = {};
4401
- for (const [k, v] of val.entries()) {
4402
- obj[String(k)] = sanitize(v);
4403
- }
4404
- return obj;
4411
+ return withinAncestors(val, () => {
4412
+ const obj = {};
4413
+ for (const [k, v] of val.entries()) {
4414
+ obj[String(k)] = sanitize(v);
4415
+ }
4416
+ return obj;
4417
+ });
4405
4418
  }
4406
4419
  if (val instanceof Set) {
4407
- return Array.from(val).map(sanitize);
4420
+ return withinAncestors(val, () => Array.from(val).map(sanitize));
4408
4421
  }
4409
4422
  if (Array.isArray(val)) {
4410
- if (seen.has(val)) {
4411
- return void 0;
4412
- }
4413
- seen.add(val);
4414
- return val.map(sanitize);
4423
+ return withinAncestors(val, () => val.map(sanitize));
4415
4424
  }
4416
4425
  if (val && typeof val === "object") {
4417
- if (seen.has(val)) {
4418
- return void 0;
4419
- }
4420
- seen.add(val);
4421
- const sanitized = {};
4422
- for (const [key, value2] of Object.entries(val)) {
4423
- const clean = sanitize(value2);
4424
- if (clean !== void 0) {
4425
- sanitized[key] = clean;
4426
+ return withinAncestors(val, () => {
4427
+ const sanitized = {};
4428
+ for (const [key, value2] of Object.entries(val)) {
4429
+ const clean = sanitize(value2);
4430
+ if (clean !== void 0) {
4431
+ sanitized[key] = clean;
4432
+ }
4426
4433
  }
4427
- }
4428
- return sanitized;
4434
+ return sanitized;
4435
+ });
4429
4436
  }
4430
4437
  return val;
4431
4438
  }
@@ -4587,6 +4594,134 @@ function getCookie(cookieHeader, name) {
4587
4594
  return parseCookies(cookieHeader)[name];
4588
4595
  }
4589
4596
 
4597
+ // libs/utils/src/net/ip-address.ts
4598
+ var IPV4_OCTET_PATTERN = /^(0|[1-9]\d{0,2})$/;
4599
+ var IPV6_GROUP_PATTERN = /^[0-9a-f]{1,4}$/;
4600
+ var IPV4_BLOCKS = compileBlocks(
4601
+ [
4602
+ ["0.0.0.0/8", "this-network"],
4603
+ ["10.0.0.0/8", "private"],
4604
+ ["100.64.0.0/10", "carrier-grade-nat"],
4605
+ ["127.0.0.0/8", "loopback"],
4606
+ ["169.254.0.0/16", "link-local"],
4607
+ ["172.16.0.0/12", "private"],
4608
+ ["192.0.0.0/24", "ietf-protocol"],
4609
+ ["192.168.0.0/16", "private"],
4610
+ ["198.18.0.0/15", "benchmarking"],
4611
+ ["224.0.0.0/4", "multicast"],
4612
+ ["255.255.255.255/32", "reserved"],
4613
+ ["240.0.0.0/4", "reserved"]
4614
+ ],
4615
+ parseIpv4
4616
+ );
4617
+ var IPV6_BLOCKS = compileBlocks(
4618
+ [
4619
+ ["::/128", "unspecified"],
4620
+ ["::1/128", "loopback"],
4621
+ ["fd00:ec2::254/128", "cloud-metadata"],
4622
+ ["fc00::/7", "private"],
4623
+ ["fe80::/10", "link-local"],
4624
+ ["fec0::/10", "site-local"],
4625
+ // RFC 8215 local-use NAT64: where the IPv4 address sits depends on the operator's prefix length.
4626
+ ["64:ff9b:1::/48", "local-use-nat64"],
4627
+ ["ff00::/8", "multicast"]
4628
+ ],
4629
+ expandIpv6
4630
+ );
4631
+ var IPV4_EMBEDDINGS = compileBlocks(
4632
+ [
4633
+ ["::ffff:0:0/96", 6],
4634
+ ["::ffff:0:0:0/96", 6],
4635
+ ["::/96", 6],
4636
+ ["64:ff9b::/96", 6],
4637
+ ["2002::/16", 1]
4638
+ ],
4639
+ expandIpv6
4640
+ );
4641
+ function parseIpv4(address) {
4642
+ const parts = address.split(".");
4643
+ if (parts.length !== 4 || !parts.every((part) => IPV4_OCTET_PATTERN.test(part))) return void 0;
4644
+ const octets = parts.map(Number);
4645
+ return octets.every((octet) => octet <= 255) ? octets : void 0;
4646
+ }
4647
+ function expandIpv6(address) {
4648
+ const zoneIndex = address.indexOf("%");
4649
+ let text = (zoneIndex === -1 ? address : address.slice(0, zoneIndex)).toLowerCase();
4650
+ const lastColonIndex = text.lastIndexOf(":");
4651
+ const trailingSegment = text.slice(lastColonIndex + 1);
4652
+ if (lastColonIndex !== -1 && trailingSegment.includes(".")) {
4653
+ const octets = parseIpv4(trailingSegment);
4654
+ if (!octets) return void 0;
4655
+ const highGroup = (octets[0] << 8 | octets[1]).toString(16);
4656
+ const lowGroup = (octets[2] << 8 | octets[3]).toString(16);
4657
+ text = `${text.slice(0, lastColonIndex + 1)}${highGroup}:${lowGroup}`;
4658
+ }
4659
+ const halves = text.split("::");
4660
+ if (halves.length > 2) return void 0;
4661
+ const head = parseIpv6Groups(halves[0]);
4662
+ if (!head) return void 0;
4663
+ if (halves.length === 1) return head.length === 8 ? head : void 0;
4664
+ const tail = parseIpv6Groups(halves[1]);
4665
+ if (!tail || head.length + tail.length > 7) return void 0;
4666
+ return [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail];
4667
+ }
4668
+ function extractEmbeddedIpv4(groups) {
4669
+ const octets = findEmbeddedIpv4Octets(groups);
4670
+ return octets ? octets.join(".") : void 0;
4671
+ }
4672
+ function classifyIpAddress(address) {
4673
+ const isBracketed = address.startsWith("[") && address.endsWith("]");
4674
+ const octets = isBracketed ? void 0 : parseIpv4(address);
4675
+ if (octets) return classifyIpv4(octets);
4676
+ const groups = expandIpv6(isBracketed ? address.slice(1, -1) : address);
4677
+ if (!groups) return void 0;
4678
+ const block = IPV6_BLOCKS.find((candidate) => matchesBlock(groups, candidate, 16));
4679
+ if (block) return { family: 6, range: block.value, cidr: block.cidr };
4680
+ const embeddedOctets = findEmbeddedIpv4Octets(groups);
4681
+ if (!embeddedOctets) return { family: 6, range: "public" };
4682
+ return { ...classifyIpv4(embeddedOctets), family: 6, embeddedIpv4: embeddedOctets.join(".") };
4683
+ }
4684
+ function classifyIpv4(octets) {
4685
+ const block = IPV4_BLOCKS.find((candidate) => matchesBlock(octets, candidate, 8));
4686
+ return block ? { family: 4, range: block.value, cidr: block.cidr } : { family: 4, range: "public" };
4687
+ }
4688
+ function findEmbeddedIpv4Octets(groups) {
4689
+ const embedding = IPV4_EMBEDDINGS.find((block) => matchesBlock(groups, block, 16));
4690
+ if (!embedding) return void 0;
4691
+ const highGroup = groups[embedding.value];
4692
+ const lowGroup = groups[embedding.value + 1];
4693
+ return [highGroup >> 8, highGroup & 255, lowGroup >> 8, lowGroup & 255];
4694
+ }
4695
+ function parseIpv6Groups(part) {
4696
+ if (part === "") return [];
4697
+ const groups = part.split(":");
4698
+ if (!groups.every((group) => IPV6_GROUP_PATTERN.test(group))) return void 0;
4699
+ return groups.map((group) => parseInt(group, 16));
4700
+ }
4701
+ function matchesBlock(words, block, bitsPerWord) {
4702
+ let remainingBits = block.prefixLength;
4703
+ for (let index = 0; remainingBits > 0; index++) {
4704
+ const comparedBits = Math.min(bitsPerWord, remainingBits);
4705
+ const ignoredBits = bitsPerWord - comparedBits;
4706
+ if (words[index] >> ignoredBits !== block.network[index] >> ignoredBits) return false;
4707
+ remainingBits -= comparedBits;
4708
+ }
4709
+ return true;
4710
+ }
4711
+ function compileBlocks(entries, parseNetwork) {
4712
+ return entries.map(([cidr, value]) => {
4713
+ const [networkText, prefixText] = cidr.split("/");
4714
+ const network = parseNetwork(networkText);
4715
+ if (!network) throw new Error(`Invalid network in address block "${cidr}"`);
4716
+ return { cidr, network, prefixLength: Number(prefixText), value };
4717
+ });
4718
+ }
4719
+
4720
+ // libs/utils/src/net/redirect.ts
4721
+ function isRedirectResponse(response) {
4722
+ return response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
4723
+ }
4724
+
4590
4725
  // libs/utils/src/index.ts
4591
4726
  init_fs2();
4592
4727
 
@@ -5776,6 +5911,7 @@ export {
5776
5911
  buildSetCookie,
5777
5912
  bytesToHex,
5778
5913
  checkEntryAvailability,
5914
+ classifyIpAddress,
5779
5915
  clearCachedSecret,
5780
5916
  collapseChar,
5781
5917
  collapseWhitespace,
@@ -5808,11 +5944,13 @@ export {
5808
5944
  escapeHtmlAttr,
5809
5945
  escapeJsString,
5810
5946
  escapeScriptClose,
5947
+ expandIpv6,
5811
5948
  expandTemplate,
5812
5949
  expandUriTemplate,
5813
5950
  expiresAtToTTL,
5814
5951
  extname,
5815
5952
  extractBracedParams,
5953
+ extractEmbeddedIpv4,
5816
5954
  extractTemplateParams,
5817
5955
  extractUriScheme,
5818
5956
  fileExists,
@@ -5852,6 +5990,7 @@ export {
5852
5990
  isNode,
5853
5991
  isPatternSafe,
5854
5992
  isProduction,
5993
+ isRedirectResponse,
5855
5994
  isRsaPssAlg,
5856
5995
  isSecretCached,
5857
5996
  isSecretKeyData,
@@ -5874,6 +6013,7 @@ export {
5874
6013
  mkdtemp,
5875
6014
  normalizeTTL,
5876
6015
  parseCookies,
6016
+ parseIpv4,
5877
6017
  parseKeyData,
5878
6018
  parseSecretData,
5879
6019
  parseUriTemplate,
package/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { trimSlashes, joinPath, pathResolve, pathJoin, basename, dirname, extnam
13
13
  export { sanitizeToJson, inferMimeType, findNonFiniteNumber } from './content';
14
14
  export { validateBaseUrl, buildSetCookie, parseCookies, getCookie, isLocalhost, isSecureRequest } from './http';
15
15
  export type { CookieOptions, SecureDetectionRequest } from './http';
16
+ export { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, parseIpv4, isRedirectResponse } from './net';
17
+ export type { IpAddressClassification, IpAddressRange, SpecialPurposeIpRange, RedirectCandidate } from './net';
16
18
  export { readFile, readFileSync, readFileBuffer, writeFile, mkdir, rename, unlink, stat, copyFile, cp, readdir, readdirSync, realpath, rm, mkdtemp, symlink, access, fileExists, readJSON, writeJSON, ensureDir, ensureDirSync, isDirEmpty, runCmd, watchFile, getSpawnFn, } from './fs';
17
19
  export type { FileWatcherHandle } from './fs';
18
20
  export { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJsonForScript } from './escape';
package/index.js CHANGED
@@ -4022,6 +4022,7 @@ __export(index_exports, {
4022
4022
  buildSetCookie: () => buildSetCookie,
4023
4023
  bytesToHex: () => bytesToHex,
4024
4024
  checkEntryAvailability: () => checkEntryAvailability,
4025
+ classifyIpAddress: () => classifyIpAddress,
4025
4026
  clearCachedSecret: () => clearCachedSecret,
4026
4027
  collapseChar: () => collapseChar,
4027
4028
  collapseWhitespace: () => collapseWhitespace,
@@ -4054,11 +4055,13 @@ __export(index_exports, {
4054
4055
  escapeHtmlAttr: () => escapeHtmlAttr,
4055
4056
  escapeJsString: () => escapeJsString,
4056
4057
  escapeScriptClose: () => escapeScriptClose,
4058
+ expandIpv6: () => expandIpv6,
4057
4059
  expandTemplate: () => expandTemplate,
4058
4060
  expandUriTemplate: () => expandUriTemplate,
4059
4061
  expiresAtToTTL: () => expiresAtToTTL,
4060
4062
  extname: () => import_path.extname,
4061
4063
  extractBracedParams: () => extractBracedParams,
4064
+ extractEmbeddedIpv4: () => extractEmbeddedIpv4,
4062
4065
  extractTemplateParams: () => extractTemplateParams,
4063
4066
  extractUriScheme: () => extractUriScheme,
4064
4067
  fileExists: () => fileExists,
@@ -4098,6 +4101,7 @@ __export(index_exports, {
4098
4101
  isNode: () => isNode,
4099
4102
  isPatternSafe: () => isPatternSafe,
4100
4103
  isProduction: () => import_env3.isProduction,
4104
+ isRedirectResponse: () => isRedirectResponse,
4101
4105
  isRsaPssAlg: () => isRsaPssAlg,
4102
4106
  isSecretCached: () => isSecretCached,
4103
4107
  isSecretKeyData: () => isSecretKeyData,
@@ -4120,6 +4124,7 @@ __export(index_exports, {
4120
4124
  mkdtemp: () => mkdtemp,
4121
4125
  normalizeTTL: () => normalizeTTL,
4122
4126
  parseCookies: () => parseCookies,
4127
+ parseIpv4: () => parseIpv4,
4123
4128
  parseKeyData: () => parseKeyData,
4124
4129
  parseSecretData: () => parseSecretData,
4125
4130
  parseUriTemplate: () => parseUriTemplate,
@@ -4603,7 +4608,18 @@ var import_url = require("url");
4603
4608
 
4604
4609
  // libs/utils/src/content/content.ts
4605
4610
  function sanitizeToJson(value) {
4606
- const seen = /* @__PURE__ */ new WeakSet();
4611
+ const ancestors = /* @__PURE__ */ new WeakSet();
4612
+ function withinAncestors(container, build) {
4613
+ if (ancestors.has(container)) {
4614
+ return void 0;
4615
+ }
4616
+ ancestors.add(container);
4617
+ try {
4618
+ return build();
4619
+ } finally {
4620
+ ancestors.delete(container);
4621
+ }
4622
+ }
4607
4623
  function sanitize(val) {
4608
4624
  if (typeof val === "function" || typeof val === "symbol") {
4609
4625
  return void 0;
@@ -4622,35 +4638,31 @@ function sanitizeToJson(value) {
4622
4638
  };
4623
4639
  }
4624
4640
  if (val instanceof Map) {
4625
- const obj = {};
4626
- for (const [k, v] of val.entries()) {
4627
- obj[String(k)] = sanitize(v);
4628
- }
4629
- return obj;
4641
+ return withinAncestors(val, () => {
4642
+ const obj = {};
4643
+ for (const [k, v] of val.entries()) {
4644
+ obj[String(k)] = sanitize(v);
4645
+ }
4646
+ return obj;
4647
+ });
4630
4648
  }
4631
4649
  if (val instanceof Set) {
4632
- return Array.from(val).map(sanitize);
4650
+ return withinAncestors(val, () => Array.from(val).map(sanitize));
4633
4651
  }
4634
4652
  if (Array.isArray(val)) {
4635
- if (seen.has(val)) {
4636
- return void 0;
4637
- }
4638
- seen.add(val);
4639
- return val.map(sanitize);
4653
+ return withinAncestors(val, () => val.map(sanitize));
4640
4654
  }
4641
4655
  if (val && typeof val === "object") {
4642
- if (seen.has(val)) {
4643
- return void 0;
4644
- }
4645
- seen.add(val);
4646
- const sanitized = {};
4647
- for (const [key, value2] of Object.entries(val)) {
4648
- const clean = sanitize(value2);
4649
- if (clean !== void 0) {
4650
- sanitized[key] = clean;
4656
+ return withinAncestors(val, () => {
4657
+ const sanitized = {};
4658
+ for (const [key, value2] of Object.entries(val)) {
4659
+ const clean = sanitize(value2);
4660
+ if (clean !== void 0) {
4661
+ sanitized[key] = clean;
4662
+ }
4651
4663
  }
4652
- }
4653
- return sanitized;
4664
+ return sanitized;
4665
+ });
4654
4666
  }
4655
4667
  return val;
4656
4668
  }
@@ -4812,6 +4824,134 @@ function getCookie(cookieHeader, name) {
4812
4824
  return parseCookies(cookieHeader)[name];
4813
4825
  }
4814
4826
 
4827
+ // libs/utils/src/net/ip-address.ts
4828
+ var IPV4_OCTET_PATTERN = /^(0|[1-9]\d{0,2})$/;
4829
+ var IPV6_GROUP_PATTERN = /^[0-9a-f]{1,4}$/;
4830
+ var IPV4_BLOCKS = compileBlocks(
4831
+ [
4832
+ ["0.0.0.0/8", "this-network"],
4833
+ ["10.0.0.0/8", "private"],
4834
+ ["100.64.0.0/10", "carrier-grade-nat"],
4835
+ ["127.0.0.0/8", "loopback"],
4836
+ ["169.254.0.0/16", "link-local"],
4837
+ ["172.16.0.0/12", "private"],
4838
+ ["192.0.0.0/24", "ietf-protocol"],
4839
+ ["192.168.0.0/16", "private"],
4840
+ ["198.18.0.0/15", "benchmarking"],
4841
+ ["224.0.0.0/4", "multicast"],
4842
+ ["255.255.255.255/32", "reserved"],
4843
+ ["240.0.0.0/4", "reserved"]
4844
+ ],
4845
+ parseIpv4
4846
+ );
4847
+ var IPV6_BLOCKS = compileBlocks(
4848
+ [
4849
+ ["::/128", "unspecified"],
4850
+ ["::1/128", "loopback"],
4851
+ ["fd00:ec2::254/128", "cloud-metadata"],
4852
+ ["fc00::/7", "private"],
4853
+ ["fe80::/10", "link-local"],
4854
+ ["fec0::/10", "site-local"],
4855
+ // RFC 8215 local-use NAT64: where the IPv4 address sits depends on the operator's prefix length.
4856
+ ["64:ff9b:1::/48", "local-use-nat64"],
4857
+ ["ff00::/8", "multicast"]
4858
+ ],
4859
+ expandIpv6
4860
+ );
4861
+ var IPV4_EMBEDDINGS = compileBlocks(
4862
+ [
4863
+ ["::ffff:0:0/96", 6],
4864
+ ["::ffff:0:0:0/96", 6],
4865
+ ["::/96", 6],
4866
+ ["64:ff9b::/96", 6],
4867
+ ["2002::/16", 1]
4868
+ ],
4869
+ expandIpv6
4870
+ );
4871
+ function parseIpv4(address) {
4872
+ const parts = address.split(".");
4873
+ if (parts.length !== 4 || !parts.every((part) => IPV4_OCTET_PATTERN.test(part))) return void 0;
4874
+ const octets = parts.map(Number);
4875
+ return octets.every((octet) => octet <= 255) ? octets : void 0;
4876
+ }
4877
+ function expandIpv6(address) {
4878
+ const zoneIndex = address.indexOf("%");
4879
+ let text = (zoneIndex === -1 ? address : address.slice(0, zoneIndex)).toLowerCase();
4880
+ const lastColonIndex = text.lastIndexOf(":");
4881
+ const trailingSegment = text.slice(lastColonIndex + 1);
4882
+ if (lastColonIndex !== -1 && trailingSegment.includes(".")) {
4883
+ const octets = parseIpv4(trailingSegment);
4884
+ if (!octets) return void 0;
4885
+ const highGroup = (octets[0] << 8 | octets[1]).toString(16);
4886
+ const lowGroup = (octets[2] << 8 | octets[3]).toString(16);
4887
+ text = `${text.slice(0, lastColonIndex + 1)}${highGroup}:${lowGroup}`;
4888
+ }
4889
+ const halves = text.split("::");
4890
+ if (halves.length > 2) return void 0;
4891
+ const head = parseIpv6Groups(halves[0]);
4892
+ if (!head) return void 0;
4893
+ if (halves.length === 1) return head.length === 8 ? head : void 0;
4894
+ const tail = parseIpv6Groups(halves[1]);
4895
+ if (!tail || head.length + tail.length > 7) return void 0;
4896
+ return [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail];
4897
+ }
4898
+ function extractEmbeddedIpv4(groups) {
4899
+ const octets = findEmbeddedIpv4Octets(groups);
4900
+ return octets ? octets.join(".") : void 0;
4901
+ }
4902
+ function classifyIpAddress(address) {
4903
+ const isBracketed = address.startsWith("[") && address.endsWith("]");
4904
+ const octets = isBracketed ? void 0 : parseIpv4(address);
4905
+ if (octets) return classifyIpv4(octets);
4906
+ const groups = expandIpv6(isBracketed ? address.slice(1, -1) : address);
4907
+ if (!groups) return void 0;
4908
+ const block = IPV6_BLOCKS.find((candidate) => matchesBlock(groups, candidate, 16));
4909
+ if (block) return { family: 6, range: block.value, cidr: block.cidr };
4910
+ const embeddedOctets = findEmbeddedIpv4Octets(groups);
4911
+ if (!embeddedOctets) return { family: 6, range: "public" };
4912
+ return { ...classifyIpv4(embeddedOctets), family: 6, embeddedIpv4: embeddedOctets.join(".") };
4913
+ }
4914
+ function classifyIpv4(octets) {
4915
+ const block = IPV4_BLOCKS.find((candidate) => matchesBlock(octets, candidate, 8));
4916
+ return block ? { family: 4, range: block.value, cidr: block.cidr } : { family: 4, range: "public" };
4917
+ }
4918
+ function findEmbeddedIpv4Octets(groups) {
4919
+ const embedding = IPV4_EMBEDDINGS.find((block) => matchesBlock(groups, block, 16));
4920
+ if (!embedding) return void 0;
4921
+ const highGroup = groups[embedding.value];
4922
+ const lowGroup = groups[embedding.value + 1];
4923
+ return [highGroup >> 8, highGroup & 255, lowGroup >> 8, lowGroup & 255];
4924
+ }
4925
+ function parseIpv6Groups(part) {
4926
+ if (part === "") return [];
4927
+ const groups = part.split(":");
4928
+ if (!groups.every((group) => IPV6_GROUP_PATTERN.test(group))) return void 0;
4929
+ return groups.map((group) => parseInt(group, 16));
4930
+ }
4931
+ function matchesBlock(words, block, bitsPerWord) {
4932
+ let remainingBits = block.prefixLength;
4933
+ for (let index = 0; remainingBits > 0; index++) {
4934
+ const comparedBits = Math.min(bitsPerWord, remainingBits);
4935
+ const ignoredBits = bitsPerWord - comparedBits;
4936
+ if (words[index] >> ignoredBits !== block.network[index] >> ignoredBits) return false;
4937
+ remainingBits -= comparedBits;
4938
+ }
4939
+ return true;
4940
+ }
4941
+ function compileBlocks(entries, parseNetwork) {
4942
+ return entries.map(([cidr, value]) => {
4943
+ const [networkText, prefixText] = cidr.split("/");
4944
+ const network = parseNetwork(networkText);
4945
+ if (!network) throw new Error(`Invalid network in address block "${cidr}"`);
4946
+ return { cidr, network, prefixLength: Number(prefixText), value };
4947
+ });
4948
+ }
4949
+
4950
+ // libs/utils/src/net/redirect.ts
4951
+ function isRedirectResponse(response) {
4952
+ return response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
4953
+ }
4954
+
4815
4955
  // libs/utils/src/index.ts
4816
4956
  init_fs2();
4817
4957
 
@@ -5991,6 +6131,7 @@ function setMachineIdOverride(id) {
5991
6131
  buildSetCookie,
5992
6132
  bytesToHex,
5993
6133
  checkEntryAvailability,
6134
+ classifyIpAddress,
5994
6135
  clearCachedSecret,
5995
6136
  collapseChar,
5996
6137
  collapseWhitespace,
@@ -6023,11 +6164,13 @@ function setMachineIdOverride(id) {
6023
6164
  escapeHtmlAttr,
6024
6165
  escapeJsString,
6025
6166
  escapeScriptClose,
6167
+ expandIpv6,
6026
6168
  expandTemplate,
6027
6169
  expandUriTemplate,
6028
6170
  expiresAtToTTL,
6029
6171
  extname,
6030
6172
  extractBracedParams,
6173
+ extractEmbeddedIpv4,
6031
6174
  extractTemplateParams,
6032
6175
  extractUriScheme,
6033
6176
  fileExists,
@@ -6067,6 +6210,7 @@ function setMachineIdOverride(id) {
6067
6210
  isNode,
6068
6211
  isPatternSafe,
6069
6212
  isProduction,
6213
+ isRedirectResponse,
6070
6214
  isRsaPssAlg,
6071
6215
  isSecretCached,
6072
6216
  isSecretKeyData,
@@ -6089,6 +6233,7 @@ function setMachineIdOverride(id) {
6089
6233
  mkdtemp,
6090
6234
  normalizeTTL,
6091
6235
  parseCookies,
6236
+ parseIpv4,
6092
6237
  parseKeyData,
6093
6238
  parseSecretData,
6094
6239
  parseUriTemplate,
package/net/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { classifyIpAddress, expandIpv6, extractEmbeddedIpv4, parseIpv4 } from './ip-address';
2
+ export type { IpAddressClassification, IpAddressRange, SpecialPurposeIpRange } from './ip-address';
3
+ export { isRedirectResponse } from './redirect';
4
+ export type { RedirectCandidate } from './redirect';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * IP-literal parsing and special-purpose range classification, shared by every SSRF guard.
3
+ *
4
+ * An IPv6 address has many spellings of the same destination (`::ffff:169.254.169.254`,
5
+ * `[::ffff:a9fe:a9fe]`, `0:0:0:0:0:ffff:a9fe:a9fe`) and several forms that carry an IPv4 address
6
+ * a translator or tunnel will reach. Guards therefore classify the parsed value, never the text.
7
+ */
8
+ export type SpecialPurposeIpRange = 'unspecified' | 'this-network' | 'loopback' | 'private' | 'carrier-grade-nat' | 'link-local' | 'cloud-metadata' | 'ietf-protocol' | 'benchmarking' | 'site-local' | 'multicast' | 'local-use-nat64' | 'reserved';
9
+ export type IpAddressRange = 'public' | SpecialPurposeIpRange;
10
+ /**
11
+ * `cidr` is the special-purpose block that matched (e.g. `172.16.0.0/12`); `embeddedIpv4` is the
12
+ * IPv4 address an IPv6 address carries (mapped, translated, compatible, NAT64 or 6to4).
13
+ */
14
+ export type IpAddressClassification = {
15
+ family: 4 | 6;
16
+ embeddedIpv4?: string;
17
+ } & ({
18
+ range: 'public';
19
+ } | {
20
+ range: SpecialPurposeIpRange;
21
+ cidr: string;
22
+ });
23
+ /** Parse a strict dotted-quad IPv4 address; leading zeros are refused because some stacks read them as octal. */
24
+ export declare function parseIpv4(address: string): number[] | undefined;
25
+ /** Expand an IPv6 address (zone id stripped, trailing dotted quad allowed) to its eight 16-bit groups. */
26
+ export declare function expandIpv6(address: string): number[] | undefined;
27
+ /** The IPv4 address carried by an expanded IPv6 address, where it carries one. */
28
+ export declare function extractEmbeddedIpv4(groups: readonly number[]): string | undefined;
29
+ /**
30
+ * Classify an IPv4 or IPv6 literal (IPv6 optionally in URL brackets) by the special-purpose block
31
+ * it falls in. An IPv6 address that carries an IPv4 address is judged by that IPv4 address.
32
+ *
33
+ * @returns undefined when the input is not a well-formed IP literal.
34
+ */
35
+ export declare function classifyIpAddress(address: string): IpAddressClassification | undefined;
@@ -0,0 +1,9 @@
1
+ export interface RedirectCandidate {
2
+ status: number;
3
+ type?: string;
4
+ }
5
+ /**
6
+ * Whether a `fetch` response made with `redirect: 'manual'` is a redirect. Browser runtimes return
7
+ * an `opaqueredirect` response with status 0 and no readable `Location`, so status alone misses it.
8
+ */
9
+ export declare function isRedirectResponse(response: RedirectCandidate): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/utils",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Shared utility functions for FrontMCP - string manipulation, URI handling, path utilities, and more",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "@noble/hashes": "^2.0.1"
33
33
  },
34
34
  "peerDependencies": {
35
- "@frontmcp/lazy-zod": "1.8.0",
35
+ "@frontmcp/lazy-zod": "1.8.1",
36
36
  "@vercel/kv": "^2.0.0 || ^3.0.0",
37
37
  "ioredis": "^5.0.0"
38
38
  },