@ligelog/redact 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ligelog contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createRedactHook: () => createRedactHook
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ function compilePaths(paths) {
27
+ return paths.map((p) => p.split("."));
28
+ }
29
+ function applyRedaction(obj, compiledPaths, censorFn, remove) {
30
+ let result = obj;
31
+ for (const segments of compiledPaths) {
32
+ result = redactPath(result, segments, 0, censorFn, remove, "", /* @__PURE__ */ new Set());
33
+ }
34
+ return result;
35
+ }
36
+ function redactPath(obj, segments, index, censorFn, remove, currentPath, seen) {
37
+ if (index >= segments.length) return obj;
38
+ if (seen.has(obj)) return obj;
39
+ seen.add(obj);
40
+ const segment = segments[index];
41
+ const isLast = index === segments.length - 1;
42
+ const keys = segment === "*" ? Object.keys(obj) : [segment];
43
+ let result = obj;
44
+ let cloned = false;
45
+ for (const key of keys) {
46
+ if (!Object.hasOwn(obj, key)) continue;
47
+ const fullPath = currentPath ? `${currentPath}.${key}` : key;
48
+ if (isLast) {
49
+ if (!cloned) {
50
+ result = { ...obj };
51
+ cloned = true;
52
+ }
53
+ if (remove) {
54
+ delete result[key];
55
+ } else {
56
+ result[key] = censorFn(obj[key], fullPath);
57
+ }
58
+ } else {
59
+ const child = obj[key];
60
+ if (child !== null && typeof child === "object" && !Array.isArray(child)) {
61
+ const updated = redactPath(
62
+ child,
63
+ segments,
64
+ index + 1,
65
+ censorFn,
66
+ remove,
67
+ fullPath,
68
+ seen
69
+ );
70
+ if (updated !== child) {
71
+ if (!cloned) {
72
+ result = { ...obj };
73
+ cloned = true;
74
+ }
75
+ result[key] = updated;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ seen.delete(obj);
81
+ return result;
82
+ }
83
+ var CENSOR_ERROR_FALLBACK = "[CENSOR_ERROR]";
84
+ var DEFAULT_CENSOR = "[REDACTED]";
85
+ function createRedactHook(opts) {
86
+ const { paths, censor = DEFAULT_CENSOR, remove = false } = opts;
87
+ if (paths.length === 0) {
88
+ return {};
89
+ }
90
+ const compiledPaths = compilePaths(paths);
91
+ const censorFn = typeof censor === "function" ? (value, path) => {
92
+ try {
93
+ return censor(value, path);
94
+ } catch {
95
+ return CENSOR_ERROR_FALLBACK;
96
+ }
97
+ } : () => censor;
98
+ return {
99
+ onBeforeWrite: [
100
+ (ctx) => {
101
+ const redacted = applyRedaction(
102
+ ctx.record,
103
+ compiledPaths,
104
+ censorFn,
105
+ remove
106
+ );
107
+ if (redacted === ctx.record) {
108
+ return ctx;
109
+ }
110
+ return { ...ctx, record: redacted };
111
+ }
112
+ ]
113
+ };
114
+ }
115
+ // Annotate the CommonJS export names for ESM import in node:
116
+ 0 && (module.exports = {
117
+ createRedactHook
118
+ });
@@ -0,0 +1,75 @@
1
+ import { Hooks } from 'ligelog';
2
+
3
+ /**
4
+ * @file index.ts
5
+ * PII field masking hook for ligelog.
6
+ *
7
+ * Redacts sensitive fields from LogRecord before serialization using
8
+ * dot-path patterns with wildcard support.
9
+ *
10
+ * ## Setup
11
+ *
12
+ * ```ts
13
+ * import { createLogger } from 'ligelog';
14
+ * import { createRedactHook } from '@ligelog/redact';
15
+ *
16
+ * const logger = createLogger();
17
+ * logger.use(createRedactHook({ paths: ['password', 'user.*.ssn'] }));
18
+ * ```
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+
23
+ /** Allowed return types for a censor function. */
24
+ type CensorValue = string | number | boolean | null | undefined;
25
+ /** Options accepted by `createRedactHook`. */
26
+ interface RedactOptions {
27
+ /**
28
+ * Dot-separated field paths to redact.
29
+ * Supports `*` as a wildcard to match any key at that level.
30
+ *
31
+ * @example ['password', 'headers.authorization', 'user.*.ssn']
32
+ */
33
+ readonly paths: readonly string[];
34
+ /**
35
+ * Replacement value or function for redacted fields.
36
+ * A function receives the original value and the full dot-path.
37
+ * If the function throws, `'[CENSOR_ERROR]'` is used as a fallback.
38
+ * @default '[REDACTED]'
39
+ */
40
+ censor?: string | ((value: unknown, path: string) => CensorValue);
41
+ /**
42
+ * When `true`, redacted keys are removed entirely instead of replaced.
43
+ * @default false
44
+ */
45
+ remove?: boolean;
46
+ }
47
+ /**
48
+ * Create a ligelog hook that redacts sensitive fields from log records.
49
+ *
50
+ * Runs as an `onBeforeWrite` hook using Copy-on-Write semantics:
51
+ * the original `ctx.record` is never mutated.
52
+ *
53
+ * @param opts - Redaction configuration.
54
+ * @returns A `Hooks` object ready for `logger.use()`.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * // Basic usage
59
+ * logger.use(createRedactHook({ paths: ['password', 'token'] }));
60
+ *
61
+ * // Custom censor function
62
+ * logger.use(createRedactHook({
63
+ * paths: ['email'],
64
+ * censor: (value) => typeof value === 'string'
65
+ * ? value.replace(/(.{2}).+(@.+)/, '$1***$2')
66
+ * : '[REDACTED]',
67
+ * }));
68
+ *
69
+ * // Remove keys entirely
70
+ * logger.use(createRedactHook({ paths: ['secret'], remove: true }));
71
+ * ```
72
+ */
73
+ declare function createRedactHook(opts: RedactOptions): Hooks;
74
+
75
+ export { type CensorValue, type RedactOptions, createRedactHook };
@@ -0,0 +1,75 @@
1
+ import { Hooks } from 'ligelog';
2
+
3
+ /**
4
+ * @file index.ts
5
+ * PII field masking hook for ligelog.
6
+ *
7
+ * Redacts sensitive fields from LogRecord before serialization using
8
+ * dot-path patterns with wildcard support.
9
+ *
10
+ * ## Setup
11
+ *
12
+ * ```ts
13
+ * import { createLogger } from 'ligelog';
14
+ * import { createRedactHook } from '@ligelog/redact';
15
+ *
16
+ * const logger = createLogger();
17
+ * logger.use(createRedactHook({ paths: ['password', 'user.*.ssn'] }));
18
+ * ```
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+
23
+ /** Allowed return types for a censor function. */
24
+ type CensorValue = string | number | boolean | null | undefined;
25
+ /** Options accepted by `createRedactHook`. */
26
+ interface RedactOptions {
27
+ /**
28
+ * Dot-separated field paths to redact.
29
+ * Supports `*` as a wildcard to match any key at that level.
30
+ *
31
+ * @example ['password', 'headers.authorization', 'user.*.ssn']
32
+ */
33
+ readonly paths: readonly string[];
34
+ /**
35
+ * Replacement value or function for redacted fields.
36
+ * A function receives the original value and the full dot-path.
37
+ * If the function throws, `'[CENSOR_ERROR]'` is used as a fallback.
38
+ * @default '[REDACTED]'
39
+ */
40
+ censor?: string | ((value: unknown, path: string) => CensorValue);
41
+ /**
42
+ * When `true`, redacted keys are removed entirely instead of replaced.
43
+ * @default false
44
+ */
45
+ remove?: boolean;
46
+ }
47
+ /**
48
+ * Create a ligelog hook that redacts sensitive fields from log records.
49
+ *
50
+ * Runs as an `onBeforeWrite` hook using Copy-on-Write semantics:
51
+ * the original `ctx.record` is never mutated.
52
+ *
53
+ * @param opts - Redaction configuration.
54
+ * @returns A `Hooks` object ready for `logger.use()`.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * // Basic usage
59
+ * logger.use(createRedactHook({ paths: ['password', 'token'] }));
60
+ *
61
+ * // Custom censor function
62
+ * logger.use(createRedactHook({
63
+ * paths: ['email'],
64
+ * censor: (value) => typeof value === 'string'
65
+ * ? value.replace(/(.{2}).+(@.+)/, '$1***$2')
66
+ * : '[REDACTED]',
67
+ * }));
68
+ *
69
+ * // Remove keys entirely
70
+ * logger.use(createRedactHook({ paths: ['secret'], remove: true }));
71
+ * ```
72
+ */
73
+ declare function createRedactHook(opts: RedactOptions): Hooks;
74
+
75
+ export { type CensorValue, type RedactOptions, createRedactHook };
package/dist/index.js ADDED
@@ -0,0 +1,93 @@
1
+ // src/index.ts
2
+ function compilePaths(paths) {
3
+ return paths.map((p) => p.split("."));
4
+ }
5
+ function applyRedaction(obj, compiledPaths, censorFn, remove) {
6
+ let result = obj;
7
+ for (const segments of compiledPaths) {
8
+ result = redactPath(result, segments, 0, censorFn, remove, "", /* @__PURE__ */ new Set());
9
+ }
10
+ return result;
11
+ }
12
+ function redactPath(obj, segments, index, censorFn, remove, currentPath, seen) {
13
+ if (index >= segments.length) return obj;
14
+ if (seen.has(obj)) return obj;
15
+ seen.add(obj);
16
+ const segment = segments[index];
17
+ const isLast = index === segments.length - 1;
18
+ const keys = segment === "*" ? Object.keys(obj) : [segment];
19
+ let result = obj;
20
+ let cloned = false;
21
+ for (const key of keys) {
22
+ if (!Object.hasOwn(obj, key)) continue;
23
+ const fullPath = currentPath ? `${currentPath}.${key}` : key;
24
+ if (isLast) {
25
+ if (!cloned) {
26
+ result = { ...obj };
27
+ cloned = true;
28
+ }
29
+ if (remove) {
30
+ delete result[key];
31
+ } else {
32
+ result[key] = censorFn(obj[key], fullPath);
33
+ }
34
+ } else {
35
+ const child = obj[key];
36
+ if (child !== null && typeof child === "object" && !Array.isArray(child)) {
37
+ const updated = redactPath(
38
+ child,
39
+ segments,
40
+ index + 1,
41
+ censorFn,
42
+ remove,
43
+ fullPath,
44
+ seen
45
+ );
46
+ if (updated !== child) {
47
+ if (!cloned) {
48
+ result = { ...obj };
49
+ cloned = true;
50
+ }
51
+ result[key] = updated;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ seen.delete(obj);
57
+ return result;
58
+ }
59
+ var CENSOR_ERROR_FALLBACK = "[CENSOR_ERROR]";
60
+ var DEFAULT_CENSOR = "[REDACTED]";
61
+ function createRedactHook(opts) {
62
+ const { paths, censor = DEFAULT_CENSOR, remove = false } = opts;
63
+ if (paths.length === 0) {
64
+ return {};
65
+ }
66
+ const compiledPaths = compilePaths(paths);
67
+ const censorFn = typeof censor === "function" ? (value, path) => {
68
+ try {
69
+ return censor(value, path);
70
+ } catch {
71
+ return CENSOR_ERROR_FALLBACK;
72
+ }
73
+ } : () => censor;
74
+ return {
75
+ onBeforeWrite: [
76
+ (ctx) => {
77
+ const redacted = applyRedaction(
78
+ ctx.record,
79
+ compiledPaths,
80
+ censorFn,
81
+ remove
82
+ );
83
+ if (redacted === ctx.record) {
84
+ return ctx;
85
+ }
86
+ return { ...ctx, record: redacted };
87
+ }
88
+ ]
89
+ };
90
+ }
91
+ export {
92
+ createRedactHook
93
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ligelog/redact",
3
+ "version": "1.0.0",
4
+ "description": "PII field masking hook for ligelog — redact sensitive data from log records",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "ligelog",
23
+ "redact",
24
+ "pii",
25
+ "gdpr",
26
+ "masking",
27
+ "logging",
28
+ "hook"
29
+ ],
30
+ "author": "Akihiro Seino",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/seino/ligelog.git",
35
+ "directory": "packages/redact"
36
+ },
37
+ "homepage": "https://github.com/seino/ligelog#ecosystem",
38
+ "bugs": {
39
+ "url": "https://github.com/seino/ligelog/issues"
40
+ },
41
+ "devDependencies": {
42
+ "ligelog": "1.1.0"
43
+ },
44
+ "peerDependencies": {
45
+ "ligelog": ">=1.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "provenance": true
53
+ },
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
56
+ "test": "vitest run",
57
+ "typecheck": "tsc --noEmit"
58
+ }
59
+ }