syntropylog 0.9.12 → 0.9.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.13
4
+
5
+ ### Patch Changes
6
+
7
+ - a1498cb: - **MaskingEngine**: On masking failure (timeout/error), return a safe fallback payload with `_maskingFailed` and allowed keys only (`level`, `timestamp`, `message`, `service`) instead of raw metadata to avoid leaking sensitive data.
8
+ - **RedisConnectionManager**: Call `removeAllListeners()` when client was never open in `disconnect()` to avoid listener leaks.
9
+ - **RedisManager**: Clear `instances` and `defaultInstance` in `shutdown()` after closing connections.
10
+ - eca5f56: **Fix: ~3–6s delay per log call (logger.info/warn/error)**
11
+ - **Cause**: `MaskingEngine` used the `regex-test` package for every key×rule check. That package runs each test in a child-process worker with a single queue, so many sequential IPC round-trips added up to several seconds per log.
12
+ - **Change**: Built-in default rules (password, email, token, credit_card, SSN, phone) now use synchronous `RegExp.test()` in-process; they use safe, known patterns with no ReDoS risk. Custom rules added via `masking.rules` still use `regex-test` with timeout for safety.
13
+ - **Result**: Log calls complete in milliseconds again. README documents the behavior under "Data Masking → Performance".
14
+
3
15
  All notable changes to this project will be documented in this file.
4
16
 
5
17
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
package/CONTRIBUTING.md CHANGED
@@ -86,6 +86,16 @@ We welcome code contributions! To contribute code, please follow these steps:
86
86
  - Keep API documentation current
87
87
  - Include usage examples
88
88
 
89
+ ## Release process (maintainers)
90
+
91
+ Releases use [Changesets](https://github.com/changesets/changesets) and GitHub Actions:
92
+
93
+ 1. **Add a changeset** when changing the library: `pnpm changeset` (choose version type and describe the change).
94
+ 2. **Push to `main`** (or merge a PR that includes the changeset). The Release workflow runs and:
95
+ - Creates a **"Version Packages"** PR with the version bump (e.g. 0.9.12 → 0.9.13) and CHANGELOG updates.
96
+ - **Publishes to npm** from that same run (so npm may already have the new version).
97
+ 3. **Merge the "Version Packages" PR** into `main`. This step is required so that `main` has the same `package.json` version and CHANGELOG as what was published to npm. If you skip it, `main` will stay on the old version while npm has the new one.
98
+
89
99
  ## Getting Help
90
100
 
91
101
  If you need help with your contribution, please:
package/README.md CHANGED
@@ -446,6 +446,8 @@ await syntropyLog.init({
446
446
 
447
447
  > **Silent Observer guarantee**: if the masking engine fails for any reason, it returns the original object and the application keeps running — it never throws.
448
448
 
449
+ **Performance**: Built-in rules use synchronous regex matching (safe, known patterns). Custom rules you add still use the timeout-protected `regex-test` worker to guard against ReDoS. This avoids the ~3–6s delay per log that occurred when every key was tested via the worker queue.
450
+
449
451
  ---
450
452
 
451
453
  ## 💾 Universal Persistence — Log to Any Database
package/dist/index.cjs CHANGED
@@ -150,36 +150,42 @@ class MaskingEngine {
150
150
  strategy: MaskingStrategy.CREDIT_CARD,
151
151
  preserveLength: true,
152
152
  maskChar: this.maskChar,
153
+ _isDefaultRule: true,
153
154
  },
154
155
  {
155
156
  pattern: /ssn|social_security|security_number/i,
156
157
  strategy: MaskingStrategy.SSN,
157
158
  preserveLength: true,
158
159
  maskChar: this.maskChar,
160
+ _isDefaultRule: true,
159
161
  },
160
162
  {
161
163
  pattern: /email/i,
162
164
  strategy: MaskingStrategy.EMAIL,
163
165
  preserveLength: true,
164
166
  maskChar: this.maskChar,
167
+ _isDefaultRule: true,
165
168
  },
166
169
  {
167
170
  pattern: /phone|phone_number|mobile_number/i,
168
171
  strategy: MaskingStrategy.PHONE,
169
172
  preserveLength: true,
170
173
  maskChar: this.maskChar,
174
+ _isDefaultRule: true,
171
175
  },
172
176
  {
173
177
  pattern: /password|pass|pwd|secret/i,
174
178
  strategy: MaskingStrategy.PASSWORD,
175
179
  preserveLength: true,
176
180
  maskChar: this.maskChar,
181
+ _isDefaultRule: true,
177
182
  },
178
183
  {
179
184
  pattern: /token|api_key|auth_token|jwt|bearer/i,
180
185
  strategy: MaskingStrategy.TOKEN,
181
186
  preserveLength: true,
182
187
  maskChar: this.maskChar,
188
+ _isDefaultRule: true,
183
189
  },
184
190
  ];
185
191
  for (const rule of defaultRules) {
@@ -206,8 +212,10 @@ class MaskingEngine {
206
212
  /**
207
213
  * Processes a metadata object and applies the configured masking rules.
208
214
  * Uses JSON flattening strategy for extreme performance.
215
+ * On failure (timeout, rule error, etc.) returns a safe redacted object with an explicit message
216
+ * instead of the original data, to avoid leaking sensitive content.
209
217
  * @param meta - The metadata object to process
210
- * @returns A new object with the masked data
218
+ * @returns A new object with the masked data, or a safe fallback object if masking fails
211
219
  */
212
220
  async process(meta) {
213
221
  // Set initialized flag on first use
@@ -222,9 +230,27 @@ class MaskingEngine {
222
230
  return masked;
223
231
  }
224
232
  catch {
225
- // Silent observer - return original data if masking fails
226
- return meta;
233
+ // Do not return original data: emit a safe placeholder so sensitive payload is never logged
234
+ return {
235
+ ...MaskingEngine.buildSafeFallbackFromMeta(meta),
236
+ _maskingFailed: true,
237
+ _maskingFailedMessage: MaskingEngine.MASKING_FAILED_MESSAGE,
238
+ };
239
+ }
240
+ }
241
+ /**
242
+ * Builds a minimal safe object from meta (level, timestamp, message, service) for fallback.
243
+ * Avoids leaking any arbitrary keys/values when masking fails.
244
+ */
245
+ static buildSafeFallbackFromMeta(meta) {
246
+ const safe = {};
247
+ const allowedKeys = ['level', 'timestamp', 'message', 'service'];
248
+ for (const key of allowedKeys) {
249
+ if (key in meta && meta[key] !== undefined) {
250
+ safe[key] = meta[key];
251
+ }
227
252
  }
253
+ return safe;
228
254
  }
229
255
  /**
230
256
  * Applies masking rules to data recursively.
@@ -254,13 +280,20 @@ class MaskingEngine {
254
280
  for (const rule of this.rules) {
255
281
  let isMatch = false;
256
282
  if (rule._compiledPattern) {
257
- try {
258
- // Use regex-test for safe execution with timeout
259
- isMatch = await this.regexTest.test(rule._compiledPattern, key);
283
+ if (rule._isDefaultRule) {
284
+ // Default rules use safe, known patterns (no ReDoS); sync test avoids
285
+ // regex-test worker IPC queue which caused ~3–6s delay per log.
286
+ isMatch = rule._compiledPattern.test(key);
260
287
  }
261
- catch {
262
- // Silent failure on timeout/error - treat as no match
263
- isMatch = false;
288
+ else {
289
+ try {
290
+ // Custom rules: use regex-test for safe execution with timeout
291
+ isMatch = await this.regexTest.test(rule._compiledPattern, key);
292
+ }
293
+ catch {
294
+ // Silent failure on timeout/error - treat as no match
295
+ isMatch = false;
296
+ }
264
297
  }
265
298
  }
266
299
  if (isMatch) {
@@ -477,6 +510,8 @@ class MaskingEngine {
477
510
  }
478
511
  }
479
512
  }
513
+ /** Message used when masking fails (e.g. timeout) so we never emit raw payload. */
514
+ MaskingEngine.MASKING_FAILED_MESSAGE = '[SyntropyLog] Masking could not be applied (e.g. timeout or error); payload redacted for safety.';
480
515
 
481
516
  /**
482
517
  * FILE: src/config.schema.ts
@@ -3296,6 +3331,10 @@ class RedisConnectionManager {
3296
3331
  }
3297
3332
  }
3298
3333
  else {
3334
+ // Client never connected or already closed: remove listeners to avoid leak
3335
+ if (typeof this.client.removeAllListeners === 'function') {
3336
+ this.client.removeAllListeners();
3337
+ }
3299
3338
  this.logger.info('Client was not open. Quit operation effectively complete.');
3300
3339
  }
3301
3340
  }
@@ -4537,6 +4576,8 @@ class RedisManager {
4537
4576
  this.logger.info('Closing all Redis connections...');
4538
4577
  const shutdownPromises = Array.from(this.instances.values()).map((instance) => instance.quit());
4539
4578
  await Promise.allSettled(shutdownPromises);
4579
+ this.instances.clear();
4580
+ this.defaultInstance = undefined;
4540
4581
  this.logger.info('All Redis connections have been closed.');
4541
4582
  }
4542
4583
  }