@socprime/logtotal-sanitizer 0.0.1-beta.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,933 @@
1
+ import { InvalidKeyError, InvalidOptionError, builtinRuleIds, SanitizationAbortedError, InvalidRuleError, getBuiltinRule, UnknownRuleError, validateRule } from './chunk-TAMK67JP.js';
2
+
3
+ // src/core/constants.ts
4
+ var DEFAULT_PREVIEW_BYTES = 256 * 1024;
5
+ var DEFAULT_MAX_LINE_CHARS = 1024 * 1024;
6
+ var DEFAULT_LINE_OVERLAP_CHARS = 1024;
7
+ var DEFAULT_KEY_BYTES = 32;
8
+ var MASK_TOKEN_PREFIX = "R";
9
+ var ALWAYS_REDACT_RULE_ID = "custom";
10
+ var ALWAYS_REDACT_TOKEN = "CUSTOM";
11
+
12
+ // src/core/hmac.ts
13
+ var BLOCK_BYTES = 64;
14
+ var DIGEST_BYTES = 32;
15
+ var K = new Uint32Array([
16
+ 1116352408,
17
+ 1899447441,
18
+ 3049323471,
19
+ 3921009573,
20
+ 961987163,
21
+ 1508970993,
22
+ 2453635748,
23
+ 2870763221,
24
+ 3624381080,
25
+ 310598401,
26
+ 607225278,
27
+ 1426881987,
28
+ 1925078388,
29
+ 2162078206,
30
+ 2614888103,
31
+ 3248222580,
32
+ 3835390401,
33
+ 4022224774,
34
+ 264347078,
35
+ 604807628,
36
+ 770255983,
37
+ 1249150122,
38
+ 1555081692,
39
+ 1996064986,
40
+ 2554220882,
41
+ 2821834349,
42
+ 2952996808,
43
+ 3210313671,
44
+ 3336571891,
45
+ 3584528711,
46
+ 113926993,
47
+ 338241895,
48
+ 666307205,
49
+ 773529912,
50
+ 1294757372,
51
+ 1396182291,
52
+ 1695183700,
53
+ 1986661051,
54
+ 2177026350,
55
+ 2456956037,
56
+ 2730485921,
57
+ 2820302411,
58
+ 3259730800,
59
+ 3345764771,
60
+ 3516065817,
61
+ 3600352804,
62
+ 4094571909,
63
+ 275423344,
64
+ 430227734,
65
+ 506948616,
66
+ 659060556,
67
+ 883997877,
68
+ 958139571,
69
+ 1322822218,
70
+ 1537002063,
71
+ 1747873779,
72
+ 1955562222,
73
+ 2024104815,
74
+ 2227730452,
75
+ 2361852424,
76
+ 2428436474,
77
+ 2756734187,
78
+ 3204031479,
79
+ 3329325298
80
+ ]);
81
+ var INITIAL_STATE = new Uint32Array([
82
+ 1779033703,
83
+ 3144134277,
84
+ 1013904242,
85
+ 2773480762,
86
+ 1359893119,
87
+ 2600822924,
88
+ 528734635,
89
+ 1541459225
90
+ ]);
91
+ function rotr(value, bits) {
92
+ return (value >>> bits | value << 32 - bits) >>> 0;
93
+ }
94
+ function concatBytes(a, b) {
95
+ const out = new Uint8Array(a.length + b.length);
96
+ out.set(a, 0);
97
+ out.set(b, a.length);
98
+ return out;
99
+ }
100
+ function padMessage(message) {
101
+ const bitLength = message.length * 8;
102
+ const paddedLength = Math.ceil((message.length + 9) / BLOCK_BYTES) * BLOCK_BYTES;
103
+ const padded = new Uint8Array(paddedLength);
104
+ padded.set(message);
105
+ padded[message.length] = 128;
106
+ const view = new DataView(padded.buffer);
107
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 2 ** 32), false);
108
+ view.setUint32(paddedLength - 4, bitLength >>> 0, false);
109
+ return padded;
110
+ }
111
+ function sha256(message) {
112
+ const padded = padMessage(message);
113
+ const view = new DataView(padded.buffer);
114
+ const state = Uint32Array.from(INITIAL_STATE);
115
+ const w = new Uint32Array(64);
116
+ for (let offset = 0; offset < padded.length; offset += BLOCK_BYTES) {
117
+ for (let i = 0; i < 16; i += 1) {
118
+ w[i] = view.getUint32(offset + i * 4, false);
119
+ }
120
+ for (let i = 16; i < 64; i += 1) {
121
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
122
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
123
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
124
+ }
125
+ let a = state[0];
126
+ let b = state[1];
127
+ let c = state[2];
128
+ let d = state[3];
129
+ let e = state[4];
130
+ let f = state[5];
131
+ let g = state[6];
132
+ let h = state[7];
133
+ for (let i = 0; i < 64; i += 1) {
134
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
135
+ const ch = e & f ^ ~e & g;
136
+ const temp1 = h + s1 + ch + K[i] + w[i] >>> 0;
137
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
138
+ const maj = a & b ^ a & c ^ b & c;
139
+ const temp2 = s0 + maj >>> 0;
140
+ h = g;
141
+ g = f;
142
+ f = e;
143
+ e = d + temp1 >>> 0;
144
+ d = c;
145
+ c = b;
146
+ b = a;
147
+ a = temp1 + temp2 >>> 0;
148
+ }
149
+ state[0] = state[0] + a >>> 0;
150
+ state[1] = state[1] + b >>> 0;
151
+ state[2] = state[2] + c >>> 0;
152
+ state[3] = state[3] + d >>> 0;
153
+ state[4] = state[4] + e >>> 0;
154
+ state[5] = state[5] + f >>> 0;
155
+ state[6] = state[6] + g >>> 0;
156
+ state[7] = state[7] + h >>> 0;
157
+ }
158
+ const digest = new Uint8Array(DIGEST_BYTES);
159
+ const digestView = new DataView(digest.buffer);
160
+ for (let i = 0; i < 8; i += 1) {
161
+ digestView.setUint32(i * 4, state[i], false);
162
+ }
163
+ return digest;
164
+ }
165
+ function normalizeKey(key) {
166
+ const hashed = key.length > BLOCK_BYTES ? sha256(key) : key;
167
+ if (hashed.length === BLOCK_BYTES) {
168
+ return hashed;
169
+ }
170
+ const padded = new Uint8Array(BLOCK_BYTES);
171
+ padded.set(hashed);
172
+ return padded;
173
+ }
174
+ function createHmacSha256(key) {
175
+ const block = normalizeKey(key);
176
+ const ipad = new Uint8Array(BLOCK_BYTES);
177
+ const opad = new Uint8Array(BLOCK_BYTES);
178
+ for (let i = 0; i < BLOCK_BYTES; i += 1) {
179
+ ipad[i] = block[i] ^ 54;
180
+ opad[i] = block[i] ^ 92;
181
+ }
182
+ return (message) => sha256(concatBytes(opad, sha256(concatBytes(ipad, message))));
183
+ }
184
+ function hexToBytes(hex) {
185
+ const bytes = new Uint8Array(hex.length / 2);
186
+ for (let i = 0; i < bytes.length; i += 1) {
187
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
188
+ }
189
+ return bytes;
190
+ }
191
+ function bytesToHex(bytes) {
192
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
193
+ }
194
+
195
+ // src/core/key.ts
196
+ var HEX_PATTERN = /^(?:[0-9a-fA-F]{2})+$/;
197
+ function generateKey(byteLength = DEFAULT_KEY_BYTES) {
198
+ if (!Number.isInteger(byteLength) || byteLength < 16) {
199
+ throw new InvalidKeyError(`Key length must be an integer of at least 16 bytes, got ${byteLength}.`);
200
+ }
201
+ const source = globalThis.crypto;
202
+ if (typeof source?.getRandomValues !== "function") {
203
+ throw new InvalidKeyError(
204
+ "No secure random source available. Pass an explicit `key` or provide a Web Crypto implementation on globalThis.crypto."
205
+ );
206
+ }
207
+ const bytes = new Uint8Array(byteLength);
208
+ source.getRandomValues(bytes);
209
+ return bytesToHex(bytes);
210
+ }
211
+ function assertKey(key, encoding) {
212
+ if (key.length === 0) {
213
+ throw new InvalidKeyError("Key must not be empty.");
214
+ }
215
+ if (encoding === "hex" && !HEX_PATTERN.test(key)) {
216
+ throw new InvalidKeyError(
217
+ 'A hex key must contain an even number of hexadecimal characters. Pass `keyEncoding: "utf8"` for a passphrase.'
218
+ );
219
+ }
220
+ }
221
+
222
+ // src/rules/alwaysRedact.ts
223
+ function escapeRegExp(value) {
224
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
225
+ }
226
+ function normalizeValues(values) {
227
+ const unique = new Set(values.map((value) => value.trim()).filter((value) => value.length > 0));
228
+ return [...unique].sort((left, right) => right.length - left.length || left.localeCompare(right));
229
+ }
230
+ function normalizePatterns(patterns) {
231
+ return patterns.map((pattern) => {
232
+ const source = typeof pattern === "string" ? pattern : pattern.source;
233
+ try {
234
+ new RegExp(source, "u");
235
+ } catch (cause) {
236
+ throw new InvalidOptionError(
237
+ `alwaysRedact.patterns contains an invalid regular expression: ${String(cause)}`
238
+ );
239
+ }
240
+ return `(?:${source})`;
241
+ });
242
+ }
243
+ function createAlwaysRedactRule(options) {
244
+ if (!options) {
245
+ return null;
246
+ }
247
+ const patterns = [
248
+ ...normalizeValues(options.values ?? []).map((value) => `(?:${escapeRegExp(value)})`),
249
+ ...normalizePatterns(options.patterns ?? [])
250
+ ];
251
+ if (patterns.length === 0) {
252
+ return null;
253
+ }
254
+ return {
255
+ id: options.ruleId ?? ALWAYS_REDACT_RULE_ID,
256
+ label: "Always redacted",
257
+ description: "Values and patterns the caller marked as always sensitive.",
258
+ mode: options.mode ?? "pseudo",
259
+ token: options.token ?? ALWAYS_REDACT_TOKEN,
260
+ patterns
261
+ };
262
+ }
263
+
264
+ // src/core/allowlist.ts
265
+ function patternSource(pattern) {
266
+ return typeof pattern === "string" ? pattern : pattern.source;
267
+ }
268
+ function compileAnchored(patterns) {
269
+ const sources = patterns.map(patternSource).filter((source) => source.length > 0);
270
+ if (sources.length === 0) {
271
+ return null;
272
+ }
273
+ const combined = sources.map((source) => `(?:${source})`).join("|");
274
+ try {
275
+ return new RegExp(`^(?:${combined})$`, "u");
276
+ } catch (cause) {
277
+ throw new InvalidOptionError(
278
+ `neverRedact.patterns contains an invalid regular expression: ${String(cause)}`
279
+ );
280
+ }
281
+ }
282
+ function createAllowList(options) {
283
+ const values = new Set((options?.values ?? []).filter((value) => value.length > 0));
284
+ const pattern = compileAnchored(options?.patterns ?? []);
285
+ const byRule = /* @__PURE__ */ new Map();
286
+ for (const entry of options?.byRule ?? []) {
287
+ const existing = byRule.get(entry.ruleId) ?? /* @__PURE__ */ new Set();
288
+ for (const value of entry.values) {
289
+ if (value.length > 0) {
290
+ existing.add(value);
291
+ }
292
+ }
293
+ byRule.set(entry.ruleId, existing);
294
+ }
295
+ return {
296
+ empty: values.size === 0 && pattern === null && byRule.size === 0,
297
+ values,
298
+ pattern,
299
+ byRule
300
+ };
301
+ }
302
+ function isAllowed(allow, ruleId, value) {
303
+ if (allow.empty) {
304
+ return false;
305
+ }
306
+ if (allow.values.has(value)) {
307
+ return true;
308
+ }
309
+ if (allow.byRule.get(ruleId)?.has(value) === true) {
310
+ return true;
311
+ }
312
+ return allow.pattern !== null && allow.pattern.test(value);
313
+ }
314
+
315
+ // src/core/pseudonymize.ts
316
+ var TOKEN_HEX_LENGTH = 16;
317
+ var TOKEN_BYTES = TOKEN_HEX_LENGTH / 2;
318
+ var textEncoder = new TextEncoder();
319
+ function createPseudonymizer(key, encoding) {
320
+ const keyBytes = encoding === "utf8" ? textEncoder.encode(key) : hexToBytes(key);
321
+ const sign = createHmacSha256(keyBytes);
322
+ const cache = /* @__PURE__ */ new Map();
323
+ return (ruleId, value) => {
324
+ const cacheKey = `${ruleId}\0${value}`;
325
+ const cached = cache.get(cacheKey);
326
+ if (cached !== void 0) {
327
+ return cached;
328
+ }
329
+ const digest = sign(textEncoder.encode(cacheKey));
330
+ const token = bytesToHex(digest.subarray(0, TOKEN_BYTES));
331
+ cache.set(cacheKey, token);
332
+ return token;
333
+ };
334
+ }
335
+ function replacementPrefix(rule) {
336
+ if (rule.mode === "mask") {
337
+ return MASK_TOKEN_PREFIX;
338
+ }
339
+ return rule.token ?? rule.id.toUpperCase();
340
+ }
341
+
342
+ // src/core/compile.ts
343
+ function compileRules(rules, aggressive) {
344
+ const parts = rules.map((rule) => ({
345
+ id: rule.id,
346
+ patterns: aggressive ? [...rule.patterns, ...rule.aggressivePatterns ?? []] : rule.patterns
347
+ })).filter(({ patterns }) => patterns.length > 0).map(({ id, patterns }) => `(?<${id}>${patterns.join("|")})`);
348
+ if (parts.length === 0) {
349
+ return null;
350
+ }
351
+ try {
352
+ return new RegExp(parts.join("|"), "gu");
353
+ } catch (cause) {
354
+ throw new InvalidRuleError(`Failed to compile the combined rule pattern: ${String(cause)}`);
355
+ }
356
+ }
357
+ function createRuleContext(input) {
358
+ const { rules } = input;
359
+ return {
360
+ compiled: compileRules(rules, input.aggressive),
361
+ rules,
362
+ ruleIds: rules.map((rule) => rule.id),
363
+ rulesById: new Map(rules.map((rule) => [rule.id, rule])),
364
+ prefixById: new Map(rules.map((rule) => [rule.id, replacementPrefix(rule)])),
365
+ pseudonymize: input.pseudonymize,
366
+ allow: input.allow,
367
+ contextChars: input.contextChars,
368
+ json: input.json
369
+ };
370
+ }
371
+ function buildReplacement(ctx, ruleId, value) {
372
+ return `<${ctx.prefixById.get(ruleId) ?? ruleId.toUpperCase()}:${ctx.pseudonymize(ruleId, value)}>`;
373
+ }
374
+
375
+ // src/core/lineSplitter.ts
376
+ function extractLines(buffer) {
377
+ const lines = [];
378
+ let start = 0;
379
+ for (let i = 0; i < buffer.length; i += 1) {
380
+ if (buffer[i] === "\n") {
381
+ lines.push(buffer.slice(start, i + 1));
382
+ start = i + 1;
383
+ }
384
+ }
385
+ return { lines, rest: buffer.slice(start) };
386
+ }
387
+ function createLineSplitter(options = {}) {
388
+ const maxLineChars = options.maxLineChars ?? DEFAULT_MAX_LINE_CHARS;
389
+ const overlapChars = options.overlapChars ?? DEFAULT_LINE_OVERLAP_CHARS;
390
+ if (maxLineChars < 1) {
391
+ throw new InvalidOptionError("lines.maxLineChars must be at least 1.");
392
+ }
393
+ if (overlapChars < 0 || overlapChars >= maxLineChars) {
394
+ throw new InvalidOptionError(
395
+ `lines.overlapChars must be between 0 and lines.maxLineChars - 1 (${maxLineChars - 1}), got ${overlapChars}.`
396
+ );
397
+ }
398
+ let carry = "";
399
+ function push(chunk) {
400
+ carry += chunk;
401
+ const { lines, rest } = extractLines(carry);
402
+ carry = rest;
403
+ while (carry.length > maxLineChars) {
404
+ const cut = carry.length - overlapChars;
405
+ lines.push(carry.slice(0, cut));
406
+ carry = carry.slice(cut);
407
+ }
408
+ return lines;
409
+ }
410
+ function flush() {
411
+ if (carry.length === 0) {
412
+ return [];
413
+ }
414
+ const rest = carry;
415
+ carry = "";
416
+ return [rest];
417
+ }
418
+ return { push, flush };
419
+ }
420
+
421
+ // src/core/redactLine.ts
422
+ function sliceContext(text, index, length, chars) {
423
+ return {
424
+ contextBefore: text.slice(Math.max(0, index - chars), index),
425
+ contextAfter: text.slice(index + length, index + length + chars)
426
+ };
427
+ }
428
+ function matchedRuleId(ruleIds, groups) {
429
+ for (const id of ruleIds) {
430
+ if (groups[id] !== void 0) {
431
+ return id;
432
+ }
433
+ }
434
+ return void 0;
435
+ }
436
+ function redactLine(line, ctx, options = {}) {
437
+ const { compiled, ruleIds, rulesById, allow, contextChars } = ctx;
438
+ const withSegments = options.withSegments ?? false;
439
+ const withMatches = options.withMatches ?? false;
440
+ if (!compiled) {
441
+ return {
442
+ output: line,
443
+ counts: {},
444
+ matches: [],
445
+ segments: withSegments ? { before: [{ text: line, changed: false }], after: [{ text: line, changed: false }] } : void 0
446
+ };
447
+ }
448
+ compiled.lastIndex = 0;
449
+ const counts = {};
450
+ const matches = [];
451
+ const before = [];
452
+ const after = [];
453
+ let output = "";
454
+ let cursor = 0;
455
+ let match = compiled.exec(line);
456
+ while (match !== null) {
457
+ const ruleId = match.groups ? matchedRuleId(ruleIds, match.groups) : void 0;
458
+ const rule = ruleId === void 0 ? void 0 : rulesById.get(ruleId);
459
+ const original = match[0];
460
+ if (rule === void 0 || ruleId === void 0) {
461
+ compiled.lastIndex = match.index + Math.max(original.length, 1);
462
+ match = compiled.exec(line);
463
+ continue;
464
+ }
465
+ const rejected = rule.validate !== void 0 && !rule.validate(original) || isAllowed(allow, ruleId, original);
466
+ if (rejected) {
467
+ compiled.lastIndex = match.index + Math.max(original.length, 1);
468
+ match = compiled.exec(line);
469
+ continue;
470
+ }
471
+ const replacement = buildReplacement(ctx, ruleId, original);
472
+ if (withSegments) {
473
+ if (match.index > cursor) {
474
+ const gap = line.slice(cursor, match.index);
475
+ before.push({ text: gap, changed: false });
476
+ after.push({ text: gap, changed: false });
477
+ }
478
+ before.push({ text: original, changed: true });
479
+ after.push({ text: replacement, changed: true });
480
+ }
481
+ output += line.slice(cursor, match.index) + replacement;
482
+ cursor = match.index + original.length;
483
+ counts[ruleId] = (counts[ruleId] ?? 0) + 1;
484
+ if (withMatches) {
485
+ matches.push({
486
+ ruleId,
487
+ original,
488
+ replacement,
489
+ ...contextChars > 0 ? sliceContext(line, match.index, original.length, contextChars) : {}
490
+ });
491
+ }
492
+ if (original.length === 0) {
493
+ compiled.lastIndex += 1;
494
+ }
495
+ match = compiled.exec(line);
496
+ }
497
+ output += line.slice(cursor);
498
+ if (withSegments && cursor < line.length) {
499
+ const tail = line.slice(cursor);
500
+ before.push({ text: tail, changed: false });
501
+ after.push({ text: tail, changed: false });
502
+ }
503
+ return {
504
+ output,
505
+ counts,
506
+ matches,
507
+ segments: withSegments ? { before, after } : void 0
508
+ };
509
+ }
510
+
511
+ // src/core/redactJsonLine.ts
512
+ var REPLACEMENT_TOKEN_RE = /<[A-Z][A-Z0-9]*:[0-9a-f]+>/g;
513
+ function looksLikeJson(line) {
514
+ const trimmed = line.trim();
515
+ return trimmed.length > 0 && (trimmed.startsWith("{") || trimmed.startsWith("["));
516
+ }
517
+ function normalizeKey2(key) {
518
+ return key.toLowerCase().replace(/[-_]/g, "");
519
+ }
520
+ function buildJsonKeyIndex(rules) {
521
+ const index = /* @__PURE__ */ new Map();
522
+ for (const rule of rules) {
523
+ for (const key of rule.jsonKeys ?? []) {
524
+ const normalized = normalizeKey2(key);
525
+ if (!index.has(normalized)) {
526
+ index.set(normalized, rule);
527
+ }
528
+ }
529
+ }
530
+ return index;
531
+ }
532
+ var jsonKeyIndexCache = /* @__PURE__ */ new WeakMap();
533
+ function jsonKeyIndex(rules) {
534
+ const cached = jsonKeyIndexCache.get(rules);
535
+ if (cached) {
536
+ return cached;
537
+ }
538
+ const index = buildJsonKeyIndex(rules);
539
+ jsonKeyIndexCache.set(rules, index);
540
+ return index;
541
+ }
542
+ function mergeCounts(target, source) {
543
+ for (const [id, count] of Object.entries(source)) {
544
+ target[id] = (target[id] ?? 0) + count;
545
+ }
546
+ }
547
+ function highlightReplacements(text) {
548
+ const segments = [];
549
+ let cursor = 0;
550
+ REPLACEMENT_TOKEN_RE.lastIndex = 0;
551
+ let match = REPLACEMENT_TOKEN_RE.exec(text);
552
+ while (match !== null) {
553
+ if (match.index > cursor) {
554
+ segments.push({ text: text.slice(cursor, match.index), changed: false });
555
+ }
556
+ segments.push({ text: match[0], changed: true });
557
+ cursor = match.index + match[0].length;
558
+ match = REPLACEMENT_TOKEN_RE.exec(text);
559
+ }
560
+ if (cursor < text.length) {
561
+ segments.push({ text: text.slice(cursor), changed: false });
562
+ }
563
+ return segments.length > 0 ? segments : [{ text, changed: false }];
564
+ }
565
+ function redactJsonLine(line, ctx, options = {}) {
566
+ const terminatorMatch = /(\r\n|\r|\n)$/.exec(line);
567
+ const terminator = terminatorMatch ? terminatorMatch[0] : "";
568
+ const content = terminator ? line.slice(0, -terminator.length) : line;
569
+ let parsed;
570
+ try {
571
+ parsed = JSON.parse(content);
572
+ } catch {
573
+ return null;
574
+ }
575
+ if (typeof parsed !== "object" || parsed === null) {
576
+ return null;
577
+ }
578
+ const withSegments = options.withSegments ?? false;
579
+ const withMatches = options.withMatches ?? false;
580
+ const keyIndex = jsonKeyIndex(ctx.rules);
581
+ const counts = {};
582
+ const matches = [];
583
+ const contextFor = (value) => {
584
+ if (ctx.contextChars <= 0) {
585
+ return {};
586
+ }
587
+ const index = content.indexOf(value);
588
+ if (index < 0) {
589
+ return { contextBefore: "", contextAfter: "" };
590
+ }
591
+ return sliceContext(content, index, value.length, ctx.contextChars);
592
+ };
593
+ const redactValue = (value, key) => {
594
+ if (typeof value === "string") {
595
+ const fieldRule = key === void 0 ? void 0 : keyIndex.get(normalizeKey2(key));
596
+ if (fieldRule) {
597
+ if (isAllowed(ctx.allow, fieldRule.id, value)) {
598
+ return value;
599
+ }
600
+ const replacement = buildReplacement(ctx, fieldRule.id, value);
601
+ counts[fieldRule.id] = (counts[fieldRule.id] ?? 0) + 1;
602
+ if (withMatches) {
603
+ matches.push({
604
+ ruleId: fieldRule.id,
605
+ original: value,
606
+ replacement,
607
+ ...contextFor(value)
608
+ });
609
+ }
610
+ return replacement;
611
+ }
612
+ const result = redactLine(value, ctx, { withMatches });
613
+ mergeCounts(counts, result.counts);
614
+ if (withMatches) {
615
+ for (const match of result.matches) {
616
+ matches.push({ ...match, ...contextFor(match.original) });
617
+ }
618
+ }
619
+ return result.output;
620
+ }
621
+ if (Array.isArray(value)) {
622
+ return value.map((item) => redactValue(item));
623
+ }
624
+ if (value !== null && typeof value === "object") {
625
+ const out = {};
626
+ for (const [entryKey, entryValue] of Object.entries(value)) {
627
+ out[entryKey] = redactValue(entryValue, entryKey);
628
+ }
629
+ return out;
630
+ }
631
+ return value;
632
+ };
633
+ const output = JSON.stringify(redactValue(parsed)) + terminator;
634
+ if (!withSegments) {
635
+ return { output, counts, matches };
636
+ }
637
+ const preview = redactLine(line, ctx, { withSegments: true });
638
+ return {
639
+ output,
640
+ counts,
641
+ matches,
642
+ segments: {
643
+ before: preview.segments?.before ?? [{ text: line, changed: false }],
644
+ after: highlightReplacements(output)
645
+ }
646
+ };
647
+ }
648
+
649
+ // src/core/report.ts
650
+ function createReportCollector(options = {}) {
651
+ const previewBytes = options.previewBytes ?? DEFAULT_PREVIEW_BYTES;
652
+ const collectReplacements = options.replacements ?? true;
653
+ if (previewBytes < 0) {
654
+ throw new InvalidOptionError("report.previewBytes must not be negative.");
655
+ }
656
+ const counts = {};
657
+ const replacements = /* @__PURE__ */ new Map();
658
+ const before = [];
659
+ const after = [];
660
+ let lineCount = 0;
661
+ let previewBytesUsed = 0;
662
+ let previewClosed = previewBytes === 0;
663
+ function needsSegments() {
664
+ return !previewClosed;
665
+ }
666
+ function needsMatches() {
667
+ return collectReplacements;
668
+ }
669
+ function record(result) {
670
+ lineCount += 1;
671
+ for (const [id, count] of Object.entries(result.counts)) {
672
+ counts[id] = (counts[id] ?? 0) + count;
673
+ }
674
+ if (collectReplacements) {
675
+ for (const match of result.matches) {
676
+ const key = `${match.ruleId}\0${match.original}`;
677
+ const existing = replacements.get(key);
678
+ if (existing) {
679
+ existing.count += 1;
680
+ } else {
681
+ replacements.set(key, {
682
+ ruleId: match.ruleId,
683
+ original: match.original,
684
+ replacement: match.replacement,
685
+ count: 1,
686
+ ...match.contextBefore !== void 0 ? { contextBefore: match.contextBefore, contextAfter: match.contextAfter } : {}
687
+ });
688
+ }
689
+ }
690
+ }
691
+ if (previewClosed) {
692
+ return;
693
+ }
694
+ if (result.segments) {
695
+ before.push(...result.segments.before);
696
+ after.push(...result.segments.after);
697
+ }
698
+ previewBytesUsed += result.output.length;
699
+ if (previewBytesUsed >= previewBytes) {
700
+ previewClosed = true;
701
+ }
702
+ }
703
+ function build(snapshot) {
704
+ const includeReplacements = snapshot?.includeReplacements ?? true;
705
+ let totalMatches = 0;
706
+ for (const count of Object.values(counts)) {
707
+ totalMatches += count;
708
+ }
709
+ return {
710
+ counts: { ...counts },
711
+ totalMatches,
712
+ lineCount,
713
+ replacements: includeReplacements ? [...replacements.values()] : [],
714
+ preview: { before: [...before], after: [...after] }
715
+ };
716
+ }
717
+ return { needsSegments, needsMatches, record, build };
718
+ }
719
+
720
+ // src/core/resolveRules.ts
721
+ function toRule(selector) {
722
+ if (typeof selector === "string") {
723
+ const builtin = getBuiltinRule(selector);
724
+ if (!builtin) {
725
+ throw new UnknownRuleError(
726
+ `Unknown rule "${selector}". Pass a rule object for custom rules, or one of the built-in identifiers.`
727
+ );
728
+ }
729
+ return builtin;
730
+ }
731
+ return validateRule(selector);
732
+ }
733
+ function resolveRules(selectors, extraRules = []) {
734
+ const resolved = [];
735
+ const positions = /* @__PURE__ */ new Map();
736
+ for (const selector of [...selectors, ...extraRules]) {
737
+ const rule = toRule(selector);
738
+ const existing = positions.get(rule.id);
739
+ if (existing === void 0) {
740
+ positions.set(rule.id, resolved.length);
741
+ resolved.push(rule);
742
+ } else {
743
+ resolved[existing] = rule;
744
+ }
745
+ }
746
+ return resolved;
747
+ }
748
+
749
+ // src/core/sanitizer.ts
750
+ async function* toAsyncIterable(source) {
751
+ yield* source;
752
+ }
753
+ function processLine(line, ctx, collector) {
754
+ const options = {
755
+ withSegments: collector.needsSegments(),
756
+ withMatches: collector.needsMatches()
757
+ };
758
+ const parsed = ctx.json && looksLikeJson(line) ? redactJsonLine(line, ctx, options) : null;
759
+ const result = parsed ?? redactLine(line, ctx, options);
760
+ collector.record(result);
761
+ return result.output;
762
+ }
763
+ function createSanitizer(options = {}) {
764
+ const keyEncoding = options.keyEncoding ?? "hex";
765
+ const key = options.key ?? generateKey();
766
+ assertKey(key, keyEncoding);
767
+ const contextChars = options.report?.contextChars ?? 0;
768
+ if (contextChars < 0) {
769
+ throw new InvalidOptionError("report.contextChars must not be negative.");
770
+ }
771
+ const selected = resolveWithAlwaysRedact(options);
772
+ const ctx = createRuleContext({
773
+ rules: selected,
774
+ aggressive: options.aggressive ?? false,
775
+ pseudonymize: createPseudonymizer(key, keyEncoding),
776
+ allow: createAllowList(options.neverRedact),
777
+ contextChars,
778
+ json: options.json !== false
779
+ });
780
+ const collectorOptions = {
781
+ previewBytes: options.report?.previewBytes,
782
+ replacements: options.report?.replacements
783
+ };
784
+ const rules = selected.map((rule) => ({
785
+ id: rule.id,
786
+ label: rule.label,
787
+ description: rule.description,
788
+ mode: rule.mode
789
+ }));
790
+ function sanitizeText2(text) {
791
+ const collector = createReportCollector(collectorOptions);
792
+ const splitter = createLineSplitter(options.lines);
793
+ let output = "";
794
+ for (const line of splitter.push(text)) {
795
+ output += processLine(line, ctx, collector);
796
+ }
797
+ for (const line of splitter.flush()) {
798
+ output += processLine(line, ctx, collector);
799
+ }
800
+ return { output, report: collector.build() };
801
+ }
802
+ async function sanitizeStream2(source, sink, streamOptions = {}) {
803
+ const collector = createReportCollector(collectorOptions);
804
+ const splitter = createLineSplitter(options.lines);
805
+ const { signal, onProgress } = streamOptions;
806
+ let charsRead = 0;
807
+ const writeLine = async (line) => {
808
+ if (signal?.aborted === true) {
809
+ throw new SanitizationAbortedError();
810
+ }
811
+ await sink.write(processLine(line, ctx, collector));
812
+ };
813
+ for await (const chunk of toAsyncIterable(source)) {
814
+ charsRead += chunk.length;
815
+ for (const line of splitter.push(chunk)) {
816
+ await writeLine(line);
817
+ }
818
+ onProgress?.({ charsRead, report: collector.build({ includeReplacements: false }) });
819
+ }
820
+ for (const line of splitter.flush()) {
821
+ await writeLine(line);
822
+ }
823
+ await sink.close?.();
824
+ return collector.build();
825
+ }
826
+ return { key, rules, sanitizeText: sanitizeText2, sanitizeStream: sanitizeStream2 };
827
+ }
828
+ function resolveWithAlwaysRedact(options) {
829
+ const selected = resolveRules(options.rules ?? builtinRuleIds, options.extraRules);
830
+ const alwaysRule = createAlwaysRedactRule(options.alwaysRedact);
831
+ if (!alwaysRule) {
832
+ return selected;
833
+ }
834
+ if (selected.some((rule) => rule.id === alwaysRule.id)) {
835
+ throw new InvalidOptionError(
836
+ `alwaysRedact.ruleId "${alwaysRule.id}" collides with an active rule. Choose an identifier that is not in use.`
837
+ );
838
+ }
839
+ return [alwaysRule, ...selected];
840
+ }
841
+ function sanitizeText(text, options) {
842
+ return createSanitizer(options).sanitizeText(text);
843
+ }
844
+ function sanitizeStream(source, sink, options) {
845
+ const { onProgress, signal, ...sanitizerOptions } = options ?? {};
846
+ return createSanitizer(sanitizerOptions).sanitizeStream(source, sink, { onProgress, signal });
847
+ }
848
+
849
+ // src/io/sources.ts
850
+ var DEFAULT_CHUNK_BYTES = 4 * 1024 * 1024;
851
+ function fromString(text, chunkChars = DEFAULT_CHUNK_BYTES) {
852
+ return {
853
+ *[Symbol.iterator]() {
854
+ for (let offset = 0; offset < text.length; offset += chunkChars) {
855
+ yield text.slice(offset, offset + chunkChars);
856
+ }
857
+ }
858
+ };
859
+ }
860
+ function fromBlob(blob, chunkBytes = DEFAULT_CHUNK_BYTES) {
861
+ return {
862
+ async *[Symbol.asyncIterator]() {
863
+ const decoder = new TextDecoder("utf-8");
864
+ for (let offset = 0; offset < blob.size; offset += chunkBytes) {
865
+ const slice = blob.slice(offset, Math.min(offset + chunkBytes, blob.size));
866
+ const buffer = await slice.arrayBuffer();
867
+ const text = decoder.decode(new Uint8Array(buffer), { stream: true });
868
+ if (text.length > 0) {
869
+ yield text;
870
+ }
871
+ }
872
+ const tail = decoder.decode();
873
+ if (tail.length > 0) {
874
+ yield tail;
875
+ }
876
+ }
877
+ };
878
+ }
879
+ function fromWebStream(stream) {
880
+ return {
881
+ async *[Symbol.asyncIterator]() {
882
+ const reader = stream.getReader();
883
+ const decoder = new TextDecoder("utf-8");
884
+ try {
885
+ for (; ; ) {
886
+ const { done, value } = await reader.read();
887
+ if (done) {
888
+ break;
889
+ }
890
+ if (value === void 0) {
891
+ continue;
892
+ }
893
+ const text = typeof value === "string" ? value : decoder.decode(value, { stream: true });
894
+ if (text.length > 0) {
895
+ yield text;
896
+ }
897
+ }
898
+ const tail = decoder.decode();
899
+ if (tail.length > 0) {
900
+ yield tail;
901
+ }
902
+ } finally {
903
+ reader.releaseLock();
904
+ }
905
+ }
906
+ };
907
+ }
908
+
909
+ // src/io/sinks.ts
910
+ function toStringSink() {
911
+ const chunks = [];
912
+ return {
913
+ write(chunk) {
914
+ chunks.push(chunk);
915
+ },
916
+ get text() {
917
+ return chunks.join("");
918
+ }
919
+ };
920
+ }
921
+ function toCallbackSink(onChunk, onClose) {
922
+ return {
923
+ write: onChunk,
924
+ ...onClose ? { close: onClose } : {}
925
+ };
926
+ }
927
+ function toNullSink() {
928
+ return { write: () => void 0 };
929
+ }
930
+
931
+ export { createSanitizer, fromBlob, fromString, fromWebStream, generateKey, sanitizeStream, sanitizeText, toCallbackSink, toNullSink, toStringSink };
932
+ //# sourceMappingURL=chunk-U6SJR7S7.js.map
933
+ //# sourceMappingURL=chunk-U6SJR7S7.js.map