mynth-logger 2.1.0 → 2.1.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/.github/workflows/CI.yaml +0 -3
- package/.github/workflows/publish.yml +5 -4
- package/dist/src/format.d.ts +3 -1
- package/dist/src/format.js +10 -4
- package/dist/src/logging.d.ts +2 -1
- package/dist/src/logging.js +5 -2
- package/dist/src/redact.d.ts +37 -2
- package/dist/src/redact.js +95 -57
- package/dist/tests/app.js +4 -1
- package/package.json +2 -2
- package/src/format.ts +12 -4
- package/src/logging.ts +7 -7
- package/src/redact.ts +188 -73
- package/tests/app.ts +6 -1
|
@@ -11,11 +11,13 @@ on:
|
|
|
11
11
|
- patch
|
|
12
12
|
- minor
|
|
13
13
|
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
14
18
|
jobs:
|
|
15
19
|
publish:
|
|
16
20
|
runs-on: ubuntu-latest
|
|
17
|
-
permissions:
|
|
18
|
-
contents: write
|
|
19
21
|
steps:
|
|
20
22
|
- name: Checkout code
|
|
21
23
|
uses: actions/checkout@v6.0.2
|
|
@@ -26,6 +28,7 @@ jobs:
|
|
|
26
28
|
uses: actions/setup-node@v6.2.0
|
|
27
29
|
with:
|
|
28
30
|
node-version: 24
|
|
31
|
+
registry-url: https://registry.npmjs.org
|
|
29
32
|
|
|
30
33
|
- uses: pnpm/action-setup@v4
|
|
31
34
|
|
|
@@ -43,8 +46,6 @@ jobs:
|
|
|
43
46
|
|
|
44
47
|
- name: Publish to npm
|
|
45
48
|
run: npm publish
|
|
46
|
-
env:
|
|
47
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
48
49
|
|
|
49
50
|
- name: Push to git repo
|
|
50
51
|
run: git push --follow-tags
|
package/dist/src/format.d.ts
CHANGED
package/dist/src/format.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { stringify } from "@ungap/structured-clone/json";
|
|
2
2
|
import { type } from "arktype";
|
|
3
|
-
import {
|
|
3
|
+
import { createRedact } from "./redact.js";
|
|
4
|
+
const config = {
|
|
5
|
+
redact: createRedact({}),
|
|
6
|
+
};
|
|
7
|
+
const updateConfig = (newConfig) => {
|
|
8
|
+
config.redact = createRedact(newConfig);
|
|
9
|
+
};
|
|
4
10
|
const ErrorType = type({
|
|
5
11
|
message: "string",
|
|
6
12
|
"stack?": "string",
|
|
@@ -11,7 +17,7 @@ const formatItem = (item) => {
|
|
|
11
17
|
// Remove colors from strings
|
|
12
18
|
if (typeof item === "string")
|
|
13
19
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape codes
|
|
14
|
-
return redact(item.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
20
|
+
return config.redact(item.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
15
21
|
// Check if this is an Error
|
|
16
22
|
const error = ErrorType(item);
|
|
17
23
|
if (!(error instanceof type.errors))
|
|
@@ -24,9 +30,9 @@ const formatItem = (item) => {
|
|
|
24
30
|
return String(item);
|
|
25
31
|
}
|
|
26
32
|
})();
|
|
27
|
-
return redact(stringified.replace(/^'|'$/g, ""));
|
|
33
|
+
return config.redact(stringified.replace(/^'|'$/g, ""));
|
|
28
34
|
};
|
|
29
35
|
const format = (items) => Array.from(items)
|
|
30
36
|
.map((item) => formatItem(item))
|
|
31
37
|
.join(" ");
|
|
32
|
-
export { format };
|
|
38
|
+
export { format, updateConfig };
|
package/dist/src/logging.d.ts
CHANGED
package/dist/src/logging.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { createConsola } from "consola";
|
|
2
|
+
import { updateConfig } from "./format.js";
|
|
2
3
|
import DatadogReporter from "./reporters/datadog.js";
|
|
3
4
|
import DiscordReporter from "./reporters/discord.js";
|
|
4
|
-
const setupLogging = () => {
|
|
5
|
+
const setupLogging = (config = {}) => {
|
|
6
|
+
updateConfig(config);
|
|
5
7
|
const consola = createConsola({ fancy: true, level: 5 });
|
|
6
8
|
if (process.env.NODE_ENV === "production")
|
|
7
9
|
consola.setReporters([DatadogReporter]);
|
|
8
10
|
// Set Discord reporter as first so it can remove
|
|
9
11
|
// Discord-related config before other reporters process the
|
|
10
12
|
// log
|
|
11
|
-
|
|
13
|
+
else
|
|
14
|
+
consola.setReporters([DiscordReporter, ...consola.options.reporters]);
|
|
12
15
|
consola.wrapConsole();
|
|
13
16
|
return consola;
|
|
14
17
|
};
|
package/dist/src/redact.d.ts
CHANGED
|
@@ -1,2 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Configurable redaction for strings that *look like secrets*:
|
|
3
|
+
* - hex (optionally 0x-prefixed)
|
|
4
|
+
* - base64 blobs
|
|
5
|
+
* - base58 blobs
|
|
6
|
+
* - BIP39 mnemonics (validated)
|
|
7
|
+
*
|
|
8
|
+
* Key idea:
|
|
9
|
+
* Each detector can be given "allow" context rules that prevent redaction when
|
|
10
|
+
* extra surrounding context indicates the value is safe/expected.
|
|
11
|
+
*/
|
|
12
|
+
type ContextRule = {
|
|
13
|
+
/**
|
|
14
|
+
* Test this regex against a slice of the input around the match.
|
|
15
|
+
* If it matches, the match is NOT redacted.
|
|
16
|
+
*/
|
|
17
|
+
re: RegExp;
|
|
18
|
+
/** How many chars to include before the match when building the slice. */
|
|
19
|
+
before?: number;
|
|
20
|
+
/** How many chars to include after the match when building the slice. */
|
|
21
|
+
after?: number;
|
|
22
|
+
};
|
|
23
|
+
type DetectorConfig = {
|
|
24
|
+
/**
|
|
25
|
+
* Any rule match => do not redact that specific match.
|
|
26
|
+
*/
|
|
27
|
+
allow?: ContextRule[];
|
|
28
|
+
};
|
|
29
|
+
type RedactConfig = {
|
|
30
|
+
hex?: DetectorConfig;
|
|
31
|
+
base64?: DetectorConfig;
|
|
32
|
+
base58?: DetectorConfig;
|
|
33
|
+
mnemonic?: DetectorConfig;
|
|
34
|
+
};
|
|
35
|
+
declare const createRedact: (config: RedactConfig) => (text: string) => string;
|
|
36
|
+
export { createRedact };
|
|
37
|
+
export type { RedactConfig };
|
package/dist/src/redact.js
CHANGED
|
@@ -2,67 +2,105 @@ import { DeepRedact } from "@hackylabs/deep-redact/index.ts";
|
|
|
2
2
|
import { validateMnemonic } from "@scure/bip39";
|
|
3
3
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
4
4
|
const replacement = "[REDACTED]";
|
|
5
|
+
const sliceAround = (value, offset, matchLen, rule) => {
|
|
6
|
+
const before = rule.before ?? 10;
|
|
7
|
+
const after = rule.after ?? 0;
|
|
8
|
+
const start = Math.max(0, offset - before);
|
|
9
|
+
const end = Math.min(value.length, offset + matchLen + after);
|
|
10
|
+
return value.slice(start, end);
|
|
11
|
+
};
|
|
12
|
+
const shouldAllowByRules = (value, match, offset, rules) => {
|
|
13
|
+
if (!rules?.length)
|
|
14
|
+
return false;
|
|
15
|
+
for (const rule of rules) {
|
|
16
|
+
const chunk = sliceAround(value, offset, match.length, rule);
|
|
17
|
+
if (rule.re.test(chunk))
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
};
|
|
22
|
+
const getReplaceMeta = (args) => {
|
|
23
|
+
const offset = args.at(-2);
|
|
24
|
+
const whole = args.at(-1);
|
|
25
|
+
if (typeof offset !== "number")
|
|
26
|
+
return null;
|
|
27
|
+
if (typeof whole !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
return { offset, whole };
|
|
30
|
+
};
|
|
31
|
+
const replaceAllMatchesWithContext = (value, pattern, replacement, allow) => value.replace(pattern, (...args) => {
|
|
32
|
+
const match = args[0];
|
|
33
|
+
if (typeof match !== "string")
|
|
34
|
+
return replacement;
|
|
35
|
+
const meta = getReplaceMeta(args);
|
|
36
|
+
if (!meta)
|
|
37
|
+
return replacement;
|
|
38
|
+
if (shouldAllowByRules(meta.whole, match, meta.offset, allow))
|
|
39
|
+
return match;
|
|
40
|
+
return replacement;
|
|
41
|
+
});
|
|
5
42
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* - long base64 blobs
|
|
10
|
-
* - long base58 blobs
|
|
11
|
-
* - mnemonic seed phrases (validated via BIP39 english wordlist)
|
|
43
|
+
* BIP39 contextual replacer:
|
|
44
|
+
* - Only redacts if the captured phrase validates as a BIP39 mnemonic
|
|
45
|
+
* - Still supports allow-rules (to suppress redaction in “safe” contexts)
|
|
12
46
|
*/
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
47
|
+
const replaceBip39MnemonicMatchesWithContext = (value, pattern, replacement, allow) => value.replace(pattern, (...args) => {
|
|
48
|
+
const match = args[0];
|
|
49
|
+
const phrase = args[1];
|
|
50
|
+
if (typeof match !== "string")
|
|
51
|
+
return replacement;
|
|
52
|
+
if (typeof phrase !== "string")
|
|
53
|
+
return match;
|
|
54
|
+
const meta = getReplaceMeta(args);
|
|
55
|
+
if (!meta)
|
|
56
|
+
return match;
|
|
57
|
+
if (shouldAllowByRules(meta.whole, match, meta.offset, allow))
|
|
58
|
+
return match;
|
|
18
59
|
const normalized = phrase.trim().toLowerCase().replace(/\s+/g, " ");
|
|
19
60
|
if (!validateMnemonic(normalized, wordlist))
|
|
20
61
|
return match;
|
|
21
|
-
// Replace only the phrase portion (preserves surrounding quotes/keywords if any)
|
|
22
62
|
return match.replace(phrase, replacement);
|
|
23
63
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const HEX =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
const redact = (text) => redactor.redact(text);
|
|
68
|
-
export { redact };
|
|
64
|
+
const createRedactor = (config = {}) => {
|
|
65
|
+
const HEX_MIN_LEN = 32;
|
|
66
|
+
const BASE64_MIN_BLOCKS = 8;
|
|
67
|
+
const BASE58_MIN_LEN = 32;
|
|
68
|
+
const HEX = new RegExp(String.raw `\b(?:0x)?[a-fA-F0-9]{${HEX_MIN_LEN},}\b`, "g");
|
|
69
|
+
const hexAllow = config.hex?.allow ?? [];
|
|
70
|
+
const BASE64 = new RegExp(String.raw `\b(?:[A-Za-z0-9+/]{4}){${BASE64_MIN_BLOCKS},}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\b`, "g");
|
|
71
|
+
const base64Allow = config.base64?.allow ?? [];
|
|
72
|
+
const BASE58 = new RegExp(String.raw `\b[1-9A-HJ-NP-Za-km-z]{${BASE58_MIN_LEN},}\b`, "g");
|
|
73
|
+
const base58Allow = config.base58?.allow ?? [];
|
|
74
|
+
const WORD = "[a-zA-Z]{2,8}";
|
|
75
|
+
const PHRASE_12_TO_24 = `(?:${WORD}\\s+){11,23}${WORD}`;
|
|
76
|
+
const MNEMONIC = new RegExp(String.raw `\b(${PHRASE_12_TO_24})\b`, "gi");
|
|
77
|
+
const mnemonicAllow = config.mnemonic?.allow ?? [];
|
|
78
|
+
const stringTests = [
|
|
79
|
+
{
|
|
80
|
+
pattern: HEX,
|
|
81
|
+
replacer: (v, p) => replaceAllMatchesWithContext(v, p, replacement, hexAllow),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
pattern: BASE64,
|
|
85
|
+
replacer: (v, p) => replaceAllMatchesWithContext(v, p, replacement, base64Allow),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
pattern: BASE58,
|
|
89
|
+
replacer: (v, p) => replaceAllMatchesWithContext(v, p, replacement, base58Allow),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: MNEMONIC,
|
|
93
|
+
replacer: (v, p) => replaceBip39MnemonicMatchesWithContext(v, p, replacement, mnemonicAllow),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
return new DeepRedact({
|
|
97
|
+
stringTests,
|
|
98
|
+
replacement,
|
|
99
|
+
serialise: false,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const createRedact = (config) => {
|
|
103
|
+
const redactor = createRedactor(config);
|
|
104
|
+
return (text) => redactor.redact(text);
|
|
105
|
+
};
|
|
106
|
+
export { createRedact };
|
package/dist/tests/app.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { setupLogging } from "../src/index.js";
|
|
2
2
|
const run = async () => {
|
|
3
|
-
setupLogging(
|
|
3
|
+
setupLogging({
|
|
4
|
+
hex: { allow: [{ re: /\b(intent|hash)\b/i }] },
|
|
5
|
+
});
|
|
4
6
|
console.log("Hello, this is a log");
|
|
5
7
|
console.info("Hello, this is an info log");
|
|
6
8
|
console.debug("Hello, this is a debug log");
|
|
@@ -8,6 +10,7 @@ const run = async () => {
|
|
|
8
10
|
console.error("Hello, this is an error log");
|
|
9
11
|
console.log("This is my seed phrase: ordinary quality amount solid fox guess peasant merit midnight noodle final brown pretty stable six fox beef engage waste uniform evoke flat survey crane");
|
|
10
12
|
console.log("This is my private key: 4fa7614bdd07ab15b11d2365466536ba160c06050ade0888a3aa985a5522ab22");
|
|
13
|
+
console.log("This is log for intent c8adcfef7255ad2c117f68111500997ab66de3c923e9ea9e71aaed5829d0acb8 and private key c99960eaeecb17d77d7c88f440383ccbaf93237471236a739185c0ebd62c5741");
|
|
11
14
|
try {
|
|
12
15
|
throw new Error("An Error was thrown");
|
|
13
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mynth-logger",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Package to format logs for mynth microservices.",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "
|
|
35
|
+
"url": "https://github.com/MynthAI/mynth-logger.git"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"arktype": "^2.1.29"
|
package/src/format.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { stringify } from "@ungap/structured-clone/json";
|
|
2
2
|
import { type } from "arktype";
|
|
3
|
-
import {
|
|
3
|
+
import { createRedact, type RedactConfig } from "./redact.js";
|
|
4
|
+
|
|
5
|
+
const config = {
|
|
6
|
+
redact: createRedact({}),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const updateConfig = (newConfig: RedactConfig) => {
|
|
10
|
+
config.redact = createRedact(newConfig);
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
const ErrorType = type({
|
|
6
14
|
message: "string",
|
|
@@ -13,7 +21,7 @@ const formatItem = (item: unknown): string => {
|
|
|
13
21
|
// Remove colors from strings
|
|
14
22
|
if (typeof item === "string")
|
|
15
23
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape codes
|
|
16
|
-
return redact(item.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
24
|
+
return config.redact(item.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
17
25
|
|
|
18
26
|
// Check if this is an Error
|
|
19
27
|
const error = ErrorType(item);
|
|
@@ -27,7 +35,7 @@ const formatItem = (item: unknown): string => {
|
|
|
27
35
|
}
|
|
28
36
|
})();
|
|
29
37
|
|
|
30
|
-
return redact(stringified.replace(/^'|'$/g, ""));
|
|
38
|
+
return config.redact(stringified.replace(/^'|'$/g, ""));
|
|
31
39
|
};
|
|
32
40
|
|
|
33
41
|
const format = (items: unknown[]): string =>
|
|
@@ -35,4 +43,4 @@ const format = (items: unknown[]): string =>
|
|
|
35
43
|
.map((item) => formatItem(item))
|
|
36
44
|
.join(" ");
|
|
37
45
|
|
|
38
|
-
export { format };
|
|
46
|
+
export { format, updateConfig };
|
package/src/logging.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createConsola } from "consola";
|
|
2
|
+
import { updateConfig } from "./format.js";
|
|
3
|
+
import type { RedactConfig } from "./redact.js";
|
|
2
4
|
import DatadogReporter from "./reporters/datadog.js";
|
|
3
5
|
import DiscordReporter from "./reporters/discord.js";
|
|
4
6
|
|
|
5
|
-
const setupLogging = () => {
|
|
6
|
-
|
|
7
|
+
const setupLogging = (config: RedactConfig = {}) => {
|
|
8
|
+
updateConfig(config);
|
|
9
|
+
const consola = createConsola({ fancy: true, level: 5 });
|
|
7
10
|
|
|
8
11
|
if (process.env.NODE_ENV === "production")
|
|
9
12
|
consola.setReporters([DatadogReporter]);
|
|
10
|
-
|
|
11
13
|
// Set Discord reporter as first so it can remove
|
|
12
14
|
// Discord-related config before other reporters process the
|
|
13
15
|
// log
|
|
14
|
-
consola.setReporters([DiscordReporter, ...consola.options.reporters]);
|
|
16
|
+
else consola.setReporters([DiscordReporter, ...consola.options.reporters]);
|
|
15
17
|
|
|
16
18
|
consola.wrapConsole();
|
|
17
19
|
return consola;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
type Options = ConsolaOptions & { fancy?: boolean };
|
|
21
|
-
|
|
22
22
|
export { setupLogging };
|
package/src/redact.ts
CHANGED
|
@@ -2,88 +2,203 @@ import { DeepRedact } from "@hackylabs/deep-redact/index.ts";
|
|
|
2
2
|
import { validateMnemonic } from "@scure/bip39";
|
|
3
3
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Configurable redaction for strings that *look like secrets*:
|
|
7
|
+
* - hex (optionally 0x-prefixed)
|
|
8
|
+
* - base64 blobs
|
|
9
|
+
* - base58 blobs
|
|
10
|
+
* - BIP39 mnemonics (validated)
|
|
11
|
+
*
|
|
12
|
+
* Key idea:
|
|
13
|
+
* Each detector can be given "allow" context rules that prevent redaction when
|
|
14
|
+
* extra surrounding context indicates the value is safe/expected.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type ContextRule = {
|
|
18
|
+
/**
|
|
19
|
+
* Test this regex against a slice of the input around the match.
|
|
20
|
+
* If it matches, the match is NOT redacted.
|
|
21
|
+
*/
|
|
22
|
+
re: RegExp;
|
|
23
|
+
/** How many chars to include before the match when building the slice. */
|
|
24
|
+
before?: number; // default 10
|
|
25
|
+
/** How many chars to include after the match when building the slice. */
|
|
26
|
+
after?: number; // default 0
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type DetectorConfig = {
|
|
30
|
+
/**
|
|
31
|
+
* Any rule match => do not redact that specific match.
|
|
32
|
+
*/
|
|
33
|
+
allow?: ContextRule[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type RedactConfig = {
|
|
37
|
+
hex?: DetectorConfig;
|
|
38
|
+
base64?: DetectorConfig;
|
|
39
|
+
base58?: DetectorConfig;
|
|
40
|
+
mnemonic?: DetectorConfig;
|
|
41
|
+
};
|
|
42
|
+
|
|
5
43
|
const replacement = "[REDACTED]";
|
|
6
44
|
|
|
45
|
+
const sliceAround = (
|
|
46
|
+
value: string,
|
|
47
|
+
offset: number,
|
|
48
|
+
matchLen: number,
|
|
49
|
+
rule: ContextRule,
|
|
50
|
+
) => {
|
|
51
|
+
const before = rule.before ?? 10;
|
|
52
|
+
const after = rule.after ?? 0;
|
|
53
|
+
|
|
54
|
+
const start = Math.max(0, offset - before);
|
|
55
|
+
const end = Math.min(value.length, offset + matchLen + after);
|
|
56
|
+
return value.slice(start, end);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const shouldAllowByRules = (
|
|
60
|
+
value: string,
|
|
61
|
+
match: string,
|
|
62
|
+
offset: number,
|
|
63
|
+
rules?: ContextRule[],
|
|
64
|
+
): boolean => {
|
|
65
|
+
if (!rules?.length) return false;
|
|
66
|
+
for (const rule of rules) {
|
|
67
|
+
const chunk = sliceAround(value, offset, match.length, rule);
|
|
68
|
+
if (rule.re.test(chunk)) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type ReplaceMeta = {
|
|
74
|
+
offset: number;
|
|
75
|
+
whole: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getReplaceMeta = (args: unknown[]): ReplaceMeta | null => {
|
|
79
|
+
const offset = args.at(-2);
|
|
80
|
+
const whole = args.at(-1);
|
|
81
|
+
if (typeof offset !== "number") return null;
|
|
82
|
+
if (typeof whole !== "string") return null;
|
|
83
|
+
return { offset, whole };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const replaceAllMatchesWithContext = (
|
|
87
|
+
value: string,
|
|
88
|
+
pattern: RegExp,
|
|
89
|
+
replacement: string,
|
|
90
|
+
allow?: ContextRule[],
|
|
91
|
+
) =>
|
|
92
|
+
value.replace(pattern, (...args: unknown[]) => {
|
|
93
|
+
const match = args[0];
|
|
94
|
+
if (typeof match !== "string") return replacement;
|
|
95
|
+
|
|
96
|
+
const meta = getReplaceMeta(args);
|
|
97
|
+
if (!meta) return replacement;
|
|
98
|
+
|
|
99
|
+
if (shouldAllowByRules(meta.whole, match, meta.offset, allow)) return match;
|
|
100
|
+
return replacement;
|
|
101
|
+
});
|
|
102
|
+
|
|
7
103
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* - long base64 blobs
|
|
12
|
-
* - long base58 blobs
|
|
13
|
-
* - mnemonic seed phrases (validated via BIP39 english wordlist)
|
|
104
|
+
* BIP39 contextual replacer:
|
|
105
|
+
* - Only redacts if the captured phrase validates as a BIP39 mnemonic
|
|
106
|
+
* - Still supports allow-rules (to suppress redaction in “safe” contexts)
|
|
14
107
|
*/
|
|
108
|
+
const replaceBip39MnemonicMatchesWithContext = (
|
|
109
|
+
value: string,
|
|
110
|
+
pattern: RegExp,
|
|
111
|
+
replacement: string,
|
|
112
|
+
allow?: ContextRule[],
|
|
113
|
+
) =>
|
|
114
|
+
value.replace(pattern, (...args: unknown[]) => {
|
|
115
|
+
const match = args[0];
|
|
116
|
+
const phrase = args[1];
|
|
117
|
+
|
|
118
|
+
if (typeof match !== "string") return replacement;
|
|
119
|
+
if (typeof phrase !== "string") return match;
|
|
120
|
+
|
|
121
|
+
const meta = getReplaceMeta(args);
|
|
122
|
+
if (!meta) return match;
|
|
15
123
|
|
|
16
|
-
|
|
17
|
-
const replaceAllMatches = (value: string, pattern: RegExp) =>
|
|
18
|
-
value.replace(pattern, replacement);
|
|
124
|
+
if (shouldAllowByRules(meta.whole, match, meta.offset, allow)) return match;
|
|
19
125
|
|
|
20
|
-
// Mnemonic replacer: only redact if the captured phrase is a valid BIP39 mnemonic.
|
|
21
|
-
const replaceBip39MnemonicMatches = (value: string, pattern: RegExp) =>
|
|
22
|
-
value.replace(pattern, (match: string, phrase: string) => {
|
|
23
|
-
// Normalize spacing; validateMnemonic expects words separated by single spaces
|
|
24
126
|
const normalized = phrase.trim().toLowerCase().replace(/\s+/g, " ");
|
|
25
127
|
if (!validateMnemonic(normalized, wordlist)) return match;
|
|
26
128
|
|
|
27
|
-
// Replace only the phrase portion (preserves surrounding quotes/keywords if any)
|
|
28
129
|
return match.replace(phrase, replacement);
|
|
29
130
|
});
|
|
30
131
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
String.raw`\b(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
const createRedactor = (config: RedactConfig = {}) => {
|
|
133
|
+
const HEX_MIN_LEN = 32;
|
|
134
|
+
const BASE64_MIN_BLOCKS = 8;
|
|
135
|
+
const BASE58_MIN_LEN = 32;
|
|
136
|
+
|
|
137
|
+
const HEX = new RegExp(
|
|
138
|
+
String.raw`\b(?:0x)?[a-fA-F0-9]{${HEX_MIN_LEN},}\b`,
|
|
139
|
+
"g",
|
|
140
|
+
);
|
|
141
|
+
const hexAllow: ContextRule[] = config.hex?.allow ?? [];
|
|
142
|
+
|
|
143
|
+
const BASE64 = new RegExp(
|
|
144
|
+
String.raw`\b(?:[A-Za-z0-9+/]{4}){${BASE64_MIN_BLOCKS},}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\b`,
|
|
145
|
+
"g",
|
|
146
|
+
);
|
|
147
|
+
const base64Allow = config.base64?.allow ?? [];
|
|
148
|
+
|
|
149
|
+
const BASE58 = new RegExp(
|
|
150
|
+
String.raw`\b[1-9A-HJ-NP-Za-km-z]{${BASE58_MIN_LEN},}\b`,
|
|
151
|
+
"g",
|
|
152
|
+
);
|
|
153
|
+
const base58Allow = config.base58?.allow ?? [];
|
|
154
|
+
|
|
155
|
+
const WORD = "[a-zA-Z]{2,8}";
|
|
156
|
+
const PHRASE_12_TO_24 = `(?:${WORD}\\s+){11,23}${WORD}`;
|
|
157
|
+
const MNEMONIC = new RegExp(String.raw`\b(${PHRASE_12_TO_24})\b`, "gi");
|
|
158
|
+
const mnemonicAllow = config.mnemonic?.allow ?? [];
|
|
159
|
+
|
|
160
|
+
const stringTests: Array<{
|
|
161
|
+
pattern: RegExp;
|
|
162
|
+
replacer: (v: string, p: RegExp) => string;
|
|
163
|
+
}> = [
|
|
164
|
+
{
|
|
165
|
+
pattern: HEX,
|
|
166
|
+
replacer: (v, p) =>
|
|
167
|
+
replaceAllMatchesWithContext(v, p, replacement, hexAllow),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
pattern: BASE64,
|
|
171
|
+
replacer: (v, p) =>
|
|
172
|
+
replaceAllMatchesWithContext(v, p, replacement, base64Allow),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
pattern: BASE58,
|
|
176
|
+
replacer: (v, p) =>
|
|
177
|
+
replaceAllMatchesWithContext(v, p, replacement, base58Allow),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
pattern: MNEMONIC,
|
|
181
|
+
replacer: (v, p) =>
|
|
182
|
+
replaceBip39MnemonicMatchesWithContext(
|
|
183
|
+
v,
|
|
184
|
+
p,
|
|
185
|
+
replacement,
|
|
186
|
+
mnemonicAllow,
|
|
187
|
+
),
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
return new DeepRedact({
|
|
192
|
+
stringTests,
|
|
193
|
+
replacement,
|
|
194
|
+
serialise: false,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const createRedact = (config: RedactConfig) => {
|
|
199
|
+
const redactor = createRedactor(config);
|
|
200
|
+
return (text: string) => redactor.redact(text) as string;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export { createRedact };
|
|
204
|
+
export type { RedactConfig };
|
package/tests/app.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { setupLogging } from "../src/index.js";
|
|
2
2
|
|
|
3
3
|
const run = async () => {
|
|
4
|
-
setupLogging(
|
|
4
|
+
setupLogging({
|
|
5
|
+
hex: { allow: [{ re: /\b(intent|hash)\b/i }] },
|
|
6
|
+
});
|
|
5
7
|
|
|
6
8
|
console.log("Hello, this is a log");
|
|
7
9
|
console.info("Hello, this is an info log");
|
|
@@ -14,6 +16,9 @@ const run = async () => {
|
|
|
14
16
|
console.log(
|
|
15
17
|
"This is my private key: 4fa7614bdd07ab15b11d2365466536ba160c06050ade0888a3aa985a5522ab22",
|
|
16
18
|
);
|
|
19
|
+
console.log(
|
|
20
|
+
"This is log for intent c8adcfef7255ad2c117f68111500997ab66de3c923e9ea9e71aaed5829d0acb8 and private key c99960eaeecb17d77d7c88f440383ccbaf93237471236a739185c0ebd62c5741",
|
|
21
|
+
);
|
|
17
22
|
|
|
18
23
|
try {
|
|
19
24
|
throw new Error("An Error was thrown");
|