@readium/navigator-html-injectables 2.2.5 → 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 +1107 -548
- package/dist/index.umd.cjs +23 -9
- package/package.json +1 -1
- package/src/comms/keys.ts +22 -3
- package/src/index.ts +2 -1
- package/src/modules/ModuleLibrary.ts +10 -4
- package/src/modules/Peripherals.ts +458 -13
- package/src/modules/snapper/ColumnSnapper.ts +63 -2
- package/src/modules/snapper/ScrollSnapper.ts +59 -1
- package/src/modules/snapper/WebPubSnapper.ts +57 -3
- package/src/protection/BulkCopyProtector.ts +134 -0
- package/src/protection/KeyCombinationManager.ts +139 -0
- package/src/protection/KeyboardCombinations.ts +60 -0
- package/src/protection/PatternAnalyzer.ts +117 -0
- package/src/protection/PrintProtector.ts +87 -0
- package/src/protection/SelectionAnalyzer.ts +133 -0
- package/src/protection/config.ts +33 -0
- package/src/protection/index.ts +6 -0
- package/types/src/comms/keys.d.ts +3 -2
- package/types/src/index.d.ts +1 -0
- package/types/src/modules/ModuleLibrary.d.ts +1 -1
- package/types/src/modules/Peripherals.d.ts +102 -2
- package/types/src/modules/snapper/ColumnSnapper.d.ts +10 -0
- package/types/src/modules/snapper/ScrollSnapper.d.ts +13 -0
- package/types/src/modules/snapper/WebPubSnapper.d.ts +6 -0
- package/types/src/protection/BulkCopyProtector.d.ts +20 -0
- package/types/src/protection/KeyCombinationManager.d.ts +33 -0
- package/types/src/protection/KeyboardCombinations.d.ts +15 -0
- package/types/src/protection/PatternAnalyzer.d.ts +18 -0
- package/types/src/protection/PrintProtector.d.ts +20 -0
- package/types/src/protection/SelectionAnalyzer.d.ts +20 -0
- package/types/src/protection/config.d.ts +7 -0
- package/types/src/protection/index.d.ts +6 -0
- package/src/types/protection.ts +0 -16
- package/types/src/types/protection.d.ts +0 -7
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface KeyCombo {
|
|
2
|
+
keyCode: number; // Use stable keyCode that doesn't change across layouts
|
|
3
|
+
ctrl?: boolean;
|
|
4
|
+
shift?: boolean;
|
|
5
|
+
alt?: boolean;
|
|
6
|
+
meta?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface KeyboardPeripheral {
|
|
10
|
+
type: string;
|
|
11
|
+
keyCombos: KeyCombo[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Developer tools shortcuts as KeyboardPeripheral
|
|
15
|
+
export const DEV_TOOLS: KeyboardPeripheral = {
|
|
16
|
+
type: "developer_tools",
|
|
17
|
+
keyCombos: [
|
|
18
|
+
{ keyCode: 73, meta: true, alt: true }, // I key
|
|
19
|
+
{ keyCode: 74, meta: true, alt: true }, // J key
|
|
20
|
+
{ keyCode: 85, meta: true, alt: true }, // U key
|
|
21
|
+
{ keyCode: 67, meta: true, alt: true }, // C key
|
|
22
|
+
{ keyCode: 67, meta: true, shift: true }, // C key (Cmd+Shift+C)
|
|
23
|
+
{ keyCode: 67, ctrl: true, shift: true }, // C key (Ctrl+Shift+C)
|
|
24
|
+
{ keyCode: 123 }, // F12
|
|
25
|
+
{ keyCode: 123, shift: true }, // F12+Shift
|
|
26
|
+
{ keyCode: 123, ctrl: true, shift: true }, // F12+Ctrl+Shift
|
|
27
|
+
{ keyCode: 123, meta: true, alt: true }, // F12+Meta+Alt
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Select all shortcuts as KeyboardPeripheral
|
|
32
|
+
export const SELECT_ALL: KeyboardPeripheral = {
|
|
33
|
+
type: "select_all",
|
|
34
|
+
keyCombos: [
|
|
35
|
+
{ keyCode: 65, meta: true }, // A key (Cmd+A)
|
|
36
|
+
{ keyCode: 65, ctrl: true } // A key (Ctrl+A)
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Print shortcuts as KeyboardPeripheral
|
|
41
|
+
export const PRINT: KeyboardPeripheral = {
|
|
42
|
+
type: "print",
|
|
43
|
+
keyCombos: [
|
|
44
|
+
{ keyCode: 80, meta: true }, // P key (Cmd+P)
|
|
45
|
+
{ keyCode: 80, ctrl: true }, // P key (Ctrl+P)
|
|
46
|
+
{ keyCode: 80, meta: true, shift: true }, // P key (Cmd+Shift+P)
|
|
47
|
+
{ keyCode: 80, ctrl: true, shift: true }, // P key (Ctrl+Shift+P)
|
|
48
|
+
{ keyCode: 80, meta: true, alt: true }, // P key (Cmd+Alt+P)
|
|
49
|
+
{ keyCode: 80, ctrl: true, alt: true } // P key (Ctrl+Alt+P)
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Save shortcuts as KeyboardPeripheral
|
|
54
|
+
export const SAVE: KeyboardPeripheral = {
|
|
55
|
+
type: "save",
|
|
56
|
+
keyCombos: [
|
|
57
|
+
{ keyCode: 83, meta: true }, // S key (Cmd+S)
|
|
58
|
+
{ keyCode: 83, ctrl: true } // S key (Ctrl+S)
|
|
59
|
+
]
|
|
60
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DEFAULT_PATTERN_ANALYZER_CONFIG } from "./config";
|
|
2
|
+
|
|
3
|
+
type Direction = "left" | "right" | "up" | "down";
|
|
4
|
+
|
|
5
|
+
export interface PatternAnalyzerOptions {
|
|
6
|
+
maxVelocity: number; // pixels/ms
|
|
7
|
+
minVariance: number; // 0-1
|
|
8
|
+
historySize?: number; // Number of scroll events to keep in history
|
|
9
|
+
minDirectionChanges?: number; // Minimum ratio of direction changes to total events (0-1)
|
|
10
|
+
maxConsistentScrolls?: number; // Maximum number of consistent scrolls before flagging
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// TODO: Improve for scroll protection,
|
|
14
|
+
// currently disabled because of false positives
|
|
15
|
+
export class PatternAnalyzer {
|
|
16
|
+
private options: PatternAnalyzerOptions;
|
|
17
|
+
private history: Array<{
|
|
18
|
+
timestamp: number;
|
|
19
|
+
direction: Direction;
|
|
20
|
+
velocity: number;
|
|
21
|
+
distance: number;
|
|
22
|
+
}> = [];
|
|
23
|
+
|
|
24
|
+
private consistentScrollCount = 0;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
options: Partial<PatternAnalyzerOptions> = {}
|
|
28
|
+
) {
|
|
29
|
+
// Merge provided options with default config
|
|
30
|
+
this.options = { ...DEFAULT_PATTERN_ANALYZER_CONFIG, ...options };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
analyze(direction: Direction, distance: number, timeDelta: number): boolean {
|
|
34
|
+
if (timeDelta <= 0) return false;
|
|
35
|
+
|
|
36
|
+
const velocity = Math.abs(distance) / timeDelta;
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
|
|
39
|
+
// Add new event
|
|
40
|
+
this.history.push({
|
|
41
|
+
timestamp: now,
|
|
42
|
+
direction,
|
|
43
|
+
velocity,
|
|
44
|
+
distance: Math.abs(distance)
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Clean up old events (keep last 2 seconds of data or historySize, whichever is smaller)
|
|
48
|
+
this.history = this.history
|
|
49
|
+
.filter(h => now - h.timestamp < 2000)
|
|
50
|
+
.slice(-(this.options.historySize || 20));
|
|
51
|
+
|
|
52
|
+
// Need at least 3 events to start detecting patterns
|
|
53
|
+
if (this.history.length < 3) return false;
|
|
54
|
+
|
|
55
|
+
// 1. Check for excessive speed (unrealistically fast scrolling)
|
|
56
|
+
if (velocity > this.options.maxVelocity) {
|
|
57
|
+
this.resetAfterDetection();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Check for consistent scrolling patterns
|
|
62
|
+
const velocities = this.history.map(h => h.velocity);
|
|
63
|
+
const distances = this.history.map(h => h.distance);
|
|
64
|
+
|
|
65
|
+
// Calculate mean and variance of velocities and distances
|
|
66
|
+
const meanVelocity = velocities.reduce((a, b) => a + b, 0) / velocities.length;
|
|
67
|
+
const meanDistance = distances.reduce((a, b) => a + b, 0) / distances.length;
|
|
68
|
+
|
|
69
|
+
const velocityVariance = velocities.reduce((a, b) => a + Math.pow(b - meanVelocity, 2), 0) / velocities.length;
|
|
70
|
+
const distanceVariance = distances.reduce((a, b) => a + Math.pow(b - meanDistance, 2), 0) / distances.length;
|
|
71
|
+
|
|
72
|
+
// Check for suspiciously consistent scrolling (low variance in speed and distance)
|
|
73
|
+
if (velocityVariance < this.options.minVariance &&
|
|
74
|
+
distanceVariance < (meanDistance * 0.1)) { // Allow 10% variance in distance
|
|
75
|
+
this.consistentScrollCount++;
|
|
76
|
+
|
|
77
|
+
// Only trigger if we've seen consistent scrolling for a while
|
|
78
|
+
if (this.consistentScrollCount >= (this.options.maxConsistentScrolls || 10)) {
|
|
79
|
+
this.resetAfterDetection();
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Reset counter if pattern breaks
|
|
84
|
+
this.consistentScrollCount = Math.max(0, this.consistentScrollCount - 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Check for rapid direction changes (unnatural back-and-forth scrolling)
|
|
88
|
+
let directionChanges = 0;
|
|
89
|
+
let lastDirection = this.history[0].direction;
|
|
90
|
+
|
|
91
|
+
for (let i = 1; i < this.history.length; i++) {
|
|
92
|
+
if (this.history[i].direction !== lastDirection) {
|
|
93
|
+
directionChanges++;
|
|
94
|
+
lastDirection = this.history[i].direction;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const directionChangeRatio = directionChanges / this.history.length;
|
|
99
|
+
if (directionChangeRatio > (this.options.minDirectionChanges || 0.3)) {
|
|
100
|
+
this.resetAfterDetection();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private resetAfterDetection(): void {
|
|
108
|
+
// Keep some history to prevent immediate re-triggering
|
|
109
|
+
this.history = this.history.slice(-3);
|
|
110
|
+
this.consistentScrollCount = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clear(): void {
|
|
114
|
+
this.history = [];
|
|
115
|
+
this.consistentScrollCount = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Comms } from "../comms";
|
|
2
|
+
import { ReadiumWindow } from "../helpers/dom";
|
|
3
|
+
import { Module } from "../modules/Module";
|
|
4
|
+
import { ModuleName } from "../modules/ModuleLibrary";
|
|
5
|
+
|
|
6
|
+
export interface PrintProtectionConfig {
|
|
7
|
+
disable?: boolean;
|
|
8
|
+
watermark?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PrintProtector extends Module {
|
|
12
|
+
static readonly moduleName: ModuleName = "print_protection";
|
|
13
|
+
|
|
14
|
+
private comms!: Comms;
|
|
15
|
+
private wnd!: ReadiumWindow;
|
|
16
|
+
private styleElement: HTMLStyleElement | null = null;
|
|
17
|
+
private beforePrintHandler: ((e: Event) => void) | null = null;
|
|
18
|
+
private configApplied = false;
|
|
19
|
+
|
|
20
|
+
private setupPrintProtection(wnd: Window, config: PrintProtectionConfig) {
|
|
21
|
+
if (!config.disable) return;
|
|
22
|
+
|
|
23
|
+
const style = wnd.document.createElement("style");
|
|
24
|
+
style.textContent = `
|
|
25
|
+
@media print {
|
|
26
|
+
body * {
|
|
27
|
+
display: none !important;
|
|
28
|
+
}
|
|
29
|
+
body::after {
|
|
30
|
+
content: "${config.watermark || 'Printing has been disabled'}";
|
|
31
|
+
font-size: 200%;
|
|
32
|
+
display: block;
|
|
33
|
+
text-align: center;
|
|
34
|
+
margin-top: 50vh;
|
|
35
|
+
transform: translateY(-50%);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
wnd.document.head.appendChild(style);
|
|
40
|
+
this.styleElement = style;
|
|
41
|
+
|
|
42
|
+
this.beforePrintHandler = (e: Event) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
wnd.addEventListener("beforeprint", this.beforePrintHandler);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private registerPrintHandlers() {
|
|
50
|
+
this.comms?.register("print_protection", PrintProtector.moduleName, (data: unknown) => {
|
|
51
|
+
const config = data as PrintProtectionConfig;
|
|
52
|
+
|
|
53
|
+
if (!this.configApplied) {
|
|
54
|
+
this.configApplied = true;
|
|
55
|
+
this.setupPrintProtection(this.wnd, config);
|
|
56
|
+
this.comms?.log("Print protection configuration applied");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mount(wnd: ReadiumWindow, comms: Comms): boolean {
|
|
64
|
+
this.wnd = wnd;
|
|
65
|
+
this.comms = comms;
|
|
66
|
+
this.registerPrintHandlers();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
unmount(wnd: ReadiumWindow, _comms: Comms): boolean {
|
|
71
|
+
if (this.beforePrintHandler) {
|
|
72
|
+
wnd.removeEventListener("beforeprint", this.beforePrintHandler);
|
|
73
|
+
this.beforePrintHandler = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.styleElement?.parentNode) {
|
|
77
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
78
|
+
this.styleElement = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Unregister all print protection handlers
|
|
82
|
+
this.comms?.unregisterAll(PrintProtector.moduleName);
|
|
83
|
+
|
|
84
|
+
this.configApplied = false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
interface SelectionEvent {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
position: number;
|
|
4
|
+
length: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SelectionAnalyzerOptions {
|
|
8
|
+
maxSelectionsPerSecond: number;
|
|
9
|
+
minVariance: number;
|
|
10
|
+
historySize: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
import { SELECTION_ANALYZER_CONFIG } from './config';
|
|
14
|
+
|
|
15
|
+
export class SelectionAnalyzer {
|
|
16
|
+
private events: SelectionEvent[] = [];
|
|
17
|
+
private selectionStartTime = 0;
|
|
18
|
+
private lastSelectionTime = 0;
|
|
19
|
+
private lastSelectionPosition = 0;
|
|
20
|
+
private selectionPatterns: number[] = [];
|
|
21
|
+
private lastSelectedText = "";
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly options: SelectionAnalyzerOptions = SELECTION_ANALYZER_CONFIG
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
public analyze(selection: Selection | null): boolean {
|
|
28
|
+
if (!selection) {
|
|
29
|
+
this.clear();
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const selectedText = selection.toString();
|
|
34
|
+
if (selectedText.length === 0) {
|
|
35
|
+
this.clear();
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (selection.type !== "Range" || !selection.rangeCount) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
|
|
45
|
+
// Only analyze meaningful selections (not too short, not duplicates)
|
|
46
|
+
if (selectedText.length <= 50 || selectedText === this.lastSelectedText) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Start timing if this is a new selection
|
|
51
|
+
if (this.selectionStartTime === 0) {
|
|
52
|
+
this.selectionStartTime = now;
|
|
53
|
+
this.lastSelectedText = selectedText;
|
|
54
|
+
return false; // Don't analyze first selection, just track it
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if enough time has passed for selection to be complete
|
|
58
|
+
const timeSinceStart = now - this.selectionStartTime;
|
|
59
|
+
if (timeSinceStart < 500) {
|
|
60
|
+
return false; // Still selecting, wait longer
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Track selection frequency
|
|
64
|
+
if (now - this.lastSelectionTime > 1000) {
|
|
65
|
+
this.lastSelectionTime = now;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.selectionStartTime === 0) {
|
|
69
|
+
this.selectionStartTime = now;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Store current selection for comparison
|
|
73
|
+
this.lastSelectedText = selectedText;
|
|
74
|
+
|
|
75
|
+
// Analyze selection pattern
|
|
76
|
+
const isSuspicious = this.analyzeSelectionPattern(selection, now);
|
|
77
|
+
|
|
78
|
+
// Clean up old events
|
|
79
|
+
this.cleanup(now);
|
|
80
|
+
|
|
81
|
+
return isSuspicious;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private analyzeSelectionPattern(selection: Selection, now: number): boolean {
|
|
85
|
+
if (!selection.rangeCount) return false;
|
|
86
|
+
|
|
87
|
+
const range = selection.getRangeAt(0);
|
|
88
|
+
const text = range.toString();
|
|
89
|
+
|
|
90
|
+
// Calculate selection speed (characters per second)
|
|
91
|
+
const duration = (now - this.selectionStartTime) / 1000; // Convert to seconds
|
|
92
|
+
const speed = text.length / Math.max(1, duration); // Characters per second
|
|
93
|
+
|
|
94
|
+
// Check for unnaturally fast selection
|
|
95
|
+
if (speed > this.options.maxSelectionsPerSecond) return true;
|
|
96
|
+
|
|
97
|
+
// Analyze selection pattern (human selections have more variance)
|
|
98
|
+
const currentPosition = range.startOffset;
|
|
99
|
+
const distance = Math.abs(currentPosition - this.lastSelectionPosition);
|
|
100
|
+
this.selectionPatterns.push(distance);
|
|
101
|
+
|
|
102
|
+
if (this.selectionPatterns.length > this.options.historySize) {
|
|
103
|
+
this.selectionPatterns.shift();
|
|
104
|
+
const variance = this.calculateVariance(this.selectionPatterns);
|
|
105
|
+
if (variance < this.options.minVariance) { // Very consistent patterns suggest automation
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.lastSelectionPosition = currentPosition;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private calculateVariance(numbers: number[]): number {
|
|
115
|
+
if (numbers.length === 0) return 0;
|
|
116
|
+
const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
117
|
+
return numbers.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / numbers.length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private cleanup(now: number): void {
|
|
121
|
+
// Keep events from the last second
|
|
122
|
+
this.events = this.events.filter(e => now - e.timestamp <= 1000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public clear(): void {
|
|
126
|
+
this.events = [];
|
|
127
|
+
this.selectionStartTime = 0;
|
|
128
|
+
this.lastSelectionTime = 0;
|
|
129
|
+
this.lastSelectionPosition = 0;
|
|
130
|
+
this.selectionPatterns = [];
|
|
131
|
+
this.lastSelectedText = "";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BulkCopyProtectionOptions } from "./BulkCopyProtector";
|
|
2
|
+
import { PatternAnalyzerOptions } from "./PatternAnalyzer";
|
|
3
|
+
import { SelectionAnalyzerOptions } from "./SelectionAnalyzer";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PATTERN_ANALYZER_CONFIG: PatternAnalyzerOptions = {
|
|
6
|
+
maxVelocity: 200, // Reasonable default for human-like scrolling (pixels/ms)
|
|
7
|
+
minVariance: 0.01, // Default variance threshold
|
|
8
|
+
historySize: 20, // Balanced history size for performance
|
|
9
|
+
minDirectionChanges: 0.2, // Reasonable default for detecting patterns
|
|
10
|
+
maxConsistentScrolls: 15 // Balanced threshold for flagging
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const SCROLL_PROTECTION_CONFIG: PatternAnalyzerOptions = {
|
|
14
|
+
maxVelocity: 200, // Extremely fast scrolling (pixels/ms)
|
|
15
|
+
minVariance: 0.00001, // Near-perfect consistency
|
|
16
|
+
historySize: 100, // Large history window
|
|
17
|
+
minDirectionChanges: 0.1, // Only trigger on near-perfect patterns
|
|
18
|
+
maxConsistentScrolls: 20 // Need many consistent scrolls
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const SELECTION_ANALYZER_CONFIG: SelectionAnalyzerOptions = {
|
|
22
|
+
maxSelectionsPerSecond: 500,
|
|
23
|
+
minVariance: 50,
|
|
24
|
+
historySize: 20
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const BULK_COPY_CONFIG: BulkCopyProtectionOptions = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
maxSelectionPercent: 0.1,
|
|
30
|
+
minThreshold: 100,
|
|
31
|
+
absoluteMaxChars: 5000,
|
|
32
|
+
historySize: 20
|
|
33
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export type CommsEventKey = "_pong" | "_unhandled" | "_ack" | "log" | "error" | "click" | "tap" | "tap_more" | "no_more" | "no_less" | "swipe" | "scroll" | "progress" | "first_visible_locator" | "text_selected" | "media_play" | "media_pause";
|
|
2
|
-
export type CommsCommandKey = "_ping" | "go_prev" | "go_next" | "go_id" | "go_text" | "go_end" | "go_start" | "go_progression" | "get_properties" | "update_properties" | "set_property" | "remove_property" | "first_visible_locator" | "decorate" | "protect" | "unprotect" | "unfocus" | "focus" | "activate" | "shake" | "force_webkit_recalc";
|
|
1
|
+
export type CommsEventKey = "_pong" | "_unhandled" | "_ack" | "log" | "error" | "click" | "tap" | "tap_more" | "no_more" | "no_less" | "swipe" | "scroll" | "progress" | "first_visible_locator" | "text_selected" | "context_menu" | "media_play" | "media_pause" | "content_protection" | "keyboard_peripherals";
|
|
2
|
+
export type CommsCommandKey = "_ping" | "go_prev" | "go_next" | "go_id" | "go_text" | "go_end" | "go_start" | "go_progression" | "get_properties" | "update_properties" | "set_property" | "remove_property" | "first_visible_locator" | "decorate" | "protect" | "unprotect" | "unfocus" | "focus" | "activate" | "shake" | "force_webkit_recalc" | "peripherals_protection" | "keyboard_peripherals" | "scroll_protection" | "print_protection";
|
|
3
|
+
export type SuspiciousActivityType = "developer_tools" | "select_all" | "save" | "suspicious_selection" | "bulk_copy" | "suspicious_scrolling" | "suspicious_snapping" | "drag_detected" | "drop_detected" | "print";
|
package/types/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModuleDerived } from "./Module";
|
|
2
|
-
export type ModuleName = "setup" | "snapper" | "column_snapper" | "scroll_snapper" | "webpub_snapper" | "fixed_setup" | "decorator" | "reflowable_setup" | "peripherals" | "webpub_setup";
|
|
2
|
+
export type ModuleName = "setup" | "snapper" | "column_snapper" | "scroll_snapper" | "webpub_snapper" | "fixed_setup" | "decorator" | "reflowable_setup" | "peripherals" | "webpub_setup" | "print_protection";
|
|
3
3
|
export declare const FXLModules: ModuleName[];
|
|
4
4
|
export declare const ReflowableModules: ModuleName[];
|
|
5
5
|
export declare const WebPubModules: ModuleName[];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Comms } from "../comms/comms";
|
|
2
2
|
import { Module } from "./Module";
|
|
3
3
|
import { ReadiumWindow } from "../helpers/dom";
|
|
4
|
+
import { BulkCopyProtectionOptions } from "../protection/BulkCopyProtector";
|
|
5
|
+
import { SelectionAnalyzerOptions } from "../protection/SelectionAnalyzer";
|
|
6
|
+
import { SuspiciousActivityType } from "../comms";
|
|
7
|
+
import { KeyboardPeripheralEvent } from "../protection/KeyCombinationManager";
|
|
8
|
+
import { SuspiciousScrollingEvent } from "./snapper/ScrollSnapper";
|
|
9
|
+
import { SuspiciousSnappingEvent } from "./snapper/ColumnSnapper";
|
|
4
10
|
export interface FrameClickEvent {
|
|
5
11
|
defaultPrevented: boolean;
|
|
6
12
|
doNotDisturb: boolean;
|
|
@@ -19,21 +25,115 @@ export interface BasicTextSelection {
|
|
|
19
25
|
height: number;
|
|
20
26
|
targetFrameSrc: string;
|
|
21
27
|
}
|
|
28
|
+
export interface BaseSuspiciousActivityEvent {
|
|
29
|
+
type: SuspiciousActivityType;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
targetFrameSrc: string;
|
|
32
|
+
}
|
|
33
|
+
export interface KeyboardEventData {
|
|
34
|
+
key: string;
|
|
35
|
+
code: string;
|
|
36
|
+
keyCode: number;
|
|
37
|
+
ctrlKey: boolean;
|
|
38
|
+
altKey: boolean;
|
|
39
|
+
shiftKey: boolean;
|
|
40
|
+
metaKey: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface BaseKeyboardPeripheralEvent {
|
|
43
|
+
type: string;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
targetFrameSrc: string;
|
|
46
|
+
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
47
|
+
}
|
|
48
|
+
export interface DeveloperToolsEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
49
|
+
type: "developer_tools";
|
|
50
|
+
}
|
|
51
|
+
export interface SelectAllEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
52
|
+
type: "select_all";
|
|
53
|
+
}
|
|
54
|
+
export interface PrintEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
55
|
+
type: "print";
|
|
56
|
+
}
|
|
57
|
+
export interface SaveEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
58
|
+
type: "save";
|
|
59
|
+
}
|
|
60
|
+
export interface BulkCopyEvent extends BaseSuspiciousActivityEvent {
|
|
61
|
+
type: "bulk_copy";
|
|
62
|
+
clipboardTypes: readonly string[];
|
|
63
|
+
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
64
|
+
selectionLength?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface SuspiciousSelectionEvent extends BaseSuspiciousActivityEvent {
|
|
67
|
+
type: "suspicious_selection";
|
|
68
|
+
selectionLength: number;
|
|
69
|
+
selectedText: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
70
|
+
eventType: string;
|
|
71
|
+
}
|
|
72
|
+
export interface DragDetectedEvent extends BaseSuspiciousActivityEvent {
|
|
73
|
+
type: "drag_detected";
|
|
74
|
+
dataTransferTypes: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
export interface DropDetectedEvent extends BaseSuspiciousActivityEvent {
|
|
77
|
+
type: "drop_detected";
|
|
78
|
+
dataTransferTypes: readonly string[];
|
|
79
|
+
fileCount: number;
|
|
80
|
+
}
|
|
81
|
+
export interface ContextMenuEvent extends Omit<BaseSuspiciousActivityEvent, "type"> {
|
|
82
|
+
clientX: number;
|
|
83
|
+
clientY: number;
|
|
84
|
+
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
85
|
+
}
|
|
86
|
+
export type SuspiciousActivityEvent = DeveloperToolsEvent | SelectAllEvent | BulkCopyEvent | SuspiciousSelectionEvent | DragDetectedEvent | DropDetectedEvent | PrintEvent | SaveEvent | SuspiciousScrollingEvent | SuspiciousSnappingEvent | KeyboardPeripheralEvent;
|
|
87
|
+
export interface ContentProtectionConfig {
|
|
88
|
+
monitorSelection?: boolean | SelectionAnalyzerOptions;
|
|
89
|
+
protectCopy?: boolean | Omit<BulkCopyProtectionOptions, "enabled">;
|
|
90
|
+
disableContextMenu?: boolean;
|
|
91
|
+
disableDragAndDrop?: boolean;
|
|
92
|
+
disableSelectAll?: boolean;
|
|
93
|
+
disableSave?: boolean;
|
|
94
|
+
monitorDevTools?: boolean;
|
|
95
|
+
monitorScrollingExperimental?: boolean;
|
|
96
|
+
}
|
|
22
97
|
export declare class Peripherals extends Module {
|
|
23
98
|
static readonly moduleName = "peripherals";
|
|
24
99
|
private wnd;
|
|
25
100
|
private comms;
|
|
101
|
+
private configApplied;
|
|
102
|
+
private cleanupCallbacks;
|
|
26
103
|
private pointerMoved;
|
|
104
|
+
private isContextMenuEnabled;
|
|
105
|
+
private isDragAndDropEnabled;
|
|
106
|
+
private isSelectionMonitoringEnabled;
|
|
107
|
+
private isBulkCopyProtectionEnabled;
|
|
108
|
+
private selectionAnalyzer;
|
|
109
|
+
private currentSelection;
|
|
110
|
+
private bulkCopyProtector;
|
|
111
|
+
private keyManager;
|
|
112
|
+
private addContextMenuPrevention;
|
|
113
|
+
private removeContextMenuPrevention;
|
|
114
|
+
private addDragAndDropPrevention;
|
|
115
|
+
private removeDragAndDropPrevention;
|
|
116
|
+
private keyDownHandler;
|
|
117
|
+
private enableKeyboardPeripherals;
|
|
118
|
+
private disableKeyboardPeripherals;
|
|
119
|
+
private addBulkCopyProtection;
|
|
120
|
+
private removeBulkCopyProtection;
|
|
121
|
+
private preventBulkCopy;
|
|
122
|
+
private handleSelection;
|
|
123
|
+
private addSelectionMonitoring;
|
|
124
|
+
private removeSelectionMonitoring;
|
|
125
|
+
private onDragStart;
|
|
126
|
+
private onDrop;
|
|
127
|
+
private onContext;
|
|
27
128
|
onPointUp(event: PointerEvent): void;
|
|
28
129
|
private readonly onPointerUp;
|
|
29
130
|
onPointMove(event: PointerEvent): void;
|
|
30
131
|
private readonly onPointerMove;
|
|
31
132
|
onPointDown(): void;
|
|
32
133
|
private readonly onPointerDown;
|
|
33
|
-
onContext(event: MouseEvent): void;
|
|
34
|
-
private readonly onContextMenu;
|
|
35
134
|
onClick(event: MouseEvent): void;
|
|
36
135
|
private readonly onClicker;
|
|
136
|
+
private registerProtectionHandlers;
|
|
37
137
|
mount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
38
138
|
unmount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
39
139
|
}
|
|
@@ -2,13 +2,21 @@ import { Comms } from "../../comms";
|
|
|
2
2
|
import { Snapper } from "./Snapper";
|
|
3
3
|
import { ModuleName } from "../ModuleLibrary";
|
|
4
4
|
import { ReadiumWindow } from "../../helpers/dom";
|
|
5
|
+
import { BaseSuspiciousActivityEvent } from "../Peripherals";
|
|
6
|
+
export interface SuspiciousSnappingEvent extends BaseSuspiciousActivityEvent {
|
|
7
|
+
type: "suspicious_snapping";
|
|
8
|
+
event: null;
|
|
9
|
+
}
|
|
5
10
|
/**
|
|
6
11
|
* A {Snapper} for reflowable resources using a column-based layout
|
|
7
12
|
*/
|
|
8
13
|
export declare class ColumnSnapper extends Snapper {
|
|
9
14
|
static readonly moduleName: ModuleName;
|
|
15
|
+
private isSnapProtectionEnabled;
|
|
10
16
|
private resizeObserver;
|
|
11
17
|
private mutationObserver;
|
|
18
|
+
private patternAnalyzer;
|
|
19
|
+
private lastTurnTime;
|
|
12
20
|
private wnd;
|
|
13
21
|
private comms;
|
|
14
22
|
private doc;
|
|
@@ -36,6 +44,8 @@ export declare class ColumnSnapper extends Snapper {
|
|
|
36
44
|
private readonly onWidthChanger;
|
|
37
45
|
onTouchMove(e: TouchEvent): void;
|
|
38
46
|
private readonly onTouchMover;
|
|
47
|
+
private enableSnapProtection;
|
|
48
|
+
private checkSuspiciousSnap;
|
|
39
49
|
mount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
40
50
|
unmount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
41
51
|
}
|
|
@@ -2,11 +2,23 @@ import { Comms } from "../../comms";
|
|
|
2
2
|
import { ReadiumWindow } from "../../helpers/dom";
|
|
3
3
|
import { ModuleName } from "../ModuleLibrary";
|
|
4
4
|
import { Snapper } from "./Snapper";
|
|
5
|
+
import { BaseSuspiciousActivityEvent } from "../Peripherals";
|
|
6
|
+
export interface SuspiciousScrollingEvent extends BaseSuspiciousActivityEvent {
|
|
7
|
+
type: "suspicious_scrolling";
|
|
8
|
+
scrollDelta: number;
|
|
9
|
+
scrollDirection: "up" | "down";
|
|
10
|
+
targetElement: {
|
|
11
|
+
tagName: string;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
5
14
|
export declare class ScrollSnapper extends Snapper {
|
|
6
15
|
static readonly moduleName: ModuleName;
|
|
7
16
|
private wnd;
|
|
8
17
|
private comms;
|
|
9
18
|
private resizeObserver;
|
|
19
|
+
private patternAnalyzer;
|
|
20
|
+
private lastScrollTime;
|
|
21
|
+
private isScrollProtectionEnabled;
|
|
10
22
|
private initialScrollHandled;
|
|
11
23
|
private isScrolling;
|
|
12
24
|
private lastScrollTop;
|
|
@@ -15,6 +27,7 @@ export declare class ScrollSnapper extends Snapper {
|
|
|
15
27
|
private doc;
|
|
16
28
|
private reportProgress;
|
|
17
29
|
private handleScroll;
|
|
30
|
+
private enableScrollProtection;
|
|
18
31
|
mount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
19
32
|
unmount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
20
33
|
}
|
|
@@ -2,11 +2,16 @@ import { Comms } from "../../comms/comms";
|
|
|
2
2
|
import { ReadiumWindow } from "../../helpers/dom";
|
|
3
3
|
import { ModuleName } from "../ModuleLibrary";
|
|
4
4
|
import { Snapper } from "./Snapper";
|
|
5
|
+
import { SuspiciousScrollingEvent } from "./ScrollSnapper";
|
|
6
|
+
export type { SuspiciousScrollingEvent };
|
|
5
7
|
export declare class WebPubSnapper extends Snapper {
|
|
6
8
|
static readonly moduleName: ModuleName;
|
|
7
9
|
private wnd;
|
|
8
10
|
private comms;
|
|
9
11
|
private resizeObserver;
|
|
12
|
+
private patternAnalyzer;
|
|
13
|
+
private lastScrollTime;
|
|
14
|
+
private isScrollProtectionEnabled;
|
|
10
15
|
private initialScrollHandled;
|
|
11
16
|
private isScrolling;
|
|
12
17
|
private lastScrollTop;
|
|
@@ -15,6 +20,7 @@ export declare class WebPubSnapper extends Snapper {
|
|
|
15
20
|
private doc;
|
|
16
21
|
private reportProgress;
|
|
17
22
|
private handleScroll;
|
|
23
|
+
private enableScrollProtection;
|
|
18
24
|
mount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
19
25
|
unmount(wnd: ReadiumWindow, comms: Comms): boolean;
|
|
20
26
|
}
|