humanbehavior-js 0.4.22 → 0.4.24

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 (52) hide show
  1. package/dist/cjs/angular/index.cjs +276 -387
  2. package/dist/cjs/angular/index.cjs.map +1 -1
  3. package/dist/cjs/index.cjs +272 -383
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/install-wizard.cjs +5 -5
  6. package/dist/cjs/install-wizard.cjs.map +1 -1
  7. package/dist/cjs/react/index.cjs +282 -393
  8. package/dist/cjs/react/index.cjs.map +1 -1
  9. package/dist/cjs/remix/index.cjs +272 -383
  10. package/dist/cjs/remix/index.cjs.map +1 -1
  11. package/dist/cjs/svelte/index.cjs +272 -383
  12. package/dist/cjs/svelte/index.cjs.map +1 -1
  13. package/dist/cjs/vue/index.cjs +272 -383
  14. package/dist/cjs/vue/index.cjs.map +1 -1
  15. package/dist/cjs/wizard/index.cjs +5 -5
  16. package/dist/cjs/wizard/index.cjs.map +1 -1
  17. package/dist/cli/ai-auto-install.js +5 -5
  18. package/dist/cli/ai-auto-install.js.map +1 -1
  19. package/dist/cli/auto-install.js +5 -5
  20. package/dist/cli/auto-install.js.map +1 -1
  21. package/dist/esm/angular/index.js +276 -387
  22. package/dist/esm/angular/index.js.map +1 -1
  23. package/dist/esm/index.js +272 -383
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/install-wizard.js +5 -5
  26. package/dist/esm/install-wizard.js.map +1 -1
  27. package/dist/esm/react/index.js +282 -393
  28. package/dist/esm/react/index.js.map +1 -1
  29. package/dist/esm/remix/index.js +272 -383
  30. package/dist/esm/remix/index.js.map +1 -1
  31. package/dist/esm/svelte/index.js +272 -383
  32. package/dist/esm/svelte/index.js.map +1 -1
  33. package/dist/esm/vue/index.js +272 -383
  34. package/dist/esm/vue/index.js.map +1 -1
  35. package/dist/esm/wizard/index.js +5 -5
  36. package/dist/esm/wizard/index.js.map +1 -1
  37. package/dist/index.min.js +1 -1
  38. package/dist/index.min.js.map +1 -1
  39. package/dist/types/angular/index.d.ts +39 -9
  40. package/dist/types/index.d.ts +74 -59
  41. package/dist/types/install-wizard.d.ts +1 -1
  42. package/dist/types/react/index.d.ts +40 -10
  43. package/dist/types/remix/index.d.ts +37 -7
  44. package/dist/types/svelte/index.d.ts +37 -7
  45. package/dist/types/wizard/index.d.ts +1 -1
  46. package/package.json +1 -1
  47. package/readme.md +59 -5
  48. package/src/angular/index.ts +4 -4
  49. package/src/react/index.tsx +10 -10
  50. package/src/redact.ts +205 -399
  51. package/src/tracker.ts +103 -19
  52. package/src/wizard/core/install-wizard.ts +5 -5
package/src/redact.ts CHANGED
@@ -1,6 +1,6 @@
1
- // Redaction functionality for sensitive input fields
2
- // This module provides methods to configure rrweb's built-in masking
3
- // Uses CSS selectors and classes for reliable redaction without event corruption
1
+ // Simplified redaction functionality for HumanBehavior SDK
2
+ // Since rrweb auto-redacts all input fields by default, this module only handles
3
+ // selectively unredacting specific fields (except passwords which remain protected)
4
4
 
5
5
  import { logDebug, logWarn } from './utils/logger';
6
6
 
@@ -10,12 +10,20 @@ const isBrowser = typeof window !== 'undefined';
10
10
  export interface RedactionOptions {
11
11
  redactedText?: string;
12
12
  excludeSelectors?: string[];
13
- userFields?: string[]; // Fields that the user wants to redact
13
+ userFields?: string[]; // Fields that the user wants to unredact (legacy)
14
+ redactionStrategy?: {
15
+ mode: 'privacy-first' | 'visibility-first';
16
+ unredactFields?: string[]; // Fields to make visible (when mode: 'privacy-first')
17
+ redactFields?: string[]; // Fields to hide (when mode: 'visibility-first')
18
+ };
19
+ legacyRedactFields?: string[]; // For backward compatibility
14
20
  }
15
21
 
16
22
  export class RedactionManager {
17
23
  private redactedText: string = '[REDACTED]';
18
- private userSelectedFields: Set<string> = new Set(); // User-selected fields to redact
24
+ private unredactedFields: Set<string> = new Set(); // Fields that user wants to unredact
25
+ private redactedFields: Set<string> = new Set(); // Fields that user wants to redact
26
+ private redactionMode: 'privacy-first' | 'visibility-first' = 'privacy-first';
19
27
  private excludeSelectors: string[] = [
20
28
  '[data-no-redact="true"]',
21
29
  '.human-behavior-no-redact'
@@ -28,468 +36,201 @@ export class RedactionManager {
28
36
  if (options?.excludeSelectors) {
29
37
  this.excludeSelectors = [...this.excludeSelectors, ...options.excludeSelectors];
30
38
  }
31
- if (options?.userFields) {
32
- this.setFieldsToRedact(options.userFields);
33
- }
34
- }
35
-
36
- /**
37
- * Set specific fields to be redacted using CSS selectors
38
- * These selectors are used to configure rrweb's built-in masking
39
- * @param fields Array of CSS selectors for fields to redact
40
- */
41
- public setFieldsToRedact(fields: string[]): void {
42
- this.userSelectedFields.clear();
43
- fields.forEach(field => this.userSelectedFields.add(field));
44
39
 
45
- if (fields.length > 0) {
46
- logDebug(`Redaction: Active for ${fields.length} field(s):`, fields);
40
+ // Handle new redaction strategy
41
+ if (options?.redactionStrategy) {
42
+ this.redactionMode = options.redactionStrategy.mode;
47
43
 
48
- // Debug: Check if elements exist
49
- fields.forEach(selector => {
50
- const elements = document.querySelectorAll(selector);
51
- logDebug(`Redaction: Found ${elements.length} element(s) for selector '${selector}'`);
52
- elements.forEach((el, index) => {
53
- logDebug(`Redaction: Element ${index} for '${selector}':`, el);
54
- });
55
- });
56
- } else {
57
- logDebug('Redaction: Disabled - no fields selected');
58
- }
59
- }
60
-
61
- /**
62
- * Check if redaction is currently active (has fields selected)
63
- */
64
- public isActive(): boolean {
65
- return this.userSelectedFields.size > 0;
66
- }
67
-
68
- /**
69
- * Get the currently selected fields for redaction
70
- */
71
- public getSelectedFields(): string[] {
72
- return Array.from(this.userSelectedFields);
73
- }
74
-
75
- /**
76
- * Process an event and redact sensitive data if needed
77
- * NOTE: This method is no longer used - events are handled directly by rrweb
78
- * Kept for backward compatibility but not called in the current implementation
79
- */
80
- public processEvent(event: any): any {
81
- // Only process if we have fields selected for redaction
82
- if (this.userSelectedFields.size === 0) {
83
- return event;
84
- }
85
-
86
- // Clone the event to avoid modifying the original
87
- const processedEvent = JSON.parse(JSON.stringify(event));
88
-
89
- // Handle different event types
90
- if (processedEvent.type === 3) { // IncrementalSnapshot
91
- if (processedEvent.data.source === 5) { // Input event
92
- const shouldRedact = this.isFieldSelected(processedEvent.data);
93
- if (shouldRedact) {
94
- logDebug('Redaction: Processing input event for redaction');
95
- this.redactInputEvent(processedEvent.data);
44
+ if (this.redactionMode === 'privacy-first') {
45
+ // Privacy-first: everything redacted by default, unredact specific fields
46
+ if (options.redactionStrategy.unredactFields) {
47
+ this.setFieldsToUnredact(options.redactionStrategy.unredactFields);
48
+ }
49
+ } else {
50
+ // Visibility-first: everything visible by default, redact specific fields
51
+ if (options.redactionStrategy.redactFields) {
52
+ this.setFieldsToRedact(options.redactionStrategy.redactFields);
96
53
  }
97
- }
98
- // Also check for other sources that might contain text changes
99
- else if (processedEvent.data.source === 0) { // DOM mutations
100
- this.redactDOMEvent(processedEvent.data);
101
- }
102
- // Handle other sources that might contain text
103
- else if (processedEvent.data.source === 2) { // Mouse/Touch interaction
104
- this.redactMouseEvent(processedEvent.data);
105
54
  }
106
55
  }
107
- else if (processedEvent.type === 2) { // FullSnapshot
108
- this.redactFullSnapshot(processedEvent.data);
56
+
57
+ // Handle legacy redactFields (backward compatibility)
58
+ if (options?.legacyRedactFields) {
59
+ this.setFieldsToUnredact(options.legacyRedactFields);
60
+ }
61
+
62
+ // Handle legacy userFields
63
+ if (options?.userFields) {
64
+ this.setFieldsToUnredact(options.userFields);
109
65
  }
110
-
111
- return processedEvent;
112
66
  }
113
67
 
114
68
  /**
115
- * Redact sensitive data in input events
69
+ * Set specific fields to be redacted (for visibility-first mode)
70
+ * @param fields Array of CSS selectors for fields to redact
116
71
  */
117
- private redactInputEvent(inputData: any): void {
118
- // Check if this input event is from a field we want to redact
119
- if (!this.isFieldSelected(inputData)) {
120
- return;
121
- }
72
+ public setFieldsToRedact(fields: string[]): void {
73
+ this.redactedFields.clear();
122
74
 
123
- logDebug('Redaction: Redacting input event with text:', inputData.text);
75
+ // Always include password fields in redacted list
76
+ const passwordFields = [
77
+ 'input[type="password"]',
78
+ 'input[type="password" i]',
79
+ '[type="password"]',
80
+ '[type="password" i]'
81
+ ];
124
82
 
125
- // Redact all text-related properties that could contain input data
126
- const textProperties = ['text', 'value', 'content', 'data', 'input', 'textContent'];
127
- textProperties.forEach(prop => {
128
- if (inputData[prop] !== undefined && typeof inputData[prop] === 'string') {
129
- inputData[prop] = this.redactedText;
130
- logDebug(`Redaction: Redacted property '${prop}'`);
131
- }
83
+ // Add password fields and user-specified fields
84
+ [...passwordFields, ...fields].forEach(field => {
85
+ this.redactedFields.add(field);
132
86
  });
133
87
 
134
- // Also check for any other string properties that might contain input data
135
- Object.keys(inputData).forEach(key => {
136
- if (typeof inputData[key] === 'string' && inputData[key].length > 0) {
137
- inputData[key] = this.redactedText;
138
- logDebug(`Redaction: Redacted additional property '${key}'`);
139
- }
140
- });
141
-
142
- // Handle nested objects that might contain text data
143
- if (inputData.attributes && typeof inputData.attributes === 'object') {
144
- if (inputData.attributes.value && typeof inputData.attributes.value === 'string') {
145
- inputData.attributes.value = this.redactedText;
146
- logDebug('Redaction: Redacted nested value attribute');
147
- }
148
- }
149
-
150
- logDebug('Redaction: Input event redaction complete');
151
- }
152
-
153
- /**
154
- * Redact sensitive data in DOM mutation events
155
- */
156
- private redactDOMEvent(domData: any): void {
157
- // Check for text changes in DOM mutations
158
- if (domData.texts && Array.isArray(domData.texts)) {
159
- domData.texts.forEach((textChange: any) => {
160
- if (textChange.text && typeof textChange.text === 'string' &&
161
- this.shouldRedactDOMChange(textChange)) {
162
- textChange.text = this.redactedText;
163
- }
164
- });
88
+ if (this.redactedFields.size > 0) {
89
+ logDebug(`Redaction: Active for ${this.redactedFields.size} field(s):`, Array.from(this.redactedFields));
90
+ } else {
91
+ logDebug('Redaction: No fields to redact');
165
92
  }
166
93
 
167
- // Also check for attribute changes that might contain input data
168
- if (domData.attributes && Array.isArray(domData.attributes)) {
169
- domData.attributes.forEach((attrChange: any) => {
170
- if (attrChange.attributes && attrChange.attributes.value &&
171
- typeof attrChange.attributes.value === 'string' &&
172
- this.shouldRedactDOMChange(attrChange)) {
173
- attrChange.attributes.value = this.redactedText;
174
- }
175
- });
176
- }
177
-
178
- // Check for any other properties that might contain text data
179
- if (domData.adds && Array.isArray(domData.adds)) {
180
- domData.adds.forEach((add: any) => {
181
- if (add.node && add.node.textContent && typeof add.node.textContent === 'string' &&
182
- this.shouldRedactDOMChange(add)) {
183
- add.node.textContent = this.redactedText;
184
- }
185
- });
186
- }
94
+ this.applyRedactionClasses();
187
95
  }
188
96
 
189
97
  /**
190
- * Check if a DOM change should be redacted based on its ID
98
+ * Set specific fields to be unredacted (everything else stays redacted by rrweb)
99
+ * @param fields Array of CSS selectors for fields to unredact
191
100
  */
192
- private shouldRedactDOMChange(changeData: any): boolean {
193
- if (!isBrowser) return false;
101
+ public setFieldsToUnredact(fields: string[]): void {
102
+ this.unredactedFields.clear();
194
103
 
195
- try {
196
- // Check if this change has an ID that we can use to find the element
197
- const elementId = changeData.id;
198
- if (elementId !== undefined) {
199
- // Try to find the element by data-rrweb-id attribute
200
- let element = document.querySelector(`[data-rrweb-id="${elementId}"]`) as HTMLElement;
201
-
202
- if (element) {
203
- return this.shouldRedactElement(element);
204
- }
205
- }
206
-
207
- // Also check for nodeId which is another way rrweb identifies elements
208
- const nodeId = changeData.nodeId;
209
- if (nodeId !== undefined) {
210
- const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`) as HTMLElement;
211
- if (element) {
212
- return this.shouldRedactElement(element);
213
- }
104
+ // Filter out password fields (they cannot be unredacted)
105
+ const validFields = fields.filter(field => {
106
+ const isPasswordField = this.isPasswordSelector(field);
107
+ if (isPasswordField) {
108
+ logWarn(`Cannot unredact password field: ${field} - Password fields are always protected`);
109
+ return false;
214
110
  }
215
-
216
- return false;
217
- } catch (e) {
218
- logWarn('Error checking if DOM change should be redacted:', e);
219
- return false;
220
- }
221
- }
222
-
223
- /**
224
- * Redact sensitive data in mouse/touch interaction events
225
- */
226
- private redactMouseEvent(mouseData: any): void {
227
- // Mouse events typically don't contain text data, but check for any text properties
228
- if (mouseData.text && typeof mouseData.text === 'string' &&
229
- this.isFieldSelected(mouseData)) {
230
- mouseData.text = this.redactedText;
231
- }
232
- }
233
-
234
- /**
235
- * Redact sensitive data in full snapshot events
236
- */
237
- private redactFullSnapshot(snapshotData: any): void {
238
- if (snapshotData.node && snapshotData.node.type === 2) { // Element node
239
- this.redactNode(snapshotData.node);
111
+ return true;
112
+ });
113
+
114
+ validFields.forEach(field => this.unredactedFields.add(field));
115
+
116
+ if (validFields.length > 0) {
117
+ logDebug(`Unredaction: Active for ${validFields.length} field(s):`, validFields);
118
+ } else {
119
+ logDebug('Unredaction: No valid fields to unredact');
240
120
  }
121
+
122
+ this.applyUnredactionClasses();
241
123
  }
242
124
 
243
125
  /**
244
- * Recursively redact sensitive data in DOM nodes
126
+ * Remove specific fields from unredaction (they become redacted again)
127
+ * @param fields Array of CSS selectors for fields to redact
245
128
  */
246
- private redactNode(node: any): void {
247
- if (!node) return;
248
-
249
- // Check if this node should be redacted
250
- if (node.type === 2 && node.tagName &&
251
- (node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea')) {
252
-
253
- // Check if this input/textarea should be redacted
254
- if (this.shouldRedactNode(node)) {
255
- // Redact value attribute
256
- if (node.attributes && node.attributes.value) {
257
- node.attributes.value = this.redactedText;
258
- }
259
- // Redact text content
260
- if (node.textContent) {
261
- node.textContent = this.redactedText;
262
- }
263
- }
264
- }
265
-
266
- // Recursively process child nodes
267
- if (node.childNodes && Array.isArray(node.childNodes)) {
268
- node.childNodes.forEach((childNode: any) => {
269
- this.redactNode(childNode);
270
- });
129
+ public redactFields(fields: string[]): void {
130
+ fields.forEach(field => {
131
+ this.unredactedFields.delete(field);
132
+ });
133
+
134
+ if (this.unredactedFields.size > 0) {
135
+ logDebug(`Unredaction: Removed ${fields.length} field(s), ${this.unredactedFields.size} remaining:`, Array.from(this.unredactedFields));
136
+ } else {
137
+ logDebug('Unredaction: All fields redacted');
271
138
  }
139
+
140
+ this.applyUnredactionClasses();
272
141
  }
273
142
 
274
143
  /**
275
- * Check if a node should be redacted based on its attributes
144
+ * Clear all unredacted fields (everything becomes redacted again)
276
145
  */
277
- private shouldRedactNode(node: any): boolean {
278
- if (!node.attributes) return false;
279
-
280
- // Check if any of our selectors would match this node
281
- for (const selector of this.userSelectedFields) {
282
- if (this.selectorMatchesNode(selector, node)) {
283
- return true;
284
- }
285
- }
286
-
287
- return false;
146
+ public clearUnredactedFields(): void {
147
+ this.unredactedFields.clear();
148
+ logDebug('Unredaction: All fields cleared, everything redacted');
149
+
150
+ this.removeUnredactionClasses();
288
151
  }
289
152
 
290
153
  /**
291
- * Check if a CSS selector would match a node based on its attributes
154
+ * Check if any fields are currently unredacted
292
155
  */
293
- private selectorMatchesNode(selector: string, node: any): boolean {
294
- if (!node.attributes) return false;
295
-
296
- // Create a temporary element to test the selector
297
- try {
298
- const tempElement = document.createElement(node.tagName || 'div');
299
-
300
- // Copy attributes from the node to the temp element
301
- if (node.attributes) {
302
- Object.keys(node.attributes).forEach(key => {
303
- tempElement.setAttribute(key, node.attributes[key]);
304
- });
305
- }
306
-
307
- // Test if the selector matches this element
308
- return tempElement.matches(selector);
309
- } catch (e) {
310
- // If matches() is not supported or fails, fall back to basic attribute checking
311
- return this.basicSelectorMatch(selector, node);
312
- }
156
+ public hasUnredactedFields(): boolean {
157
+ return this.unredactedFields.size > 0;
313
158
  }
314
159
 
315
160
  /**
316
- * Basic selector matching for environments where matches() is not available
161
+ * Get the current redaction mode
317
162
  */
318
- private basicSelectorMatch(selector: string, node: any): boolean {
319
- if (!node.attributes) return false;
320
-
321
- // Handle simple selectors like 'input[type="password"]'
322
- if (selector.includes('input[type=')) {
323
- const typeMatch = selector.match(/input\[type="([^"]+)"\]/);
324
- if (typeMatch && node.tagName === 'input' && node.attributes.type === typeMatch[1]) {
325
- return true;
326
- }
327
- }
328
-
329
- // Handle ID selectors like '#email'
330
- if (selector.startsWith('#')) {
331
- const id = selector.substring(1);
332
- return node.attributes.id === id;
333
- }
334
-
335
- // Handle class selectors like '.sensitive-field'
336
- if (selector.startsWith('.')) {
337
- const className = selector.substring(1);
338
- return node.attributes.class && node.attributes.class.includes(className);
339
- }
340
-
341
- // Handle tag selectors like 'input'
342
- if (!selector.includes('[') && !selector.includes('.')) {
343
- return node.tagName && node.tagName.toLowerCase() === selector.toLowerCase();
344
- }
345
-
346
- return false;
163
+ public getRedactionMode(): 'privacy-first' | 'visibility-first' {
164
+ return this.redactionMode;
347
165
  }
348
166
 
349
167
  /**
350
- * Check if an event is from a field that should be redacted
168
+ * Get the currently unredacted fields
351
169
  */
352
- private isFieldSelected(eventData: any): boolean {
353
- if (!isBrowser) return false;
354
-
355
- try {
356
- // For input events (source 5), we need to determine if this is a sensitive field
357
- if (eventData.source === 5) { // Input event
358
- const elementId = eventData.id;
359
- if (elementId !== undefined) {
360
- // Try to find the element by data-rrweb-id attribute
361
- let element = document.querySelector(`[data-rrweb-id="${elementId}"]`) as HTMLElement;
362
-
363
- if (element) {
364
- return this.shouldRedactElement(element);
365
- }
366
-
367
- // Fallback: Try to find by nodeId if available
368
- if (eventData.nodeId !== undefined) {
369
- element = document.querySelector(`[data-rrweb-id="${eventData.nodeId}"]`) as HTMLElement;
370
- if (element) {
371
- return this.shouldRedactElement(element);
372
- }
373
- }
374
-
375
- // More aggressive approach: Check all elements that match our selectors
376
- // and see if any of them are currently focused or have the same ID
377
- for (const selector of this.userSelectedFields) {
378
- const matchingElements = document.querySelectorAll(selector);
379
- if (matchingElements.length > 0) {
380
- // Check if any of these elements are currently focused
381
- for (const el of matchingElements) {
382
- if (el === document.activeElement) {
383
- logDebug('Redaction: Found focused element matching selector:', selector);
384
- return true;
385
- }
386
- }
387
- }
388
- }
389
-
390
- // If we still can't find it, try a more direct approach
391
- // Look for any input element that might be the active one
392
- const activeElement = document.activeElement;
393
- if (activeElement && this.shouldRedactElement(activeElement as HTMLElement)) {
394
- logDebug('Redaction: Active element should be redacted');
395
- return true;
396
- }
397
-
398
- return false;
399
- }
400
- }
401
-
402
- // For other event types, try to find the element
403
- const elementId = eventData.id;
404
- if (elementId !== undefined) {
405
- // First try to find by data-rrweb-id attribute
406
- let element = document.querySelector(`[data-rrweb-id="${elementId}"]`) as HTMLElement;
407
-
408
- if (element) {
409
- return this.shouldRedactElement(element);
410
- }
411
- }
412
-
413
- // Also check for nodeId which is another way rrweb identifies elements
414
- const nodeId = eventData.nodeId;
415
- if (nodeId !== undefined) {
416
- const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`) as HTMLElement;
417
- if (element) {
418
- return this.shouldRedactElement(element);
419
- }
420
- }
421
-
422
- // For DOM mutations, check if the target element should be redacted
423
- if (eventData.target && eventData.target.id) {
424
- const element = document.querySelector(`[data-rrweb-id="${eventData.target.id}"]`) as HTMLElement;
425
- if (element) {
426
- return this.shouldRedactElement(element);
427
- }
428
- }
429
-
430
- return false;
431
- } catch (e) {
432
- logWarn('Error checking if field should be redacted:', e);
433
- return false;
434
- }
170
+ public getUnredactedFields(): string[] {
171
+ return Array.from(this.unredactedFields);
435
172
  }
436
173
 
437
174
  /**
438
175
  * Get CSS selectors for rrweb masking configuration
439
- * Used to configure rrweb's maskTextSelector option
176
+ * Returns null if no fields are unredacted (everything stays redacted)
440
177
  */
441
178
  public getMaskTextSelector(): string | null {
442
- if (this.userSelectedFields.size === 0) {
443
- return null;
179
+ if (this.redactionMode === 'privacy-first') {
180
+ // Privacy-first: mask everything except unredacted fields
181
+ if (this.unredactedFields.size === 0) {
182
+ return null; // Everything stays redacted
183
+ }
184
+ return Array.from(this.unredactedFields).join(',');
185
+ } else {
186
+ // Visibility-first: mask only redacted fields
187
+ if (this.redactedFields.size === 0) {
188
+ return null; // Nothing to redact
189
+ }
190
+ return Array.from(this.redactedFields).join(',');
444
191
  }
445
- return Array.from(this.userSelectedFields).join(',');
446
192
  }
447
193
 
448
194
  /**
449
- * Check if an element should be redacted (for rrweb maskTextFn/maskInputFn)
195
+ * Apply redaction classes to DOM elements (for visibility-first mode)
196
+ * Adds 'rr-mask' class to elements that should be redacted
450
197
  */
451
- public shouldRedactElement(element: HTMLElement): boolean {
452
- if (this.userSelectedFields.size === 0) {
453
- return false;
198
+ public applyRedactionClasses(): void {
199
+ if (this.redactedFields.size === 0) {
200
+ return;
454
201
  }
455
202
 
456
- // Check if any selector matches this element
457
- for (const selector of this.userSelectedFields) {
203
+ // Add 'rr-mask' class to redacted elements
204
+ this.redactedFields.forEach(selector => {
458
205
  try {
459
- if (element.matches(selector)) {
460
- return true;
461
- }
206
+ const elements = document.querySelectorAll(selector);
207
+ elements.forEach(element => {
208
+ element.classList.add('rr-mask');
209
+ });
210
+ logDebug(`Added rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
462
211
  } catch (e) {
463
- // Invalid selector, skip
464
212
  logWarn(`Invalid selector: ${selector}`);
465
213
  }
466
- }
467
- return false;
214
+ });
468
215
  }
469
216
 
470
217
  /**
471
- * Apply rrweb masking classes to DOM elements
472
- * Adds 'rr-mask' class to elements that should be redacted
473
- * This enables rrweb's built-in masking functionality
218
+ * Apply unredaction classes to DOM elements
219
+ * Removes 'rr-mask' class from elements that should be unredacted
474
220
  */
475
- public applyRedactionClasses(): void {
476
- if (this.userSelectedFields.size === 0) {
221
+ public applyUnredactionClasses(): void {
222
+ if (this.unredactedFields.size === 0) {
477
223
  return;
478
224
  }
479
225
 
480
- // Remove existing redaction classes
481
- document.querySelectorAll('.rr-mask').forEach(element => {
482
- element.classList.remove('rr-mask');
483
- });
484
-
485
- // Add redaction classes to matching elements
486
- this.userSelectedFields.forEach(selector => {
226
+ // Remove 'rr-mask' class from unredacted elements
227
+ this.unredactedFields.forEach(selector => {
487
228
  try {
488
229
  const elements = document.querySelectorAll(selector);
489
230
  elements.forEach(element => {
490
- element.classList.add('rr-mask');
231
+ element.classList.remove('rr-mask');
491
232
  });
492
- logDebug(`Applied rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
233
+ logDebug(`Removed rr-mask class from ${elements.length} element(s) for selector: ${selector}`);
493
234
  } catch (e) {
494
235
  logWarn(`Invalid selector: ${selector}`);
495
236
  }
@@ -497,7 +238,31 @@ export class RedactionManager {
497
238
  }
498
239
 
499
240
  /**
500
- * Get the original value of a redacted element (for debugging)
241
+ * Remove all unredaction classes from DOM elements
242
+ */
243
+ public removeUnredactionClasses(): void {
244
+ // Note: This doesn't add 'rr-mask' classes back - rrweb handles that automatically
245
+ logDebug('Unredaction classes removed');
246
+ }
247
+
248
+ /**
249
+ * Check if a selector represents a password field
250
+ */
251
+ private isPasswordSelector(selector: string): boolean {
252
+ const passwordPatterns = [
253
+ 'input[type="password"]',
254
+ 'input[type="password" i]',
255
+ '[type="password"]',
256
+ '[type="password" i]'
257
+ ];
258
+
259
+ return passwordPatterns.some(pattern =>
260
+ selector.toLowerCase().includes(pattern.toLowerCase().replace(/[\[\]]/g, ''))
261
+ );
262
+ }
263
+
264
+ /**
265
+ * Get the original value of an element (for debugging)
501
266
  */
502
267
  public getOriginalValue(element: HTMLElement): string | undefined {
503
268
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
@@ -507,10 +272,51 @@ export class RedactionManager {
507
272
  }
508
273
 
509
274
  /**
510
- * Check if an element is currently being redacted
275
+ * Check if an element is currently unredacted
511
276
  */
512
- public isElementRedacted(element: HTMLElement): boolean {
513
- return this.shouldRedactElement(element);
277
+ public isElementUnredacted(element: HTMLElement): boolean {
278
+ return this.shouldUnredactElement(element);
279
+ }
280
+
281
+ /**
282
+ * Check if an element should be unredacted
283
+ */
284
+ public shouldUnredactElement(element: HTMLElement): boolean {
285
+ if (this.redactionMode === 'privacy-first') {
286
+ // Privacy-first: check if element is in unredacted fields
287
+ if (this.unredactedFields.size === 0) {
288
+ return false; // Nothing unredacted
289
+ }
290
+
291
+ // Check if any selector matches this element
292
+ for (const selector of this.unredactedFields) {
293
+ try {
294
+ if (element.matches(selector)) {
295
+ return true;
296
+ }
297
+ } catch (e) {
298
+ logWarn(`Invalid selector: ${selector}`);
299
+ }
300
+ }
301
+ return false;
302
+ } else {
303
+ // Visibility-first: check if element is NOT in redacted fields
304
+ if (this.redactedFields.size === 0) {
305
+ return true; // Nothing redacted, everything visible
306
+ }
307
+
308
+ // Check if any selector matches this element
309
+ for (const selector of this.redactedFields) {
310
+ try {
311
+ if (element.matches(selector)) {
312
+ return false; // Element is redacted
313
+ }
314
+ } catch (e) {
315
+ logWarn(`Invalid selector: ${selector}`);
316
+ }
317
+ }
318
+ return true; // Element is not redacted
319
+ }
514
320
  }
515
321
  }
516
322