@readium/navigator 2.2.9 → 2.3.1

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 (65) hide show
  1. package/dist/index.js +2786 -1791
  2. package/dist/index.umd.cjs +79 -86
  3. package/package.json +2 -3
  4. package/src/Navigator.ts +76 -0
  5. package/src/epub/EpubNavigator.ts +93 -10
  6. package/src/epub/css/Properties.ts +8 -8
  7. package/src/epub/css/ReadiumCSS.ts +7 -5
  8. package/src/epub/frame/FrameManager.ts +38 -2
  9. package/src/epub/frame/FramePoolManager.ts +9 -2
  10. package/src/epub/fxl/FXLFrameManager.ts +31 -2
  11. package/src/epub/fxl/FXLFramePoolManager.ts +9 -3
  12. package/src/epub/preferences/EpubDefaults.ts +6 -6
  13. package/src/epub/preferences/EpubPreferences.ts +6 -6
  14. package/src/epub/preferences/EpubPreferencesEditor.ts +1 -3
  15. package/src/epub/preferences/EpubSettings.ts +5 -7
  16. package/src/helpers/lineLength.ts +4 -6
  17. package/src/injection/epubInjectables.ts +11 -3
  18. package/src/injection/webpubInjectables.ts +12 -2
  19. package/src/peripherals/KeyboardPeripherals.ts +53 -0
  20. package/src/protection/AutomationDetector.ts +66 -0
  21. package/src/protection/ContextMenuProtector.ts +46 -0
  22. package/src/protection/DevToolsDetector.ts +291 -0
  23. package/src/protection/IframeEmbeddingDetector.ts +73 -0
  24. package/src/protection/NavigatorProtector.ts +95 -0
  25. package/src/protection/PrintProtector.ts +58 -0
  26. package/src/protection/utils/WorkerConsole.ts +84 -0
  27. package/src/protection/utils/console.ts +16 -0
  28. package/src/protection/utils/match.ts +18 -0
  29. package/src/protection/utils/platform.ts +22 -0
  30. package/src/webpub/WebPubFrameManager.ts +38 -5
  31. package/src/webpub/WebPubFramePoolManager.ts +9 -2
  32. package/src/webpub/WebPubNavigator.ts +91 -7
  33. package/types/src/Navigator.d.ts +14 -0
  34. package/types/src/epub/EpubNavigator.d.ts +14 -2
  35. package/types/src/epub/css/Properties.d.ts +4 -0
  36. package/types/src/epub/frame/FrameManager.d.ts +5 -1
  37. package/types/src/epub/frame/FramePoolManager.d.ts +4 -1
  38. package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -1
  39. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -2
  40. package/types/src/epub/preferences/EpubDefaults.d.ts +4 -0
  41. package/types/src/epub/preferences/EpubPreferences.d.ts +4 -0
  42. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +2 -0
  43. package/types/src/epub/preferences/EpubSettings.d.ts +4 -0
  44. package/types/src/helpers/lineLength.d.ts +2 -3
  45. package/types/src/injection/epubInjectables.d.ts +2 -2
  46. package/types/src/injection/webpubInjectables.d.ts +2 -1
  47. package/types/src/peripherals/KeyboardPeripherals.d.ts +13 -0
  48. package/types/src/protection/AutomationDetector.d.ts +14 -0
  49. package/types/src/protection/ContextMenuProtector.d.ts +11 -0
  50. package/types/src/protection/DevToolsDetector.d.ts +75 -0
  51. package/types/src/protection/IframeEmbeddingDetector.d.ts +14 -0
  52. package/types/src/protection/NavigatorProtector.d.ts +12 -0
  53. package/types/src/protection/PrintProtector.d.ts +13 -0
  54. package/types/src/protection/utils/WorkerConsole.d.ts +15 -0
  55. package/types/src/protection/utils/console.d.ts +6 -0
  56. package/types/src/protection/utils/match.d.ts +8 -0
  57. package/types/src/protection/utils/platform.d.ts +4 -0
  58. package/types/src/webpub/WebPubFrameManager.d.ts +5 -1
  59. package/types/src/webpub/WebPubFramePoolManager.d.ts +4 -1
  60. package/types/src/webpub/WebPubNavigator.d.ts +13 -1
  61. package/dist/assets/AccessibleDfA-Bold.woff2 +0 -0
  62. package/dist/assets/AccessibleDfA-Italic.woff2 +0 -0
  63. package/dist/assets/AccessibleDfA-Regular.woff +0 -0
  64. package/dist/assets/AccessibleDfA-Regular.woff2 +0 -0
  65. package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
@@ -48,8 +48,8 @@ export interface IEpubPreferences {
48
48
  scroll?: boolean | null,
49
49
  scrollPaddingTop?: number | null,
50
50
  scrollPaddingBottom?: number | null,
51
- // scrollPaddingLeft?: number | null,
52
- // scrollPaddingRight?: number | null,
51
+ scrollPaddingLeft?: number | null,
52
+ scrollPaddingRight?: number | null,
53
53
  selectionBackgroundColor?: string | null,
54
54
  selectionTextColor?: string | null,
55
55
  textAlign?: TextAlignment | null,
@@ -91,8 +91,8 @@ export class EpubPreferences implements ConfigurablePreferences {
91
91
  scroll?: boolean | null;
92
92
  scrollPaddingTop?: number | null;
93
93
  scrollPaddingBottom?: number | null;
94
- // scrollPaddingLeft?: number | null;
95
- // scrollPaddingRight?: number | null;
94
+ scrollPaddingLeft?: number | null;
95
+ scrollPaddingRight?: number | null;
96
96
  selectionBackgroundColor?: string | null;
97
97
  selectionTextColor?: string | null;
98
98
  textAlign?: TextAlignment | null;
@@ -130,8 +130,8 @@ export class EpubPreferences implements ConfigurablePreferences {
130
130
  this.scroll = ensureBoolean(preferences.scroll);
131
131
  this.scrollPaddingTop = ensureNonNegative(preferences.scrollPaddingTop);
132
132
  this.scrollPaddingBottom = ensureNonNegative(preferences.scrollPaddingBottom);
133
- // this.scrollPaddingLeft = ensureNonNegative(preferences.scrollPaddingLeft);
134
- // this.scrollPaddingRight = ensureNonNegative(preferences.scrollPaddingRight);
133
+ this.scrollPaddingLeft = ensureNonNegative(preferences.scrollPaddingLeft);
134
+ this.scrollPaddingRight = ensureNonNegative(preferences.scrollPaddingRight);
135
135
  this.selectionBackgroundColor = ensureString(preferences.selectionBackgroundColor);
136
136
  this.selectionTextColor = ensureString(preferences.selectionTextColor);
137
137
  this.textAlign = ensureEnumValue<TextAlignment>(preferences.textAlign, TextAlignment);
@@ -364,7 +364,7 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
364
364
  return new Preference<number>({
365
365
  initialValue: this.preferences.pageGutter,
366
366
  effectiveValue: this.settings.pageGutter,
367
- isEffective: this.layout !== Layout.fixed,
367
+ isEffective: this.layout !== Layout.fixed && !this.settings.scroll,
368
368
  onChange: (newValue: number | null | undefined) => {
369
369
  this.updatePreference("pageGutter", newValue || null);
370
370
  }
@@ -430,7 +430,6 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
430
430
  });
431
431
  }
432
432
 
433
- /*
434
433
  get scrollPaddingLeft(): Preference<number> {
435
434
  return new Preference<number>({
436
435
  initialValue: this.preferences.scrollPaddingLeft,
@@ -452,7 +451,6 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
452
451
  }
453
452
  });
454
453
  }
455
- */
456
454
 
457
455
  get selectionBackgroundColor(): Preference<string> {
458
456
  return new Preference<string>({
@@ -37,8 +37,8 @@ export interface IEpubSettings {
37
37
  scroll?: boolean | null,
38
38
  scrollPaddingTop?: number | null,
39
39
  scrollPaddingBottom?: number | null,
40
- // scrollPaddingLeft?: number | null,
41
- // scrollPaddingRight?: number | null,
40
+ scrollPaddingLeft?: number | null,
41
+ scrollPaddingRight?: number | null,
42
42
  selectionBackgroundColor?: string | null,
43
43
  selectionTextColor?: string | null,
44
44
  textAlign?: TextAlignment | null,
@@ -81,8 +81,8 @@ export class EpubSettings implements ConfigurableSettings {
81
81
  scroll: boolean | null;
82
82
  scrollPaddingTop: number | null;
83
83
  scrollPaddingBottom: number | null;
84
- // scrollPaddingLeft: number | null;
85
- // scrollPaddingRight: number | null;
84
+ scrollPaddingLeft: number | null;
85
+ scrollPaddingRight: number | null;
86
86
  selectionBackgroundColor: string | null;
87
87
  selectionTextColor: string | null;
88
88
  textAlign: TextAlignment | null;
@@ -205,8 +205,7 @@ export class EpubSettings implements ConfigurableSettings {
205
205
  ? preferences.scrollPaddingBottom
206
206
  : defaults.scrollPaddingBottom !== undefined
207
207
  ? defaults.scrollPaddingBottom
208
- : null;
209
- /*
208
+ : null;
210
209
  this.scrollPaddingLeft = preferences.scrollPaddingLeft !== undefined
211
210
  ? preferences.scrollPaddingLeft
212
211
  : defaults.scrollPaddingLeft !== undefined
@@ -217,7 +216,6 @@ export class EpubSettings implements ConfigurableSettings {
217
216
  : defaults.scrollPaddingRight !== undefined
218
217
  ? defaults.scrollPaddingRight
219
218
  : null;
220
- */
221
219
  this.selectionBackgroundColor = preferences.selectionBackgroundColor || defaults.selectionBackgroundColor || null;
222
220
  this.selectionTextColor = preferences.selectionTextColor || defaults.selectionTextColor || null;
223
221
  this.textAlign = preferences.textAlign || defaults.textAlign || null;
@@ -11,7 +11,7 @@ export interface ILineLengthsConfig {
11
11
  maxChars?: number | null;
12
12
  baseFontSize?: number | null;
13
13
  sample?: string | null;
14
- pageGutter?: number | null;
14
+ padding?: number | null;
15
15
  fontFace?: string | ICustomFontFace | null;
16
16
  letterSpacing?: number | null;
17
17
  wordSpacing?: number | null;
@@ -51,13 +51,12 @@ export class LineLengths {
51
51
  private _baseFontSize: number;
52
52
  private _fontFace: string | ICustomFontFace;
53
53
  private _sample: string | null;
54
- private _pageGutter: number;
54
+ private _padding: number;
55
55
  private _letterSpacing: number;
56
56
  private _wordSpacing: number;
57
57
  private _isCJK: boolean;
58
58
  private _getRelative: boolean;
59
59
 
60
- private _padding: number;
61
60
  private _minDivider: number | null;
62
61
  private _maxMultiplier: number | null;
63
62
  private _approximatedWordSpaces: number;
@@ -72,7 +71,7 @@ export class LineLengths {
72
71
  this._baseFontSize = config.baseFontSize || DEFAULT_FONT_SIZE;
73
72
  this._fontFace = config.fontFace || DEFAULT_FONT_FACE;
74
73
  this._sample = config.sample || null;
75
- this._pageGutter = config.pageGutter || 0;
74
+ this._padding = config.padding ?? 0;
76
75
  this._letterSpacing = config.letterSpacing
77
76
  ? Math.round(config.letterSpacing * this._baseFontSize)
78
77
  : 0;
@@ -81,7 +80,6 @@ export class LineLengths {
81
80
  : 0;
82
81
  this._isCJK = config.isCJK || false;
83
82
  this._getRelative = config.getRelative || false;
84
- this._padding = this._pageGutter * 2;
85
83
  this._minDivider = this._minChars && this._minChars < this._optimalChars
86
84
  ? this._optimalChars / this._minChars
87
85
  : this._minChars === null
@@ -121,7 +119,7 @@ export class LineLengths {
121
119
  if (props.letterSpacing) this._letterSpacing = props.letterSpacing;
122
120
  if (props.wordSpacing) this._wordSpacing = props.wordSpacing;
123
121
  if (props.isCJK != null) this._isCJK = props.isCJK;
124
- if (props.pageGutter) this._pageGutter = props.pageGutter;
122
+ if (props.padding !== undefined) this._padding = props.padding ?? 0;
125
123
  if (props.getRelative) this._getRelative = props.getRelative;
126
124
 
127
125
  if (props.sample) {
@@ -1,6 +1,6 @@
1
1
  import { IInjectableRule, IInjectable } from "../injection/Injectable";
2
2
  import { stripJS, stripCSS } from "../helpers/minify";
3
- import { Metadata, Layout } from "@readium/shared";
3
+ import { Metadata, Layout, Link } from "@readium/shared";
4
4
 
5
5
  import readiumCSSAfter from "@readium/css/css/dist/ReadiumCSS-after.css?raw";
6
6
  import readiumCSSBefore from "@readium/css/css/dist/ReadiumCSS-before.css?raw";
@@ -13,9 +13,17 @@ import onloadProxyContent from "../dom/_readium_executionCleanup.js?raw";
13
13
  /**
14
14
  * Creates injectable rules for EPUB content documents
15
15
  */
16
- export function createReadiumEpubRules(metadata: Metadata): IInjectableRule[] {
16
+ export function createReadiumEpubRules(metadata: Metadata, readingOrderItems: Link[]): IInjectableRule[] {
17
17
  const isFixedLayout = metadata.effectiveLayout === Layout.fixed;
18
18
 
19
+ const htmlHrefs = readingOrderItems
20
+ .filter(item => item.mediaType.isHTML)
21
+ .map(item => item.href);
22
+
23
+ const resources = htmlHrefs.length > 0
24
+ ? htmlHrefs
25
+ : [/\.xhtml$/, /\.html$/]; // fallback patterns
26
+
19
27
  // Core injectables that should be prepended
20
28
  const prependInjectables: IInjectable[] = [
21
29
  // CSS Selector Generator - always injected
@@ -82,7 +90,7 @@ export function createReadiumEpubRules(metadata: Metadata): IInjectableRule[] {
82
90
 
83
91
  return [
84
92
  {
85
- resources: [/\.xhtml$/, /\.html$/],
93
+ resources: resources,
86
94
  prepend: prependInjectables,
87
95
  append: appendInjectables
88
96
  }
@@ -1,5 +1,6 @@
1
1
  import { IInjectableRule, IInjectable } from "../injection/Injectable";
2
2
  import { stripJS, stripCSS } from "../helpers/minify";
3
+ import { Link } from "@readium/shared";
3
4
 
4
5
  import readiumCSSWebPub from "@readium/css/css/dist/webPub/ReadiumCSS-webPub.css?raw";
5
6
 
@@ -10,7 +11,16 @@ import onloadProxyContent from "../dom/_readium_executionCleanup.js?raw";
10
11
  /**
11
12
  * Creates injectable rules for WebPub content documents
12
13
  */
13
- export function createReadiumWebPubRules(): IInjectableRule[] {
14
+ export function createReadiumWebPubRules(readingOrderItems: Link[]): IInjectableRule[] {
15
+ // Create exact match patterns for manifest hrefs
16
+ const htmlHrefs = readingOrderItems
17
+ .filter(item => item.mediaType.isHTML)
18
+ .map(item => item.href);
19
+
20
+ const resources = htmlHrefs.length > 0
21
+ ? htmlHrefs
22
+ : [/\.html$/, /\.xhtml$/, /\/$/]; // fallback patterns
23
+
14
24
  // Core injectables that should be prepended
15
25
  const prependInjectables: IInjectable[] = [
16
26
  // CSS Selector Generator - always injected
@@ -51,7 +61,7 @@ export function createReadiumWebPubRules(): IInjectableRule[] {
51
61
 
52
62
  return [
53
63
  {
54
- resources: [/\.xhtml$/, /\.html$/],
64
+ resources: resources,
55
65
  prepend: prependInjectables,
56
66
  append: appendInjectables
57
67
  }
@@ -0,0 +1,53 @@
1
+ import {
2
+ KeyCombinationManager,
3
+ ActivityEventDispatcher,
4
+ KeyboardPeripheral
5
+ } from "@readium/navigator-html-injectables";
6
+
7
+ export const NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT = "readium:navigator:keyboardPeripheral";
8
+
9
+ export interface KeyboardPeripheralOptions {
10
+ /** Array of keyboard peripherals to configure */
11
+ keyboardPeripherals?: KeyboardPeripheral[];
12
+ }
13
+
14
+ export class KeyboardPeripherals {
15
+ private keydownHandler?: (event: KeyboardEvent) => void;
16
+ private keyManager = new KeyCombinationManager();
17
+
18
+ constructor(options: KeyboardPeripheralOptions = {}) {
19
+ this.setupKeyboardPeripherals(options.keyboardPeripherals || []);
20
+ }
21
+
22
+ private setupKeyboardPeripherals(keyboardPeripherals: KeyboardPeripheral[]) {
23
+ if (keyboardPeripherals.length > 0) {
24
+ // Create activity event dispatcher
25
+ const dispatcher: ActivityEventDispatcher = (activityEvent) => {
26
+ const customEvent = new CustomEvent(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, {
27
+ detail: activityEvent
28
+ });
29
+ window.dispatchEvent(customEvent);
30
+ };
31
+
32
+ // Create unified handler using the provided keyboard peripherals
33
+ this.keydownHandler = this.keyManager.createUnifiedHandler(
34
+ "", // Empty string as target frame source for main window
35
+ keyboardPeripherals,
36
+ dispatcher
37
+ );
38
+
39
+ if (this.keydownHandler) {
40
+ document.addEventListener("keydown", this.keydownHandler, true);
41
+ }
42
+ }
43
+
44
+ window.addEventListener("unload", () => this.destroy());
45
+ }
46
+
47
+ public destroy() {
48
+ if (this.keydownHandler) {
49
+ document.removeEventListener("keydown", this.keydownHandler, true);
50
+ this.keydownHandler = undefined;
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,66 @@
1
+ export interface AutomationDetectorOptions {
2
+ /** Callback when an automation tool is detected */
3
+ onDetected?: (tool: string) => void;
4
+ }
5
+
6
+ export class AutomationDetector {
7
+ private options: AutomationDetectorOptions;
8
+ private detectedTools = new Set<string>();
9
+ private observer?: MutationObserver;
10
+
11
+ constructor(options: AutomationDetectorOptions) {
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 isAutomationToolPresent(): string | null {
21
+ const win = window as any;
22
+
23
+ if (win.domAutomation || win.domAutomationController) return "Selenium";
24
+ if (navigator.webdriver === true) return "Puppeteer/Playwright";
25
+ if (win.__webdriver_evaluate || win.__selenium_evaluate) return "Chrome Automation";
26
+ if (win.callPhantom || win._phantom) return "PhantomJS";
27
+ if (win.__nightmare) return "Nightmare";
28
+ if (win.$testCafe) return "TestCafe";
29
+
30
+ return null;
31
+ }
32
+
33
+ private setupDetection() {
34
+ const tool = this.isAutomationToolPresent();
35
+ if (tool) {
36
+ this.handleDetected(tool);
37
+ return;
38
+ }
39
+
40
+ this.observer = new MutationObserver(() => {
41
+ const tool = this.isAutomationToolPresent();
42
+ if (tool && !this.detectedTools.has(tool)) {
43
+ this.handleDetected(tool);
44
+ }
45
+ });
46
+
47
+ this.observer.observe(document.documentElement, {
48
+ childList: true,
49
+ subtree: true,
50
+ attributes: true
51
+ });
52
+
53
+ window.addEventListener("unload", () => this.destroy());
54
+ }
55
+
56
+ private handleDetected(tool: string) {
57
+ this.detectedTools.add(tool);
58
+ this.options.onDetected?.(tool);
59
+ }
60
+
61
+ public destroy() {
62
+ this.observer?.disconnect();
63
+ this.observer = undefined;
64
+ this.detectedTools.clear();
65
+ }
66
+ }
@@ -0,0 +1,46 @@
1
+ import { ContextMenuEvent } from "@readium/navigator-html-injectables";
2
+
3
+ export interface ContextMenuProtectionOptions {
4
+ onContextMenuBlocked?: (event: ContextMenuEvent) => void;
5
+ }
6
+
7
+ export class ContextMenuProtector {
8
+ private contextMenuHandler?: (event: MouseEvent) => void;
9
+ private onContextMenuBlocked?: (event: ContextMenuEvent) => void;
10
+
11
+ constructor(options: ContextMenuProtectionOptions = {}) {
12
+ this.onContextMenuBlocked = options.onContextMenuBlocked;
13
+ this.contextMenuHandler = this.handleContextMenu.bind(this);
14
+ document.addEventListener("contextmenu", this.contextMenuHandler, true);
15
+
16
+ window.addEventListener("unload", () => this.destroy());
17
+ }
18
+
19
+ private handleContextMenu(event: MouseEvent) {
20
+ event.preventDefault();
21
+ event.stopPropagation();
22
+
23
+ // Create context menu event
24
+ const activityEvent: ContextMenuEvent & { type: "context_menu" } = {
25
+ type: "context_menu",
26
+ timestamp: Date.now(),
27
+ clientX: event.clientX,
28
+ clientY: event.clientY,
29
+ targetFrameSrc: ''
30
+ };
31
+
32
+ // Call the callback if provided
33
+ if (this.onContextMenuBlocked) {
34
+ this.onContextMenuBlocked(activityEvent);
35
+ }
36
+
37
+ return false;
38
+ }
39
+
40
+ public destroy() {
41
+ if (this.contextMenuHandler) {
42
+ document.removeEventListener("contextmenu", this.contextMenuHandler, true);
43
+ this.contextMenuHandler = undefined;
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,291 @@
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
+ this.workerConsole = undefined;
286
+ }
287
+
288
+ this.isOpen = false;
289
+ this.checkCount = 0;
290
+ }
291
+ }