@readium/navigator 2.2.8 → 2.3.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/dist/index.js +3079 -3225
- package/dist/index.umd.cjs +80 -87
- package/package.json +5 -4
- package/src/Navigator.ts +76 -0
- package/src/epub/EpubNavigator.ts +93 -10
- package/src/epub/css/Properties.ts +8 -8
- package/src/epub/css/ReadiumCSS.ts +7 -5
- package/src/epub/frame/FrameManager.ts +38 -2
- package/src/epub/frame/FramePoolManager.ts +9 -2
- package/src/epub/fxl/FXLFrameManager.ts +31 -2
- package/src/epub/fxl/FXLFramePoolManager.ts +9 -3
- package/src/epub/preferences/EpubDefaults.ts +6 -6
- package/src/epub/preferences/EpubPreferences.ts +6 -6
- package/src/epub/preferences/EpubPreferencesEditor.ts +1 -3
- package/src/epub/preferences/EpubSettings.ts +5 -7
- package/src/helpers/lineLength.ts +4 -6
- package/src/injection/epubInjectables.ts +11 -3
- package/src/injection/webpubInjectables.ts +12 -2
- package/src/peripherals/KeyboardPeripherals.ts +53 -0
- package/src/protection/AutomationDetector.ts +66 -0
- package/src/protection/ContextMenuProtector.ts +46 -0
- package/src/protection/DevToolsDetector.ts +290 -0
- package/src/protection/IframeEmbeddingDetector.ts +73 -0
- package/src/protection/NavigatorProtector.ts +95 -0
- package/src/protection/PrintProtector.ts +58 -0
- package/src/protection/utils/WorkerConsole.ts +84 -0
- package/src/protection/utils/console.ts +16 -0
- package/src/protection/utils/match.ts +18 -0
- package/src/protection/utils/platform.ts +22 -0
- package/src/webpub/WebPubFrameManager.ts +38 -5
- package/src/webpub/WebPubFramePoolManager.ts +9 -2
- package/src/webpub/WebPubNavigator.ts +87 -7
- package/types/src/Navigator.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +14 -2
- package/types/src/epub/css/Properties.d.ts +4 -0
- package/types/src/epub/frame/FrameManager.d.ts +5 -1
- package/types/src/epub/frame/FramePoolManager.d.ts +4 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -2
- package/types/src/epub/preferences/EpubDefaults.d.ts +4 -0
- package/types/src/epub/preferences/EpubPreferences.d.ts +4 -0
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +2 -0
- package/types/src/epub/preferences/EpubSettings.d.ts +4 -0
- package/types/src/helpers/lineLength.d.ts +2 -3
- package/types/src/injection/epubInjectables.d.ts +2 -2
- package/types/src/injection/webpubInjectables.d.ts +2 -1
- package/types/src/peripherals/KeyboardPeripherals.d.ts +13 -0
- package/types/src/protection/AutomationDetector.d.ts +14 -0
- package/types/src/protection/ContextMenuProtector.d.ts +11 -0
- package/types/src/protection/DevToolsDetector.d.ts +75 -0
- package/types/src/protection/IframeEmbeddingDetector.d.ts +14 -0
- package/types/src/protection/NavigatorProtector.d.ts +12 -0
- package/types/src/protection/PrintProtector.d.ts +13 -0
- package/types/src/protection/utils/WorkerConsole.d.ts +15 -0
- package/types/src/protection/utils/console.d.ts +6 -0
- package/types/src/protection/utils/match.d.ts +8 -0
- package/types/src/protection/utils/platform.d.ts +4 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +5 -1
- package/types/src/webpub/WebPubFramePoolManager.d.ts +4 -1
- package/types/src/webpub/WebPubNavigator.d.ts +13 -1
- package/dist/ar-DyHX_uy2-DyHX_uy2.js +0 -7
- package/dist/assets/AccessibleDfA-Bold.woff2 +0 -0
- package/dist/assets/AccessibleDfA-Italic.woff2 +0 -0
- package/dist/assets/AccessibleDfA-Regular.woff +0 -0
- package/dist/assets/AccessibleDfA-Regular.woff2 +0 -0
- package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
- package/dist/da-Dct0PS3E-Dct0PS3E.js +0 -7
- package/dist/fr-C5HEel98-C5HEel98.js +0 -7
- package/dist/it-DFOBoXGy-DFOBoXGy.js +0 -7
- package/dist/pt_PT-Di3sVjze-Di3sVjze.js +0 -7
- package/dist/sv-BfzAFsVN-BfzAFsVN.js +0 -7
- package/types/src/epub/preferences/guards.d.ts +0 -9
- package/types/src/web/WebPubBlobBuilder.d.ts +0 -10
- package/types/src/web/WebPubFrameManager.d.ts +0 -20
- package/types/src/web/WebPubNavigator.d.ts +0 -48
- package/types/src/web/index.d.ts +0 -3
- package/types/src/webpub/css/WebPubStylesheet.d.ts +0 -1
- /package/dist/{ar-DyHX_uy2-DyHX_uy2-DyHX_uy2.js → ar-DyHX_uy2.js} +0 -0
- /package/dist/{da-Dct0PS3E-Dct0PS3E-Dct0PS3E.js → da-Dct0PS3E.js} +0 -0
- /package/dist/{fr-C5HEel98-C5HEel98-C5HEel98.js → fr-C5HEel98.js} +0 -0
- /package/dist/{it-DFOBoXGy-DFOBoXGy-DFOBoXGy.js → it-DFOBoXGy.js} +0 -0
- /package/dist/{pt_PT-Di3sVjze-Di3sVjze-Di3sVjze.js → pt_PT-Di3sVjze.js} +0 -0
- /package/dist/{sv-BfzAFsVN-BfzAFsVN-BfzAFsVN.js → sv-BfzAFsVN.js} +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { sML } from "../helpers/sML";
|
|
2
|
+
import { WorkerConsole } from "./utils/WorkerConsole";
|
|
3
|
+
import { log, table, clear } from "./utils/console";
|
|
4
|
+
import { isBrave } from "./utils/platform";
|
|
5
|
+
import { match } from "./utils/match";
|
|
6
|
+
|
|
7
|
+
export interface DevToolsDetectorOptions {
|
|
8
|
+
/** Callback when Developer Tools are detected as open */
|
|
9
|
+
onDetected?: () => void;
|
|
10
|
+
/** Callback when Developer Tools are detected as closed */
|
|
11
|
+
onClosed?: () => void;
|
|
12
|
+
/** Detection interval in milliseconds (default: 1000) */
|
|
13
|
+
interval?: number;
|
|
14
|
+
/** Enable debugger-based detection (fallback, impacts UX) */
|
|
15
|
+
enableDebuggerDetection?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DevToolsDetector {
|
|
19
|
+
private options: Required<DevToolsDetectorOptions>;
|
|
20
|
+
private isOpen = false;
|
|
21
|
+
private intervalId?: number;
|
|
22
|
+
private checkCount = 0;
|
|
23
|
+
private maxChecks = 10;
|
|
24
|
+
private maxPrintTime = 0;
|
|
25
|
+
private largeObjectArray: Record<string, string>[] | null = null;
|
|
26
|
+
private workerConsole?: WorkerConsole;
|
|
27
|
+
|
|
28
|
+
constructor(options: DevToolsDetectorOptions = {}) {
|
|
29
|
+
this.options = {
|
|
30
|
+
onDetected: options.onDetected || (() => {}),
|
|
31
|
+
onClosed: options.onClosed || (() => {}),
|
|
32
|
+
interval: options.interval || 1000,
|
|
33
|
+
enableDebuggerDetection: options.enableDebuggerDetection || false
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize Web Worker for console operations (skip Firefox for now as it will fail)
|
|
37
|
+
if (!sML.UA.Firefox) {
|
|
38
|
+
try {
|
|
39
|
+
const blob = new Blob([WorkerConsole.workerScript], { type: 'application/javascript' });
|
|
40
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
41
|
+
const worker = new Worker(blobUrl);
|
|
42
|
+
this.workerConsole = new WorkerConsole(worker, blobUrl);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Fallback to regular console if Worker creation fails
|
|
45
|
+
console.warn('Failed to create Web Worker for DevTools detection:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.startDetection();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create large object array for performance testing
|
|
54
|
+
*/
|
|
55
|
+
private createLargeObjectArray(): Record<string, string>[] {
|
|
56
|
+
const largeObject: Record<string, string> = {};
|
|
57
|
+
for (let i = 0; i < 500; i++) {
|
|
58
|
+
largeObject[`${i}`] = `${i}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const largeObjectArray: Record<string, string>[] = [];
|
|
62
|
+
for (let i = 0; i < 50; i++) {
|
|
63
|
+
largeObjectArray.push(largeObject);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return largeObjectArray;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get cached large object array
|
|
71
|
+
*/
|
|
72
|
+
private getLargeObjectArray(): Record<string, string>[] {
|
|
73
|
+
if (this.largeObjectArray === null) {
|
|
74
|
+
this.largeObjectArray = this.createLargeObjectArray();
|
|
75
|
+
}
|
|
76
|
+
return this.largeObjectArray;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Performance-based detection using console.table timing
|
|
81
|
+
*/
|
|
82
|
+
private async calcTablePrintTime(): Promise<number> {
|
|
83
|
+
const largeObjectArray = this.getLargeObjectArray();
|
|
84
|
+
|
|
85
|
+
if (this.workerConsole) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await this.workerConsole.table(largeObjectArray);
|
|
88
|
+
return result.time;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Fallback to regular console
|
|
91
|
+
const start = performance.now();
|
|
92
|
+
table(largeObjectArray);
|
|
93
|
+
return performance.now() - start;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback to cached console methods
|
|
97
|
+
const start = performance.now();
|
|
98
|
+
table(largeObjectArray);
|
|
99
|
+
return performance.now() - start;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Performance-based detection using console.log timing
|
|
105
|
+
*/
|
|
106
|
+
private async calcLogPrintTime(): Promise<number> {
|
|
107
|
+
const largeObjectArray = this.getLargeObjectArray();
|
|
108
|
+
|
|
109
|
+
if (this.workerConsole) {
|
|
110
|
+
const result = await this.workerConsole.log(largeObjectArray);
|
|
111
|
+
return result.time;
|
|
112
|
+
} else {
|
|
113
|
+
// Fallback to cached console methods
|
|
114
|
+
const start = performance.now();
|
|
115
|
+
log(largeObjectArray);
|
|
116
|
+
return performance.now() - start;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if performance-based detection is enabled for current browser
|
|
122
|
+
*/
|
|
123
|
+
private isPerformanceDetectionEnabled(): boolean {
|
|
124
|
+
return match({
|
|
125
|
+
includes: [
|
|
126
|
+
() => !!sML.UA.Chrome,
|
|
127
|
+
() => !!sML.UA.Chromium,
|
|
128
|
+
() => !!sML.UA.Safari,
|
|
129
|
+
() => !!sML.UA.Firefox
|
|
130
|
+
],
|
|
131
|
+
excludes: []
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if debugger detection is enabled for current browser
|
|
137
|
+
*/
|
|
138
|
+
private isDebuggerDetectionEnabled(): boolean {
|
|
139
|
+
// Only enable debugger detection if explicitly enabled in options
|
|
140
|
+
// Note: We can't check for Brave here since isBrave() is async
|
|
141
|
+
// and this method needs to be synchronous
|
|
142
|
+
return this.options.enableDebuggerDetection;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Performance-based detection using large object timing differences
|
|
147
|
+
*/
|
|
148
|
+
private async checkPerformanceBased(): Promise<boolean> {
|
|
149
|
+
if (!this.isPerformanceDetectionEnabled()) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tablePrintTime = await this.calcTablePrintTime();
|
|
154
|
+
const logPrintTime = Math.max(await this.calcLogPrintTime(), await this.calcLogPrintTime());
|
|
155
|
+
this.maxPrintTime = Math.max(this.maxPrintTime, logPrintTime);
|
|
156
|
+
|
|
157
|
+
if (this.workerConsole) {
|
|
158
|
+
await this.workerConsole.clear();
|
|
159
|
+
} else {
|
|
160
|
+
clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (tablePrintTime === 0) return false;
|
|
164
|
+
if (this.maxPrintTime === 0) {
|
|
165
|
+
if (await isBrave()) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return tablePrintTime > this.maxPrintTime * 10;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Debugger-based detection (fallback method)
|
|
176
|
+
* WARNING: This method impacts user experience
|
|
177
|
+
*/
|
|
178
|
+
private async checkDebuggerBased(): Promise<boolean> {
|
|
179
|
+
if (!this.isDebuggerDetectionEnabled()) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Skip debugger detection in Brave (has anti-fingerprinting measures)
|
|
184
|
+
if (await isBrave()) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const startTime = performance.now();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
(() => {}).constructor('debugger')();
|
|
192
|
+
} catch {
|
|
193
|
+
debugger;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return performance.now() - startTime > 100;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Main detection method combining multiple approaches
|
|
201
|
+
* Prioritizes performance-based detection
|
|
202
|
+
*/
|
|
203
|
+
private async detectDevTools(): Promise<boolean> {
|
|
204
|
+
// Primary method: Performance-based detection (from original library)
|
|
205
|
+
const performanceResult = await this.checkPerformanceBased();
|
|
206
|
+
|
|
207
|
+
if (performanceResult) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fallback method: Debugger-based (only if enabled)
|
|
212
|
+
if (this.options.enableDebuggerDetection && this.checkCount >= this.maxChecks) {
|
|
213
|
+
const debuggerResult = await this.checkDebuggerBased();
|
|
214
|
+
return debuggerResult;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Start continuous detection monitoring
|
|
222
|
+
*/
|
|
223
|
+
private startDetection() {
|
|
224
|
+
this.intervalId = window.setInterval(async () => {
|
|
225
|
+
this.checkCount++;
|
|
226
|
+
const currentlyOpen = await this.detectDevTools();
|
|
227
|
+
|
|
228
|
+
if (currentlyOpen !== this.isOpen) {
|
|
229
|
+
this.isOpen = currentlyOpen;
|
|
230
|
+
|
|
231
|
+
if (currentlyOpen) {
|
|
232
|
+
this.options.onDetected();
|
|
233
|
+
} else {
|
|
234
|
+
this.options.onClosed();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Reset check count periodically to avoid excessive debugger usage
|
|
239
|
+
if (this.checkCount > this.maxChecks * 2) {
|
|
240
|
+
this.checkCount = 0;
|
|
241
|
+
}
|
|
242
|
+
}, this.options.interval);
|
|
243
|
+
|
|
244
|
+
// Cleanup on page unload
|
|
245
|
+
window.addEventListener('beforeunload', () => this.destroy());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get current DevTools state
|
|
250
|
+
*/
|
|
251
|
+
public isDevToolsOpen(): boolean {
|
|
252
|
+
return this.isOpen;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Force an immediate check
|
|
257
|
+
*/
|
|
258
|
+
public async checkNow(): Promise<boolean> {
|
|
259
|
+
const wasOpen = this.isOpen;
|
|
260
|
+
this.isOpen = await this.detectDevTools();
|
|
261
|
+
|
|
262
|
+
if (this.isOpen !== wasOpen) {
|
|
263
|
+
if (this.isOpen) {
|
|
264
|
+
this.options.onDetected();
|
|
265
|
+
} else {
|
|
266
|
+
this.options.onClosed();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.isOpen;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stop detection and cleanup resources
|
|
275
|
+
*/
|
|
276
|
+
public destroy() {
|
|
277
|
+
if (this.intervalId) {
|
|
278
|
+
clearInterval(this.intervalId);
|
|
279
|
+
this.intervalId = undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Cleanup Web Worker
|
|
283
|
+
if (this.workerConsole) {
|
|
284
|
+
this.workerConsole.destroy();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.isOpen = false;
|
|
288
|
+
this.checkCount = 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface IframeEmbeddingDetectorOptions {
|
|
2
|
+
/** Callback when iframe embedding is detected */
|
|
3
|
+
onDetected?: (isCrossOrigin: boolean) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class IframeEmbeddingDetector {
|
|
7
|
+
private options: IframeEmbeddingDetectorOptions;
|
|
8
|
+
private observer?: MutationObserver;
|
|
9
|
+
private detected = false;
|
|
10
|
+
|
|
11
|
+
constructor(options: IframeEmbeddingDetectorOptions) {
|
|
12
|
+
if (!options.onDetected) {
|
|
13
|
+
throw new Error('onDetected callback is required');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.setupDetection();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private isIframed(): { isEmbedded: boolean; isCrossOrigin: boolean } {
|
|
21
|
+
try {
|
|
22
|
+
// If we can access top, check if we're in an iframe
|
|
23
|
+
const isEmbedded = window.self !== window.top;
|
|
24
|
+
if (!isEmbedded) {
|
|
25
|
+
return { isEmbedded: false, isCrossOrigin: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Try to access top's location - will throw if cross-origin
|
|
29
|
+
// @ts-ignore - We know this might throw
|
|
30
|
+
const isCrossOrigin = !window.top.location.href;
|
|
31
|
+
return { isEmbedded: true, isCrossOrigin };
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// If we can't access top due to same-origin policy, it's cross-origin
|
|
34
|
+
return { isEmbedded: true, isCrossOrigin: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private setupDetection() {
|
|
39
|
+
const { isEmbedded, isCrossOrigin } = this.isIframed();
|
|
40
|
+
if (isEmbedded) {
|
|
41
|
+
this.handleDetected(isCrossOrigin);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Set up a listener for future checks in case the page is embedded later
|
|
46
|
+
this.observer = new MutationObserver(() => {
|
|
47
|
+
const { isEmbedded, isCrossOrigin } = this.isIframed();
|
|
48
|
+
if (isEmbedded && !this.detected) {
|
|
49
|
+
this.handleDetected(isCrossOrigin);
|
|
50
|
+
this.observer?.disconnect(); // No need to observe further
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.observer.observe(document.documentElement, {
|
|
55
|
+
childList: true,
|
|
56
|
+
subtree: true,
|
|
57
|
+
attributes: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
window.addEventListener("unload", () => this.destroy());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private handleDetected(isCrossOrigin: boolean) {
|
|
64
|
+
this.detected = true;
|
|
65
|
+
this.options.onDetected?.(isCrossOrigin);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public destroy() {
|
|
69
|
+
this.observer?.disconnect();
|
|
70
|
+
this.observer = undefined;
|
|
71
|
+
this.detected = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AutomationDetector } from "./AutomationDetector";
|
|
2
|
+
import { DevToolsDetector } from "./DevToolsDetector";
|
|
3
|
+
import { IframeEmbeddingDetector } from "./IframeEmbeddingDetector";
|
|
4
|
+
import { PrintProtector } from "./PrintProtector";
|
|
5
|
+
import { ContextMenuProtector } from "./ContextMenuProtector";
|
|
6
|
+
import { ContextMenuEvent } from "@readium/navigator-html-injectables";
|
|
7
|
+
import { IContentProtectionConfig } from "../Navigator";
|
|
8
|
+
|
|
9
|
+
export const NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT = "readium:navigator:suspiciousActivity";
|
|
10
|
+
|
|
11
|
+
export class NavigatorProtector {
|
|
12
|
+
private automationDetector?: AutomationDetector;
|
|
13
|
+
private devToolsDetector?: DevToolsDetector;
|
|
14
|
+
private iframeEmbeddingDetector?: IframeEmbeddingDetector;
|
|
15
|
+
private printProtector?: PrintProtector;
|
|
16
|
+
private contextMenuProtector?: ContextMenuProtector;
|
|
17
|
+
|
|
18
|
+
private dispatchSuspiciousActivity(type: string, detail: Record<string, unknown>) {
|
|
19
|
+
const event = new CustomEvent(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, {
|
|
20
|
+
detail: {
|
|
21
|
+
type,
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
...detail
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
window.dispatchEvent(event);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
config: IContentProtectionConfig = {}
|
|
31
|
+
) {
|
|
32
|
+
// Enable DevTools detection if explicitly enabled in config
|
|
33
|
+
if (config.monitorDevTools) {
|
|
34
|
+
this.devToolsDetector = new DevToolsDetector({
|
|
35
|
+
onDetected: () => {
|
|
36
|
+
this.dispatchSuspiciousActivity("developer_tools", {
|
|
37
|
+
targetFrameSrc: "",
|
|
38
|
+
key: "",
|
|
39
|
+
code: "",
|
|
40
|
+
keyCode: -1,
|
|
41
|
+
ctrlKey: false,
|
|
42
|
+
altKey: false,
|
|
43
|
+
shiftKey: false,
|
|
44
|
+
metaKey: false
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Enable automation detection if explicitly enabled in config
|
|
51
|
+
if (config.checkAutomation) {
|
|
52
|
+
this.automationDetector = new AutomationDetector({
|
|
53
|
+
onDetected: (tool: string) => {
|
|
54
|
+
this.dispatchSuspiciousActivity("automation_detected", { tool });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Enable iframe embedding detection if explicitly enabled in config
|
|
60
|
+
if (config.checkIFrameEmbedding) {
|
|
61
|
+
this.iframeEmbeddingDetector = new IframeEmbeddingDetector({
|
|
62
|
+
onDetected: (isCrossOrigin: boolean) => {
|
|
63
|
+
this.dispatchSuspiciousActivity("iframe_embedding_detected", { isCrossOrigin });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Enable print protection if configured
|
|
69
|
+
if (config.protectPrinting?.disable) {
|
|
70
|
+
this.printProtector = new PrintProtector({
|
|
71
|
+
...config.protectPrinting,
|
|
72
|
+
onPrintAttempt: () => {
|
|
73
|
+
this.dispatchSuspiciousActivity("print", {});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Enable context menu protection if configured
|
|
79
|
+
if (config.disableContextMenu) {
|
|
80
|
+
this.contextMenuProtector = new ContextMenuProtector({
|
|
81
|
+
onContextMenuBlocked: (event: ContextMenuEvent) => {
|
|
82
|
+
this.dispatchSuspiciousActivity("context_menu", event as unknown as Record<string, unknown>);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public destroy() {
|
|
89
|
+
this.automationDetector?.destroy();
|
|
90
|
+
this.devToolsDetector?.destroy();
|
|
91
|
+
this.iframeEmbeddingDetector?.destroy();
|
|
92
|
+
this.printProtector?.destroy();
|
|
93
|
+
this.contextMenuProtector?.destroy();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface NavigatorPrintProtectionConfig {
|
|
2
|
+
disable?: boolean;
|
|
3
|
+
watermark?: string;
|
|
4
|
+
onPrintAttempt?: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class PrintProtector {
|
|
8
|
+
private styleElement: HTMLStyleElement | null = null;
|
|
9
|
+
private beforePrintHandler: ((e: Event) => void) | null = null;
|
|
10
|
+
private onPrintAttempt?: () => void;
|
|
11
|
+
|
|
12
|
+
constructor(config: NavigatorPrintProtectionConfig = {}) {
|
|
13
|
+
this.onPrintAttempt = config.onPrintAttempt;
|
|
14
|
+
if (config.disable) {
|
|
15
|
+
this.setupPrintProtection(config.watermark);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private setupPrintProtection(watermark?: string) {
|
|
20
|
+
const style = document.createElement("style");
|
|
21
|
+
style.textContent = `
|
|
22
|
+
@media print {
|
|
23
|
+
body * {
|
|
24
|
+
display: none !important;
|
|
25
|
+
}
|
|
26
|
+
body::after {
|
|
27
|
+
content: "${watermark || 'Printing has been disabled'}";
|
|
28
|
+
font-size: 200%;
|
|
29
|
+
display: block;
|
|
30
|
+
text-align: center;
|
|
31
|
+
margin-top: 50vh;
|
|
32
|
+
transform: translateY(-50%);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
document.head.appendChild(style);
|
|
37
|
+
this.styleElement = style;
|
|
38
|
+
|
|
39
|
+
this.beforePrintHandler = (e: Event) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
this.onPrintAttempt?.();
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
window.addEventListener("beforeprint", this.beforePrintHandler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public destroy() {
|
|
48
|
+
if (this.beforePrintHandler) {
|
|
49
|
+
window.removeEventListener("beforeprint", this.beforePrintHandler);
|
|
50
|
+
this.beforePrintHandler = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.styleElement?.parentNode) {
|
|
54
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
55
|
+
this.styleElement = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
let idCounter = 0;
|
|
2
|
+
|
|
3
|
+
function getId(): number {
|
|
4
|
+
return ++idCounter;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const workerScript = `
|
|
8
|
+
onmessage = function(event) {
|
|
9
|
+
var action = event.data;
|
|
10
|
+
var startTime = performance.now()
|
|
11
|
+
|
|
12
|
+
console[action.type](...action.payload);
|
|
13
|
+
postMessage({
|
|
14
|
+
id: action.id,
|
|
15
|
+
time: performance.now() - startTime
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export type WorkerConsoleMethod<Args extends any[]> = (
|
|
21
|
+
...args: Args
|
|
22
|
+
) => Promise<{ time: number }>;
|
|
23
|
+
|
|
24
|
+
export class WorkerConsole {
|
|
25
|
+
static workerScript = workerScript;
|
|
26
|
+
|
|
27
|
+
private readonly worker: Worker;
|
|
28
|
+
private readonly blobUrl: string;
|
|
29
|
+
private callbacks: Map<number, (data: { time: number }) => void> = new Map();
|
|
30
|
+
|
|
31
|
+
readonly log: WorkerConsoleMethod<Parameters<Console['log']>>;
|
|
32
|
+
readonly table: WorkerConsoleMethod<Parameters<Console['table']>>;
|
|
33
|
+
readonly clear: WorkerConsoleMethod<Parameters<Console['clear']>>;
|
|
34
|
+
|
|
35
|
+
constructor(worker: Worker, blobUrl: string) {
|
|
36
|
+
this.worker = worker;
|
|
37
|
+
this.blobUrl = blobUrl;
|
|
38
|
+
|
|
39
|
+
this.worker.onmessage = (event) => {
|
|
40
|
+
const action = event.data;
|
|
41
|
+
const id = action.id;
|
|
42
|
+
const callback = this.callbacks.get(action.id);
|
|
43
|
+
if (callback) {
|
|
44
|
+
callback({
|
|
45
|
+
time: action.time,
|
|
46
|
+
});
|
|
47
|
+
this.callbacks.delete(id);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.log = (...args) => {
|
|
52
|
+
return this.send('log', ...args);
|
|
53
|
+
};
|
|
54
|
+
this.table = (...args) => {
|
|
55
|
+
return this.send('table', ...args);
|
|
56
|
+
};
|
|
57
|
+
this.clear = (...args) => {
|
|
58
|
+
return this.send('clear', ...args);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async send(type: string, ...messages: any[]) {
|
|
63
|
+
const id = getId();
|
|
64
|
+
return new Promise<{ time: number }>((resolve, reject) => {
|
|
65
|
+
this.callbacks.set(id, resolve);
|
|
66
|
+
|
|
67
|
+
this.worker.postMessage({
|
|
68
|
+
id,
|
|
69
|
+
type,
|
|
70
|
+
payload: messages,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
reject(new Error('timeout'));
|
|
75
|
+
this.callbacks.delete(id);
|
|
76
|
+
}, 2000);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public destroy() {
|
|
81
|
+
this.worker.terminate();
|
|
82
|
+
URL.revokeObjectURL(this.blobUrl);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache console methods to prevent third-party code from hooking them
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function cacheConsoleMethod<K extends keyof Console>(name: K): Console[K] {
|
|
6
|
+
if (typeof window !== 'undefined' && console) {
|
|
7
|
+
return console[name];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Fallback for non-browser environments
|
|
11
|
+
return (..._args: any[]) => {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const log = cacheConsoleMethod('log');
|
|
15
|
+
export const table = cacheConsoleMethod('table');
|
|
16
|
+
export const clear = cacheConsoleMethod('clear');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match utilities for browser compatibility checking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MatchOptions {
|
|
6
|
+
includes: (() => boolean)[];
|
|
7
|
+
excludes: (() => boolean)[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function match(options: MatchOptions): boolean {
|
|
11
|
+
// Check if any exclude condition is true
|
|
12
|
+
if (options.excludes.some(condition => condition())) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if any include condition is true
|
|
17
|
+
return options.includes.some(condition => condition());
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export async function isBrave(): Promise<boolean> {
|
|
6
|
+
// Use native Brave API if available (most reliable)
|
|
7
|
+
if (typeof navigator !== 'undefined' && (navigator as any).brave && (navigator as any).brave.isBrave) {
|
|
8
|
+
try {
|
|
9
|
+
return await Promise.race([
|
|
10
|
+
(navigator as any).brave.isBrave(),
|
|
11
|
+
new Promise(resolve => setTimeout(() => resolve(false), 1000))
|
|
12
|
+
]);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// API call failed, but we know Brave is available
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback: only when native API doesn't exist at all
|
|
20
|
+
// If we're not sure, return false to avoid false positives
|
|
21
|
+
return false;
|
|
22
|
+
}
|