@hackylabs/deep-redact 4.0.0 → 4.0.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.
package/README.md CHANGED
@@ -43,7 +43,7 @@ bun add @hackylabs/deep-redact
43
43
  ```json
44
44
  {
45
45
  "imports": {
46
- "@hackylabs/deep-redact": "npm:@hackylabs/deep-redact@4.0.0"
46
+ "@hackylabs/deep-redact": "npm:@hackylabs/deep-redact@4.0.1"
47
47
  }
48
48
  }
49
49
  ```
@@ -82,6 +82,8 @@ v4, v3, and fast-redact all return a plain JavaScript object.
82
82
 
83
83
  ![Speed comparison — structured output](docs/benchmarks/charts/speed-comparison-non-serialised.svg)
84
84
 
85
+ _† fast-redact is a third-party library, not a deep-redact version. It is shown as a throughput reference; its feature set and guarantees differ from deep-redact's, so it is not a like-for-like comparison._
86
+
85
87
  v4 is **~17× faster** than v3 on path-based workloads and **~10× faster** on wildcard workloads.
86
88
 
87
89
  ### Serialised output (`serialise: true`)
@@ -90,8 +92,14 @@ All four solutions return a JSON string.
90
92
 
91
93
  ![Speed comparison — serialised output](docs/benchmarks/charts/speed-comparison-serialised.svg)
92
94
 
95
+ _† fast-redact is a third-party library; json-stringify-regex is a naive native approach (`JSON.stringify(value).replace(pattern, replacement)`), not a library. Neither performs deep-redact's structured redaction or offers its guarantees, so both are shown as throughput references rather than like-for-like comparisons._
96
+
93
97
  v4 remains faster than v3 in serialised mode. fast-redact and json-stringify-regex have a throughput advantage because their output path is oriented entirely toward string production.
94
98
 
99
+ Against deep-redact v2 the serialised picture is mixed: v4 is roughly at parity on path-based workloads but slower on breadth-heavy (wildcard) ones. That gap is the cost of safety v2 does not provide — under `serialise: true`, v4 runs the type transformers that make `BigInt`, `Date`, `Map`, `Set`, `Error`, `RegExp`, and `URL` values JSON-safe, and it detects and neutralises circular references instead of throwing on them. (Node and depth budgeting via `maxNodes`/`maxDepth` is opt-in and unlimited by default, so it adds nothing unless you enable it.)
100
+
101
+ These safety passes are intrinsic to `serialise: true` and cannot be switched off individually. If you do not need those guarantees and want to match v2's throughput (or better), set `serialise: false` and run your own `JSON.stringify`: the structured-output path skips the transformer and circular-reference passes entirely. This restores v2's trade-offs too — your `JSON.stringify` will throw on `BigInt` and circular references, `Map`/`Set` serialise as `{}`, and `undefined` is dropped.
102
+
95
103
  Full speed and resource benchmark results: [`docs/benchmarks/speed-results.md`](docs/benchmarks/speed-results.md) and [`docs/benchmarks/resource-results.md`](docs/benchmarks/resource-results.md).
96
104
 
97
105
  ## Configuration
package/dist/index.cjs CHANGED
@@ -2630,6 +2630,8 @@ const validateConfig = (options) => {
2630
2630
  //#endregion
2631
2631
  //#region src/core/replacement/serialise-output.ts
2632
2632
  const bareIdentifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2633
+ const MAX_SERIALISE_DEPTH = 3e3;
2634
+ const NO_INDEX = -1;
2633
2635
  const buildObjectChildPath = (parentPath, key) => {
2634
2636
  if (!bareIdentifierPattern.test(key)) return `${parentPath ?? ""}["${key.replaceAll("\\", "\\\\").replaceAll("\"", String.raw`\"`)}"]`;
2635
2637
  return parentPath === void 0 ? key : `${parentPath}.${key}`;
@@ -2642,26 +2644,49 @@ const isStrictDescendantPath = (ancestor, path) => {
2642
2644
  if (ancestor === "") return true;
2643
2645
  return path.startsWith(`${ancestor}.`) || path.startsWith(`${ancestor}[`);
2644
2646
  };
2645
- const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, cycleRegistry) => {
2647
+ const materialiseFramePath = (frame) => {
2648
+ const chain = [];
2649
+ for (let node = frame; node !== void 0; node = node.parent) chain.push(node);
2650
+ let path;
2651
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
2652
+ const node = chain[i];
2653
+ if (node.index >= 0) path = buildArrayChildPath(path, node.index);
2654
+ else if (node.key !== void 0) path = buildObjectChildPath(path, node.key);
2655
+ }
2656
+ return path;
2657
+ };
2658
+ const materialiseStepPath = (parentFrame, key, index) => {
2659
+ const parentPath = materialiseFramePath(parentFrame);
2660
+ if (index >= 0) return buildArrayChildPath(parentPath, index);
2661
+ if (key !== void 0) return buildObjectChildPath(parentPath, key);
2662
+ return parentPath;
2663
+ };
2664
+ const materialiseAncestorPath = (frame, identity) => {
2665
+ for (let node = frame; node !== void 0; node = node.parent) if (node.identity === identity) return materialiseFramePath(node);
2666
+ };
2667
+ const buildSafeGraph = (value, transformers, seen, parentFrame, stepKey, stepIndex, cycleRegistry, depth) => {
2668
+ if (depth > MAX_SERIALISE_DEPTH) return "[UNSUPPORTED]";
2646
2669
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === void 0) return value;
2647
2670
  if (typeof value === "function" || typeof value === "symbol") return "[UNSUPPORTED]";
2648
2671
  const supportedKind = resolveSupportedTransformableValueKind(value);
2649
2672
  if (supportedKind !== void 0) {
2650
2673
  const runtimeIdentity = supportedKind === "bigint" ? void 0 : value;
2651
- if (runtimeIdentity !== void 0) {
2652
- if (seen.has(runtimeIdentity)) return {
2653
- _transformer: "circular",
2654
- path: currentPath ?? "",
2655
- value: identityPaths.get(runtimeIdentity) ?? ""
2656
- };
2657
- const currentPathStr = currentPath ?? "";
2658
- identityPaths.set(runtimeIdentity, currentPathStr);
2659
- seen.add(runtimeIdentity);
2660
- }
2674
+ if (runtimeIdentity !== void 0 && seen.has(runtimeIdentity)) return {
2675
+ _transformer: "circular",
2676
+ path: materialiseStepPath(parentFrame, stepKey, stepIndex) ?? "",
2677
+ value: materialiseAncestorPath(parentFrame, runtimeIdentity) ?? ""
2678
+ };
2679
+ const frame = {
2680
+ parent: parentFrame,
2681
+ key: stepKey,
2682
+ index: stepIndex,
2683
+ identity: runtimeIdentity
2684
+ };
2685
+ if (runtimeIdentity !== void 0) seen.add(runtimeIdentity);
2661
2686
  try {
2662
2687
  const transformed = resolveTransformedValue(value, transformers);
2663
2688
  if (transformed === void 0) return "[UNSUPPORTED]";
2664
- return buildSafeGraph(transformed, transformers, seen, identityPaths, currentPath, cycleRegistry);
2689
+ return buildTransformedGraph(transformed, transformers, seen, frame, cycleRegistry, depth + 1);
2665
2690
  } catch {
2666
2691
  return "[UNSUPPORTED]";
2667
2692
  } finally {
@@ -2671,19 +2696,24 @@ const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, c
2671
2696
  const identity = value;
2672
2697
  if (seen.has(identity)) return {
2673
2698
  _transformer: "circular",
2674
- path: currentPath ?? "",
2675
- value: identityPaths.get(identity) ?? ""
2699
+ path: materialiseStepPath(parentFrame, stepKey, stepIndex) ?? "",
2700
+ value: materialiseAncestorPath(parentFrame, identity) ?? ""
2676
2701
  };
2677
2702
  if (cycleRegistry?.has(identity)) {
2678
2703
  const registryPath = cycleRegistry.get(identity);
2679
- if (isStrictDescendantPath(registryPath, currentPath ?? "")) return {
2704
+ const currentPath = materialiseStepPath(parentFrame, stepKey, stepIndex) ?? "";
2705
+ if (isStrictDescendantPath(registryPath, currentPath)) return {
2680
2706
  _transformer: "circular",
2681
- path: currentPath ?? "",
2707
+ path: currentPath,
2682
2708
  value: registryPath
2683
2709
  };
2684
2710
  }
2685
- const currentPathStr = currentPath ?? "";
2686
- identityPaths.set(identity, currentPathStr);
2711
+ const frame = {
2712
+ parent: parentFrame,
2713
+ key: stepKey,
2714
+ index: stepIndex,
2715
+ identity
2716
+ };
2687
2717
  seen.add(identity);
2688
2718
  try {
2689
2719
  if (Array.isArray(value)) {
@@ -2691,18 +2721,18 @@ const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, c
2691
2721
  result.length = value.length;
2692
2722
  for (let index = 0; index < value.length; index += 1) {
2693
2723
  if (!(index in value)) continue;
2694
- result[index] = buildSafeGraph(value[index], transformers, seen, identityPaths, buildArrayChildPath(currentPath, index), cycleRegistry);
2724
+ result[index] = buildSafeGraph(value[index], transformers, seen, frame, void 0, index, cycleRegistry, depth + 1);
2695
2725
  }
2696
2726
  return result;
2697
2727
  }
2698
2728
  if (isPlainObject$1(value)) {
2699
2729
  const result = {};
2700
- for (const key of Object.keys(value)) result[key] = buildSafeGraph(value[key], transformers, seen, identityPaths, buildObjectChildPath(currentPath, key), cycleRegistry);
2730
+ for (const key of Object.keys(value)) result[key] = buildSafeGraph(value[key], transformers, seen, frame, key, NO_INDEX, cycleRegistry, depth + 1);
2701
2731
  return result;
2702
2732
  }
2703
2733
  try {
2704
2734
  const transformed = resolveTransformedValue(value, transformers);
2705
- if (transformed !== void 0) return buildSafeGraph(transformed, transformers, seen, identityPaths, currentPath, cycleRegistry);
2735
+ if (transformed !== void 0) return buildTransformedGraph(transformed, transformers, seen, frame, cycleRegistry, depth + 1);
2706
2736
  } catch {
2707
2737
  return "[UNSUPPORTED]";
2708
2738
  }
@@ -2711,9 +2741,18 @@ const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, c
2711
2741
  seen.delete(identity);
2712
2742
  }
2713
2743
  };
2744
+ const buildTransformedGraph = (transformed, transformers, seen, containerFrame, cycleRegistry, depth) => {
2745
+ if (isPlainObject$1(transformed) && typeof transformed._transformer === "string" && "value" in transformed) {
2746
+ const wrapper = transformed;
2747
+ const result = {};
2748
+ for (const key of Object.keys(wrapper)) result[key] = key === "value" ? buildSafeGraph(wrapper.value, transformers, seen, containerFrame, void 0, NO_INDEX, cycleRegistry, depth + 1) : buildSafeGraph(wrapper[key], transformers, seen, containerFrame, key, NO_INDEX, cycleRegistry, depth + 1);
2749
+ return result;
2750
+ }
2751
+ return buildSafeGraph(transformed, transformers, seen, containerFrame, void 0, NO_INDEX, cycleRegistry, depth + 1);
2752
+ };
2714
2753
  const serialiseOutput = (value, transformers, serialise, cycleRegistry) => {
2715
2754
  if (!serialise) return value;
2716
- const safeGraph = value === void 0 ? "[UNSUPPORTED]" : buildSafeGraph(value, transformers, /* @__PURE__ */ new WeakSet(), /* @__PURE__ */ new WeakMap(), void 0, cycleRegistry);
2755
+ const safeGraph = value === void 0 ? "[UNSUPPORTED]" : buildSafeGraph(value, transformers, /* @__PURE__ */ new WeakSet(), void 0, void 0, NO_INDEX, cycleRegistry, 0);
2717
2756
  if (serialise === true) return JSON.stringify(safeGraph);
2718
2757
  return serialise(safeGraph);
2719
2758
  };
package/dist/index.js CHANGED
@@ -2629,6 +2629,8 @@ const validateConfig = (options) => {
2629
2629
  //#endregion
2630
2630
  //#region src/core/replacement/serialise-output.ts
2631
2631
  const bareIdentifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2632
+ const MAX_SERIALISE_DEPTH = 3e3;
2633
+ const NO_INDEX = -1;
2632
2634
  const buildObjectChildPath = (parentPath, key) => {
2633
2635
  if (!bareIdentifierPattern.test(key)) return `${parentPath ?? ""}["${key.replaceAll("\\", "\\\\").replaceAll("\"", String.raw`\"`)}"]`;
2634
2636
  return parentPath === void 0 ? key : `${parentPath}.${key}`;
@@ -2641,26 +2643,49 @@ const isStrictDescendantPath = (ancestor, path) => {
2641
2643
  if (ancestor === "") return true;
2642
2644
  return path.startsWith(`${ancestor}.`) || path.startsWith(`${ancestor}[`);
2643
2645
  };
2644
- const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, cycleRegistry) => {
2646
+ const materialiseFramePath = (frame) => {
2647
+ const chain = [];
2648
+ for (let node = frame; node !== void 0; node = node.parent) chain.push(node);
2649
+ let path;
2650
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
2651
+ const node = chain[i];
2652
+ if (node.index >= 0) path = buildArrayChildPath(path, node.index);
2653
+ else if (node.key !== void 0) path = buildObjectChildPath(path, node.key);
2654
+ }
2655
+ return path;
2656
+ };
2657
+ const materialiseStepPath = (parentFrame, key, index) => {
2658
+ const parentPath = materialiseFramePath(parentFrame);
2659
+ if (index >= 0) return buildArrayChildPath(parentPath, index);
2660
+ if (key !== void 0) return buildObjectChildPath(parentPath, key);
2661
+ return parentPath;
2662
+ };
2663
+ const materialiseAncestorPath = (frame, identity) => {
2664
+ for (let node = frame; node !== void 0; node = node.parent) if (node.identity === identity) return materialiseFramePath(node);
2665
+ };
2666
+ const buildSafeGraph = (value, transformers, seen, parentFrame, stepKey, stepIndex, cycleRegistry, depth) => {
2667
+ if (depth > MAX_SERIALISE_DEPTH) return "[UNSUPPORTED]";
2645
2668
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === void 0) return value;
2646
2669
  if (typeof value === "function" || typeof value === "symbol") return "[UNSUPPORTED]";
2647
2670
  const supportedKind = resolveSupportedTransformableValueKind(value);
2648
2671
  if (supportedKind !== void 0) {
2649
2672
  const runtimeIdentity = supportedKind === "bigint" ? void 0 : value;
2650
- if (runtimeIdentity !== void 0) {
2651
- if (seen.has(runtimeIdentity)) return {
2652
- _transformer: "circular",
2653
- path: currentPath ?? "",
2654
- value: identityPaths.get(runtimeIdentity) ?? ""
2655
- };
2656
- const currentPathStr = currentPath ?? "";
2657
- identityPaths.set(runtimeIdentity, currentPathStr);
2658
- seen.add(runtimeIdentity);
2659
- }
2673
+ if (runtimeIdentity !== void 0 && seen.has(runtimeIdentity)) return {
2674
+ _transformer: "circular",
2675
+ path: materialiseStepPath(parentFrame, stepKey, stepIndex) ?? "",
2676
+ value: materialiseAncestorPath(parentFrame, runtimeIdentity) ?? ""
2677
+ };
2678
+ const frame = {
2679
+ parent: parentFrame,
2680
+ key: stepKey,
2681
+ index: stepIndex,
2682
+ identity: runtimeIdentity
2683
+ };
2684
+ if (runtimeIdentity !== void 0) seen.add(runtimeIdentity);
2660
2685
  try {
2661
2686
  const transformed = resolveTransformedValue(value, transformers);
2662
2687
  if (transformed === void 0) return "[UNSUPPORTED]";
2663
- return buildSafeGraph(transformed, transformers, seen, identityPaths, currentPath, cycleRegistry);
2688
+ return buildTransformedGraph(transformed, transformers, seen, frame, cycleRegistry, depth + 1);
2664
2689
  } catch {
2665
2690
  return "[UNSUPPORTED]";
2666
2691
  } finally {
@@ -2670,19 +2695,24 @@ const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, c
2670
2695
  const identity = value;
2671
2696
  if (seen.has(identity)) return {
2672
2697
  _transformer: "circular",
2673
- path: currentPath ?? "",
2674
- value: identityPaths.get(identity) ?? ""
2698
+ path: materialiseStepPath(parentFrame, stepKey, stepIndex) ?? "",
2699
+ value: materialiseAncestorPath(parentFrame, identity) ?? ""
2675
2700
  };
2676
2701
  if (cycleRegistry?.has(identity)) {
2677
2702
  const registryPath = cycleRegistry.get(identity);
2678
- if (isStrictDescendantPath(registryPath, currentPath ?? "")) return {
2703
+ const currentPath = materialiseStepPath(parentFrame, stepKey, stepIndex) ?? "";
2704
+ if (isStrictDescendantPath(registryPath, currentPath)) return {
2679
2705
  _transformer: "circular",
2680
- path: currentPath ?? "",
2706
+ path: currentPath,
2681
2707
  value: registryPath
2682
2708
  };
2683
2709
  }
2684
- const currentPathStr = currentPath ?? "";
2685
- identityPaths.set(identity, currentPathStr);
2710
+ const frame = {
2711
+ parent: parentFrame,
2712
+ key: stepKey,
2713
+ index: stepIndex,
2714
+ identity
2715
+ };
2686
2716
  seen.add(identity);
2687
2717
  try {
2688
2718
  if (Array.isArray(value)) {
@@ -2690,18 +2720,18 @@ const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, c
2690
2720
  result.length = value.length;
2691
2721
  for (let index = 0; index < value.length; index += 1) {
2692
2722
  if (!(index in value)) continue;
2693
- result[index] = buildSafeGraph(value[index], transformers, seen, identityPaths, buildArrayChildPath(currentPath, index), cycleRegistry);
2723
+ result[index] = buildSafeGraph(value[index], transformers, seen, frame, void 0, index, cycleRegistry, depth + 1);
2694
2724
  }
2695
2725
  return result;
2696
2726
  }
2697
2727
  if (isPlainObject$1(value)) {
2698
2728
  const result = {};
2699
- for (const key of Object.keys(value)) result[key] = buildSafeGraph(value[key], transformers, seen, identityPaths, buildObjectChildPath(currentPath, key), cycleRegistry);
2729
+ for (const key of Object.keys(value)) result[key] = buildSafeGraph(value[key], transformers, seen, frame, key, NO_INDEX, cycleRegistry, depth + 1);
2700
2730
  return result;
2701
2731
  }
2702
2732
  try {
2703
2733
  const transformed = resolveTransformedValue(value, transformers);
2704
- if (transformed !== void 0) return buildSafeGraph(transformed, transformers, seen, identityPaths, currentPath, cycleRegistry);
2734
+ if (transformed !== void 0) return buildTransformedGraph(transformed, transformers, seen, frame, cycleRegistry, depth + 1);
2705
2735
  } catch {
2706
2736
  return "[UNSUPPORTED]";
2707
2737
  }
@@ -2710,9 +2740,18 @@ const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, c
2710
2740
  seen.delete(identity);
2711
2741
  }
2712
2742
  };
2743
+ const buildTransformedGraph = (transformed, transformers, seen, containerFrame, cycleRegistry, depth) => {
2744
+ if (isPlainObject$1(transformed) && typeof transformed._transformer === "string" && "value" in transformed) {
2745
+ const wrapper = transformed;
2746
+ const result = {};
2747
+ for (const key of Object.keys(wrapper)) result[key] = key === "value" ? buildSafeGraph(wrapper.value, transformers, seen, containerFrame, void 0, NO_INDEX, cycleRegistry, depth + 1) : buildSafeGraph(wrapper[key], transformers, seen, containerFrame, key, NO_INDEX, cycleRegistry, depth + 1);
2748
+ return result;
2749
+ }
2750
+ return buildSafeGraph(transformed, transformers, seen, containerFrame, void 0, NO_INDEX, cycleRegistry, depth + 1);
2751
+ };
2713
2752
  const serialiseOutput = (value, transformers, serialise, cycleRegistry) => {
2714
2753
  if (!serialise) return value;
2715
- const safeGraph = value === void 0 ? "[UNSUPPORTED]" : buildSafeGraph(value, transformers, /* @__PURE__ */ new WeakSet(), /* @__PURE__ */ new WeakMap(), void 0, cycleRegistry);
2754
+ const safeGraph = value === void 0 ? "[UNSUPPORTED]" : buildSafeGraph(value, transformers, /* @__PURE__ */ new WeakSet(), void 0, void 0, NO_INDEX, cycleRegistry, 0);
2716
2755
  if (serialise === true) return JSON.stringify(safeGraph);
2717
2756
  return serialise(safeGraph);
2718
2757
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackylabs/deep-redact",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Deeply redact sensitive data from objects, arrays and arbitrary strings (e.g. XML or raw cookies data) with a composable, function-first API.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "url": "git+https://github.com/hackylabs/deep-redact"
61
61
  },
62
62
  "//": [
63
- "deep-redact-v3 and fast-redact are installed only as internal benchmark comparisons and are not used in the library",
63
+ "deep-redact-v2, deep-redact-v3, deep-redact-v4-baseline (released 4.0.0) and fast-redact are installed only as internal benchmark comparisons and are not used in the library",
64
64
  "all dependencies are for development purposes only"
65
65
  ],
66
66
  "devDependencies": {
@@ -68,7 +68,9 @@
68
68
  "@stylistic/eslint-plugin": "4.4.1",
69
69
  "@types/fast-redact": "3.0.4",
70
70
  "@types/node": "24.1.0",
71
+ "deep-redact-v2": "npm:@hackylabs/deep-redact@2.2.1",
71
72
  "deep-redact-v3": "npm:@hackylabs/deep-redact@^3.0.0",
73
+ "deep-redact-v4-baseline": "npm:@hackylabs/deep-redact@4.0.0",
72
74
  "eslint": "9.39.4",
73
75
  "eslint-plugin-unicorn": "59.0.1",
74
76
  "fast-redact": "3.5.0",