@procedures/privacy-scan 0.0.0-stage → 0.3.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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The procedures of `@procedures/privacy-scan`: `privacyScan.text` (pure) and
3
+ * `privacyScan.files` (it reads files). Both change nothing.
4
+ */
5
+ import { createProcedure, zodAdapter } from "@procedures/client";
6
+ import { z } from "zod";
7
+ import { FINDING_KINDS, readTermList, scanFiles, scanText } from "./scan.js";
8
+ const kindSchema = z.enum(FINDING_KINDS);
9
+ const optionFields = {
10
+ terms: z.array(z.string()).optional().describe("The private terms (names, places, accounts). The match ignores case and finds only whole words."),
11
+ allow: z.array(z.string()).optional().describe("The values that are not private. The match ignores case."),
12
+ kinds: z.array(kindSchema).optional().describe("The kinds to find: term, email, phone, coordinate (default: all)"),
13
+ mask: z.boolean().optional().describe("Show only the first character of each match (default: false)"),
14
+ };
15
+ const findingFields = {
16
+ kind: kindSchema,
17
+ line: z.number().int(),
18
+ column: z.number().int(),
19
+ match: z.string(),
20
+ };
21
+ // privacyScan.text
22
+ const TextInputSchema = z.object({
23
+ text: z.string().describe("The text to scan"),
24
+ ...optionFields,
25
+ });
26
+ const TextOutputSchema = z.object({ findings: z.array(z.object(findingFields)) });
27
+ export const privacyScanTextProcedure = createProcedure()
28
+ .path(["privacyScan", "text"])
29
+ .input(zodAdapter(TextInputSchema))
30
+ .output(zodAdapter(TextOutputSchema))
31
+ .meta({
32
+ description: "Find private data in a text: terms of a list, email addresses, phone numbers, coordinates",
33
+ args: ["text"],
34
+ output: "json",
35
+ effects: { reads: [], writes: [] },
36
+ })
37
+ .handler(async (input) => ({
38
+ findings: scanText(input.text, { terms: input.terms, allow: input.allow, kinds: input.kinds, mask: input.mask }),
39
+ }))
40
+ .build();
41
+ // privacyScan.files
42
+ const FilesInputSchema = z.object({
43
+ paths: z.array(z.string().min(1)).min(1).describe("The files and folders to scan. A folder gives all its files."),
44
+ ...optionFields,
45
+ termsFile: z.string().min(1).optional().describe("A file with more terms, one on each line. A line that starts with # is a comment."),
46
+ allowFile: z.string().min(1).optional().describe("A file with more allowed values, in the format of termsFile"),
47
+ });
48
+ const FilesOutputSchema = z.object({
49
+ findings: z.array(z.object({ file: z.string(), ...findingFields })),
50
+ files: z.number().int(),
51
+ skipped: z.array(z.string()),
52
+ });
53
+ export const privacyScanFilesProcedure = createProcedure()
54
+ .path(["privacyScan", "files"])
55
+ .input(zodAdapter(FilesInputSchema))
56
+ .output(zodAdapter(FilesOutputSchema))
57
+ .meta({
58
+ description: "Find private data in files and folders: terms of a list, email addresses, phone numbers, coordinates",
59
+ args: ["paths"],
60
+ output: "json",
61
+ effects: { reads: ["fs"], writes: [] },
62
+ })
63
+ .handler(async (input) => {
64
+ const terms = [...(input.terms ?? []), ...(input.termsFile === undefined ? [] : await readTermList(input.termsFile))];
65
+ const allow = [...(input.allow ?? []), ...(input.allowFile === undefined ? [] : await readTermList(input.allowFile))];
66
+ return scanFiles(input.paths, { terms, allow, kinds: input.kinds, mask: input.mask });
67
+ })
68
+ .build();
69
+ export const privacyScanProcedures = [privacyScanTextProcedure, privacyScanFilesProcedure];
70
+ //# sourceMappingURL=procedures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedures.js","sourceRoot":"","sources":["../src/procedures.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/privacy-scan in the global registry:
3
+ * `import "@procedures/privacy-scan/register"`. The package root registers nothing.
4
+ */
5
+ export * from "./module.js";
6
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,cAAc,aAAa,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/privacy-scan in the global registry:
3
+ * `import "@procedures/privacy-scan/register"`. The package root registers nothing.
4
+ */
5
+ import { registerPrivacyScanProcedures } from "./module.js";
6
+ export * from "./module.js";
7
+ registerPrivacyScanProcedures();
8
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAE5D,cAAc,aAAa,CAAC;AAE5B,6BAA6B,EAAE,CAAC"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The scan for private data in text: email addresses, phone numbers, coordinate pairs and the
3
+ * terms of a list that the caller gives. Use it before a text or a file goes public.
4
+ */
5
+ /** The kinds of private data that the scan finds. */
6
+ export type FindingKind = "term" | "email" | "phone" | "coordinate";
7
+ /** All the kinds, in the order of the findings on one position. */
8
+ export declare const FINDING_KINDS: readonly FindingKind[];
9
+ /** A place in a text that holds private data. */
10
+ export interface Finding {
11
+ /** The kind of the data. */
12
+ readonly kind: FindingKind;
13
+ /** The line of the match. The first line is 1. */
14
+ readonly line: number;
15
+ /** The position of the first character of the match in its line. The first position is 1. */
16
+ readonly column: number;
17
+ /** The matched text. With the option `mask`, only its first character stays. */
18
+ readonly match: string;
19
+ }
20
+ /** A finding in a file. */
21
+ export interface FileFinding extends Finding {
22
+ /** The path of the file. */
23
+ readonly file: string;
24
+ }
25
+ /** The options of the scan. */
26
+ export interface ScanOptions {
27
+ /** The private terms: names, places, accounts. The match ignores case and finds only whole words. */
28
+ readonly terms?: readonly string[] | undefined;
29
+ /** The values that are not private. The scan does not report them, for each kind. The match ignores case. */
30
+ readonly allow?: readonly string[] | undefined;
31
+ /** The kinds to find (default: all). */
32
+ readonly kinds?: readonly FindingKind[] | undefined;
33
+ /** True: each match shows only its first character, and a `*` for each other character. */
34
+ readonly mask?: boolean | undefined;
35
+ }
36
+ /** The result of `scanFiles`. */
37
+ export interface FileScan {
38
+ /** The findings, file by file. */
39
+ readonly findings: FileFinding[];
40
+ /** The number of text files that the scan examined. */
41
+ readonly files: number;
42
+ /** The binary files, which the scan does not examine. */
43
+ readonly skipped: string[];
44
+ }
45
+ /** An email address. Use it with `matchAll`, or make a copy: the `g` flag keeps a state. */
46
+ export declare const EMAIL_PATTERN: RegExp;
47
+ /** A phone number: `+` and 10 to 15 digits, or the North American form `(555) 555-0100`. */
48
+ export declare const PHONE_PATTERN: RegExp;
49
+ /** A pair of coordinates with 4 decimals or more: `12.3456, -65.4321`. */
50
+ export declare const COORDINATE_PATTERN: RegExp;
51
+ /** This function hides a match: it keeps the first character and puts a `*` for each other character. */
52
+ export declare function maskMatch(text: string): string;
53
+ /**
54
+ * This function finds the private data in a text: the terms of the list, email addresses that
55
+ * are not examples, phone numbers and coordinate pairs. The findings are in text order.
56
+ */
57
+ export declare function scanText(text: string, options?: ScanOptions): Finding[];
58
+ /**
59
+ * This function scans files for private data. A folder in `paths` gives all its files, without
60
+ * `node_modules`, `.git`, `dist`, `coverage` and links. A missing path is an error.
61
+ */
62
+ export declare function scanFiles(paths: readonly string[], options?: ScanOptions): Promise<FileScan>;
63
+ /**
64
+ * This function reads a list file: one entry on each line. A line that starts with `#` is a
65
+ * comment. With `optional`, a missing file gives an empty list.
66
+ */
67
+ export declare function readTermList(file: string, options?: {
68
+ readonly optional?: boolean | undefined;
69
+ }): Promise<string[]>;
70
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,qDAAqD;AACrD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,YAAY,CAAC;AAEpE,mEAAmE;AACnE,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAA6C,CAAC;AAE9F,iDAAiD;AACjD,MAAM,WAAW,OAAO;IACtB,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,2BAA2B;AAC3B,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,+BAA+B;AAC/B,MAAM,WAAW,WAAW;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/C,6GAA6G;IAC7G,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/C,wCAAwC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS,CAAC;IACpD,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED,iCAAiC;AACjC,MAAM,WAAW,QAAQ;IACvB,kCAAkC;IAClC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACjC,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,4FAA4F;AAC5F,eAAO,MAAM,aAAa,EAAE,MAA0D,CAAC;AAEvF,4FAA4F;AAC5F,eAAO,MAAM,aAAa,EAAE,MAA6D,CAAC;AAE1F,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,EAAE,MAAuD,CAAC;AAuBzF,yGAAyG;AACzG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9C;AA2CD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,EAAE,CAE3E;AAgBD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0BtG;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAY7H"}
package/dist/scan.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The scan for private data in text: email addresses, phone numbers, coordinate pairs and the
3
+ * terms of a list that the caller gives. Use it before a text or a file goes public.
4
+ */
5
+ import { readdir, readFile, stat } from "node:fs/promises";
6
+ import { join, resolve } from "node:path";
7
+ /** All the kinds, in the order of the findings on one position. */
8
+ export const FINDING_KINDS = ["term", "email", "phone", "coordinate"];
9
+ /** An email address. Use it with `matchAll`, or make a copy: the `g` flag keeps a state. */
10
+ export const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
11
+ /** A phone number: `+` and 10 to 15 digits, or the North American form `(555) 555-0100`. */
12
+ export const PHONE_PATTERN = /\+\d{10,15}\b|\(?\b\d{3}\)?[ .-]\d{3}[ .-]\d{4}\b/g;
13
+ /** A pair of coordinates with 4 decimals or more: `12.3456, -65.4321`. */
14
+ export const COORDINATE_PATTERN = /-?\b\d{1,3}\.\d{4,}\s*,\s*-?\d{1,3}\.\d{4,}/g;
15
+ /** An address of a domain for examples and tests (RFC 2606) is not private. */
16
+ const EXAMPLE_EMAIL = /@(?:[a-z0-9-]+\.)*(?:example\.(?:com|org|net)|test|invalid|localhost)$/i;
17
+ /** The folders that the scan of a folder does not enter. */
18
+ const SKIPPED_FOLDERS = new Set(["node_modules", ".git", "dist", "coverage", ".turbo"]);
19
+ /** The scan reads the first bytes of a file to find a binary file: a text file has no NUL byte. */
20
+ const BINARY_PROBE_BYTES = 8192;
21
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ /**
23
+ * This function makes the pattern of a term. The match ignores case. A term that starts or ends
24
+ * with a letter or a digit matches only when no letter or digit is next to it.
25
+ */
26
+ function termPattern(term) {
27
+ const before = /^[A-Za-z0-9]/.test(term) ? "(?<![A-Za-z0-9])" : "";
28
+ const after = /[A-Za-z0-9]$/.test(term) ? "(?![A-Za-z0-9])" : "";
29
+ return new RegExp(`${before}${escapeRegExp(term)}${after}`, "gi");
30
+ }
31
+ /** This function hides a match: it keeps the first character and puts a `*` for each other character. */
32
+ export function maskMatch(text) {
33
+ const characters = Array.from(text);
34
+ if (characters.length <= 1)
35
+ return "*".repeat(characters.length);
36
+ return `${characters[0]}${"*".repeat(characters.length - 1)}`;
37
+ }
38
+ /** This function makes the scanner of the options. The scanner of a text gives its findings. */
39
+ function createScanner(options) {
40
+ const allowed = new Set((options.allow ?? []).map((value) => value.trim().toLowerCase()));
41
+ const kinds = new Set(options.kinds ?? FINDING_KINDS);
42
+ const rules = [];
43
+ if (kinds.has("term")) {
44
+ const seen = new Set();
45
+ for (const raw of options.terms ?? []) {
46
+ const term = raw.trim();
47
+ const key = term.toLowerCase();
48
+ if (term.length === 0 || seen.has(key) || allowed.has(key))
49
+ continue;
50
+ seen.add(key);
51
+ rules.push({ kind: "term", pattern: termPattern(term) });
52
+ }
53
+ }
54
+ if (kinds.has("email"))
55
+ rules.push({ kind: "email", pattern: EMAIL_PATTERN, keep: (match) => !EXAMPLE_EMAIL.test(match) });
56
+ if (kinds.has("phone"))
57
+ rules.push({ kind: "phone", pattern: PHONE_PATTERN });
58
+ if (kinds.has("coordinate"))
59
+ rules.push({ kind: "coordinate", pattern: COORDINATE_PATTERN });
60
+ const order = (kind) => FINDING_KINDS.indexOf(kind);
61
+ return (text) => {
62
+ const findings = [];
63
+ text.split(/\r?\n/).forEach((content, index) => {
64
+ for (const rule of rules) {
65
+ for (const match of content.matchAll(rule.pattern)) {
66
+ const value = match[0];
67
+ if (allowed.has(value.toLowerCase()) || (rule.keep !== undefined && !rule.keep(value)))
68
+ continue;
69
+ findings.push({ kind: rule.kind, line: index + 1, column: match.index + 1, match: options.mask === true ? maskMatch(value) : value });
70
+ }
71
+ }
72
+ });
73
+ return findings.sort((a, b) => a.line - b.line || a.column - b.column || order(a.kind) - order(b.kind));
74
+ };
75
+ }
76
+ /**
77
+ * This function finds the private data in a text: the terms of the list, email addresses that
78
+ * are not examples, phone numbers and coordinate pairs. The findings are in text order.
79
+ */
80
+ export function scanText(text, options = {}) {
81
+ return createScanner(options)(text);
82
+ }
83
+ /** This function gives the files of a folder and of its subfolders, in name order. It does not follow links. */
84
+ async function filesIn(dir) {
85
+ const entries = await readdir(dir, { withFileTypes: true });
86
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
87
+ const files = [];
88
+ for (const entry of entries) {
89
+ if (SKIPPED_FOLDERS.has(entry.name) || entry.isSymbolicLink())
90
+ continue;
91
+ const path = join(dir, entry.name);
92
+ if (entry.isDirectory())
93
+ files.push(...(await filesIn(path)));
94
+ else if (entry.isFile())
95
+ files.push(path);
96
+ }
97
+ return files;
98
+ }
99
+ /**
100
+ * This function scans files for private data. A folder in `paths` gives all its files, without
101
+ * `node_modules`, `.git`, `dist`, `coverage` and links. A missing path is an error.
102
+ */
103
+ export async function scanFiles(paths, options = {}) {
104
+ const scan = createScanner(options);
105
+ const files = [];
106
+ const seen = new Set();
107
+ for (const path of paths) {
108
+ const list = (await stat(path)).isDirectory() ? await filesIn(path) : [path];
109
+ for (const file of list) {
110
+ const key = resolve(file);
111
+ if (seen.has(key))
112
+ continue;
113
+ seen.add(key);
114
+ files.push(file);
115
+ }
116
+ }
117
+ const findings = [];
118
+ const skipped = [];
119
+ for (const file of files) {
120
+ const buffer = await readFile(file);
121
+ if (buffer.subarray(0, BINARY_PROBE_BYTES).includes(0)) {
122
+ skipped.push(file);
123
+ continue;
124
+ }
125
+ // A byte order mark is not a character of the first line
126
+ const text = buffer.toString("utf8").replace(/^/, "");
127
+ for (const finding of scan(text))
128
+ findings.push({ file, ...finding });
129
+ }
130
+ return { findings, files: files.length - skipped.length, skipped };
131
+ }
132
+ /**
133
+ * This function reads a list file: one entry on each line. A line that starts with `#` is a
134
+ * comment. With `optional`, a missing file gives an empty list.
135
+ */
136
+ export async function readTermList(file, options = {}) {
137
+ let text;
138
+ try {
139
+ text = await readFile(file, "utf8");
140
+ }
141
+ catch (error) {
142
+ if (options.optional === true && error.code === "ENOENT")
143
+ return [];
144
+ throw error;
145
+ }
146
+ return text
147
+ .split(/\r?\n/)
148
+ .map((line) => line.trim())
149
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
150
+ }
151
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@procedures/privacy-scan",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.3.0",
5
+ "description": "Find private data in text before it goes public: email addresses, phone numbers, coordinates and the terms of your list.",
6
+ "keywords": [
7
+ "privacy",
8
+ "pii",
9
+ "scan",
10
+ "secrets",
11
+ "lint",
12
+ "procedures"
13
+ ],
14
+ "homepage": "https://mark1russell7.github.io/client/",
15
+ "bugs": {
16
+ "url": "https://github.com/mark1russell7/client/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mark1russell7/client.git",
21
+ "directory": "packages/privacy-scan"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Mark Russell (https://github.com/mark1russell7)",
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./module": {
34
+ "types": "./dist/module.d.ts",
35
+ "import": "./dist/module.js"
36
+ },
37
+ "./register": {
38
+ "types": "./dist/register.d.ts",
39
+ "import": "./dist/register.js"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "src",
45
+ "!src/**/*.test.ts",
46
+ "!src/**/*.test.tsx",
47
+ "!src/**/__tests__",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "sideEffects": [
51
+ "./dist/register.js"
52
+ ],
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "provenance": true
59
+ },
60
+ "dependencies": {
61
+ "@procedures/client": "0.3.0",
62
+ "zod": "^4.6.5"
63
+ },
64
+ "devDependencies": {
65
+ "typescript": "^7.0.2",
66
+ "@types/node": "^22.0.0",
67
+ "@mark1russell7/cue": "^1.0.0",
68
+ "vitest": "^5.0.3"
69
+ },
70
+ "client": {
71
+ "procedures": "./dist/register.js"
72
+ },
73
+ "scripts": {
74
+ "build": "tsc -b",
75
+ "typecheck": "tsc --noEmit",
76
+ "clean": "rm -rf dist .tsbuildinfo",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest"
79
+ }
6
80
  }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ // The root registers nothing: import "<package>/register", or give the module to createRegistry()
2
+ export * from "./module.js";
3
+ export * from "./procedures.js";
4
+ export * from "./scan.js";
package/src/module.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The procedures of @procedures/privacy-scan as data. Importing this file registers nothing.
3
+ */
4
+
5
+ import { defineModule, registerModules, PROCEDURE_REGISTRY, type ProcedureModule, type ProcedureRegistry } from "@procedures/client";
6
+ import { privacyScanProcedures } from "./procedures.js";
7
+
8
+ export const privacyScanModule: ProcedureModule = defineModule("@procedures/privacy-scan", privacyScanProcedures);
9
+
10
+ export default privacyScanModule;
11
+
12
+ /**
13
+ * This function registers the procedures of @procedures/privacy-scan in a registry. The default is the
14
+ * global registry.
15
+ */
16
+ export function registerPrivacyScanProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
17
+ registerModules(registry, [privacyScanModule]);
18
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The procedures of `@procedures/privacy-scan`: `privacyScan.text` (pure) and
3
+ * `privacyScan.files` (it reads files). Both change nothing.
4
+ */
5
+
6
+ import { createProcedure, zodAdapter, type AnyProcedure } from "@procedures/client";
7
+ import { z } from "zod";
8
+ import { FINDING_KINDS, readTermList, scanFiles, scanText, type Finding, type FileScan, type FindingKind } from "./scan.js";
9
+
10
+ const kindSchema = z.enum(FINDING_KINDS as [FindingKind, ...FindingKind[]]);
11
+
12
+ const optionFields = {
13
+ terms: z.array(z.string()).optional().describe("The private terms (names, places, accounts). The match ignores case and finds only whole words."),
14
+ allow: z.array(z.string()).optional().describe("The values that are not private. The match ignores case."),
15
+ kinds: z.array(kindSchema).optional().describe("The kinds to find: term, email, phone, coordinate (default: all)"),
16
+ mask: z.boolean().optional().describe("Show only the first character of each match (default: false)"),
17
+ };
18
+
19
+ const findingFields = {
20
+ kind: kindSchema,
21
+ line: z.number().int(),
22
+ column: z.number().int(),
23
+ match: z.string(),
24
+ };
25
+
26
+ // privacyScan.text
27
+
28
+ const TextInputSchema = z.object({
29
+ text: z.string().describe("The text to scan"),
30
+ ...optionFields,
31
+ });
32
+ type TextInput = z.infer<typeof TextInputSchema>;
33
+
34
+ interface TextOutput {
35
+ /** The findings, in text order. */
36
+ findings: Finding[];
37
+ }
38
+ const TextOutputSchema = z.object({ findings: z.array(z.object(findingFields)) });
39
+
40
+ export const privacyScanTextProcedure: AnyProcedure = createProcedure()
41
+ .path(["privacyScan", "text"])
42
+ .input(zodAdapter<TextInput>(TextInputSchema))
43
+ .output(zodAdapter<TextOutput>(TextOutputSchema))
44
+ .meta({
45
+ description: "Find private data in a text: terms of a list, email addresses, phone numbers, coordinates",
46
+ args: ["text"],
47
+ output: "json",
48
+ effects: { reads: [], writes: [] },
49
+ })
50
+ .handler(async (input: TextInput): Promise<TextOutput> => ({
51
+ findings: scanText(input.text, { terms: input.terms, allow: input.allow, kinds: input.kinds, mask: input.mask }),
52
+ }))
53
+ .build() as AnyProcedure;
54
+
55
+ // privacyScan.files
56
+
57
+ const FilesInputSchema = z.object({
58
+ paths: z.array(z.string().min(1)).min(1).describe("The files and folders to scan. A folder gives all its files."),
59
+ ...optionFields,
60
+ termsFile: z.string().min(1).optional().describe("A file with more terms, one on each line. A line that starts with # is a comment."),
61
+ allowFile: z.string().min(1).optional().describe("A file with more allowed values, in the format of termsFile"),
62
+ });
63
+ type FilesInput = z.infer<typeof FilesInputSchema>;
64
+
65
+ const FilesOutputSchema = z.object({
66
+ findings: z.array(z.object({ file: z.string(), ...findingFields })),
67
+ files: z.number().int(),
68
+ skipped: z.array(z.string()),
69
+ });
70
+
71
+ export const privacyScanFilesProcedure: AnyProcedure = createProcedure()
72
+ .path(["privacyScan", "files"])
73
+ .input(zodAdapter<FilesInput>(FilesInputSchema))
74
+ .output(zodAdapter<FileScan>(FilesOutputSchema))
75
+ .meta({
76
+ description: "Find private data in files and folders: terms of a list, email addresses, phone numbers, coordinates",
77
+ args: ["paths"],
78
+ output: "json",
79
+ effects: { reads: ["fs"], writes: [] },
80
+ })
81
+ .handler(async (input: FilesInput): Promise<FileScan> => {
82
+ const terms = [...(input.terms ?? []), ...(input.termsFile === undefined ? [] : await readTermList(input.termsFile))];
83
+ const allow = [...(input.allow ?? []), ...(input.allowFile === undefined ? [] : await readTermList(input.allowFile))];
84
+ return scanFiles(input.paths, { terms, allow, kinds: input.kinds, mask: input.mask });
85
+ })
86
+ .build() as AnyProcedure;
87
+
88
+ export const privacyScanProcedures: AnyProcedure[] = [privacyScanTextProcedure, privacyScanFilesProcedure];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/privacy-scan in the global registry:
3
+ * `import "@procedures/privacy-scan/register"`. The package root registers nothing.
4
+ */
5
+
6
+ import { registerPrivacyScanProcedures } from "./module.js";
7
+
8
+ export * from "./module.js";
9
+
10
+ registerPrivacyScanProcedures();