no-pii 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.
Files changed (3) hide show
  1. package/README.md +208 -0
  2. package/nopii.js +175 -0
  3. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # nopii
2
+
3
+ > **No PII** — PII detection and redaction with secure vault storage
4
+
5
+ [![npm version](https://img.shields.io/npm/v/nopii.svg)](https://www.npmjs.com/package/nopii)
6
+ [![license](https://img.shields.io/npm/l/nopii.svg)](https://github.com/littlejustnode/nopii/blob/main/LICENSE)
7
+ [![node](https://img.shields.io/node/v/nopii.svg)](https://nodejs.org/)
8
+
9
+ Zero-dependency PII (Personally Identifiable Information) detection and redaction library with pluggable strategies, secure vault storage, and state persistence.
10
+
11
+ ## Features
12
+
13
+ - 🔒 **PII Redaction** — Automatically detect and replace sensitive data with safe placeholders
14
+ - 🗄️ **Secure Vault** — Store redacted values in an encrypted vault for later restoration
15
+ - 🔧 **Pluggable Strategies** — Define custom regex patterns or word-based masking rules
16
+ - 💾 **Persistent Storage** — Save/load configurations to OS keychain, encrypted JSON, or memory
17
+ - 🚀 **Zero Dependencies** — Uses only Node.js built-in modules (except optional `cross-keychain` for OS storage)
18
+ - 📦 **ESM Native** — Modern ES module syntax with named exports
19
+ - 🔄 **Bidirectional** — Redact and restore text without data loss
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install nopii
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```javascript
30
+ import { nopii } from 'nopii';
31
+
32
+ // Create instance with in-memory storage
33
+ const pii = nopii();
34
+
35
+ // Add masking strategies
36
+ await pii.addMasking('email', 'user@example.com', 'admin@company.org');
37
+ await pii.addStrategy('ssn', /\\b\\d{3}-\\d{2}-\\d{4}\\b/g);
38
+
39
+ // Redact sensitive text
40
+ const { safeText, vault } = pii.redact(
41
+ 'Contact user@example.com or admin@company.org. SSN: 123-45-6789'
42
+ );
43
+
44
+ console.log(safeText);
45
+ // 'Contact _P1_ or _P2_. SSN: _P3_'
46
+
47
+ // Restore original text
48
+ const restored = pii.restore(safeText, vault);
49
+ console.log(restored);
50
+ // 'Contact user@example.com or admin@company.org. SSN: 123-45-6789'
51
+ ```
52
+
53
+ ## API Reference
54
+
55
+ ### `nopii(options)`
56
+
57
+ Factory function to create a new NoPii instance.
58
+
59
+ **Options:**
60
+
61
+ | Option | Type | Default | Description |
62
+ |--------|------|---------|-------------|
63
+ | `verbose` | `boolean` | `false` | Enable verbose logging |
64
+ | `service` | `string` | `'nopii'` | Service name for OS keychain storage |
65
+ | `account` | `string` | `'default'` | Account name for OS keychain storage |
66
+ | `memory` | `string\\|object` | `'memory'` | Storage backend: `'os'`, `'memory'`, or custom object |
67
+ | `aesKey` | `string` | — | Required for encrypted JSON file storage |
68
+
69
+ **Storage Backends:**
70
+
71
+ - `'memory'` (default) — In-memory Map storage
72
+ - `'os'` — OS native keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service) via `cross-keychain`
73
+ - `'./path/to/file.json'` — Encrypted JSON file storage (requires `aesKey`)
74
+ - Custom object — Must implement `{ get(key), set(key, value) }`
75
+
76
+ ### `instance.addMasking(label, ...words)`
77
+
78
+ Add word-based masking strategy. Escapes special regex characters automatically.
79
+
80
+ ```javascript
81
+ await pii.addMasking('names', 'John Doe', 'Jane Smith');
82
+ ```
83
+
84
+ ### `instance.addStrategy(name, regex)`
85
+
86
+ Add custom regex strategy.
87
+
88
+ ```javascript
89
+ await pii.addStrategy('credit-card', /\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/g);
90
+ ```
91
+
92
+ ### `instance.load(input)`
93
+
94
+ Load configuration from file, string, or storage.
95
+
96
+ ```javascript
97
+ // Load from default storage
98
+ await pii.load();
99
+
100
+ // Load from JSON file
101
+ await pii.load('./config.json');
102
+
103
+ // Load from object
104
+ await pii.load({
105
+ masking: { names: ['Alice', 'Bob'] },
106
+ strategies: { phone: '^\\\\d{3}-\\\\d{3}-\\\\d{4}$' }
107
+ });
108
+ ```
109
+
110
+ ### `instance.redact(text)`
111
+
112
+ Redact PII from text. Returns `{ safeText, vault }`.
113
+
114
+ ```javascript
115
+ const { safeText, vault } = pii.redact('Sensitive data here');
116
+ // safeText: Redacted text with placeholders
117
+ // vault: Map of placeholder → original value
118
+ ```
119
+
120
+ ### `instance.restore(redactedText, vault)`
121
+
122
+ Restore original text from redacted version using vault.
123
+
124
+ ```javascript
125
+ const original = pii.restore(safeText, vault);
126
+ ```
127
+
128
+ ## Configuration Persistence
129
+
130
+ ### OS Keychain Storage
131
+
132
+ ```javascript
133
+ const pii = nopii({ memory: 'os', service: 'myapp', account: 'pii' });
134
+
135
+ // Strategies are persisted automatically
136
+ await pii.addMasking('api-keys', 'sk-abc123', 'sk-xyz789');
137
+
138
+ // Load on next run
139
+ await pii.load();
140
+ ```
141
+
142
+ ### Encrypted JSON Storage
143
+
144
+ ```javascript
145
+ const pii = nopii({
146
+ memory: './secrets/pii-store.json',
147
+ aesKey: 'your-32-char-secret-key-here!!' // Must be 32+ chars for AES-256-GCM
148
+ });
149
+
150
+ await pii.addStrategy('token', /Bearer\\s+[a-zA-Z0-9]+/g);
151
+ // Automatically encrypted and saved to ./secrets/pii-store.json
152
+ ```
153
+
154
+ ## Advanced Example
155
+
156
+ ```javascript
157
+ import { nopii } from 'nopii';
158
+ import fs from 'node:fs/promises';
159
+
160
+ // Initialize with OS keychain storage
161
+ const pii = nopii({
162
+ memory: 'os',
163
+ service: 'my-cli-tool',
164
+ account: 'redaction-config'
165
+ });
166
+
167
+ // Load existing configuration
168
+ await pii.load();
169
+
170
+ // Add comprehensive PII patterns
171
+ await pii.addStrategy('email', /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g);
172
+ await pii.addStrategy('phone', /\\b\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b/g);
173
+ await pii.addStrategy('ip', /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g);
174
+ await pii.addMasking('internal-codes', 'PROJ-1234', 'SECRET-5678');
175
+
176
+ // Process log file
177
+ const logs = await fs.readFile('./app.log', 'utf8');
178
+ const { safeText, vault } = pii.redact(logs);
179
+
180
+ // Save redacted logs (safe for sharing)
181
+ await fs.writeFile('./app-redacted.log', safeText);
182
+
183
+ // Save vault separately (keep secure!)
184
+ await fs.writeFile('./vault.json', JSON.stringify([...vault]));
185
+
186
+ // Later: restore original
187
+ const restored = pii.restore(safeText, new Map(JSON.parse(await fs.readFile('./vault.json', 'utf8'))));
188
+ ```
189
+
190
+ ## Security Considerations
191
+
192
+ - **Vault Security**: The vault contains the actual PII. Store it encrypted or in a secure location.
193
+ - **AES Key Management**: When using JSON storage, protect your `aesKey` — it decrypts the entire store.
194
+ - **OS Keychain**: Relies on system security. On shared machines, ensure proper user isolation.
195
+ - **Memory Storage**: Not persisted; cleared on process exit. Suitable for one-time operations.
196
+
197
+ ## Requirements
198
+
199
+ - Node.js ≥ 18.0.0
200
+ - For OS storage: `cross-keychain` peer dependency (auto-installed)
201
+
202
+ ## License
203
+
204
+ MIT © [littlejustnode](https://github.com/littlejustnode)
205
+
206
+ ---
207
+
208
+ **Note**: This package is designed for data processing pipelines, log sanitization, and GDPR/privacy compliance workflows. Always follow your organization's data handling policies when processing PII.
package/nopii.js ADDED
@@ -0,0 +1,175 @@
1
+ import fs from 'node:fs/promises';
2
+ import crypto from 'node:crypto';
3
+ import { Buffer } from 'node:buffer';
4
+ import { getPassword, setPassword } from 'cross-keychain';
5
+
6
+ export class NoPii {
7
+ #strategies = [];
8
+ #combinedRegex = null;
9
+ #storage = null;
10
+ #config = {};
11
+ #currentConfig = { masking: {}, strategies: {} };
12
+
13
+ constructor(options = {}) {
14
+ this.#config = {
15
+ verbose: false,
16
+ service: 'nopii',
17
+ account: 'default',
18
+ ...options
19
+ };
20
+ this.#storage = this.#initStorage(this.#config.memory);
21
+ }
22
+
23
+ #initStorage(memoryOption) {
24
+ if (memoryOption && typeof memoryOption === 'object' && typeof memoryOption.get === 'function') {
25
+ return memoryOption;
26
+ }
27
+
28
+ if (memoryOption === 'os') {
29
+ const { service, account } = this.#config;
30
+ return {
31
+ get: async (key) => {
32
+ // Use underscore instead of colon for Windows compatibility
33
+ const val = await getPassword(service, `${account}_${key}`);
34
+ return val ? JSON.parse(val) : null;
35
+ },
36
+ set: async (key, val) => {
37
+ await setPassword(service, `${account}_${key}`, JSON.stringify(val));
38
+ }
39
+ };
40
+ }
41
+
42
+ if (typeof memoryOption === 'string' && memoryOption.endsWith('.json')) {
43
+ const aesKey = this.#config.aesKey;
44
+ if (!aesKey) throw new Error("nopii: aesKey required for JSON storage.");
45
+
46
+ return {
47
+ path: memoryOption,
48
+ key: Buffer.alloc(32, aesKey),
49
+ async get() {
50
+ try {
51
+ const raw = await fs.readFile(this.path, 'utf8');
52
+ return JSON.parse(this.decrypt(raw));
53
+ } catch (e) {
54
+ // Rethrow decryption/auth errors, ignore "file not found"
55
+ if (e.message.includes('Decryption failed')) throw e;
56
+ return null;
57
+ }
58
+ },
59
+ async set(key, val) {
60
+ const encrypted = this.encrypt(JSON.stringify(val));
61
+ await fs.writeFile(this.path, encrypted);
62
+ },
63
+ encrypt(text) {
64
+ const iv = crypto.randomBytes(12);
65
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.key, iv);
66
+ const enc = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
67
+ return Buffer.concat([iv, cipher.getAuthTag(), enc]).toString('base64');
68
+ },
69
+ decrypt(data) {
70
+ try {
71
+ const buf = Buffer.from(data, 'base64');
72
+ const iv = buf.subarray(0, 12);
73
+ const tag = buf.subarray(12, 28);
74
+ const enc = buf.subarray(28);
75
+ const decipher = crypto.createDecipheriv('aes-256-gcm', this.key, iv);
76
+ decipher.setAuthTag(tag);
77
+ // .final() throws if the tag (GCM auth) is invalid
78
+ return decipher.update(enc, 'utf8') + decipher.final('utf8');
79
+ } catch (err) {
80
+ throw new Error(`Decryption failed: ${err.message}`);
81
+ }
82
+ }
83
+ };
84
+ }
85
+
86
+ const _mem = new Map();
87
+ return {
88
+ get: async (key) => _mem.get(key),
89
+ set: async (key, val) => _mem.set(key, val)
90
+ };
91
+ }
92
+
93
+ async load(input) {
94
+ let configData = null;
95
+ if (!input && this.#config.memory) {
96
+ configData = await this.#storage.get('config');
97
+ } else if (typeof input === 'string') {
98
+ const raw = input.endsWith('.json') ? await fs.readFile(input, 'utf8') : input;
99
+ configData = JSON.parse(raw);
100
+ } else {
101
+ configData = input;
102
+ }
103
+
104
+ if (configData) {
105
+ await this.#applyConfig(configData, false);
106
+ }
107
+ return this;
108
+ }
109
+
110
+ async #persist() {
111
+ if (this.#config.memory) {
112
+ await this.#storage.set('config', this.#currentConfig);
113
+ }
114
+ }
115
+
116
+ async #applyConfig(data, shouldPersist = true) {
117
+ if (data.masking) {
118
+ for (const [label, words] of Object.entries(data.masking)) {
119
+ await this.addMasking(label, ...words);
120
+ }
121
+ }
122
+ if (data.strategies) {
123
+ for (const [name, pattern] of Object.entries(data.strategies)) {
124
+ await this.addStrategy(name, new RegExp(pattern, 'u'));
125
+ }
126
+ }
127
+ if (shouldPersist) await this.#persist();
128
+ }
129
+
130
+ async addMasking(label, ...words) {
131
+ const pattern = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
132
+ this.#currentConfig.masking[label] = words;
133
+ this.#strategies.push({ name: label, regex: new RegExp(`(${pattern})`, 'u') });
134
+ this.#compile();
135
+ await this.#persist();
136
+ return this;
137
+ }
138
+
139
+ async addStrategy(name, regex) {
140
+ this.#currentConfig.strategies[name] = regex.source;
141
+ this.#strategies.push({ name, regex });
142
+ this.#compile();
143
+ await this.#persist();
144
+ return this;
145
+ }
146
+
147
+ #compile() {
148
+ if (this.#strategies.length === 0) return;
149
+ const source = this.#strategies.map(s => `(?<${s.name}>${s.regex.source})`).join('|');
150
+ this.#combinedRegex = new RegExp(source, 'gu');
151
+ }
152
+
153
+ redact(text) {
154
+ if (!this.#combinedRegex || !text) return { safeText: text, vault: new Map() };
155
+ const vault = new Map();
156
+ let count = 0;
157
+ const safeText = text.replace(this.#combinedRegex, (...args) => {
158
+ const groups = args.at(-1);
159
+ const label = Object.keys(groups).find(key => groups[key] !== undefined);
160
+ const placeholder = `_P${++count}_`;
161
+ vault.set(placeholder, groups[label]);
162
+ return placeholder;
163
+ });
164
+ return { safeText, vault };
165
+ }
166
+
167
+ restore(redactedText, vault) {
168
+ if (!redactedText || !vault || vault.size === 0) return redactedText;
169
+ const keys = Array.from(vault.keys()).sort((a, b) => b.length - a.length);
170
+ const restoreRegex = new RegExp(keys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
171
+ return redactedText.replace(restoreRegex, (m) => vault.get(m));
172
+ }
173
+ }
174
+
175
+ export const nopii = (options) => new NoPii(options);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "no-pii",
3
+ "version": "1.0.0",
4
+ "description": "PII redaction utility with encrypted local persistence and OS keychain support.",
5
+ "type": "module",
6
+ "main": "nopii.js",
7
+ "files": [
8
+ "nopii.js",
9
+ "LICENSE",
10
+ "README.md"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "scripts": {
16
+ "test": "node --test test.js",
17
+ "lint": "eslint .",
18
+ "example": "node examples/basic-usage.js"
19
+ },
20
+ "keywords": [
21
+ "nopii",
22
+ "pii",
23
+ "redaction",
24
+ "security",
25
+ "keychain",
26
+ "encryption",
27
+ "node-js",
28
+ "esm"
29
+ ],
30
+ "author": "littlejustnode",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "cross-keychain": "^1.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "eslint": "^9.0.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/littlejustnode/nopii.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/littlejustnode/nopii/issues"
44
+ },
45
+ "homepage": "https://github.com/littlejustnode/nopii#readme"
46
+ }