@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
package/dist/otel.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ContentProtector } from '@/core/index.js';
|
|
2
|
+
function emit(emitter, name, attrs) {
|
|
3
|
+
try {
|
|
4
|
+
emitter(name, attrs);
|
|
5
|
+
}
|
|
6
|
+
catch { /* never let telemetry crash the app */ }
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a ContentProtector with all callbacks wired to the provided SpanEmitter.
|
|
10
|
+
* Each security event fires a call to emitter(), which should create and immediately
|
|
11
|
+
* end a child span — so events are exported to Tempo without waiting for the
|
|
12
|
+
* long-lived navigation span to close.
|
|
13
|
+
*
|
|
14
|
+
* Any existing customHandlers in options are preserved and called after the emit.
|
|
15
|
+
*/
|
|
16
|
+
export function attachShieldToSpan(options, emitter) {
|
|
17
|
+
const existing = options.customHandlers ?? {};
|
|
18
|
+
const handlers = {
|
|
19
|
+
...existing,
|
|
20
|
+
onDevToolsOpen(isOpen) {
|
|
21
|
+
emit(emitter, isOpen ? 'shield.devtools.opened' : 'shield.devtools.closed');
|
|
22
|
+
existing.onDevToolsOpen?.(isOpen);
|
|
23
|
+
},
|
|
24
|
+
onSelectionAttempt(event) {
|
|
25
|
+
emit(emitter, 'shield.selection.attempted');
|
|
26
|
+
existing.onSelectionAttempt?.(event);
|
|
27
|
+
},
|
|
28
|
+
onContextMenuAttempt(event) {
|
|
29
|
+
emit(emitter, 'shield.context_menu.attempted');
|
|
30
|
+
existing.onContextMenuAttempt?.(event);
|
|
31
|
+
},
|
|
32
|
+
onPrintAttempt(event) {
|
|
33
|
+
emit(emitter, 'shield.print.attempted');
|
|
34
|
+
existing.onPrintAttempt?.(event);
|
|
35
|
+
},
|
|
36
|
+
onKeyboardShortcutBlocked(event) {
|
|
37
|
+
emit(emitter, 'shield.keyboard_shortcut.blocked', {
|
|
38
|
+
'shield.keyboard.key': event.key,
|
|
39
|
+
'shield.keyboard.code': event.code,
|
|
40
|
+
});
|
|
41
|
+
existing.onKeyboardShortcutBlocked?.(event);
|
|
42
|
+
},
|
|
43
|
+
onClipboardAttempt(event, action) {
|
|
44
|
+
emit(emitter, `shield.clipboard.${action}`);
|
|
45
|
+
existing.onClipboardAttempt?.(event, action);
|
|
46
|
+
},
|
|
47
|
+
onScreenshotAttempt(event) {
|
|
48
|
+
emit(emitter, 'shield.screenshot.attempted');
|
|
49
|
+
existing.onScreenshotAttempt?.(event);
|
|
50
|
+
},
|
|
51
|
+
onExtensionDetected(id, name, risk) {
|
|
52
|
+
emit(emitter, 'shield.extension.detected', {
|
|
53
|
+
'shield.extension.id': id,
|
|
54
|
+
'shield.extension.name': name,
|
|
55
|
+
'shield.extension.risk': risk,
|
|
56
|
+
});
|
|
57
|
+
existing.onExtensionDetected?.(id, name, risk);
|
|
58
|
+
},
|
|
59
|
+
onFrameEmbeddingDetected(isEmbedded, isExternal) {
|
|
60
|
+
if (isEmbedded) {
|
|
61
|
+
emit(emitter, 'shield.frame.embedding.detected', {
|
|
62
|
+
'shield.frame.external': isExternal,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
existing.onFrameEmbeddingDetected?.(isEmbedded, isExternal);
|
|
66
|
+
},
|
|
67
|
+
onProtectionBypassed(method, event) {
|
|
68
|
+
emit(emitter, 'shield.protection.bypassed', {
|
|
69
|
+
'shield.bypass.method': method,
|
|
70
|
+
});
|
|
71
|
+
existing.onProtectionBypassed?.(method, event);
|
|
72
|
+
},
|
|
73
|
+
onContentHidden(reason, target) {
|
|
74
|
+
emit(emitter, 'shield.content.hidden', { 'shield.hidden.reason': reason });
|
|
75
|
+
existing.onContentHidden?.(reason, target);
|
|
76
|
+
},
|
|
77
|
+
onContentRestored(target) {
|
|
78
|
+
emit(emitter, 'shield.content.restored');
|
|
79
|
+
existing.onContentRestored?.(target);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return new ContentProtector({ ...options, customHandlers: handlers });
|
|
83
|
+
}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ContentProtector } from './core/index.js';
|
|
2
|
+
import type { AssessOptions, ShieldAssessment, ShieldSignals } from './types/assessment.js';
|
|
3
|
+
import type { CustomEventHandlers, WatermarkOptions } from './types/index.js';
|
|
4
|
+
import type { SpanEmitter } from './otel.js';
|
|
5
|
+
/**
|
|
6
|
+
* Boolean strategy keys available in ContentProtectionOptions.
|
|
7
|
+
* Used to declare which strategies a PolicyRule should activate.
|
|
8
|
+
*/
|
|
9
|
+
export type StrategyKey = 'preventSelection' | 'preventContextMenu' | 'preventKeyboardShortcuts' | 'preventPrinting' | 'preventScreenshots' | 'enableWatermark' | 'preventDevTools' | 'preventClipboard' | 'preventEmbedding' | 'preventExtensions';
|
|
10
|
+
/**
|
|
11
|
+
* Conditions that must all be satisfied for a PolicyRule to trigger.
|
|
12
|
+
* An empty condition always matches.
|
|
13
|
+
*/
|
|
14
|
+
export interface PolicyCondition {
|
|
15
|
+
/** Trigger when the assess() risk score is within the given range. */
|
|
16
|
+
riskScore?: {
|
|
17
|
+
/** Score must be >= this value. */
|
|
18
|
+
gte?: number;
|
|
19
|
+
/** Score must be < this value. */
|
|
20
|
+
lt?: number;
|
|
21
|
+
};
|
|
22
|
+
/** Trigger when all listed signal values match the assessment result. */
|
|
23
|
+
signals?: Partial<Record<keyof ShieldSignals, boolean>>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A single policy rule: activate a set of strategies when a condition is met.
|
|
27
|
+
*/
|
|
28
|
+
export interface PolicyRule {
|
|
29
|
+
when: PolicyCondition;
|
|
30
|
+
/** Strategy keys to activate when the condition matches. */
|
|
31
|
+
enable: StrategyKey[];
|
|
32
|
+
/**
|
|
33
|
+
* Watermark options to apply when 'enableWatermark' is in `enable`.
|
|
34
|
+
* Pass a factory function to embed session-specific data (e.g. the
|
|
35
|
+
* assess() session token) into the watermark text — useful for tracing
|
|
36
|
+
* scraped content back to the session that extracted it.
|
|
37
|
+
*/
|
|
38
|
+
watermarkOptions?: WatermarkOptions | ((assessment: ShieldAssessment) => WatermarkOptions);
|
|
39
|
+
}
|
|
40
|
+
export interface PolicyEngineOptions {
|
|
41
|
+
/** Ordered list of policy rules. All matching rules are merged. */
|
|
42
|
+
policies: PolicyRule[];
|
|
43
|
+
/** Element to protect. Defaults to document.body inside ContentProtector. */
|
|
44
|
+
targetElement?: HTMLElement | null;
|
|
45
|
+
/** Custom event handlers forwarded to ContentProtector. */
|
|
46
|
+
customHandlers?: CustomEventHandlers;
|
|
47
|
+
/**
|
|
48
|
+
* OTel span emitter. When provided, policy trigger events are emitted as
|
|
49
|
+
* span events and ContentProtector callbacks are wired via attachShieldToSpan.
|
|
50
|
+
*/
|
|
51
|
+
spanEmitter?: SpanEmitter;
|
|
52
|
+
/** Options forwarded to the internal assess() call. */
|
|
53
|
+
assessOptions?: AssessOptions;
|
|
54
|
+
/**
|
|
55
|
+
* Override the assess function. Primarily useful for testing.
|
|
56
|
+
* Defaults to the built-in assess().
|
|
57
|
+
*/
|
|
58
|
+
assessFn?: (options?: AssessOptions) => Promise<ShieldAssessment>;
|
|
59
|
+
}
|
|
60
|
+
export interface PolicyResult {
|
|
61
|
+
/** The assessment that was used to evaluate policies. */
|
|
62
|
+
assessment: ShieldAssessment;
|
|
63
|
+
/**
|
|
64
|
+
* The active ContentProtector, already started via protect().
|
|
65
|
+
* null when no policy rules matched (no protection was activated).
|
|
66
|
+
*/
|
|
67
|
+
protector: ContentProtector | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs assess(), evaluates the provided policy rules against the result,
|
|
71
|
+
* and activates a ContentProtector with the union of all matched strategies.
|
|
72
|
+
*
|
|
73
|
+
* Protection is proportional to detected risk — legitimate sessions see no
|
|
74
|
+
* overhead; automation, headless browsers, and high-risk sessions trigger
|
|
75
|
+
* only the strategies that are warranted.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const { assessment, protector } = await assessAndProtect(contentEl, {
|
|
79
|
+
* policies: [
|
|
80
|
+
* // Watermark all medium-risk sessions — embed session token for traceability
|
|
81
|
+
* {
|
|
82
|
+
* when: { riskScore: { gte: 0.3 } },
|
|
83
|
+
* enable: ['enableWatermark'],
|
|
84
|
+
* watermarkOptions: (a) => ({ text: `PROTECTED-${a.risk.flags.join(',')}` }),
|
|
85
|
+
* },
|
|
86
|
+
* // Full protection for confirmed automation
|
|
87
|
+
* {
|
|
88
|
+
* when: { signals: { 'shield.automation.headless': true } },
|
|
89
|
+
* enable: ['preventSelection', 'preventClipboard', 'preventScreenshots'],
|
|
90
|
+
* },
|
|
91
|
+
* ],
|
|
92
|
+
* spanEmitter: (name, attrs) => {
|
|
93
|
+
* const span = getTracer().startSpan(name, { attributes: attrs }, getRouteContext());
|
|
94
|
+
* span.end();
|
|
95
|
+
* },
|
|
96
|
+
* });
|
|
97
|
+
*/
|
|
98
|
+
export declare function assessAndProtect(element: HTMLElement | null, options: PolicyEngineOptions): Promise<PolicyResult>;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { assess } from './assess.js';
|
|
2
|
+
import { ContentProtector } from './core/index.js';
|
|
3
|
+
import { attachShieldToSpan } from './otel.js';
|
|
4
|
+
function matchesCondition(assessment, condition) {
|
|
5
|
+
if (condition.riskScore !== undefined) {
|
|
6
|
+
const { gte, lt } = condition.riskScore;
|
|
7
|
+
if (gte !== undefined && assessment.risk.score < gte)
|
|
8
|
+
return false;
|
|
9
|
+
if (lt !== undefined && assessment.risk.score >= lt)
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (condition.signals !== undefined) {
|
|
13
|
+
for (const [key, expected] of Object.entries(condition.signals)) {
|
|
14
|
+
if (assessment.signals[key] !== expected)
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs assess(), evaluates the provided policy rules against the result,
|
|
22
|
+
* and activates a ContentProtector with the union of all matched strategies.
|
|
23
|
+
*
|
|
24
|
+
* Protection is proportional to detected risk — legitimate sessions see no
|
|
25
|
+
* overhead; automation, headless browsers, and high-risk sessions trigger
|
|
26
|
+
* only the strategies that are warranted.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const { assessment, protector } = await assessAndProtect(contentEl, {
|
|
30
|
+
* policies: [
|
|
31
|
+
* // Watermark all medium-risk sessions — embed session token for traceability
|
|
32
|
+
* {
|
|
33
|
+
* when: { riskScore: { gte: 0.3 } },
|
|
34
|
+
* enable: ['enableWatermark'],
|
|
35
|
+
* watermarkOptions: (a) => ({ text: `PROTECTED-${a.risk.flags.join(',')}` }),
|
|
36
|
+
* },
|
|
37
|
+
* // Full protection for confirmed automation
|
|
38
|
+
* {
|
|
39
|
+
* when: { signals: { 'shield.automation.headless': true } },
|
|
40
|
+
* enable: ['preventSelection', 'preventClipboard', 'preventScreenshots'],
|
|
41
|
+
* },
|
|
42
|
+
* ],
|
|
43
|
+
* spanEmitter: (name, attrs) => {
|
|
44
|
+
* const span = getTracer().startSpan(name, { attributes: attrs }, getRouteContext());
|
|
45
|
+
* span.end();
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
48
|
+
*/
|
|
49
|
+
export async function assessAndProtect(element, options) {
|
|
50
|
+
const assessment = await (options.assessFn ?? assess)(options.assessOptions);
|
|
51
|
+
const enabledStrategies = new Set();
|
|
52
|
+
let resolvedWatermarkOptions;
|
|
53
|
+
let matchedRules = 0;
|
|
54
|
+
for (const rule of options.policies) {
|
|
55
|
+
if (!matchesCondition(assessment, rule.when))
|
|
56
|
+
continue;
|
|
57
|
+
matchedRules++;
|
|
58
|
+
for (const key of rule.enable) {
|
|
59
|
+
enabledStrategies.add(key);
|
|
60
|
+
}
|
|
61
|
+
// Last matched rule's watermarkOptions wins
|
|
62
|
+
if (rule.watermarkOptions !== undefined && rule.enable.includes('enableWatermark')) {
|
|
63
|
+
resolvedWatermarkOptions = typeof rule.watermarkOptions === 'function'
|
|
64
|
+
? rule.watermarkOptions(assessment)
|
|
65
|
+
: rule.watermarkOptions;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (enabledStrategies.size === 0) {
|
|
69
|
+
options.spanEmitter?.('shield.policy.evaluated', {
|
|
70
|
+
'shield.policy.matched_rules': 0,
|
|
71
|
+
'shield.policy.risk_score': assessment.risk.score,
|
|
72
|
+
'shield.policy.protection_activated': false,
|
|
73
|
+
});
|
|
74
|
+
return { assessment, protector: null };
|
|
75
|
+
}
|
|
76
|
+
const protectionOptions = {
|
|
77
|
+
targetElement: element,
|
|
78
|
+
customHandlers: options.customHandlers,
|
|
79
|
+
};
|
|
80
|
+
for (const key of enabledStrategies) {
|
|
81
|
+
protectionOptions[key] = true;
|
|
82
|
+
}
|
|
83
|
+
if (resolvedWatermarkOptions !== undefined) {
|
|
84
|
+
protectionOptions.watermarkOptions = resolvedWatermarkOptions;
|
|
85
|
+
}
|
|
86
|
+
const protector = options.spanEmitter
|
|
87
|
+
? attachShieldToSpan(protectionOptions, options.spanEmitter)
|
|
88
|
+
: new ContentProtector(protectionOptions);
|
|
89
|
+
options.spanEmitter?.('shield.policy.triggered', {
|
|
90
|
+
'shield.policy.matched_rules': matchedRules,
|
|
91
|
+
'shield.policy.risk_score': assessment.risk.score,
|
|
92
|
+
'shield.policy.enabled_strategies': [...enabledStrategies].join(','),
|
|
93
|
+
'shield.policy.protection_activated': true,
|
|
94
|
+
});
|
|
95
|
+
protector.protect();
|
|
96
|
+
return { assessment, protector };
|
|
97
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ProtectionStrategy } from "../types";
|
|
2
|
+
import type { MediatorAware, ProtectionMediator } from "../core/mediator/types";
|
|
3
|
+
import { LoggableComponent } from "../utils/base/LoggableComponent";
|
|
4
|
+
/**
|
|
5
|
+
* Error types for protection strategies
|
|
6
|
+
*/
|
|
7
|
+
export declare enum StrategyErrorType {
|
|
8
|
+
INITIALIZATION = "initialization",
|
|
9
|
+
APPLICATION = "application",
|
|
10
|
+
REMOVAL = "removal",
|
|
11
|
+
EVENT_HANDLING = "event_handling",
|
|
12
|
+
OPTION_UPDATE = "option_update",
|
|
13
|
+
UNKNOWN = "unknown"
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Custom error class for protection strategies
|
|
17
|
+
*/
|
|
18
|
+
export declare class StrategyError extends Error {
|
|
19
|
+
readonly strategyName: string;
|
|
20
|
+
readonly errorType: StrategyErrorType;
|
|
21
|
+
readonly originalError?: (Error | unknown) | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Create a new StrategyError
|
|
24
|
+
* @param strategyName Name of the strategy where the error occurred
|
|
25
|
+
* @param errorType Type of error that occurred
|
|
26
|
+
* @param message Error message
|
|
27
|
+
* @param originalError Original error that was caught (if any)
|
|
28
|
+
*/
|
|
29
|
+
constructor(strategyName: string, errorType: StrategyErrorType, message: string, originalError?: (Error | unknown) | undefined);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Abstract base class for protection strategies
|
|
33
|
+
* Implements common functionality to reduce duplication
|
|
34
|
+
*/
|
|
35
|
+
export declare abstract class AbstractStrategy extends LoggableComponent implements ProtectionStrategy, MediatorAware {
|
|
36
|
+
/** Alias of `COMPONENT_NAME` retained for the strategy public API (and used as the owner key in `eventManager` registrations). */
|
|
37
|
+
readonly STRATEGY_NAME: string;
|
|
38
|
+
protected mediator: ProtectionMediator | null;
|
|
39
|
+
protected isAppliedFlag: boolean;
|
|
40
|
+
protected eventIds: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Create a new strategy
|
|
43
|
+
* @param strategyName Unique name for the strategy
|
|
44
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
45
|
+
*/
|
|
46
|
+
constructor(strategyName: string, debugMode?: boolean);
|
|
47
|
+
/**
|
|
48
|
+
* Set the mediator
|
|
49
|
+
* to communicate with the other components
|
|
50
|
+
*/
|
|
51
|
+
setMediator(mediator: ProtectionMediator): void;
|
|
52
|
+
/**
|
|
53
|
+
* Apply the protection strategy
|
|
54
|
+
* Must be implemented by subclasses
|
|
55
|
+
*/
|
|
56
|
+
abstract apply(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Remove the protection strategy
|
|
59
|
+
* Can be overridden by subclasses for custom cleanup
|
|
60
|
+
*/
|
|
61
|
+
remove(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Check if the strategy is currently applied
|
|
64
|
+
*/
|
|
65
|
+
isApplied(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Update strategy options
|
|
68
|
+
* Should be implemented by subclasses that support options
|
|
69
|
+
*/
|
|
70
|
+
updateOptions(options: Record<string, unknown>): void;
|
|
71
|
+
/**
|
|
72
|
+
* Handle an error that occurred in the strategy
|
|
73
|
+
* @param errorType Type of error
|
|
74
|
+
* @param message Error message
|
|
75
|
+
* @param originalError Original error that was caught
|
|
76
|
+
*/
|
|
77
|
+
protected handleError(errorType: StrategyErrorType, message: string, originalError?: unknown): void;
|
|
78
|
+
/**
|
|
79
|
+
* Execute a function with error handling
|
|
80
|
+
* @param operation Name of the operation for error reporting
|
|
81
|
+
* @param errorType Type of error for categorization
|
|
82
|
+
* @param fn Function to execute
|
|
83
|
+
* @returns The result of the function or undefined if an error occurred
|
|
84
|
+
*/
|
|
85
|
+
protected safeExecute<T>(operation: string, errorType: StrategyErrorType, fn: () => T): T | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Execute an async function with error handling
|
|
88
|
+
* @param operation Name of the operation for error reporting
|
|
89
|
+
* @param errorType Type of error for categorization
|
|
90
|
+
* @param fn Async function to execute
|
|
91
|
+
* @returns Promise resolving to the result of the function or undefined if an error occurred
|
|
92
|
+
*/
|
|
93
|
+
protected safeExecuteAsync<T>(operation: string, errorType: StrategyErrorType, fn: () => Promise<T>): Promise<T | undefined>;
|
|
94
|
+
/**
|
|
95
|
+
* Register an event with the EventManager
|
|
96
|
+
* @param target The target element, document, or window
|
|
97
|
+
* @param eventType The type of event (e.g., "click", "keydown")
|
|
98
|
+
* @param handler The event handler function
|
|
99
|
+
* @param options Additional options for the event listener
|
|
100
|
+
* @returns The ID of the registered event
|
|
101
|
+
*/
|
|
102
|
+
protected registerEvent(target: EventTarget | null, eventType: string, handler: EventListener, options?: AddEventListenerOptions & {
|
|
103
|
+
priority?: number;
|
|
104
|
+
id?: string;
|
|
105
|
+
}): string;
|
|
106
|
+
/**
|
|
107
|
+
* Remove all event listeners for this strategy
|
|
108
|
+
* @returns The number of events removed
|
|
109
|
+
*/
|
|
110
|
+
protected removeEventsByOwner(): number;
|
|
111
|
+
/**
|
|
112
|
+
* Remove all event listeners for a specific target
|
|
113
|
+
* @param target The target to remove events from
|
|
114
|
+
* @returns The number of events removed
|
|
115
|
+
*/
|
|
116
|
+
protected removeAllEventsForTarget(target: EventTarget | null): number;
|
|
117
|
+
/**
|
|
118
|
+
* Remove event listeners by CSS selector
|
|
119
|
+
* @param selector CSS selector to match elements
|
|
120
|
+
* @param eventType Type of event to remove
|
|
121
|
+
* @returns The number of events removed
|
|
122
|
+
*/
|
|
123
|
+
protected removeEventsBySelector(selector: string, eventType: string): number;
|
|
124
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { eventManager } from "../utils/eventManager";
|
|
2
|
+
import { isBrowser } from "../utils/environment";
|
|
3
|
+
import { LoggableComponent } from "../utils/base/LoggableComponent";
|
|
4
|
+
/**
|
|
5
|
+
* Error types for protection strategies
|
|
6
|
+
*/
|
|
7
|
+
export var StrategyErrorType;
|
|
8
|
+
(function (StrategyErrorType) {
|
|
9
|
+
StrategyErrorType["INITIALIZATION"] = "initialization";
|
|
10
|
+
StrategyErrorType["APPLICATION"] = "application";
|
|
11
|
+
StrategyErrorType["REMOVAL"] = "removal";
|
|
12
|
+
StrategyErrorType["EVENT_HANDLING"] = "event_handling";
|
|
13
|
+
StrategyErrorType["OPTION_UPDATE"] = "option_update";
|
|
14
|
+
StrategyErrorType["UNKNOWN"] = "unknown";
|
|
15
|
+
})(StrategyErrorType || (StrategyErrorType = {}));
|
|
16
|
+
/**
|
|
17
|
+
* Custom error class for protection strategies
|
|
18
|
+
*/
|
|
19
|
+
export class StrategyError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* Create a new StrategyError
|
|
22
|
+
* @param strategyName Name of the strategy where the error occurred
|
|
23
|
+
* @param errorType Type of error that occurred
|
|
24
|
+
* @param message Error message
|
|
25
|
+
* @param originalError Original error that was caught (if any)
|
|
26
|
+
*/
|
|
27
|
+
constructor(strategyName, errorType, message, originalError) {
|
|
28
|
+
super(`[${strategyName}] ${message}${originalError instanceof Error ? `: ${originalError.message}` : ""}`);
|
|
29
|
+
this.strategyName = strategyName;
|
|
30
|
+
this.errorType = errorType;
|
|
31
|
+
this.originalError = originalError;
|
|
32
|
+
this.name = "StrategyError";
|
|
33
|
+
// Maintain the stack trace
|
|
34
|
+
if (Error.captureStackTrace) {
|
|
35
|
+
Error.captureStackTrace(this, StrategyError);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Abstract base class for protection strategies
|
|
41
|
+
* Implements common functionality to reduce duplication
|
|
42
|
+
*/
|
|
43
|
+
export class AbstractStrategy extends LoggableComponent {
|
|
44
|
+
/**
|
|
45
|
+
* Create a new strategy
|
|
46
|
+
* @param strategyName Unique name for the strategy
|
|
47
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
48
|
+
*/
|
|
49
|
+
constructor(strategyName, debugMode = false) {
|
|
50
|
+
super(strategyName, debugMode);
|
|
51
|
+
this.mediator = null;
|
|
52
|
+
this.isAppliedFlag = false;
|
|
53
|
+
this.eventIds = [];
|
|
54
|
+
this.STRATEGY_NAME = strategyName;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Set the mediator
|
|
58
|
+
* to communicate with the other components
|
|
59
|
+
*/
|
|
60
|
+
setMediator(mediator) {
|
|
61
|
+
this.mediator = mediator;
|
|
62
|
+
this.logger.log("Mediator set");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Remove the protection strategy
|
|
66
|
+
* Can be overridden by subclasses for custom cleanup
|
|
67
|
+
*/
|
|
68
|
+
remove() {
|
|
69
|
+
try {
|
|
70
|
+
if (!this.isAppliedFlag) {
|
|
71
|
+
this.logger.log("Protection not applied");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (isBrowser()) {
|
|
75
|
+
// Remove all event listeners using EventManager
|
|
76
|
+
const removedCount = this.removeEventsByOwner();
|
|
77
|
+
// Clear the event IDs array
|
|
78
|
+
this.eventIds = [];
|
|
79
|
+
this.isAppliedFlag = false;
|
|
80
|
+
this.logger.log(`Protection removed (${removedCount} events)`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.handleError(StrategyErrorType.REMOVAL, "Failed to remove protection", error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if the strategy is currently applied
|
|
89
|
+
*/
|
|
90
|
+
isApplied() {
|
|
91
|
+
return this.isAppliedFlag;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Update strategy options
|
|
95
|
+
* Should be implemented by subclasses that support options
|
|
96
|
+
*/
|
|
97
|
+
updateOptions(options) {
|
|
98
|
+
try {
|
|
99
|
+
// Default implementation just logs that the method is not implemented
|
|
100
|
+
this.logger.log("Method updateOptions not implemented", options);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.handleError(StrategyErrorType.OPTION_UPDATE, "Failed to update options", error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handle an error that occurred in the strategy
|
|
108
|
+
* @param errorType Type of error
|
|
109
|
+
* @param message Error message
|
|
110
|
+
* @param originalError Original error that was caught
|
|
111
|
+
*/
|
|
112
|
+
handleError(errorType, message, originalError) {
|
|
113
|
+
const error = new StrategyError(this.STRATEGY_NAME, errorType, message, originalError);
|
|
114
|
+
if (this.debugMode) {
|
|
115
|
+
console.error(error);
|
|
116
|
+
if (error.originalError instanceof Error && error.originalError.stack) {
|
|
117
|
+
console.error("Original stack:", error.originalError.stack);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.error(error.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Execute a function with error handling
|
|
126
|
+
* @param operation Name of the operation for error reporting
|
|
127
|
+
* @param errorType Type of error for categorization
|
|
128
|
+
* @param fn Function to execute
|
|
129
|
+
* @returns The result of the function or undefined if an error occurred
|
|
130
|
+
*/
|
|
131
|
+
safeExecute(operation, errorType, fn) {
|
|
132
|
+
try {
|
|
133
|
+
return fn();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.handleError(errorType, `Error during ${operation}`, error);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Execute an async function with error handling
|
|
142
|
+
* @param operation Name of the operation for error reporting
|
|
143
|
+
* @param errorType Type of error for categorization
|
|
144
|
+
* @param fn Async function to execute
|
|
145
|
+
* @returns Promise resolving to the result of the function or undefined if an error occurred
|
|
146
|
+
*/
|
|
147
|
+
async safeExecuteAsync(operation, errorType, fn) {
|
|
148
|
+
try {
|
|
149
|
+
return await fn();
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.handleError(errorType, `Error during ${operation}`, error);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Register an event with the EventManager
|
|
158
|
+
* @param target The target element, document, or window
|
|
159
|
+
* @param eventType The type of event (e.g., "click", "keydown")
|
|
160
|
+
* @param handler The event handler function
|
|
161
|
+
* @param options Additional options for the event listener
|
|
162
|
+
* @returns The ID of the registered event
|
|
163
|
+
*/
|
|
164
|
+
registerEvent(target, eventType, handler, options) {
|
|
165
|
+
if (!target || !isBrowser())
|
|
166
|
+
return "";
|
|
167
|
+
// Defensive check: ensure the provided target supports addEventListener
|
|
168
|
+
// This prevents runtime TypeError when non-DOM objects (Vue refs, selectors, etc.) are passed
|
|
169
|
+
// and provides a clearer StrategyError for easier debugging.
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
if (typeof target.addEventListener !== "function") {
|
|
172
|
+
this.handleError(StrategyErrorType.EVENT_HANDLING, `Invalid event target for ${eventType} - target does not implement addEventListener`, new Error("target.addEventListener is not a function"));
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Create a wrapped handler that includes error handling
|
|
177
|
+
const wrappedHandler = (event) => {
|
|
178
|
+
try {
|
|
179
|
+
return handler(event);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.handleError(StrategyErrorType.EVENT_HANDLING, `Error handling ${eventType} event`, error);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
// Pass all options including priority to the eventManager
|
|
186
|
+
const eventId = eventManager.addEventListener(target, eventType, wrappedHandler, this.STRATEGY_NAME, options);
|
|
187
|
+
if (eventId) {
|
|
188
|
+
this.eventIds.push(eventId);
|
|
189
|
+
this.logger.log(`Registered ${eventType} event (ID: ${eventId})`);
|
|
190
|
+
}
|
|
191
|
+
return eventId;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.handleError(StrategyErrorType.EVENT_HANDLING, `Failed to register ${eventType} event`, error);
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Remove all event listeners for this strategy
|
|
200
|
+
* @returns The number of events removed
|
|
201
|
+
*/
|
|
202
|
+
removeEventsByOwner() {
|
|
203
|
+
try {
|
|
204
|
+
const removedCount = eventManager.removeEventsByOwner(this.STRATEGY_NAME);
|
|
205
|
+
if (removedCount > 0) {
|
|
206
|
+
this.logger.log(`Removed ${removedCount} events by owner ID`);
|
|
207
|
+
}
|
|
208
|
+
return removedCount;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
this.handleError(StrategyErrorType.REMOVAL, "Failed to remove events by owner", error);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Remove all event listeners for a specific target
|
|
217
|
+
* @param target The target to remove events from
|
|
218
|
+
* @returns The number of events removed
|
|
219
|
+
*/
|
|
220
|
+
removeAllEventsForTarget(target) {
|
|
221
|
+
if (!target || !isBrowser())
|
|
222
|
+
return 0;
|
|
223
|
+
try {
|
|
224
|
+
const removedCount = eventManager.removeAllEventsForTarget(target);
|
|
225
|
+
if (removedCount > 0) {
|
|
226
|
+
this.logger.log(`Removed ${removedCount} events from target`);
|
|
227
|
+
}
|
|
228
|
+
return removedCount;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this.handleError(StrategyErrorType.REMOVAL, "Failed to remove events for target", error);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Remove event listeners by CSS selector
|
|
237
|
+
* @param selector CSS selector to match elements
|
|
238
|
+
* @param eventType Type of event to remove
|
|
239
|
+
* @returns The number of events removed
|
|
240
|
+
*/
|
|
241
|
+
removeEventsBySelector(selector, eventType) {
|
|
242
|
+
if (!isBrowser())
|
|
243
|
+
return 0;
|
|
244
|
+
try {
|
|
245
|
+
const removedCount = eventManager.removeEventsBySelector(selector, eventType, this.STRATEGY_NAME);
|
|
246
|
+
if (removedCount > 0) {
|
|
247
|
+
this.logger.log(`Removed ${removedCount} ${eventType} events via selector "${selector}"`);
|
|
248
|
+
}
|
|
249
|
+
return removedCount;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
this.handleError(StrategyErrorType.REMOVAL, `Failed to remove events by selector "${selector}"`, error);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|