@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.
- package/LICENSE +9 -0
- package/README.md +357 -0
- package/dist/assess.d.ts +16 -0
- package/dist/assess.js +220 -0
- package/dist/config/default-extensions-config.json +103 -0
- package/dist/core/ContentProtector.d.ts +63 -0
- package/dist/core/ContentProtector.js +281 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core/mediator/ContentProtectionMediator.d.ts +86 -0
- package/dist/core/mediator/ContentProtectionMediator.js +238 -0
- package/dist/core/mediator/eventDataTypes.d.ts +112 -0
- package/dist/core/mediator/eventDataTypes.js +23 -0
- package/dist/core/mediator/handlers/abstractEventHandler.d.ts +41 -0
- package/dist/core/mediator/handlers/abstractEventHandler.js +59 -0
- package/dist/core/mediator/handlers/devToolsEventHandler.d.ts +9 -0
- package/dist/core/mediator/handlers/devToolsEventHandler.js +95 -0
- package/dist/core/mediator/handlers/eventHandlerRegistry.d.ts +9 -0
- package/dist/core/mediator/handlers/eventHandlerRegistry.js +34 -0
- package/dist/core/mediator/handlers/extensionEventHandlers.d.ts +40 -0
- package/dist/core/mediator/handlers/extensionEventHandlers.js +140 -0
- package/dist/core/mediator/handlers/iFrameEventHandlers.d.ts +27 -0
- package/dist/core/mediator/handlers/iFrameEventHandlers.js +93 -0
- package/dist/core/mediator/handlers/screenShotEventHandlers.d.ts +34 -0
- package/dist/core/mediator/handlers/screenShotEventHandlers.js +111 -0
- package/dist/core/mediator/protection-event.d.ts +77 -0
- package/dist/core/mediator/protection-event.js +32 -0
- package/dist/core/mediator/types.d.ts +105 -0
- package/dist/core/mediator/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/otel.d.ts +24 -0
- package/dist/otel.js +83 -0
- package/dist/policy.d.ts +98 -0
- package/dist/policy.js +97 -0
- package/dist/strategies/AbstractStrategy.d.ts +124 -0
- package/dist/strategies/AbstractStrategy.js +256 -0
- package/dist/strategies/ClipboardStrategy.d.ts +67 -0
- package/dist/strategies/ClipboardStrategy.js +291 -0
- package/dist/strategies/ContextMenuStrategy.d.ts +60 -0
- package/dist/strategies/ContextMenuStrategy.js +454 -0
- package/dist/strategies/DevToolsStrategy.d.ts +55 -0
- package/dist/strategies/DevToolsStrategy.js +314 -0
- package/dist/strategies/ExtensionStrategy.d.ts +66 -0
- package/dist/strategies/ExtensionStrategy.js +486 -0
- package/dist/strategies/IFrameStrategy.d.ts +49 -0
- package/dist/strategies/IFrameStrategy.js +255 -0
- package/dist/strategies/KeyboardStrategy.d.ts +35 -0
- package/dist/strategies/KeyboardStrategy.js +130 -0
- package/dist/strategies/PrintStrategy.d.ts +47 -0
- package/dist/strategies/PrintStrategy.js +201 -0
- package/dist/strategies/ScreenshotStrategy.d.ts +90 -0
- package/dist/strategies/ScreenshotStrategy.js +502 -0
- package/dist/strategies/SelectionStrategy.d.ts +49 -0
- package/dist/strategies/SelectionStrategy.js +216 -0
- package/dist/strategies/WatermarkStrategy.d.ts +56 -0
- package/dist/strategies/WatermarkStrategy.js +287 -0
- package/dist/strategies/index.d.ts +10 -0
- package/dist/strategies/index.js +11 -0
- package/dist/types/assessment.d.ts +62 -0
- package/dist/types/assessment.js +1 -0
- package/dist/types/index.d.ts +278 -0
- package/dist/types/index.js +17 -0
- package/dist/utils/DOMObserver.d.ts +68 -0
- package/dist/utils/DOMObserver.js +134 -0
- package/dist/utils/base/LoggableComponent.d.ts +44 -0
- package/dist/utils/base/LoggableComponent.js +56 -0
- package/dist/utils/detectors/AbstractDevToolsDetector.d.ts +98 -0
- package/dist/utils/detectors/AbstractDevToolsDetector.js +127 -0
- package/dist/utils/detectors/dateToStringDetector.d.ts +43 -0
- package/dist/utils/detectors/dateToStringDetector.js +96 -0
- package/dist/utils/detectors/debugLibDetector.d.ts +64 -0
- package/dist/utils/detectors/debugLibDetector.js +195 -0
- package/dist/utils/detectors/debuggerDetector.d.ts +51 -0
- package/dist/utils/detectors/debuggerDetector.js +211 -0
- package/dist/utils/detectors/defineGetterDetector.d.ts +48 -0
- package/dist/utils/detectors/defineGetterDetector.js +150 -0
- package/dist/utils/detectors/detectorInterface.d.ts +36 -0
- package/dist/utils/detectors/detectorInterface.js +1 -0
- package/dist/utils/detectors/devToolsDetectorManager.d.ts +88 -0
- package/dist/utils/detectors/devToolsDetectorManager.js +243 -0
- package/dist/utils/detectors/funcToStringDetector.d.ts +43 -0
- package/dist/utils/detectors/funcToStringDetector.js +90 -0
- package/dist/utils/detectors/regToStringDetector.d.ts +43 -0
- package/dist/utils/detectors/regToStringDetector.js +129 -0
- package/dist/utils/detectors/sizeDetector.d.ts +54 -0
- package/dist/utils/detectors/sizeDetector.js +134 -0
- package/dist/utils/detectors/timingDetector.d.ts +55 -0
- package/dist/utils/detectors/timingDetector.js +143 -0
- package/dist/utils/dom.d.ts +20 -0
- package/dist/utils/dom.js +83 -0
- package/dist/utils/environment.d.ts +29 -0
- package/dist/utils/environment.js +267 -0
- package/dist/utils/eventManager.d.ts +162 -0
- package/dist/utils/eventManager.js +548 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/intervalManager.d.ts +91 -0
- package/dist/utils/intervalManager.js +221 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.d.ts +41 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.js +135 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcuts.d.ts +18 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcuts.js +195 -0
- package/dist/utils/logging/simple/Loggable.d.ts +33 -0
- package/dist/utils/logging/simple/Loggable.js +1 -0
- package/dist/utils/logging/simple/LoggingDelegate.d.ts +42 -0
- package/dist/utils/logging/simple/LoggingDelegate.js +53 -0
- package/dist/utils/logging/simple/SimpleLoggingService.d.ts +39 -0
- package/dist/utils/logging/simple/SimpleLoggingService.js +58 -0
- package/dist/utils/orientation.d.ts +15 -0
- package/dist/utils/orientation.js +32 -0
- package/dist/utils/protectedContentManager.d.ts +155 -0
- package/dist/utils/protectedContentManager.js +424 -0
- package/dist/utils/securityOverlayManager.d.ts +253 -0
- package/dist/utils/securityOverlayManager.js +786 -0
- package/dist/utils/timeoutManager.d.ts +50 -0
- package/dist/utils/timeoutManager.js +113 -0
- package/package.json +61 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { isBrowser, isMobile } from "../utils/environment";
|
|
2
|
+
import { AbstractStrategy, StrategyErrorType } from "./AbstractStrategy";
|
|
3
|
+
/**
|
|
4
|
+
* Strategy for preventing text selection
|
|
5
|
+
*/
|
|
6
|
+
export class SelectionStrategy extends AbstractStrategy {
|
|
7
|
+
/**
|
|
8
|
+
* Create a new SelectionStrategy
|
|
9
|
+
* @param targetElement Element to protect (defaults to document.body)
|
|
10
|
+
* @param customHandler Optional custom handler for selection attempts
|
|
11
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
12
|
+
*/
|
|
13
|
+
constructor(targetElement, customHandler, debugMode = false) {
|
|
14
|
+
super("SelectionStrategy", debugMode);
|
|
15
|
+
this.targetElement = null;
|
|
16
|
+
this.styleElement = null;
|
|
17
|
+
this.preventDrag = true;
|
|
18
|
+
this.targetElement = targetElement || (isBrowser() ? document.body : null);
|
|
19
|
+
this.customHandler = customHandler;
|
|
20
|
+
this.selectionHandler = this.handleSelection.bind(this);
|
|
21
|
+
this.dragHandler = this.handleDrag.bind(this);
|
|
22
|
+
this.log("Initialized with target:", this.targetElement === document.body ? "document.body" : "custom element");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Handle selection event
|
|
26
|
+
*/
|
|
27
|
+
handleSelection(e) {
|
|
28
|
+
return (this.safeExecute("handleSelection", StrategyErrorType.EVENT_HANDLING, () => {
|
|
29
|
+
this.log("Selection attempt detected", {
|
|
30
|
+
eventType: e.type,
|
|
31
|
+
target: e.target,
|
|
32
|
+
});
|
|
33
|
+
// Call custom handler if provided
|
|
34
|
+
if (this.customHandler) {
|
|
35
|
+
this.customHandler(e);
|
|
36
|
+
}
|
|
37
|
+
if (isBrowser() && window.getSelection) {
|
|
38
|
+
const selection = window.getSelection();
|
|
39
|
+
if (selection) {
|
|
40
|
+
selection.removeAllRanges();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.stopPropagation();
|
|
45
|
+
return false;
|
|
46
|
+
}) || false);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Handle drag event
|
|
50
|
+
*/
|
|
51
|
+
handleDrag(e) {
|
|
52
|
+
this.safeExecute("handleDrag", StrategyErrorType.EVENT_HANDLING, () => {
|
|
53
|
+
this.log("Drag attempt detected", {
|
|
54
|
+
eventType: e.type,
|
|
55
|
+
target: e.target,
|
|
56
|
+
});
|
|
57
|
+
// Call custom handler if provided
|
|
58
|
+
if (this.customHandler) {
|
|
59
|
+
this.customHandler(e);
|
|
60
|
+
}
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Inject CSS to prevent selection
|
|
67
|
+
*/
|
|
68
|
+
injectSelectionStyles() {
|
|
69
|
+
this.safeExecute("injectSelectionStyles", StrategyErrorType.APPLICATION, () => {
|
|
70
|
+
if (!isBrowser())
|
|
71
|
+
return;
|
|
72
|
+
this.styleElement = document.createElement("style");
|
|
73
|
+
this.styleElement.setAttribute("type", "text/css");
|
|
74
|
+
this.styleElement.setAttribute("data-content-security", "selection-blocker");
|
|
75
|
+
const selector = this.targetElement === document.body ? "body" : ".protected-content";
|
|
76
|
+
const css = `
|
|
77
|
+
${selector} {
|
|
78
|
+
-webkit-user-select: none;
|
|
79
|
+
-moz-user-select: none;
|
|
80
|
+
-ms-user-select: none;
|
|
81
|
+
user-select: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
${selector} ::selection {
|
|
85
|
+
background: transparent;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
this.styleElement.textContent = css;
|
|
89
|
+
document.head.appendChild(this.styleElement);
|
|
90
|
+
// Add class if not targeting body
|
|
91
|
+
if (this.targetElement !== document.body) {
|
|
92
|
+
this.targetElement?.classList.add("protected-content");
|
|
93
|
+
}
|
|
94
|
+
this.log("Selection-blocking CSS injected");
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Remove selection-blocking CSS
|
|
99
|
+
*/
|
|
100
|
+
removeSelectionStyles() {
|
|
101
|
+
this.safeExecute("removeSelectionStyles", StrategyErrorType.REMOVAL, () => {
|
|
102
|
+
if (!this.styleElement || !isBrowser())
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
document.head.removeChild(this.styleElement);
|
|
106
|
+
this.styleElement = null;
|
|
107
|
+
// Remove class if not targeting body
|
|
108
|
+
if (this.targetElement !== document.body) {
|
|
109
|
+
this.targetElement?.classList.remove("protected-content");
|
|
110
|
+
}
|
|
111
|
+
this.log("Selection-blocking CSS removed");
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.handleError(StrategyErrorType.REMOVAL, "Error removing selection styles", error);
|
|
115
|
+
// Try to find and remove by selector as fallback
|
|
116
|
+
try {
|
|
117
|
+
const styles = document.querySelectorAll('style[data-content-security="selection-blocker"]');
|
|
118
|
+
styles.forEach((style) => {
|
|
119
|
+
if (style.parentNode) {
|
|
120
|
+
style.parentNode.removeChild(style);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
this.styleElement = null;
|
|
124
|
+
if (styles.length > 0) {
|
|
125
|
+
this.log(`Removed ${styles.length} selection-blocking styles by selector`);
|
|
126
|
+
}
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
128
|
+
}
|
|
129
|
+
catch (fallbackError) {
|
|
130
|
+
// Last resort fallback
|
|
131
|
+
this.styleElement = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Apply selection protection
|
|
138
|
+
*/
|
|
139
|
+
apply() {
|
|
140
|
+
this.safeExecute("apply", StrategyErrorType.APPLICATION, () => {
|
|
141
|
+
if (this.isAppliedFlag || !this.targetElement)
|
|
142
|
+
return;
|
|
143
|
+
this.log("Applying selection protection", {
|
|
144
|
+
targetElement: this.targetElement === document.body ? "document.body" : "custom element",
|
|
145
|
+
hasCustomHandler: !!this.customHandler,
|
|
146
|
+
preventDrag: this.preventDrag,
|
|
147
|
+
isMobile: isMobile(),
|
|
148
|
+
});
|
|
149
|
+
// Add CSS
|
|
150
|
+
this.injectSelectionStyles();
|
|
151
|
+
// Add event listeners using the registerEvent method from AbstractStrategy
|
|
152
|
+
this.registerEvent(this.targetElement, "selectstart", this.selectionHandler);
|
|
153
|
+
this.registerEvent(this.targetElement, "mousedown", this.selectionHandler, { capture: true });
|
|
154
|
+
// Disable drag
|
|
155
|
+
if (this.preventDrag) {
|
|
156
|
+
this.registerEvent(this.targetElement, "dragstart", this.dragHandler);
|
|
157
|
+
}
|
|
158
|
+
this.isAppliedFlag = true;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Remove selection protection
|
|
163
|
+
*/
|
|
164
|
+
remove() {
|
|
165
|
+
this.safeExecute("remove", StrategyErrorType.REMOVAL, () => {
|
|
166
|
+
if (!this.isAppliedFlag || !this.targetElement)
|
|
167
|
+
return;
|
|
168
|
+
// Remove CSS
|
|
169
|
+
this.removeSelectionStyles();
|
|
170
|
+
// Remove all events for this owner using the parent class method
|
|
171
|
+
this.removeEventsByOwner();
|
|
172
|
+
// Second attempt - try direct DOM removal as fallback
|
|
173
|
+
try {
|
|
174
|
+
if (this.targetElement) {
|
|
175
|
+
this.targetElement.removeEventListener("selectstart", this.selectionHandler);
|
|
176
|
+
this.targetElement.removeEventListener("mousedown", this.selectionHandler, { capture: true });
|
|
177
|
+
this.targetElement.removeEventListener("dragstart", this.dragHandler);
|
|
178
|
+
this.log("Removed events via direct DOM API");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (domError) {
|
|
182
|
+
// Ignore errors in direct DOM removal
|
|
183
|
+
this.handleError(StrategyErrorType.REMOVAL, "Error in fallback DOM removal", domError);
|
|
184
|
+
}
|
|
185
|
+
// Clear tracked event IDs
|
|
186
|
+
this.eventIds = [];
|
|
187
|
+
this.isAppliedFlag = false;
|
|
188
|
+
this.log("Selection protection removed");
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Update selection protection options
|
|
193
|
+
* @param options New options for selection protection
|
|
194
|
+
*/
|
|
195
|
+
updateOptions(options) {
|
|
196
|
+
this.safeExecute("updateOptions", StrategyErrorType.OPTION_UPDATE, () => {
|
|
197
|
+
this.log("Updating options", options);
|
|
198
|
+
// Update debug mode if specified
|
|
199
|
+
if (options.debugMode !== undefined) {
|
|
200
|
+
this.setDebugMode(!!options.debugMode);
|
|
201
|
+
}
|
|
202
|
+
// Update preventDrag if specified
|
|
203
|
+
if (options.preventDrag !== undefined) {
|
|
204
|
+
const oldPreventDrag = this.preventDrag;
|
|
205
|
+
this.preventDrag = !!options.preventDrag;
|
|
206
|
+
// If we need to update the applied strategy
|
|
207
|
+
if (this.isAppliedFlag && oldPreventDrag !== this.preventDrag) {
|
|
208
|
+
// Remove and reapply to update event listeners
|
|
209
|
+
this.remove();
|
|
210
|
+
this.apply();
|
|
211
|
+
this.log("Reapplied with updated options");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { WatermarkOptions } from "../types";
|
|
2
|
+
import { AbstractStrategy } from "./AbstractStrategy";
|
|
3
|
+
/**
|
|
4
|
+
* Strategy for adding watermarks to content
|
|
5
|
+
*/
|
|
6
|
+
export declare class WatermarkStrategy extends AbstractStrategy {
|
|
7
|
+
private targetElement;
|
|
8
|
+
private options;
|
|
9
|
+
private watermarkElements;
|
|
10
|
+
private domObserver;
|
|
11
|
+
private isFullPageWatermark;
|
|
12
|
+
private watermarkContainer;
|
|
13
|
+
private osInfo;
|
|
14
|
+
/**
|
|
15
|
+
* Unique per-instance id. Watermark containers are tagged with this so that
|
|
16
|
+
* cleanup queries only ever match THIS instance's container. Without it, two
|
|
17
|
+
* concurrent WatermarkStrategy instances (e.g. two ContentProtectors on the
|
|
18
|
+
* same or nested elements) would delete each other's containers, and each
|
|
19
|
+
* auto-restore would re-trigger the other's observer — an infinite
|
|
20
|
+
* MutationObserver loop that freezes the page.
|
|
21
|
+
*/
|
|
22
|
+
private readonly instanceId;
|
|
23
|
+
/**
|
|
24
|
+
* Create a new WatermarkStrategy
|
|
25
|
+
* @param targetElement Element to watermark (defaults to document.body)
|
|
26
|
+
* @param options Watermark options
|
|
27
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
28
|
+
*/
|
|
29
|
+
constructor(options?: WatermarkOptions, targetElement?: HTMLElement | null, debugMode?: boolean);
|
|
30
|
+
/**
|
|
31
|
+
* Create watermarks
|
|
32
|
+
*/
|
|
33
|
+
private createWatermarks;
|
|
34
|
+
/**
|
|
35
|
+
* Set up DOM observer to detect watermark removal
|
|
36
|
+
*/
|
|
37
|
+
private setupObserver;
|
|
38
|
+
/**
|
|
39
|
+
* Remove watermark elements
|
|
40
|
+
*/
|
|
41
|
+
private removeWatermarkElements;
|
|
42
|
+
/**
|
|
43
|
+
* Apply watermark protection
|
|
44
|
+
*/
|
|
45
|
+
apply(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Remove watermark protection
|
|
48
|
+
* Override the base implementation to handle additional cleanup
|
|
49
|
+
*/
|
|
50
|
+
remove(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Update watermark options
|
|
53
|
+
* @param options New watermark options
|
|
54
|
+
*/
|
|
55
|
+
updateOptions(options: Record<string, unknown>): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { DomObserver } from "../utils/DOMObserver";
|
|
2
|
+
import { isBrowser, getOS } from "../utils/environment";
|
|
3
|
+
import { AbstractStrategy, StrategyErrorType } from "./AbstractStrategy";
|
|
4
|
+
/**
|
|
5
|
+
* Strategy for adding watermarks to content
|
|
6
|
+
*/
|
|
7
|
+
export class WatermarkStrategy extends AbstractStrategy {
|
|
8
|
+
/**
|
|
9
|
+
* Create a new WatermarkStrategy
|
|
10
|
+
* @param targetElement Element to watermark (defaults to document.body)
|
|
11
|
+
* @param options Watermark options
|
|
12
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
13
|
+
*/
|
|
14
|
+
constructor(options, targetElement, debugMode = false) {
|
|
15
|
+
super("WatermarkStrategy", debugMode);
|
|
16
|
+
this.targetElement = null;
|
|
17
|
+
this.watermarkElements = [];
|
|
18
|
+
this.domObserver = null;
|
|
19
|
+
this.isFullPageWatermark = false;
|
|
20
|
+
this.watermarkContainer = null;
|
|
21
|
+
/**
|
|
22
|
+
* Unique per-instance id. Watermark containers are tagged with this so that
|
|
23
|
+
* cleanup queries only ever match THIS instance's container. Without it, two
|
|
24
|
+
* concurrent WatermarkStrategy instances (e.g. two ContentProtectors on the
|
|
25
|
+
* same or nested elements) would delete each other's containers, and each
|
|
26
|
+
* auto-restore would re-trigger the other's observer — an infinite
|
|
27
|
+
* MutationObserver loop that freezes the page.
|
|
28
|
+
*/
|
|
29
|
+
this.instanceId = `wm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
30
|
+
this.targetElement = targetElement || (isBrowser() ? document.body : null);
|
|
31
|
+
this.options = {
|
|
32
|
+
text: "CONFIDENTIAL",
|
|
33
|
+
opacity: 0.15,
|
|
34
|
+
density: 3,
|
|
35
|
+
...options,
|
|
36
|
+
};
|
|
37
|
+
this.osInfo = getOS();
|
|
38
|
+
this.watermarkElements = [];
|
|
39
|
+
// Determine if we're doing a full-page watermark
|
|
40
|
+
if (isBrowser() && this.targetElement === document.body) {
|
|
41
|
+
this.isFullPageWatermark = true;
|
|
42
|
+
}
|
|
43
|
+
this.log("Initialized with OS:", this.osInfo, "Full-page:", this.isFullPageWatermark);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create watermarks
|
|
47
|
+
*/
|
|
48
|
+
createWatermarks() {
|
|
49
|
+
return this.safeExecute("createWatermarks", StrategyErrorType.APPLICATION, () => {
|
|
50
|
+
if (!this.targetElement || !isBrowser())
|
|
51
|
+
return;
|
|
52
|
+
// Check if THIS instance's watermark container already exists. Scoped to
|
|
53
|
+
// instanceId so we never remove a container owned by another concurrent
|
|
54
|
+
// WatermarkStrategy (doing so would start a cross-instance restore loop).
|
|
55
|
+
const existingContainer = document.querySelector(`[data-watermark-instance="${this.instanceId}"]`);
|
|
56
|
+
if (existingContainer) {
|
|
57
|
+
this.log("Watermark container already exists, removing first");
|
|
58
|
+
if (existingContainer.parentNode) {
|
|
59
|
+
existingContainer.parentNode.removeChild(existingContainer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Clear any existing watermarks
|
|
63
|
+
this.removeWatermarkElements();
|
|
64
|
+
// Calculate density
|
|
65
|
+
const density = Math.min(Math.max(this.options.density || 3, 1), 10);
|
|
66
|
+
const rows = density * 3;
|
|
67
|
+
const cols = density * 3;
|
|
68
|
+
// Create watermark container
|
|
69
|
+
const container = document.createElement("div");
|
|
70
|
+
container.className = "content-security-watermark-container";
|
|
71
|
+
container.setAttribute("data-watermark-id", `watermark-${Date.now()}`);
|
|
72
|
+
container.setAttribute("data-watermark-instance", this.instanceId);
|
|
73
|
+
this.watermarkContainer = container;
|
|
74
|
+
// Set container styles based on whether it's full-page or element-specific
|
|
75
|
+
if (this.isFullPageWatermark) {
|
|
76
|
+
// Full-page watermark (fixed position covering viewport)
|
|
77
|
+
Object.assign(container.style, {
|
|
78
|
+
position: "fixed",
|
|
79
|
+
top: "0",
|
|
80
|
+
left: "0",
|
|
81
|
+
width: "100%",
|
|
82
|
+
height: "100%",
|
|
83
|
+
overflow: "hidden",
|
|
84
|
+
pointerEvents: "none",
|
|
85
|
+
zIndex: "2147483647", // Max z-index
|
|
86
|
+
userSelect: "none",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Element-specific watermark
|
|
91
|
+
// Get the computed style of the target element
|
|
92
|
+
const targetStyle = window.getComputedStyle(this.targetElement);
|
|
93
|
+
const targetPosition = targetStyle.position;
|
|
94
|
+
// If the target element doesn't have a position set, we need to set it to relative
|
|
95
|
+
// so that our absolutely positioned watermark container stays within it
|
|
96
|
+
if (targetPosition === "static") {
|
|
97
|
+
this.targetElement.style.position = "relative";
|
|
98
|
+
}
|
|
99
|
+
Object.assign(container.style, {
|
|
100
|
+
position: "absolute",
|
|
101
|
+
top: "0",
|
|
102
|
+
left: "0",
|
|
103
|
+
width: "100%",
|
|
104
|
+
height: "100%",
|
|
105
|
+
overflow: "hidden",
|
|
106
|
+
pointerEvents: "none",
|
|
107
|
+
zIndex: "999", // High z-index but not max to avoid breaking page layout
|
|
108
|
+
userSelect: "none",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Generate timestamp and user info for watermark
|
|
112
|
+
const timestamp = new Date().toISOString();
|
|
113
|
+
const userInfo = this.options.userId ? ` - User: ${this.options.userId}` : "";
|
|
114
|
+
const watermarkText = `${this.options.text}${userInfo} - ${timestamp}`;
|
|
115
|
+
// Create watermark pattern
|
|
116
|
+
for (let i = 0; i < rows; i++) {
|
|
117
|
+
for (let j = 0; j < cols; j++) {
|
|
118
|
+
const watermark = document.createElement("div");
|
|
119
|
+
watermark.className = "content-security-watermark";
|
|
120
|
+
watermark.textContent = watermarkText;
|
|
121
|
+
// Position watermark - use % for element-specific watermarks instead of vh/vw
|
|
122
|
+
const positionUnit = this.isFullPageWatermark ? "vh" : "%";
|
|
123
|
+
const horizontalUnit = this.isFullPageWatermark ? "vw" : "%";
|
|
124
|
+
Object.assign(watermark.style, {
|
|
125
|
+
position: "absolute",
|
|
126
|
+
top: `${(i * 100) / rows}${positionUnit}`,
|
|
127
|
+
left: `${(j * 100) / cols}${horizontalUnit}`,
|
|
128
|
+
transform: "rotate(-45deg) translateX(-30%) translateY(-200%)",
|
|
129
|
+
transformOrigin: "center",
|
|
130
|
+
opacity: String(this.options.opacity || 0.15),
|
|
131
|
+
fontSize: this.isFullPageWatermark ? "16px" : "14px", // Slightly smaller for element watermarks
|
|
132
|
+
color: "rgba(0, 0, 0, 0.7)",
|
|
133
|
+
whiteSpace: "nowrap",
|
|
134
|
+
pointerEvents: "none",
|
|
135
|
+
userSelect: "none",
|
|
136
|
+
...this.options.style,
|
|
137
|
+
});
|
|
138
|
+
container.appendChild(watermark);
|
|
139
|
+
this.watermarkElements.push(watermark);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Add container to target
|
|
143
|
+
this.targetElement.appendChild(container);
|
|
144
|
+
this.watermarkElements.push(container);
|
|
145
|
+
this.log("Created watermark container with", this.watermarkElements.length - 1, "watermarks");
|
|
146
|
+
// Set up observer to detect if watermarks are removed
|
|
147
|
+
this.setupObserver();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Set up DOM observer to detect watermark removal
|
|
152
|
+
*/
|
|
153
|
+
setupObserver() {
|
|
154
|
+
return this.safeExecute("setupObserver", StrategyErrorType.APPLICATION, () => {
|
|
155
|
+
if (!this.targetElement || !isBrowser())
|
|
156
|
+
return;
|
|
157
|
+
// Create a handler for element removal
|
|
158
|
+
const handleElementsRemoved = (removedElements) => {
|
|
159
|
+
this.log("Watermark elements removed from DOM", removedElements);
|
|
160
|
+
// Only restore if auto-restore is enabled
|
|
161
|
+
if (this.isAppliedFlag) {
|
|
162
|
+
this.log("Auto-restoring watermarks");
|
|
163
|
+
// Restore the watermarks
|
|
164
|
+
this.createWatermarks();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Create a new observer if needed
|
|
168
|
+
if (!this.domObserver) {
|
|
169
|
+
this.domObserver = new DomObserver({
|
|
170
|
+
targetElement: this.targetElement,
|
|
171
|
+
elementsToWatch: this.watermarkContainer ? [this.watermarkContainer] : [],
|
|
172
|
+
onElementsRemoved: handleElementsRemoved,
|
|
173
|
+
observeSubtree: true,
|
|
174
|
+
debugMode: this.debugMode,
|
|
175
|
+
name: "WatermarkStrategy",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Update the elements to watch
|
|
180
|
+
this.domObserver.updateElementsToWatch(this.watermarkContainer ? [this.watermarkContainer] : []);
|
|
181
|
+
}
|
|
182
|
+
// Start observing
|
|
183
|
+
this.domObserver.startObserving();
|
|
184
|
+
this.log("DOM observer set up to detect watermark removal");
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Remove watermark elements
|
|
189
|
+
*/
|
|
190
|
+
removeWatermarkElements() {
|
|
191
|
+
return this.safeExecute("removeWatermarkElements", StrategyErrorType.REMOVAL, () => {
|
|
192
|
+
if (!isBrowser())
|
|
193
|
+
return;
|
|
194
|
+
// First, try to remove by container reference
|
|
195
|
+
if (this.watermarkContainer && this.watermarkContainer.parentNode) {
|
|
196
|
+
this.watermarkContainer.parentNode.removeChild(this.watermarkContainer);
|
|
197
|
+
this.watermarkContainer = null;
|
|
198
|
+
}
|
|
199
|
+
// Then try to remove individual elements
|
|
200
|
+
for (const element of this.watermarkElements) {
|
|
201
|
+
if (element.parentNode) {
|
|
202
|
+
element.parentNode.removeChild(element);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Also try to remove by instance id in case references were lost. Scoped
|
|
206
|
+
// to this instance so we never tear down another concurrent strategy's
|
|
207
|
+
// watermark (which would trigger its auto-restore observer in a loop).
|
|
208
|
+
const containers = document.querySelectorAll(`[data-watermark-instance="${this.instanceId}"]`);
|
|
209
|
+
containers.forEach((container) => {
|
|
210
|
+
if (container.parentNode) {
|
|
211
|
+
container.parentNode.removeChild(container);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// If we modified the target element's position, restore it
|
|
215
|
+
if (!this.isFullPageWatermark && this.targetElement) {
|
|
216
|
+
// Only remove the position if we added it
|
|
217
|
+
// This is a simplification - ideally we'd store the original position
|
|
218
|
+
if (this.targetElement.style.position === "relative") {
|
|
219
|
+
this.targetElement.style.position = "";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.watermarkElements = [];
|
|
223
|
+
this.log("Removed all watermark elements");
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Apply watermark protection
|
|
228
|
+
*/
|
|
229
|
+
apply() {
|
|
230
|
+
return this.safeExecute("apply", StrategyErrorType.APPLICATION, () => {
|
|
231
|
+
if (this.isAppliedFlag) {
|
|
232
|
+
this.log("Already applied, skipping");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.log("Applying watermark protection", {
|
|
236
|
+
text: this.options.text,
|
|
237
|
+
opacity: this.options.opacity,
|
|
238
|
+
density: this.options.density,
|
|
239
|
+
userId: this.options.userId,
|
|
240
|
+
isFullPage: this.isFullPageWatermark,
|
|
241
|
+
os: this.osInfo.name,
|
|
242
|
+
});
|
|
243
|
+
this.createWatermarks();
|
|
244
|
+
this.isAppliedFlag = true;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Remove watermark protection
|
|
249
|
+
* Override the base implementation to handle additional cleanup
|
|
250
|
+
*/
|
|
251
|
+
remove() {
|
|
252
|
+
return this.safeExecute("remove", StrategyErrorType.REMOVAL, () => {
|
|
253
|
+
if (!this.isAppliedFlag) {
|
|
254
|
+
this.log("Not applied, skipping removal");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (this.domObserver) {
|
|
258
|
+
this.domObserver.stopObserving();
|
|
259
|
+
this.domObserver = null;
|
|
260
|
+
this.log("DOM observer stopped");
|
|
261
|
+
}
|
|
262
|
+
this.removeWatermarkElements();
|
|
263
|
+
this.isAppliedFlag = false;
|
|
264
|
+
this.log("Watermark protection removed");
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Update watermark options
|
|
269
|
+
* @param options New watermark options
|
|
270
|
+
*/
|
|
271
|
+
updateOptions(options) {
|
|
272
|
+
return this.safeExecute("updateOptions", StrategyErrorType.OPTION_UPDATE, () => {
|
|
273
|
+
const typedOptions = options;
|
|
274
|
+
this.log("Updating options", typedOptions);
|
|
275
|
+
this.options = {
|
|
276
|
+
...this.options,
|
|
277
|
+
...typedOptions,
|
|
278
|
+
};
|
|
279
|
+
if (this.isAppliedFlag) {
|
|
280
|
+
// Reapply with new options
|
|
281
|
+
this.remove();
|
|
282
|
+
this.apply();
|
|
283
|
+
this.log("Reapplied watermarks with updated options");
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './KeyboardStrategy';
|
|
2
|
+
export * from './ContextMenuStrategy';
|
|
3
|
+
export * from './PrintStrategy';
|
|
4
|
+
export * from './WatermarkStrategy';
|
|
5
|
+
export * from './SelectionStrategy';
|
|
6
|
+
export * from './DevToolsStrategy';
|
|
7
|
+
export * from './ScreenshotStrategy';
|
|
8
|
+
export * from './ExtensionStrategy';
|
|
9
|
+
export * from './IFrameStrategy';
|
|
10
|
+
export * from './ClipboardStrategy';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// BARREL FILE
|
|
2
|
+
export * from './KeyboardStrategy';
|
|
3
|
+
export * from './ContextMenuStrategy';
|
|
4
|
+
export * from './PrintStrategy';
|
|
5
|
+
export * from './WatermarkStrategy';
|
|
6
|
+
export * from './SelectionStrategy';
|
|
7
|
+
export * from './DevToolsStrategy';
|
|
8
|
+
export * from './ScreenshotStrategy';
|
|
9
|
+
export * from './ExtensionStrategy';
|
|
10
|
+
export * from './IFrameStrategy';
|
|
11
|
+
export * from './ClipboardStrategy';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal keys produced by assess(). All keys follow the shield.* OTel namespace
|
|
3
|
+
* so they can be attached directly to Blindspot / OpenTelemetry spans.
|
|
4
|
+
*/
|
|
5
|
+
export interface ShieldSignals {
|
|
6
|
+
/** DevTools panel is open in the current tab. */
|
|
7
|
+
'shield.devtools.open': boolean;
|
|
8
|
+
/** navigator.webdriver is set — Selenium, Playwright, Puppeteer, etc. */
|
|
9
|
+
'shield.automation.webdriver': boolean;
|
|
10
|
+
/** Browser is running in headless mode (no visible UI). */
|
|
11
|
+
'shield.automation.headless': boolean;
|
|
12
|
+
/** Page is embedded inside a cross-origin or unauthorized iframe. */
|
|
13
|
+
'shield.frame.embedded': boolean;
|
|
14
|
+
/** At least one known browser extension has been detected. */
|
|
15
|
+
'shield.extension.detected': boolean;
|
|
16
|
+
/** Comma-separated list of detected extension names (empty string if none). */
|
|
17
|
+
'shield.extension.names': string;
|
|
18
|
+
}
|
|
19
|
+
export interface ShieldRisk {
|
|
20
|
+
/** Normalised threat score in the [0, 1] range. */
|
|
21
|
+
score: number;
|
|
22
|
+
/** Machine-readable flag codes for each active threat. */
|
|
23
|
+
flags: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Structured result returned by assess().
|
|
27
|
+
*/
|
|
28
|
+
export interface ShieldAssessment {
|
|
29
|
+
signals: ShieldSignals;
|
|
30
|
+
risk: ShieldRisk;
|
|
31
|
+
/**
|
|
32
|
+
* OTel-compatible span attributes. Only truthy / non-default signal values
|
|
33
|
+
* are included so spans stay lean. Attach these directly to a Blindspot
|
|
34
|
+
* span via span.setAttributes(result.spanAttributes).
|
|
35
|
+
*/
|
|
36
|
+
spanAttributes: Record<string, string | boolean | number>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Options for the assess() call.
|
|
40
|
+
*/
|
|
41
|
+
export interface AssessOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Run DevTools detection. Slightly slower (async timing-based).
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
devtools?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Run browser-extension detection (async DOM scan).
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
extensions?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Maximum milliseconds to wait for async detections before resolving.
|
|
54
|
+
* @default 400
|
|
55
|
+
*/
|
|
56
|
+
timeout?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Inline extension config to check against. If omitted, uses the built-in
|
|
59
|
+
* signatures bundled with Shield.
|
|
60
|
+
*/
|
|
61
|
+
extensionConfig?: import('./index.js').ExtensionConfig[];
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|