privacy-brush 0.0.3 → 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/.editorconfig ADDED
@@ -0,0 +1,19 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = space
6
+ indent_size = 2
7
+ end_of_line = lf
8
+ charset = utf-8
9
+ trim_trailing_whitespace = true
10
+ insert_final_newline = true
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = false
14
+
15
+ [Makefile]
16
+ indent_style = tab
17
+
18
+ [.nvmrc]
19
+ insert_final_newline = false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privacy-brush",
3
- "version": "0.0.3",
3
+ "version": "1.0.0",
4
4
  "description": "Automatically mask sensitive information in terminal outputs and logs. Keep your data safe when sharing.",
5
5
  "main": "src/index.mjs",
6
6
  "module": "src/index.mjs",
package/src/cli.mjs CHANGED
@@ -1,116 +1,154 @@
1
- import { parseArgs } from "node:util"
2
- import { PrivacyBrush } from "./index.mjs"
3
-
4
- // 流式处理:将 stdin 通过 masker 的 Transform 流处理后输出到 stdout
5
- // Usage: some_command | node src/cli.mjs
6
- // Example 1: echo "password" | node src/cli.mjs
7
- // Example 2: flutter devices | node src/cli.mjs
8
- // Example 3: ❯ echo 'Microsoft Windows [版本 10.0.12345.6785]' | node src/cli.mjs
9
- // => Microsoft Windows [版本 10.█.█████.████]
10
- // Example 4: ❯ node src/cli.mjs
11
- // Input: Microsoft Windows [版本 10.0.12345.6785]
12
- // Output: Microsoft Windows [版本 10.█.█████.████]
13
-
14
- // get config from command line arguments use native `parseArgs`
15
- // node src/cli.mjs --mask X --preserve-first false
16
- const args = process.argv.slice(2)
17
- // console.log("args:", args) // args: [ '--mask', 'X', '--preserve-first', 'false' ]
18
-
19
- const verbose = args.includes("--verbose")
20
-
21
- const [err, result] = safeCall(() =>
22
- parseArgs({
23
- allowPositionals: true,
24
- allowNegative: true,
25
-
26
- args,
27
-
28
- options: {
29
- mask: {
30
- type: "string",
31
- short: "m",
32
- default: "█",
33
- },
34
- "preserve-first": {
35
- type: "boolean",
36
- short: "p",
37
- default: true,
38
- },
39
- // help
40
- help: {
41
- type: "boolean",
42
- short: "h",
43
- },
44
- // verbose
45
- verbose: {
46
- type: "boolean",
47
- },
48
- },
49
- }),
50
- )
51
-
52
- await main()
53
-
54
- async function main() {
55
- if (err) {
56
- console.error(verbose ? err : String(err))
57
- console.error()
58
- printHelp()
59
-
60
- process.exit(1)
61
- }
62
-
63
- const { values } = result
64
-
65
- if (values.help) {
66
- printHelp()
67
- process.exit(0)
68
- }
69
-
70
- // console.log("values:", values)
71
- // console.log("positionals:", positionals)
72
-
73
- const config = values
74
-
75
- const masker = new PrivacyBrush({
76
- maskChar: config.mask,
77
- preserveFirstPart: config["preserve-first"],
78
- })
79
- const maskStream = await masker.createMaskStream()
80
-
81
- // 检查 stdin 是否连接到管道(有数据输入)
82
- const isPipedInput = !process.stdin.isTTY
83
-
84
- // 如果不是管道输入(交互模式),才显示提示
85
- if (!isPipedInput) {
86
- process.stdout.write("Input (Press Ctrl+C to exit...):\n")
87
- }
88
-
89
- // 处理所有数据
90
- process.stdin.pipe(maskStream).pipe(process.stdout)
91
- }
92
- /**
93
- * @template T
94
- * @param {(...args: any[]) => T} fn
95
- * @returns {[null, T] | [Error, null]}
96
- */
97
- function safeCall(fn) {
98
- try {
99
- const result = fn()
100
- return [null, result]
101
- } catch (error) {
102
- // @ts-expect-error
103
- return [error, null]
104
- }
105
- }
106
-
107
- function printHelp() {
108
- console.log(`Usage: node src/cli.mjs [options]
109
-
110
- Options:
111
- --mask, -m Character to use for masking (default: "█")
112
- --preserve-first, -p Whether to preserve the first part of version numbers (default: true, \`--no-preserve-first\` to false)
113
- --help, -h Show this help message (default: false)
114
- --verbose Enable verbose output (default: false)
115
- `)
116
- }
1
+ import { PrivacyBrush } from "./index.mjs"
2
+ import { parsedResult, verbose } from "./lib/parse-args.mjs"
3
+
4
+ // 流式处理:将 stdin 通过 masker 的 Transform 流处理后输出到 stdout
5
+ // Usage: some_command | node src/cli.mjs
6
+ // Example 1: echo "password" | node src/cli.mjs
7
+ // Example 2: flutter devices | node src/cli.mjs
8
+ // Example 3: ❯ echo 'Microsoft Windows [版本 10.0.12345.6785]' | node src/cli.mjs
9
+ // => Microsoft Windows [版本 10.█.█████.████]
10
+ // Example 4: ❯ node src/cli.mjs
11
+ // Input: Microsoft Windows [版本 10.0.12345.6785]
12
+ // Output: Microsoft Windows [版本 10.█.█████.████]
13
+
14
+ await main()
15
+
16
+ async function main() {
17
+ const [err, result] = parsedResult
18
+
19
+ if (err) {
20
+ console.error(verbose ? err : String(err))
21
+ console.error()
22
+ await printHelp()
23
+
24
+ process.exit(1)
25
+ return
26
+ }
27
+
28
+ const { values } = result
29
+
30
+ if (values.help) {
31
+ await printHelp()
32
+ return
33
+ }
34
+
35
+ if (values.version) {
36
+ await printVersion()
37
+ return
38
+ }
39
+
40
+ verbose && console.log("values:", values)
41
+ // console.log("positionals:", positionals)
42
+
43
+ const config = values
44
+
45
+ const masker = new PrivacyBrush({
46
+ maskChar: config.mask,
47
+ preserveFirstPart: config["preserve-first"],
48
+ customPatterns: config.pattern,
49
+ })
50
+
51
+ // If an input file is provided, process it. If --output-file is provided write there, otherwise print to stdout.
52
+ if (config["input-file"]) {
53
+ try {
54
+ if (config["output-file"]) {
55
+ // maskFile will write to the output path
56
+ masker.maskFile(config["input-file"], config["output-file"])
57
+ return
58
+ }
59
+
60
+ const masked = masker.maskFile(config["input-file"])
61
+
62
+ process.stdout.write(masked)
63
+ return
64
+ } catch {
65
+ process.exitCode = 1
66
+ return
67
+ }
68
+ }
69
+
70
+ // If an output file was requested for piped or interactive stdin, collect all input, mask and write to file.
71
+ if (config["output-file"]) {
72
+ try {
73
+ const chunks = []
74
+ for await (const chunk of process.stdin) {
75
+ chunks.push(String(chunk))
76
+ }
77
+
78
+ const inputText = chunks.join("")
79
+ console.log(`inputText: |${inputText}|`)
80
+ const masked = masker.maskText(inputText)
81
+
82
+ const fs = await import("node:fs/promises")
83
+ await fs.writeFile(config["output-file"], masked, "utf8")
84
+ return
85
+ } catch (error) {
86
+ console.error(error)
87
+ process.exitCode = 1
88
+ return
89
+ }
90
+ }
91
+
92
+ // 检查 stdin 是否连接到管道(有数据输入)
93
+ const isPipedInput = !process.stdin.isTTY
94
+
95
+ // 如果不是管道输入(交互模式),才显示提示
96
+ if (!isPipedInput) {
97
+ process.stdout.write("Input (Press Ctrl+C to exit...):\n")
98
+ }
99
+
100
+ const maskStream = await masker.createMaskStream()
101
+
102
+ // Default: stream masking to stdout
103
+ process.stdin.pipe(maskStream).pipe(process.stdout)
104
+ }
105
+
106
+ async function printHelp() {
107
+ console.log(`
108
+ # ${await getNameVersion()}
109
+
110
+ ## Usage:
111
+
112
+ pnpx privacy-brush [options]
113
+
114
+
115
+ ## Options:
116
+ --input-file, -i Path to input file to mask and print to stdout
117
+ --output-file, -o Path to write masked output (if given, write to this file)
118
+ --mask, -m Character to use for masking (default: "█")
119
+ --preserve-first, -p Whether to preserve the first part of version numbers (default: true, \`--no-preserve-first\` to false)
120
+ --help, -h Show this help message (default: false)
121
+ --verbose Enable verbose output (default: false)
122
+ --version, -v Show version information (default: false)
123
+
124
+
125
+ ## Examples:
126
+
127
+ flutter devices | pnpx privacy-brush
128
+
129
+ echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush
130
+ echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush --mask "X" --no-preserve-first
131
+
132
+ pnpx privacy-brush --input-file test/fixtures/terminal_log.md # mask and print to stdout
133
+ pnpx privacy-brush --input-file test/fixtures/terminal_log.md --output-file masked_log.md
134
+ `)
135
+ }
136
+
137
+ async function parsePackageJSON() {
138
+ const fs = await import("node:fs")
139
+ const pkgPath = new URL("../package.json", import.meta.url)
140
+
141
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
142
+
143
+ return pkg
144
+ }
145
+
146
+ async function getNameVersion() {
147
+ const pkg = await parsePackageJSON()
148
+
149
+ return `${pkg.name}@${pkg.version}`
150
+ }
151
+
152
+ async function printVersion() {
153
+ console.log(await getNameVersion())
154
+ }
package/src/cli.test.mjs CHANGED
@@ -1,28 +1,160 @@
1
- // 流式处理
2
-
3
- import { strict as assert } from "node:assert"
4
- import { execSync } from "node:child_process"
5
- import { test } from "node:test"
6
-
7
- test("❯ echo 'Microsoft Windows [Version 10.0.12345.6785]' | node src/cli.mjs", async () => {
8
- const actual = execSync(
9
- "echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs",
10
- ).toString("utf8")
11
-
12
- // console.log(`actual:|${actual}|`)
13
-
14
- const expected = "Microsoft Windows [Version 10.█.█████.████] \r\n"
15
- assert.strictEqual(actual, expected)
16
- })
17
-
18
- test("--mask", async () => {
19
- const actual = execSync(
20
- 'echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs --mask "🔒" --no-preserve-first',
21
- ).toString("utf8")
22
-
23
- // console.log(`actual:|${actual}|`)
24
-
25
- const expected =
26
- "Microsoft Windows [Version 🔒🔒.🔒.🔒🔒🔒🔒🔒.🔒🔒🔒🔒] \r\n"
27
- assert.strictEqual(actual, expected)
28
- })
1
+ // 流式处理
2
+
3
+ import { strict as assert } from "node:assert"
4
+ import { execSync, spawnSync } from "node:child_process"
5
+ import { test } from "node:test"
6
+
7
+ test("❯ echo | node src/cli.mjs", async () => {
8
+ const text = "Microsoft Windows [Version 10.0.12345.6785]"
9
+
10
+ // `echo "${text}"` will print text + newline to stdout
11
+ // `echo -n "${text}"` will print -n and text + newline to stdout
12
+ // So we use spawn with args [`-n`, text] to avoid the extra newline
13
+ function pipeCommands() {
14
+ const node = spawnSync("node", ["src/cli.mjs"], {
15
+ input: text,
16
+ })
17
+
18
+ return node.stdout.toString("utf8")
19
+ }
20
+
21
+ const actual = pipeCommands()
22
+
23
+ const expected = "Microsoft Windows [Version 10.█.█████.████]"
24
+ assert.strictEqual(actual, expected)
25
+ })
26
+
27
+ test("--mask", async () => {
28
+ const actual = execSync(
29
+ 'echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs --mask "🔒" --no-preserve-first',
30
+ ).toString("utf8")
31
+
32
+ // console.log(`actual:|${actual}|`)
33
+
34
+ const expected =
35
+ "Microsoft Windows [Version 🔒🔒.🔒.🔒🔒🔒🔒🔒.🔒🔒🔒🔒] \r\n"
36
+ assert.strictEqual(actual, expected)
37
+ })
38
+
39
+ test("--input-file outputs masked file content", async () => {
40
+ const { execSync } = await import("node:child_process")
41
+
42
+ const inputPath = "test/fixtures/terminal_log.md"
43
+
44
+ const actual = execSync(
45
+ `node src/cli.mjs --input-file ${inputPath}`,
46
+ ).toString("utf8")
47
+
48
+ // compute expected via masker.maskFile and normalize EOLs
49
+ const expected = `# h1
50
+
51
+ ## flutter devices
52
+
53
+ ❯ flutter devices
54
+ Flutter assets will be downloaded from <https://storage.flutter-io.cn>. Make sure you trust this source!
55
+ Found 4 connected devices:
56
+ sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
57
+ Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.█.█████.████]
58
+ Chrome (web) • chrome • web-javascript • Google Chrome 144.█.████.██
59
+ Edge (web) • edge • web-javascript • Microsoft Edge 144.█.████.██
60
+
61
+ Run "flutter emulators" to list and start any available device emulators.
62
+
63
+ If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try
64
+ increasing the time to wait for connected devices with the "--device-timeout" flag. Visit <https://flutter.dev/setup/> for
65
+ troubleshooting tips.
66
+
67
+ ## log
68
+
69
+ DEBUG: User login from IP 192.███.█.███
70
+ DEBUG: Session ID: abc123def456
71
+ DEBUG: Browser: Chrome/144.0.1234.60
72
+ DEBUG: OS: Windows 10.0.12345
73
+ `
74
+
75
+ assert.strictEqual(actual, expected)
76
+ })
77
+
78
+ test(`custom patterns`, () => {
79
+ const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
80
+
81
+ const actual = execSync(
82
+ `echo ${input} | node src/cli.mjs --pattern /sk-([a-z0-9]{20,})/`,
83
+ ).toString("utf8")
84
+
85
+ const expected = "DEEPSEEK_API_KEY=sk-████████████████████████████████ \r\n"
86
+ assert.strictEqual(actual, expected)
87
+ })
88
+
89
+ test(`custom patterns`, () => {
90
+ const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
91
+
92
+ const actual = execSync(
93
+ `echo ${input} | node src/cli.mjs --pattern /sk-[a-z0-9]{20,}/`,
94
+ ).toString("utf8")
95
+
96
+ const expected = "DEEPSEEK_API_KEY=███████████████████████████████████ \r\n"
97
+ assert.strictEqual(actual, expected)
98
+ })
99
+
100
+ test(`custom patterns`, () => {
101
+ const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
102
+
103
+ const actual = execSync(
104
+ `echo ${input} | node src/cli.mjs --pattern /([0-9]{2,})/`,
105
+ ).toString("utf8")
106
+
107
+ const expected = "DEEPSEEK_API_KEY=sk-af███████████eb██eb███bf████c8e8 \r\n"
108
+ assert.strictEqual(actual, expected)
109
+ })
110
+
111
+ /**
112
+ * 将两个或多个命令通过管道连接起来执行,并返回最后一个命令的输出结果
113
+ * @param {string} cmdStrWithPipes
114
+ * @returns {string}
115
+ */
116
+ function pipeCommands(cmdStrWithPipes) {
117
+ // const echo = spawnSync("echo", ["-n", text])
118
+ // const node = spawnSync("node", ["src/cli.mjs"], {
119
+ // input: echo.stdout,
120
+ // })
121
+ // 以下代码是以上代码的泛化版本
122
+ const commands = cmdStrWithPipes
123
+ .split("|")
124
+ .map(cmd => cmd.trim())
125
+ .filter(Boolean)
126
+
127
+ if (commands.length < 2) {
128
+ throw new Error("At least two commands are required for piping.")
129
+ }
130
+
131
+ const [first, ...restCommand] = commands
132
+ const [cmdName, args] = splitCmdAndArgs(first)
133
+
134
+ const lastCommand = restCommand.reduce(
135
+ (prevProcess, cmdStr) => {
136
+ const [cmd, args] = splitCmdAndArgs(cmdStr)
137
+
138
+ const currentProcess = spawnSync(cmd, args, {
139
+ input: prevProcess.stdout,
140
+ })
141
+
142
+ return currentProcess
143
+ },
144
+ spawnSync(cmdName, args),
145
+ )
146
+
147
+ return lastCommand.stdout.toString("utf8")
148
+ }
149
+
150
+ /**
151
+ *
152
+ * @param {string} cmd
153
+ * @returns {[cmdName: string, args: string[]]}
154
+ */
155
+ function splitCmdAndArgs(cmd) {
156
+ const [cmdName, ...args] = cmd.split(" ").filter(Boolean)
157
+ // console.log(`cmdName:|${cmdName}|`)
158
+ // console.log("args:", args)
159
+ return [cmdName, args]
160
+ }
package/src/index.mjs CHANGED
@@ -1,162 +1,239 @@
1
- /** @import { IConfig, IPattern, IPatternName } from "./type.js" */
2
-
3
- import { defaultConfig } from "./lib/config.mjs"
4
-
5
- export class PrivacyBrush {
6
- /**
7
- * @param {IConfig} [config]
8
- */
9
- constructor(config) {
10
- this.config = {
11
- ...defaultConfig,
12
- ...config,
13
- }
14
- }
15
-
16
- get defaultSensitivePatterns() {
17
- /** @type {IPattern[]} */
18
- const allPatterns = [
19
- // 操作系统版本 (Windows 10.0.19045.6456)
20
- {
21
- /** @type {IPatternName} */
22
- name: "windows_version",
23
- // match both Chinese "版本" and English "Version"
24
- regex: /(\[(?:版本|Version)\s+)(\d+\.\d+\.\d+\.\d+)(\])/i,
25
- /**
26
- * Handle windows version masking.
27
- * @param {string} match
28
- * @param {string} prefix
29
- * @param {string} version
30
- * @param {string} suffix
31
- * @returns {string}
32
- */
33
- replacer: (match, prefix, version, suffix) => {
34
- return prefix + this.maskVersion(version) + suffix
35
- },
36
- },
37
-
38
- // 浏览器版本 (Chrome 144.0.7559.60)
39
- {
40
- /** @type {IPatternName} */
41
- name: "browser_version",
42
- regex: /(Chrome|Edge)\s+(\d+\.\d+\.\d+\.\d+)/gi,
43
- /**
44
- * Handle browser version masking.
45
- * @param {string} match
46
- * @param {string} browser
47
- * @param {string} version
48
- * @returns {string}
49
- */
50
- replacer: (match, browser, version) => {
51
- return `${browser} ${this.maskVersion(version)}`
52
- },
53
- },
54
-
55
- // IP地址
56
- {
57
- /** @type {IPatternName} */
58
- name: "ip_address",
59
- regex: /\b(\d{1,3}\.){3}\d{1,3}\b/g,
60
- /**
61
- * Handle IP address masking.
62
- * @param {string} match
63
- * @returns {string}
64
- */
65
- replacer: match => {
66
- return this.maskVersion(match)
67
- },
68
- },
69
- ]
70
-
71
- return allPatterns
72
- }
73
-
74
- get maskChar() {
75
- return this.config.maskChar ?? defaultConfig.maskChar
76
- }
77
-
78
- /**
79
- *
80
- * @returns {IPattern[]}
81
- */
82
- get sensitivePatterns() {
83
- return this.defaultSensitivePatterns.filter(({ name }) =>
84
- this.config.maskPatternNames?.includes(name),
85
- )
86
- }
87
-
88
- /**
89
- * Mask a version string.
90
- * @param {string} version
91
- * @returns {string}
92
- */
93
- maskVersion(version) {
94
- const parts = version.split(".")
95
-
96
- if (!this.config.preserveFirstPart) {
97
- return parts.map(part => this.maskChar.repeat(part.length)).join(".")
98
- }
99
-
100
- return parts
101
- .map((part, index) => {
102
- return index === 0 ? part : this.maskChar.repeat(part.length)
103
- })
104
- .join(".")
105
- }
106
-
107
- /**
108
- *
109
- * @param {string} text
110
- * @returns
111
- */
112
- maskText(text) {
113
- let result = text
114
-
115
- this.sensitivePatterns.forEach(pattern => {
116
- result = result.replace(pattern.regex, pattern.replacer)
117
- })
118
-
119
- return result
120
- }
121
-
122
- /**
123
- * 批量处理文件
124
- * @param {string} inputPath
125
- * @param {string} outputPath
126
- * @returns
127
- */
128
- maskFile(inputPath, outputPath) {
129
- try {
130
- const fs = require("node:fs")
131
-
132
- const content = fs.readFileSync(inputPath, "utf8")
133
- const maskedContent = this.maskText(content)
134
-
135
- if (outputPath) {
136
- fs.writeFileSync(outputPath, maskedContent, "utf8")
137
- console.log(`Masked file saved to: ${outputPath}`)
138
- }
139
-
140
- return maskedContent
141
- } catch (error) {
142
- console.error(
143
- "Error processing file:",
144
- error instanceof Error ? error.message : String(error),
145
- )
146
- throw error
147
- }
148
- }
149
-
150
- // 实时流处理
151
- async createMaskStream() {
152
- const { Transform } = await import("node:stream")
153
-
154
- return new Transform({
155
- transform: (chunk, encoding, callback) => {
156
- const text = String(chunk)
157
- const masked = this.maskText(text)
158
- callback(null, masked)
159
- },
160
- })
161
- }
162
- }
1
+ /** @import { IConfig, IPattern, IPatternName } from "./type.js" */
2
+
3
+ import { createRequire } from "node:module"
4
+ import { defaultConfig } from "./lib/config.mjs"
5
+ import { verbose } from "./lib/parse-args.mjs"
6
+
7
+ export class PrivacyBrush {
8
+ /**
9
+ * @param {IConfig} [config]
10
+ */
11
+ constructor(config) {
12
+ this.config = {
13
+ ...defaultConfig,
14
+ ...config,
15
+ }
16
+ }
17
+
18
+ get defaultSensitivePatterns() {
19
+ /** @type {IPattern[]} */
20
+ const allPatterns = [
21
+ // 操作系统版本 (Windows 10.0.19045.6456)
22
+ {
23
+ /** @type {IPatternName} */
24
+ name: "windows_version",
25
+ // match both Chinese "版本" and English "Version"
26
+ regex: /(\[(?:版本|Version)\s+)(\d+\.\d+\.\d+\.\d+)(\])/i,
27
+ /**
28
+ * Handle windows version masking.
29
+ * @param {string} match
30
+ * @param {string} prefix
31
+ * @param {string} version
32
+ * @param {string} suffix
33
+ * @returns {string}
34
+ */
35
+ replacer: (match, prefix, version, suffix) => {
36
+ return prefix + this.maskVersion(version) + suffix
37
+ },
38
+ },
39
+
40
+ // 浏览器版本 (Chrome 144.0.7559.60)
41
+ {
42
+ /** @type {IPatternName} */
43
+ name: "browser_version",
44
+ regex: /(Chrome|Edge)\s+(\d+\.\d+\.\d+\.\d+)/gi,
45
+ /**
46
+ * Handle browser version masking.
47
+ * @param {string} match
48
+ * @param {string} browser
49
+ * @param {string} version
50
+ * @returns {string}
51
+ */
52
+ replacer: (match, browser, version) => {
53
+ return `${browser} ${this.maskVersion(version)}`
54
+ },
55
+ },
56
+
57
+ // IP地址
58
+ {
59
+ /** @type {IPatternName} */
60
+ name: "ip_address",
61
+ regex: /\b(\d{1,3}\.){3}\d{1,3}\b/g,
62
+ /**
63
+ * Handle IP address masking.
64
+ * @param {string} match
65
+ * @returns {string}
66
+ */
67
+ replacer: match => {
68
+ return this.maskVersion(match)
69
+ },
70
+ },
71
+ ]
72
+
73
+ return allPatterns
74
+ }
75
+
76
+ /**
77
+ * Parse custom pattern inputs into maskable patterns.
78
+ * Accepts strings like '/sk-[a-z0-9]{20,}/i' or raw pattern bodies.
79
+ * @returns {IPattern[]}
80
+ */
81
+ get customSensitivePatterns() {
82
+ const customPatterns = this.config.customPatterns || []
83
+
84
+ /**
85
+ * Parse string pattern to RegExp.
86
+ * @param {string | RegExp} pattern
87
+ * @returns {RegExp} returns Parsed RegExp with global flag
88
+ * @example
89
+ * '/\\d{1,}/' => /\d{1,}/g
90
+ */
91
+ const parse = pattern => {
92
+ if (pattern instanceof RegExp) {
93
+ // always replace globally
94
+ const flags = pattern.flags.includes("g")
95
+ ? pattern.flags
96
+ : `${pattern.flags}g`
97
+
98
+ return new RegExp(pattern.source, flags)
99
+ }
100
+
101
+ const patternStr = pattern.trim()
102
+ // `/\d{1,}/g`.match(/^\/(.*)\/(\w*)$/) => [/\d{1,}/g, '\d{1,}', 'g']
103
+ const matches = patternStr.match(/^\/(.*)\/(\w*)$/)
104
+
105
+ if (matches) {
106
+ const body = matches[1]
107
+ let flags = matches[2] ?? ""
108
+
109
+ if (!flags.includes("g")) flags += "g"
110
+
111
+ return new RegExp(body, flags)
112
+ }
113
+
114
+ // treat as literal body
115
+ return new RegExp(patternStr, "g")
116
+ }
117
+
118
+ return customPatterns.map((pattern, i) => {
119
+ const regex = parse(pattern)
120
+ return {
121
+ name: `custom_${i + 1}`,
122
+ regex,
123
+ replacer: (match, group) => {
124
+ // const groups = rest.slice(0, -2) // last two are offset and input string
125
+ verbose && console.log("custom pattern match:", { match, group })
126
+
127
+ if (typeof group === "string") {
128
+ return match.replace(group, this.maskChar.repeat(group.length))
129
+ }
130
+
131
+ return this.maskChar.repeat(match.length)
132
+ },
133
+ }
134
+ })
135
+ }
136
+
137
+ get maskChar() {
138
+ return this.config.maskChar ?? defaultConfig.maskChar
139
+ }
140
+
141
+ /**
142
+ *
143
+ * @returns {IPattern[]}
144
+ */
145
+ get sensitivePatterns() {
146
+ const base = this.defaultSensitivePatterns.filter(({ name }) =>
147
+ this.config.maskPatternNames?.includes(name),
148
+ )
149
+
150
+ const customSensitivePatterns = this.customSensitivePatterns
151
+ verbose &&
152
+ console.log(
153
+ "🚀 ~ PrivacyBrush ~ sensitivePatterns ~ customSensitivePatterns:",
154
+ customSensitivePatterns,
155
+ )
156
+
157
+ return base.concat(customSensitivePatterns)
158
+ }
159
+
160
+ /**
161
+ * Mask a version string.
162
+ * @param {string} version
163
+ * @returns {string}
164
+ */
165
+ maskVersion(version) {
166
+ const parts = version.split(".")
167
+
168
+ if (!this.config.preserveFirstPart) {
169
+ return parts.map(part => this.maskChar.repeat(part.length)).join(".")
170
+ }
171
+
172
+ return parts
173
+ .map((part, index) => {
174
+ return index === 0 ? part : this.maskChar.repeat(part.length)
175
+ })
176
+ .join(".")
177
+ }
178
+
179
+ /**
180
+ *
181
+ * @param {string} text
182
+ * @returns
183
+ */
184
+ maskText(text) {
185
+ verbose && console.log(`[PrivacyBrush] Masking text: |${text}|`)
186
+
187
+ let result = text
188
+
189
+ this.sensitivePatterns.forEach(pattern => {
190
+ result = result.replace(pattern.regex, pattern.replacer)
191
+ })
192
+
193
+ return result
194
+ }
195
+
196
+ /**
197
+ * 批量处理文件
198
+ * @param {string} inputPath
199
+ * @param {string} [outputPath]
200
+ * @returns
201
+ */
202
+ maskFile(inputPath, outputPath) {
203
+ try {
204
+ // Delay-load fs to avoid startup cost when this method is not used
205
+ const require = createRequire(import.meta.url)
206
+ const fs = require("node:fs")
207
+
208
+ const content = fs.readFileSync(inputPath, "utf8")
209
+ const maskedContent = this.maskText(content)
210
+
211
+ if (outputPath) {
212
+ fs.writeFileSync(outputPath, maskedContent, "utf8")
213
+ verbose && console.log(`Masked file saved to: ${outputPath}`)
214
+ }
215
+
216
+ return maskedContent
217
+ } catch (error) {
218
+ console.error(
219
+ "Error processing file:",
220
+ error instanceof Error ? error.message : String(error),
221
+ )
222
+ throw error
223
+ }
224
+ }
225
+
226
+ // 实时流处理
227
+ async createMaskStream() {
228
+ const { Transform } = await import("node:stream")
229
+
230
+ return new Transform({
231
+ transform: (chunk, encoding, callback) => {
232
+ const text = String(chunk)
233
+
234
+ const masked = this.maskText(text)
235
+ callback(null, masked)
236
+ },
237
+ })
238
+ }
239
+ }
@@ -1,8 +1,9 @@
1
- /**
2
- * @type {Required<import('../type.js').IConfig>}
3
- */
4
- export const defaultConfig = {
5
- maskChar: "█",
6
- preserveFirstPart: true, // 是否保留版本号的第一部分
7
- maskPatternNames: ["windows_version", "browser_version", "ip_address"],
8
- }
1
+ /**
2
+ * @satisfies {import('../type.js').IConfig}
3
+ */
4
+ export const defaultConfig = {
5
+ maskChar: "█",
6
+ preserveFirstPart: true, // 是否保留版本号的第一部分
7
+ maskPatternNames: ["windows_version", "browser_version", "ip_address"],
8
+ customPatterns: [],
9
+ }
@@ -0,0 +1,75 @@
1
+ import { parseArgs } from "node:util"
2
+
3
+ // get config from command line arguments use native `parseArgs`
4
+ // node src/cli.mjs --mask X --preserve-first false
5
+ const args = process.argv.slice(2)
6
+ // console.log("args:", args) // args: [ '--mask', 'X', '--preserve-first', 'false' ]
7
+
8
+ export const verbose = args.includes("--verbose")
9
+
10
+ export const parsedResult = safeCall(() =>
11
+ parseArgs({
12
+ allowPositionals: true,
13
+ allowNegative: true,
14
+
15
+ args,
16
+
17
+ options: {
18
+ // input file to mask
19
+ "input-file": {
20
+ type: "string",
21
+ short: "i",
22
+ },
23
+ // custom regex pattern(s) to mask
24
+ pattern: {
25
+ type: "string",
26
+ short: "r",
27
+ multiple: true,
28
+ },
29
+ // output file to write masked content
30
+ "output-file": {
31
+ type: "string",
32
+ short: "o",
33
+ },
34
+ mask: {
35
+ type: "string",
36
+ short: "m",
37
+ default: "█",
38
+ },
39
+ "preserve-first": {
40
+ type: "boolean",
41
+ short: "p",
42
+ default: true,
43
+ },
44
+ // help
45
+ help: {
46
+ type: "boolean",
47
+ short: "h",
48
+ },
49
+ // verbose
50
+ verbose: {
51
+ type: "boolean",
52
+ },
53
+ // version
54
+ version: {
55
+ type: "boolean",
56
+ short: "v",
57
+ },
58
+ },
59
+ }),
60
+ )
61
+
62
+ /**
63
+ * @template T
64
+ * @param {(...args: any[]) => T} fn
65
+ * @returns {[null, T] | [Error, null]}
66
+ */
67
+ function safeCall(fn) {
68
+ try {
69
+ const result = fn()
70
+ return [null, result]
71
+ } catch (error) {
72
+ // @ts-expect-error
73
+ return [error, null]
74
+ }
75
+ }
package/src/type.ts CHANGED
@@ -1,17 +1,19 @@
1
- export type IConfig = {
2
- maskChar?: string
3
- preserveFirstPart?: boolean
4
- maskPatternNames?: IPatternName[]
5
- }
6
-
7
- export type IPatternName =
8
- | "windows_version"
9
- | "browser_version"
10
- | "android_version"
11
- | "ip_address"
12
-
13
- export type IPattern = {
14
- name: IPatternName
15
- regex: RegExp
16
- replacer: (substring: string, ...args: any[]) => string
17
- }
1
+ export type IConfig = {
2
+ maskChar?: string
3
+ preserveFirstPart?: boolean
4
+ maskPatternNames?: IPatternName[]
5
+ customPatterns?: (string | RegExp)[]
6
+ }
7
+
8
+ export type IPatternName =
9
+ | "windows_version"
10
+ | "browser_version"
11
+ | "android_version"
12
+ | "ip_address"
13
+ | (string & {})
14
+
15
+ export type IPattern = {
16
+ name: IPatternName
17
+ regex: RegExp
18
+ replacer: (substring: string, ...args: any[]) => string
19
+ }
@@ -1,13 +1,24 @@
1
- flutter devices
2
- Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
3
- Found 4 connected devices:
4
- sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
5
- Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.12345.6456]
6
- Chrome (web) • chrome • web-javascript Google Chrome 144.0.1234.60
7
- Edge (web) • edge • web-javascript • Microsoft Edge 144.0.1234.82
8
-
9
- Run "flutter emulators" to list and start any available device emulators.
10
-
11
- If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try
12
- increasing the time to wait for connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for
13
- troubleshooting tips.
1
+ # h1
2
+
3
+ ## flutter devices
4
+
5
+ flutter devices
6
+ Flutter assets will be downloaded from <https://storage.flutter-io.cn>. Make sure you trust this source!
7
+ Found 4 connected devices:
8
+ sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
9
+ Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.12345.6456]
10
+ Chrome (web) • chrome • web-javascript • Google Chrome 144.0.1234.60
11
+ Edge (web) • edge • web-javascript Microsoft Edge 144.0.1234.82
12
+
13
+ Run "flutter emulators" to list and start any available device emulators.
14
+
15
+ If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try
16
+ increasing the time to wait for connected devices with the "--device-timeout" flag. Visit <https://flutter.dev/setup/> for
17
+ troubleshooting tips.
18
+
19
+ ## log
20
+
21
+ DEBUG: User login from IP 192.168.1.100
22
+ DEBUG: Session ID: abc123def456
23
+ DEBUG: Browser: Chrome/144.0.1234.60
24
+ DEBUG: OS: Windows 10.0.12345