@tindalabs/shield 0.1.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 (118) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +357 -0
  3. package/dist/assess.d.ts +16 -0
  4. package/dist/assess.js +220 -0
  5. package/dist/config/default-extensions-config.json +103 -0
  6. package/dist/core/ContentProtector.d.ts +63 -0
  7. package/dist/core/ContentProtector.js +281 -0
  8. package/dist/core/index.d.ts +1 -0
  9. package/dist/core/index.js +2 -0
  10. package/dist/core/mediator/ContentProtectionMediator.d.ts +86 -0
  11. package/dist/core/mediator/ContentProtectionMediator.js +238 -0
  12. package/dist/core/mediator/eventDataTypes.d.ts +112 -0
  13. package/dist/core/mediator/eventDataTypes.js +23 -0
  14. package/dist/core/mediator/handlers/abstractEventHandler.d.ts +41 -0
  15. package/dist/core/mediator/handlers/abstractEventHandler.js +59 -0
  16. package/dist/core/mediator/handlers/devToolsEventHandler.d.ts +9 -0
  17. package/dist/core/mediator/handlers/devToolsEventHandler.js +95 -0
  18. package/dist/core/mediator/handlers/eventHandlerRegistry.d.ts +9 -0
  19. package/dist/core/mediator/handlers/eventHandlerRegistry.js +34 -0
  20. package/dist/core/mediator/handlers/extensionEventHandlers.d.ts +40 -0
  21. package/dist/core/mediator/handlers/extensionEventHandlers.js +140 -0
  22. package/dist/core/mediator/handlers/iFrameEventHandlers.d.ts +27 -0
  23. package/dist/core/mediator/handlers/iFrameEventHandlers.js +93 -0
  24. package/dist/core/mediator/handlers/screenShotEventHandlers.d.ts +34 -0
  25. package/dist/core/mediator/handlers/screenShotEventHandlers.js +111 -0
  26. package/dist/core/mediator/protection-event.d.ts +77 -0
  27. package/dist/core/mediator/protection-event.js +32 -0
  28. package/dist/core/mediator/types.d.ts +105 -0
  29. package/dist/core/mediator/types.js +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +7 -0
  32. package/dist/otel.d.ts +24 -0
  33. package/dist/otel.js +83 -0
  34. package/dist/policy.d.ts +98 -0
  35. package/dist/policy.js +97 -0
  36. package/dist/strategies/AbstractStrategy.d.ts +124 -0
  37. package/dist/strategies/AbstractStrategy.js +256 -0
  38. package/dist/strategies/ClipboardStrategy.d.ts +67 -0
  39. package/dist/strategies/ClipboardStrategy.js +291 -0
  40. package/dist/strategies/ContextMenuStrategy.d.ts +60 -0
  41. package/dist/strategies/ContextMenuStrategy.js +454 -0
  42. package/dist/strategies/DevToolsStrategy.d.ts +55 -0
  43. package/dist/strategies/DevToolsStrategy.js +314 -0
  44. package/dist/strategies/ExtensionStrategy.d.ts +66 -0
  45. package/dist/strategies/ExtensionStrategy.js +486 -0
  46. package/dist/strategies/IFrameStrategy.d.ts +49 -0
  47. package/dist/strategies/IFrameStrategy.js +255 -0
  48. package/dist/strategies/KeyboardStrategy.d.ts +35 -0
  49. package/dist/strategies/KeyboardStrategy.js +130 -0
  50. package/dist/strategies/PrintStrategy.d.ts +47 -0
  51. package/dist/strategies/PrintStrategy.js +201 -0
  52. package/dist/strategies/ScreenshotStrategy.d.ts +90 -0
  53. package/dist/strategies/ScreenshotStrategy.js +502 -0
  54. package/dist/strategies/SelectionStrategy.d.ts +49 -0
  55. package/dist/strategies/SelectionStrategy.js +216 -0
  56. package/dist/strategies/WatermarkStrategy.d.ts +56 -0
  57. package/dist/strategies/WatermarkStrategy.js +287 -0
  58. package/dist/strategies/index.d.ts +10 -0
  59. package/dist/strategies/index.js +11 -0
  60. package/dist/types/assessment.d.ts +62 -0
  61. package/dist/types/assessment.js +1 -0
  62. package/dist/types/index.d.ts +278 -0
  63. package/dist/types/index.js +17 -0
  64. package/dist/utils/DOMObserver.d.ts +68 -0
  65. package/dist/utils/DOMObserver.js +134 -0
  66. package/dist/utils/base/LoggableComponent.d.ts +44 -0
  67. package/dist/utils/base/LoggableComponent.js +56 -0
  68. package/dist/utils/detectors/AbstractDevToolsDetector.d.ts +98 -0
  69. package/dist/utils/detectors/AbstractDevToolsDetector.js +127 -0
  70. package/dist/utils/detectors/dateToStringDetector.d.ts +43 -0
  71. package/dist/utils/detectors/dateToStringDetector.js +96 -0
  72. package/dist/utils/detectors/debugLibDetector.d.ts +64 -0
  73. package/dist/utils/detectors/debugLibDetector.js +195 -0
  74. package/dist/utils/detectors/debuggerDetector.d.ts +51 -0
  75. package/dist/utils/detectors/debuggerDetector.js +211 -0
  76. package/dist/utils/detectors/defineGetterDetector.d.ts +48 -0
  77. package/dist/utils/detectors/defineGetterDetector.js +150 -0
  78. package/dist/utils/detectors/detectorInterface.d.ts +36 -0
  79. package/dist/utils/detectors/detectorInterface.js +1 -0
  80. package/dist/utils/detectors/devToolsDetectorManager.d.ts +88 -0
  81. package/dist/utils/detectors/devToolsDetectorManager.js +243 -0
  82. package/dist/utils/detectors/funcToStringDetector.d.ts +43 -0
  83. package/dist/utils/detectors/funcToStringDetector.js +90 -0
  84. package/dist/utils/detectors/regToStringDetector.d.ts +43 -0
  85. package/dist/utils/detectors/regToStringDetector.js +129 -0
  86. package/dist/utils/detectors/sizeDetector.d.ts +54 -0
  87. package/dist/utils/detectors/sizeDetector.js +134 -0
  88. package/dist/utils/detectors/timingDetector.d.ts +55 -0
  89. package/dist/utils/detectors/timingDetector.js +143 -0
  90. package/dist/utils/dom.d.ts +20 -0
  91. package/dist/utils/dom.js +83 -0
  92. package/dist/utils/environment.d.ts +29 -0
  93. package/dist/utils/environment.js +267 -0
  94. package/dist/utils/eventManager.d.ts +162 -0
  95. package/dist/utils/eventManager.js +548 -0
  96. package/dist/utils/index.d.ts +2 -0
  97. package/dist/utils/index.js +3 -0
  98. package/dist/utils/intervalManager.d.ts +91 -0
  99. package/dist/utils/intervalManager.js +221 -0
  100. package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.d.ts +41 -0
  101. package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.js +135 -0
  102. package/dist/utils/keyboardShortcutManager/keyboardShortcuts.d.ts +18 -0
  103. package/dist/utils/keyboardShortcutManager/keyboardShortcuts.js +195 -0
  104. package/dist/utils/logging/simple/Loggable.d.ts +33 -0
  105. package/dist/utils/logging/simple/Loggable.js +1 -0
  106. package/dist/utils/logging/simple/LoggingDelegate.d.ts +42 -0
  107. package/dist/utils/logging/simple/LoggingDelegate.js +53 -0
  108. package/dist/utils/logging/simple/SimpleLoggingService.d.ts +39 -0
  109. package/dist/utils/logging/simple/SimpleLoggingService.js +58 -0
  110. package/dist/utils/orientation.d.ts +15 -0
  111. package/dist/utils/orientation.js +32 -0
  112. package/dist/utils/protectedContentManager.d.ts +155 -0
  113. package/dist/utils/protectedContentManager.js +424 -0
  114. package/dist/utils/securityOverlayManager.d.ts +253 -0
  115. package/dist/utils/securityOverlayManager.js +786 -0
  116. package/dist/utils/timeoutManager.d.ts +50 -0
  117. package/dist/utils/timeoutManager.js +113 -0
  118. package/package.json +61 -0
@@ -0,0 +1,786 @@
1
+ import { DomObserver } from "./DOMObserver";
2
+ import { isBrowser } from "./environment";
3
+ import { ProtectionEventType } from "../core/mediator/protection-event";
4
+ import { isEventType } from "../core/mediator/eventDataTypes";
5
+ import { eventManager } from "./eventManager";
6
+ import { LoggableComponent } from "./base/LoggableComponent";
7
+ /**
8
+ * Utility class to manage security overlays
9
+ */
10
+ export class SecurityOverlayManager extends LoggableComponent {
11
+ /**
12
+ * Create a new SecurityOverlayManager
13
+ * @param debugMode Enable debug mode for troubleshooting
14
+ */
15
+ constructor(debugMode = false) {
16
+ super("SecurityOverlayManager", debugMode);
17
+ this.mediator = null;
18
+ this.domObserver = null;
19
+ // Main storage for overlays
20
+ this.overlays = new Map();
21
+ // Currently active/visible overlay
22
+ this.activeOverlayId = null;
23
+ // Queue of overlay IDs waiting to be shown
24
+ this.overlayQueue = [];
25
+ this.onElementsRemovedCallbacks = [];
26
+ this.logger.log("Initialized");
27
+ }
28
+ /**
29
+ * Set the mediator to communicate with the other components
30
+ * @param mediator The protection mediator
31
+ */
32
+ setMediator(mediator) {
33
+ this.mediator = mediator;
34
+ // Subscribe only to general overlay events
35
+ this.mediator.subscribe(ProtectionEventType.OVERLAY_SHOWN, this.handleOverlayShown.bind(this), {
36
+ context: this.COMPONENT_NAME,
37
+ });
38
+ this.mediator.subscribe(ProtectionEventType.OVERLAY_REMOVED, this.handleOverlayRemoved.bind(this), {
39
+ context: this.COMPONENT_NAME,
40
+ });
41
+ this.mediator.subscribe(ProtectionEventType.OVERLAY_RESTORED, this.handleOverlayRestored.bind(this), {
42
+ context: this.COMPONENT_NAME,
43
+ });
44
+ this.logger.log("Mediator set and subscriptions established");
45
+ }
46
+ /**
47
+ * Check if an overlay with the same owner and type already exists
48
+ * @param owner The owner to check
49
+ * @param overlayType The overlay type to check
50
+ * @returns True if a matching overlay exists
51
+ */
52
+ hasOverlayByOwnerAndType(owner, overlayType) {
53
+ for (const overlay of this.overlays.values()) {
54
+ if (overlay.owner === owner && overlay.overlayType === overlayType) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Handle overlay shown event
62
+ * @param event The protection event
63
+ */
64
+ handleOverlayShown(event) {
65
+ try {
66
+ this.logger.log(`Received overlay shown event from ${event.source}`, event.data);
67
+ // Use type guard for type-safe access
68
+ if (!isEventType(event, ProtectionEventType.OVERLAY_SHOWN)) {
69
+ this.logger.error("Received invalid event type for OVERLAY_SHOWN");
70
+ return;
71
+ }
72
+ const { data } = event;
73
+ if (!data || !data.options)
74
+ return;
75
+ // Check for duplicate registrations
76
+ if (this.hasOverlayByOwnerAndType(data.strategyName, data.overlayType)) {
77
+ this.logger.log(`Duplicate overlay registration detected for ${data.strategyName}/${data.overlayType}, removing existing overlay first`);
78
+ // Remove existing overlays with the same owner and type
79
+ for (const [overlayId, overlay] of this.overlays.entries()) {
80
+ if (overlay.owner === data.strategyName && overlay.overlayType === data.overlayType) {
81
+ this.removeOverlayById(overlayId);
82
+ break; // Only remove one to avoid potential issues
83
+ }
84
+ }
85
+ }
86
+ // Register and show the overlay
87
+ this.registerOverlay(data.strategyName, data.overlayType, data.options, data.priority || 0);
88
+ }
89
+ catch (error) {
90
+ this.logger.error("Error handling overlay shown event", error);
91
+ }
92
+ }
93
+ /**
94
+ * Handle overlay removed event
95
+ * @param event The protection event
96
+ */
97
+ handleOverlayRemoved(event) {
98
+ try {
99
+ this.logger.log(`Received overlay removed event from ${event.source}`, event.data);
100
+ // Use type guard for type-safe access
101
+ if (!isEventType(event, ProtectionEventType.OVERLAY_REMOVED)) {
102
+ this.logger.error("Received invalid event type for OVERLAY_REMOVED");
103
+ return;
104
+ }
105
+ const { data } = event;
106
+ if (!data)
107
+ return;
108
+ // Remove overlays by owner
109
+ this.removeOverlaysByOwner(data.strategyName);
110
+ }
111
+ catch (error) {
112
+ this.logger.error("Error handling overlay removed event", error);
113
+ }
114
+ }
115
+ /**
116
+ * Handle overlay restored event
117
+ * @param event The protection event
118
+ */
119
+ handleOverlayRestored(event) {
120
+ try {
121
+ this.logger.log(`Received overlay restored event from ${event.source}`, event.data);
122
+ const data = event.data;
123
+ if (!data)
124
+ return;
125
+ // Check and restore overlays for this owner
126
+ this.checkAndRestoreOverlaysByOwner(data.strategyName);
127
+ }
128
+ catch (error) {
129
+ this.logger.error("Error handling overlay restored event", error);
130
+ }
131
+ }
132
+ /**
133
+ * Register a new overlay
134
+ * @param owner The strategy or component that owns this overlay
135
+ * @param overlayType The type of overlay (e.g., "screenshot", "devtools")
136
+ * @param options Options for the overlay
137
+ * @param priority Priority for display order (higher displays on top)
138
+ * @returns The ID of the registered overlay
139
+ */
140
+ registerOverlay(owner, overlayType, options, priority = 0) {
141
+ if (!isBrowser())
142
+ return "";
143
+ // Generate a unique ID for the overlay
144
+ const overlayId = `overlay-${owner}-${overlayType}-${Date.now()}`;
145
+ // Create the stored overlay object
146
+ const storedOverlay = {
147
+ id: overlayId,
148
+ overlayType,
149
+ options: { ...options },
150
+ owner,
151
+ priority,
152
+ element: null,
153
+ blocker: null,
154
+ timeoutId: null,
155
+ isVisible: false,
156
+ createdAt: Date.now(),
157
+ };
158
+ // Store the overlay
159
+ this.overlays.set(overlayId, storedOverlay);
160
+ this.logger.log(`Registered overlay ${overlayId} for ${owner} (${overlayType})`);
161
+ // If no active overlay, show this one immediately
162
+ if (!this.activeOverlayId) {
163
+ this.showOverlayById(overlayId);
164
+ }
165
+ else {
166
+ // Otherwise, add to queue based on priority
167
+ this.addToQueue(overlayId);
168
+ // Check if this overlay should replace the current one based on priority
169
+ const activeOverlay = this.overlays.get(this.activeOverlayId);
170
+ const newOverlay = this.overlays.get(overlayId);
171
+ if (activeOverlay && newOverlay && newOverlay.priority > activeOverlay.priority) {
172
+ this.logger.log(`New overlay has higher priority, replacing active overlay`);
173
+ // Re-queue the displaced overlay so it reappears once the new one is
174
+ // dismissed. Without this it stays in storage but is dropped from the
175
+ // visibility pipeline entirely — effectively orphaned. addToQueue is
176
+ // idempotent and sorts by priority, so ordering is preserved.
177
+ this.addToQueue(this.activeOverlayId);
178
+ // Hide current overlay (processQueue=false: we don't want it to pick
179
+ // the next queued item, we explicitly show the new high-priority one)
180
+ this.hideOverlayById(this.activeOverlayId, false);
181
+ // Show new overlay
182
+ this.showOverlayById(overlayId);
183
+ }
184
+ }
185
+ return overlayId;
186
+ }
187
+ /**
188
+ * Add an overlay to the queue
189
+ * @param overlayId ID of the overlay to add to queue
190
+ */
191
+ addToQueue(overlayId) {
192
+ // Add to queue if not already in it
193
+ if (!this.overlayQueue.includes(overlayId)) {
194
+ this.overlayQueue.push(overlayId);
195
+ // Sort queue by priority (highest first)
196
+ this.overlayQueue.sort((a, b) => {
197
+ const overlayA = this.overlays.get(a);
198
+ const overlayB = this.overlays.get(b);
199
+ if (!overlayA || !overlayB)
200
+ return 0;
201
+ return overlayB.priority - overlayA.priority;
202
+ });
203
+ this.logger.log(`Added overlay ${overlayId} to queue, position ${this.overlayQueue.indexOf(overlayId) + 1}/${this.overlayQueue.length}`);
204
+ }
205
+ }
206
+ /**
207
+ * Show a specific overlay by ID
208
+ * @param overlayId ID of the overlay to show
209
+ * @returns True if the overlay was shown successfully
210
+ */
211
+ showOverlayById(overlayId) {
212
+ const overlay = this.overlays.get(overlayId);
213
+ if (!overlay)
214
+ return false;
215
+ this.logger.log(`Showing overlay ${overlayId} (${overlay.overlayType})`);
216
+ // Create the DOM elements
217
+ const result = this.createOverlayElements(overlay);
218
+ if (!result.overlay) {
219
+ this.logger.log(`Failed to create overlay elements for ${overlayId}`);
220
+ return false;
221
+ }
222
+ // Update the stored overlay with references to the elements
223
+ overlay.element = result.overlay;
224
+ overlay.blocker = result.blocker;
225
+ overlay.isVisible = true;
226
+ // Set as active overlay
227
+ this.activeOverlayId = overlayId;
228
+ // Remove from queue if it's in there
229
+ this.overlayQueue = this.overlayQueue.filter((id) => id !== overlayId);
230
+ // Set up auto-removal timeout if duration is specified
231
+ if (overlay.options.duration && overlay.options.duration > 0) {
232
+ overlay.timeoutId = window.setTimeout(() => {
233
+ this.removeOverlayById(overlayId);
234
+ overlay.timeoutId = null;
235
+ }, overlay.options.duration);
236
+ this.logger.log(`Overlay ${overlayId} will auto-remove after ${overlay.options.duration}ms`);
237
+ }
238
+ // Set up observer for auto-restoration if enabled
239
+ if (overlay.options.autoRestore !== false) {
240
+ this.setupObserver(overlay);
241
+ }
242
+ return true;
243
+ }
244
+ /**
245
+ * Hide a specific overlay by ID without removing it from storage
246
+ * @param overlayId ID of the overlay to hide
247
+ * @param processQueue Whether to process the queue after hiding
248
+ * @returns True if the overlay was hidden successfully
249
+ */
250
+ hideOverlayById(overlayId, processQueue = true) {
251
+ const overlay = this.overlays.get(overlayId);
252
+ if (!overlay || !overlay.isVisible)
253
+ return false;
254
+ this.logger.log(`Hiding overlay ${overlayId} (${overlay.overlayType})`);
255
+ // Clear any existing timeout
256
+ if (overlay.timeoutId !== null) {
257
+ window.clearTimeout(overlay.timeoutId);
258
+ overlay.timeoutId = null;
259
+ }
260
+ // Remove DOM elements
261
+ if (overlay.element && overlay.element.parentNode) {
262
+ overlay.element.parentNode.removeChild(overlay.element);
263
+ }
264
+ if (overlay.blocker && overlay.blocker.parentNode) {
265
+ overlay.blocker.parentNode.removeChild(overlay.blocker);
266
+ }
267
+ // Update state
268
+ overlay.element = null;
269
+ overlay.blocker = null;
270
+ overlay.isVisible = false;
271
+ // Clear active overlay reference if this was the active one
272
+ if (this.activeOverlayId === overlayId) {
273
+ this.activeOverlayId = null;
274
+ // Remove global event listeners when the active overlay is hidden
275
+ this.removeGlobalEventListeners();
276
+ // Process queue to show next overlay if requested
277
+ if (processQueue && this.overlayQueue.length > 0) {
278
+ const nextOverlayId = this.overlayQueue.shift();
279
+ if (nextOverlayId) {
280
+ this.showOverlayById(nextOverlayId);
281
+ }
282
+ }
283
+ }
284
+ return true;
285
+ }
286
+ /**
287
+ * Create overlay and blocker elements
288
+ * @param overlay The stored overlay information
289
+ * @returns Object containing the created elements
290
+ */
291
+ createOverlayElements(overlay) {
292
+ if (!isBrowser()) {
293
+ return { overlay: null, blocker: null };
294
+ }
295
+ // Create event blocker if requested
296
+ let blocker = null;
297
+ if (overlay.options.blockEvents) {
298
+ blocker = this.createEventBlocker();
299
+ document.body.appendChild(blocker);
300
+ this.logger.log(`Event blocker created for ${overlay.id}`);
301
+ }
302
+ // Create the overlay element
303
+ const overlayElement = this.createOverlay(overlay.options, overlay.owner);
304
+ document.body.appendChild(overlayElement);
305
+ this.logger.log(`Overlay element created for ${overlay.id}`);
306
+ return {
307
+ overlay: overlayElement,
308
+ blocker: blocker,
309
+ };
310
+ }
311
+ /**
312
+ * Remove a specific overlay by ID
313
+ * @param overlayId ID of the overlay to remove
314
+ * @returns True if the overlay was removed successfully
315
+ */
316
+ removeOverlayById(overlayId) {
317
+ const overlay = this.overlays.get(overlayId);
318
+ if (!overlay)
319
+ return false;
320
+ this.logger.log(`Removing overlay ${overlayId} (${overlay.overlayType})`);
321
+ // Check if this is the active overlay before hiding it
322
+ const isActive = this.activeOverlayId === overlayId;
323
+ // IMPORTANT: Stop the DOMObserver BEFORE removing DOM elements
324
+ // MutationObserver fires synchronously during DOM removal, so we must
325
+ // stop it first to prevent auto-restore from creating a new overlay
326
+ if (this.domObserver) {
327
+ this.domObserver.stopObserving();
328
+ this.domObserver = null;
329
+ this.logger.log(`Stopped DOM observer before removing overlay ${overlayId}`);
330
+ }
331
+ // Hide the overlay (removes DOM elements)
332
+ this.hideOverlayById(overlayId);
333
+ // If this WAS the active overlay, ensure global event listeners are removed
334
+ // (hideOverlayById should have already done this, but this is a safety net)
335
+ if (isActive) {
336
+ this.removeGlobalEventListeners();
337
+ }
338
+ // Remove from storage
339
+ this.overlays.delete(overlayId);
340
+ return true;
341
+ }
342
+ /**
343
+ * Remove all overlays for a specific owner
344
+ * @param owner The owner to remove overlays for
345
+ * @returns The number of overlays removed
346
+ */
347
+ removeOverlaysByOwner(owner) {
348
+ if (!isBrowser())
349
+ return 0;
350
+ let removedCount = 0;
351
+ const overlaysToRemove = [];
352
+ // Find all overlays owned by this owner
353
+ for (const [overlayId, overlay] of this.overlays.entries()) {
354
+ if (overlay.owner === owner) {
355
+ overlaysToRemove.push(overlayId);
356
+ }
357
+ }
358
+ // Remove each overlay
359
+ for (const overlayId of overlaysToRemove) {
360
+ if (this.removeOverlayById(overlayId)) {
361
+ removedCount++;
362
+ }
363
+ }
364
+ if (removedCount > 0) {
365
+ this.logger.log(`Removed ${removedCount} overlays for owner ${owner}`);
366
+ }
367
+ return removedCount;
368
+ }
369
+ /**
370
+ * Check and restore overlays for a specific owner
371
+ * @param owner The owner to check overlays for
372
+ * @returns The number of overlays restored
373
+ */
374
+ checkAndRestoreOverlaysByOwner(owner) {
375
+ if (!isBrowser())
376
+ return 0;
377
+ let restoredCount = 0;
378
+ // Find all overlays owned by this owner
379
+ for (const [overlayId, overlay] of this.overlays.entries()) {
380
+ if (overlay.owner === owner && overlay.options.autoRestore !== false && !overlay.isVisible) {
381
+ // If this overlay should be visible but isn't, restore it
382
+ if (this.activeOverlayId) {
383
+ // If there's already an active overlay, add this to the queue
384
+ this.addToQueue(overlayId);
385
+ }
386
+ else {
387
+ // Otherwise show it immediately
388
+ if (this.showOverlayById(overlayId)) {
389
+ restoredCount++;
390
+ }
391
+ }
392
+ }
393
+ }
394
+ if (restoredCount > 0) {
395
+ this.logger.log(`Restored ${restoredCount} overlays for owner ${owner}`);
396
+ }
397
+ return restoredCount;
398
+ }
399
+ /**
400
+ * Create an overlay element
401
+ * @param options Options for the overlay
402
+ * @param owner The owner of the overlay (for data attribute)
403
+ * @returns The created overlay element
404
+ */
405
+ createOverlay(options, owner) {
406
+ if (!isBrowser()) {
407
+ throw new Error("Document is not available");
408
+ }
409
+ // Create overlay element
410
+ const overlay = document.createElement("div");
411
+ overlay.id = "security-overlay";
412
+ // Add data attribute for owner identification
413
+ overlay.setAttribute("data-owner", owner);
414
+ // Apply styles
415
+ const styles = {
416
+ position: "fixed",
417
+ top: "0",
418
+ left: "0",
419
+ width: "100%",
420
+ height: "100%",
421
+ backgroundColor: options.backgroundColor || "rgba(220, 38, 38, 0.9)",
422
+ zIndex: options.zIndex || "2147483647",
423
+ display: "flex",
424
+ flexDirection: "column",
425
+ justifyContent: "center",
426
+ alignItems: "center",
427
+ fontFamily: "sans-serif",
428
+ color: options.textColor || "white",
429
+ padding: "20px",
430
+ textAlign: "center",
431
+ pointerEvents: "auto", // Always allow interaction with the overlay itself
432
+ ...options.customStyles,
433
+ };
434
+ // Apply styles to the overlay
435
+ Object.entries(styles).forEach(([key, value]) => {
436
+ if (value !== undefined) {
437
+ overlay.style[key] = value;
438
+ }
439
+ });
440
+ // Create HTML content
441
+ let content = "";
442
+ if (options.title) {
443
+ content += `<h2 style="font-size: 24px; margin-bottom: 20px;">${options.title}</h2>`;
444
+ }
445
+ if (options.message) {
446
+ content += `<p style="font-size: 16px; margin-bottom: 10px;">${options.message}</p>`;
447
+ }
448
+ if (options.secondaryMessage) {
449
+ content += `<p style="font-size: 16px; margin-top: 10px;">${options.secondaryMessage}</p>`;
450
+ }
451
+ if (options.additionalContent) {
452
+ content += options.additionalContent;
453
+ }
454
+ if (options.showCloseButton) {
455
+ content += `
456
+ <button id="security-overlay-close" style="margin-top: 20px; padding: 10px 20px; background-color: white; color: black; border: none; border-radius: 4px; cursor: pointer; pointer-events: auto;">
457
+ ${options.closeButtonText || "Close"}
458
+ </button>
459
+ `;
460
+ }
461
+ overlay.innerHTML = content;
462
+ // Add event listener to close button if present
463
+ if (options.showCloseButton) {
464
+ setTimeout(() => {
465
+ const closeButton = document.getElementById("security-overlay-close");
466
+ if (closeButton) {
467
+ closeButton.addEventListener("click", () => {
468
+ if (options.onCloseButtonClick) {
469
+ options.onCloseButtonClick();
470
+ }
471
+ else {
472
+ // Find the overlay ID by owner and remove it
473
+ for (const [overlayId, storedOverlay] of this.overlays.entries()) {
474
+ if (storedOverlay.owner === owner && storedOverlay.element === overlay) {
475
+ this.removeOverlayById(overlayId);
476
+ break;
477
+ }
478
+ }
479
+ }
480
+ });
481
+ }
482
+ }, 0);
483
+ }
484
+ return overlay;
485
+ }
486
+ /**
487
+ * Create an event blocker that prevents interaction with the page
488
+ * @returns The created event blocker element
489
+ */
490
+ createEventBlocker() {
491
+ const blocker = document.createElement("div");
492
+ blocker.id = "security-event-blocker";
493
+ // Apply styles to make it cover the entire page and block all events
494
+ blocker.style.position = "fixed";
495
+ blocker.style.top = "0";
496
+ blocker.style.left = "0";
497
+ blocker.style.width = "100%";
498
+ blocker.style.height = "100%";
499
+ blocker.style.backgroundColor = "transparent"; // Transparent but will block events
500
+ blocker.style.zIndex = "2147483646"; // Just below the overlay
501
+ blocker.style.cursor = "not-allowed"; // Show not-allowed cursor
502
+ // Add event listeners to block all interactions
503
+ const blockEvent = (e) => {
504
+ e.preventDefault();
505
+ e.stopPropagation();
506
+ return false;
507
+ };
508
+ // Block all common events
509
+ const events = [
510
+ "click",
511
+ "dblclick",
512
+ "mousedown",
513
+ "mouseup",
514
+ "mousemove",
515
+ "touchstart",
516
+ "touchend",
517
+ "touchmove",
518
+ "touchcancel",
519
+ "keydown",
520
+ "keyup",
521
+ "keypress",
522
+ "contextmenu",
523
+ "selectstart",
524
+ "dragstart",
525
+ "wheel", // Add wheel event to block scrolling
526
+ "scroll", // Add scroll event as well
527
+ ];
528
+ events.forEach((eventType) => {
529
+ blocker.addEventListener(eventType, blockEvent, { capture: true, passive: false });
530
+ });
531
+ // Additional handling for wheel events on document and window
532
+ if (isBrowser()) {
533
+ // Use eventManager to register global event listeners
534
+ eventManager.addEventListener(document, "wheel", blockEvent, this.COMPONENT_NAME, {
535
+ capture: true,
536
+ passive: false,
537
+ priority: 10,
538
+ });
539
+ eventManager.addEventListener(window, "wheel", blockEvent, this.COMPONENT_NAME, {
540
+ capture: true,
541
+ passive: false,
542
+ priority: 10,
543
+ });
544
+ // Also prevent scrolling via touch on mobile
545
+ eventManager.addEventListener(document, "touchmove", blockEvent, this.COMPONENT_NAME, {
546
+ capture: true,
547
+ passive: false,
548
+ priority: 10,
549
+ });
550
+ eventManager.addEventListener(window, "touchmove", blockEvent, this.COMPONENT_NAME, {
551
+ capture: true,
552
+ passive: false,
553
+ priority: 10,
554
+ });
555
+ this.logger.log("Added global event listeners to prevent scrolling");
556
+ }
557
+ return blocker;
558
+ }
559
+ /**
560
+ * Set up DOM observer to detect when overlay elements are removed
561
+ * @param overlay The overlay to observe
562
+ */
563
+ setupObserver(overlay) {
564
+ if (!isBrowser() || !overlay.element)
565
+ return;
566
+ const elementsToWatch = [overlay.element];
567
+ if (overlay.blocker) {
568
+ elementsToWatch.push(overlay.blocker);
569
+ }
570
+ // Create a handler for element removal
571
+ const handleElementsRemoved = (removedElements) => {
572
+ this.logger.log(`Overlay elements removed from DOM for ${overlay.id}`, removedElements);
573
+ // Only restore if auto-restore is enabled and the overlay is still in our storage
574
+ const currentOverlay = this.overlays.get(overlay.id);
575
+ if (currentOverlay && currentOverlay.options.autoRestore !== false) {
576
+ this.logger.log(`Auto-restoring overlay ${overlay.id}`);
577
+ // Stop observing before we detach any surviving element below —
578
+ // otherwise our own removeChild re-enters this handler and would null
579
+ // the freshly created references.
580
+ if (this.domObserver) {
581
+ this.domObserver.stopObserving();
582
+ }
583
+ // If only one of the (overlay, blocker) pair was removed manually
584
+ // (e.g. devtools → delete node on the overlay), the other is still
585
+ // attached to document.body. Detach it now, otherwise the upcoming
586
+ // showOverlayById() appends a fresh copy and the orphan accumulates
587
+ // on every restore cycle (#security-event-blocker stack).
588
+ if (currentOverlay.element && currentOverlay.element.parentNode) {
589
+ currentOverlay.element.parentNode.removeChild(currentOverlay.element);
590
+ }
591
+ if (currentOverlay.blocker && currentOverlay.blocker.parentNode) {
592
+ currentOverlay.blocker.parentNode.removeChild(currentOverlay.blocker);
593
+ }
594
+ // Mark as not visible
595
+ currentOverlay.isVisible = false;
596
+ currentOverlay.element = null;
597
+ currentOverlay.blocker = null;
598
+ // If this was the active overlay, restore it immediately
599
+ if (this.activeOverlayId === overlay.id) {
600
+ this.showOverlayById(overlay.id);
601
+ }
602
+ else {
603
+ // Otherwise add to queue
604
+ this.addToQueue(overlay.id);
605
+ }
606
+ // Notify callbacks
607
+ this.notifyElementsRemovedCallbacks(removedElements);
608
+ }
609
+ };
610
+ // Stop any existing observer
611
+ if (this.domObserver) {
612
+ this.domObserver.stopObserving();
613
+ }
614
+ this.domObserver = new DomObserver({
615
+ targetElement: document.body,
616
+ elementsToWatch,
617
+ onElementsRemoved: handleElementsRemoved,
618
+ observeSubtree: true,
619
+ debugMode: this.debugMode,
620
+ name: "SecurityOverlayManager",
621
+ });
622
+ this.domObserver.startObserving();
623
+ this.logger.log(`DOM observer set up for overlay ${overlay.id}`);
624
+ }
625
+ /**
626
+ * Remove global event listeners that were added to document and window
627
+ */
628
+ removeGlobalEventListeners() {
629
+ if (!isBrowser())
630
+ return;
631
+ // Use eventManager to remove all events registered by this component
632
+ const removedCount = eventManager.removeEventsByOwner(this.COMPONENT_NAME);
633
+ this.logger.log(`Removed ${removedCount} global event listeners`);
634
+ // Re-enable scrolling on body
635
+ if (document.body) {
636
+ document.body.style.overflow = "";
637
+ document.body.style.position = "";
638
+ document.body.style.height = "";
639
+ document.body.style.width = "";
640
+ document.body.style.top = "";
641
+ document.body.style.left = "";
642
+ this.logger.log("Re-enabled scrolling on body");
643
+ }
644
+ // Re-enable scrolling on html
645
+ const htmlElement = document.documentElement;
646
+ if (htmlElement) {
647
+ htmlElement.style.overflow = "";
648
+ this.logger.log("Re-enabled scrolling on html");
649
+ }
650
+ }
651
+ /**
652
+ * Notify all callbacks when elements are removed
653
+ * @param removedElements The elements that were removed
654
+ */
655
+ notifyElementsRemovedCallbacks(removedElements) {
656
+ for (const callback of this.onElementsRemovedCallbacks) {
657
+ try {
658
+ callback(removedElements);
659
+ }
660
+ catch (error) {
661
+ this.logger.error("Error in elements removed callback:", error);
662
+ }
663
+ }
664
+ }
665
+ /**
666
+ * Add a callback to be called when overlay elements are removed
667
+ * @param callback Callback function
668
+ */
669
+ addElementsRemovedCallback(callback) {
670
+ this.onElementsRemovedCallbacks.push(callback);
671
+ }
672
+ /**
673
+ * Remove a callback
674
+ * @param callback Callback function to remove
675
+ */
676
+ removeElementsRemovedCallback(callback) {
677
+ this.onElementsRemovedCallbacks = this.onElementsRemovedCallbacks.filter((cb) => cb !== callback);
678
+ }
679
+ /**
680
+ * Get all overlays for a specific owner
681
+ * @param owner The owner to get overlays for
682
+ * @returns Array of overlay IDs
683
+ */
684
+ getOverlaysByOwner(owner) {
685
+ const overlayIds = [];
686
+ for (const [overlayId, overlay] of this.overlays.entries()) {
687
+ if (overlay.owner === owner) {
688
+ overlayIds.push(overlayId);
689
+ }
690
+ }
691
+ return overlayIds;
692
+ }
693
+ /**
694
+ * Get all active overlays
695
+ * @returns Array of active overlay IDs
696
+ */
697
+ getActiveOverlays() {
698
+ const activeOverlays = [];
699
+ for (const [overlayId, overlay] of this.overlays.entries()) {
700
+ if (overlay.isVisible) {
701
+ activeOverlays.push(overlayId);
702
+ }
703
+ }
704
+ return activeOverlays;
705
+ }
706
+ /**
707
+ * Check if an overlay exists
708
+ * @param overlayId The overlay ID
709
+ * @returns True if the overlay exists
710
+ */
711
+ hasOverlay(overlayId) {
712
+ return this.overlays.has(overlayId);
713
+ }
714
+ /**
715
+ * Get the currently active overlay ID
716
+ * @returns The active overlay ID or null if none is active
717
+ */
718
+ getActiveOverlayId() {
719
+ return this.activeOverlayId;
720
+ }
721
+ /**
722
+ * Get the overlay queue
723
+ * @returns Array of overlay IDs in the queue
724
+ */
725
+ getOverlayQueue() {
726
+ return [...this.overlayQueue];
727
+ }
728
+ /**
729
+ * Clear all overlays
730
+ * @returns The number of overlays removed
731
+ */
732
+ clearAllOverlays() {
733
+ if (!isBrowser())
734
+ return 0;
735
+ const overlayIds = Array.from(this.overlays.keys());
736
+ let removedCount = 0;
737
+ for (const overlayId of overlayIds) {
738
+ if (this.removeOverlayById(overlayId)) {
739
+ removedCount++;
740
+ }
741
+ }
742
+ // Clear queue
743
+ this.overlayQueue = [];
744
+ this.activeOverlayId = null;
745
+ // Remove global event listeners
746
+ this.removeGlobalEventListeners();
747
+ if (removedCount > 0) {
748
+ this.logger.log(`Cleared all ${removedCount} overlays`);
749
+ }
750
+ return removedCount;
751
+ }
752
+ /**
753
+ * Get debug information about registered overlays
754
+ * @returns Object with debug information
755
+ */
756
+ getDebugInfo() {
757
+ const overlaysByOwner = {};
758
+ const overlaysByType = {};
759
+ let totalOverlays = 0;
760
+ const overlayDetails = [];
761
+ for (const [overlayId, overlay] of this.overlays.entries()) {
762
+ totalOverlays++;
763
+ // Count by owner
764
+ overlaysByOwner[overlay.owner] = (overlaysByOwner[overlay.owner] || 0) + 1;
765
+ // Count by type
766
+ overlaysByType[overlay.overlayType] = (overlaysByType[overlay.overlayType] || 0) + 1;
767
+ // Add detailed overlay info
768
+ overlayDetails.push({
769
+ id: overlayId,
770
+ owner: overlay.owner,
771
+ type: overlay.overlayType,
772
+ isVisible: overlay.isVisible,
773
+ priority: overlay.priority,
774
+ createdAt: overlay.createdAt,
775
+ });
776
+ }
777
+ return {
778
+ totalOverlays,
779
+ overlaysByOwner,
780
+ overlaysByType,
781
+ activeOverlayId: this.activeOverlayId,
782
+ queueLength: this.overlayQueue.length,
783
+ overlayDetails,
784
+ };
785
+ }
786
+ }