privacy-brush 0.0.4 → 1.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/src/index.mjs CHANGED
@@ -1,162 +1,256 @@
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
+ // user name `/Users/test/code/` => `/Users/████/code/`
73
+ {
74
+ /** @type {IPatternName} */
75
+ name: "user_name_in_path",
76
+ regex: /\/Users\/([^\/]+)\//g,
77
+ /**
78
+ * Handle user name in file path masking.
79
+ * @param {string} match
80
+ * @param {string} userName
81
+ * @returns {string}
82
+ */
83
+ replacer: (match, userName) => {
84
+ return match.replace(userName, this.maskChar.repeat(userName.length))
85
+ },
86
+ },
87
+ ]
88
+
89
+ return allPatterns
90
+ }
91
+
92
+ /**
93
+ * Parse custom pattern inputs into maskable patterns.
94
+ * Accepts strings like '/sk-[a-z0-9]{20,}/i' or raw pattern bodies.
95
+ * @returns {IPattern[]}
96
+ */
97
+ get customSensitivePatterns() {
98
+ const customPatterns = this.config.customPatterns || []
99
+
100
+ /**
101
+ * Parse string pattern to RegExp.
102
+ * @param {string | RegExp} pattern
103
+ * @returns {RegExp} returns Parsed RegExp with global flag
104
+ * @example
105
+ * '/\\d{1,}/' => /\d{1,}/g
106
+ */
107
+ const parse = pattern => {
108
+ if (pattern instanceof RegExp) {
109
+ // always replace globally
110
+ const flags = pattern.flags.includes("g")
111
+ ? pattern.flags
112
+ : `${pattern.flags}g`
113
+
114
+ return new RegExp(pattern.source, flags)
115
+ }
116
+
117
+ const patternStr = pattern.trim()
118
+ // `/\d{1,}/g`.match(/^\/(.*)\/(\w*)$/) => [/\d{1,}/g, '\d{1,}', 'g']
119
+ const matches = patternStr.match(/^\/(.*)\/(\w*)$/)
120
+
121
+ if (matches) {
122
+ const body = matches[1]
123
+ let flags = matches[2] ?? ""
124
+
125
+ if (!flags.includes("g")) flags += "g"
126
+
127
+ return new RegExp(body, flags)
128
+ }
129
+
130
+ // treat as literal body
131
+ return new RegExp(patternStr, "g")
132
+ }
133
+
134
+ return customPatterns.map((pattern, i) => {
135
+ const regex = parse(pattern)
136
+ return {
137
+ name: `custom_${i + 1}`,
138
+ regex,
139
+ replacer: (match, group) => {
140
+ // const groups = rest.slice(0, -2) // last two are offset and input string
141
+ verbose && console.log("custom pattern match:", { match, group })
142
+
143
+ if (typeof group === "string") {
144
+ return match.replace(group, this.maskChar.repeat(group.length))
145
+ }
146
+
147
+ return this.maskChar.repeat(match.length)
148
+ },
149
+ }
150
+ })
151
+ }
152
+
153
+ get maskChar() {
154
+ return this.config.maskChar ?? defaultConfig.maskChar
155
+ }
156
+
157
+ /**
158
+ *
159
+ * @returns {IPattern[]}
160
+ */
161
+ get sensitivePatterns() {
162
+ const base = this.defaultSensitivePatterns.filter(({ name }) =>
163
+ this.config.maskPatternNames?.includes(name),
164
+ )
165
+
166
+ const customSensitivePatterns = this.customSensitivePatterns
167
+ verbose &&
168
+ console.log(
169
+ "🚀 ~ PrivacyBrush ~ sensitivePatterns ~ customSensitivePatterns:",
170
+ customSensitivePatterns,
171
+ )
172
+
173
+ return base.concat(customSensitivePatterns)
174
+ }
175
+
176
+ /**
177
+ * Mask a version string.
178
+ * @param {string} version
179
+ * @returns {string}
180
+ */
181
+ maskVersion(version) {
182
+ const parts = version.split(".")
183
+
184
+ if (!this.config.preserveFirstPart) {
185
+ return parts.map(part => this.maskChar.repeat(part.length)).join(".")
186
+ }
187
+
188
+ return parts
189
+ .map((part, index) => {
190
+ return index === 0 ? part : this.maskChar.repeat(part.length)
191
+ })
192
+ .join(".")
193
+ }
194
+
195
+ /**
196
+ *
197
+ * @param {string} text
198
+ * @returns
199
+ */
200
+ maskText(text) {
201
+ verbose && console.log(`[PrivacyBrush] Masking text: |${text}|`)
202
+
203
+ let result = text
204
+
205
+ this.sensitivePatterns.forEach(pattern => {
206
+ verbose && console.log("pattern:", pattern)
207
+ result = result.replace(pattern.regex, pattern.replacer)
208
+ })
209
+
210
+ return result
211
+ }
212
+
213
+ /**
214
+ * 批量处理文件
215
+ * @param {string} inputPath
216
+ * @param {string} [outputPath]
217
+ * @returns
218
+ */
219
+ maskFile(inputPath, outputPath) {
220
+ try {
221
+ // Delay-load fs to avoid startup cost when this method is not used
222
+ const require = createRequire(import.meta.url)
223
+ const fs = require("node:fs")
224
+
225
+ const content = fs.readFileSync(inputPath, "utf8")
226
+ const maskedContent = this.maskText(content)
227
+
228
+ if (outputPath) {
229
+ fs.writeFileSync(outputPath, maskedContent, "utf8")
230
+ verbose && console.log(`Masked file saved to: ${outputPath}`)
231
+ }
232
+
233
+ return maskedContent
234
+ } catch (error) {
235
+ console.error(
236
+ "Error processing file:",
237
+ error instanceof Error ? error.message : String(error),
238
+ )
239
+ throw error
240
+ }
241
+ }
242
+
243
+ // 实时流处理
244
+ async createMaskStream() {
245
+ const { Transform } = await import("node:stream")
246
+
247
+ return new Transform({
248
+ transform: (chunk, encoding, callback) => {
249
+ const text = String(chunk)
250
+
251
+ const masked = this.maskText(text)
252
+ callback(null, masked)
253
+ },
254
+ })
255
+ }
256
+ }
@@ -1,8 +1,14 @@
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: [
8
+ "windows_version",
9
+ "browser_version",
10
+ "ip_address",
11
+ "user_name_in_path",
12
+ ],
13
+ customPatterns: [],
14
+ }
@@ -0,0 +1,79 @@
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
+ // list built-in patterns
59
+ "list-patterns": {
60
+ type: "boolean",
61
+ },
62
+ },
63
+ }),
64
+ )
65
+
66
+ /**
67
+ * @template T
68
+ * @param {(...args: any[]) => T} fn
69
+ * @returns {[null, T] | [Error, null]}
70
+ */
71
+ function safeCall(fn) {
72
+ try {
73
+ const result = fn()
74
+ return [null, result]
75
+ } catch (error) {
76
+ // @ts-expect-error
77
+ return [error, null]
78
+ }
79
+ }
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