@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.
Files changed (118) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +357 -0
  3. package/dist/assess.d.ts +16 -0
  4. package/dist/assess.js +220 -0
  5. package/dist/config/default-extensions-config.json +103 -0
  6. package/dist/core/ContentProtector.d.ts +63 -0
  7. package/dist/core/ContentProtector.js +281 -0
  8. package/dist/core/index.d.ts +1 -0
  9. package/dist/core/index.js +2 -0
  10. package/dist/core/mediator/ContentProtectionMediator.d.ts +86 -0
  11. package/dist/core/mediator/ContentProtectionMediator.js +238 -0
  12. package/dist/core/mediator/eventDataTypes.d.ts +112 -0
  13. package/dist/core/mediator/eventDataTypes.js +23 -0
  14. package/dist/core/mediator/handlers/abstractEventHandler.d.ts +41 -0
  15. package/dist/core/mediator/handlers/abstractEventHandler.js +59 -0
  16. package/dist/core/mediator/handlers/devToolsEventHandler.d.ts +9 -0
  17. package/dist/core/mediator/handlers/devToolsEventHandler.js +95 -0
  18. package/dist/core/mediator/handlers/eventHandlerRegistry.d.ts +9 -0
  19. package/dist/core/mediator/handlers/eventHandlerRegistry.js +34 -0
  20. package/dist/core/mediator/handlers/extensionEventHandlers.d.ts +40 -0
  21. package/dist/core/mediator/handlers/extensionEventHandlers.js +140 -0
  22. package/dist/core/mediator/handlers/iFrameEventHandlers.d.ts +27 -0
  23. package/dist/core/mediator/handlers/iFrameEventHandlers.js +93 -0
  24. package/dist/core/mediator/handlers/screenShotEventHandlers.d.ts +34 -0
  25. package/dist/core/mediator/handlers/screenShotEventHandlers.js +111 -0
  26. package/dist/core/mediator/protection-event.d.ts +77 -0
  27. package/dist/core/mediator/protection-event.js +32 -0
  28. package/dist/core/mediator/types.d.ts +105 -0
  29. package/dist/core/mediator/types.js +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +7 -0
  32. package/dist/otel.d.ts +24 -0
  33. package/dist/otel.js +83 -0
  34. package/dist/policy.d.ts +98 -0
  35. package/dist/policy.js +97 -0
  36. package/dist/strategies/AbstractStrategy.d.ts +124 -0
  37. package/dist/strategies/AbstractStrategy.js +256 -0
  38. package/dist/strategies/ClipboardStrategy.d.ts +67 -0
  39. package/dist/strategies/ClipboardStrategy.js +291 -0
  40. package/dist/strategies/ContextMenuStrategy.d.ts +60 -0
  41. package/dist/strategies/ContextMenuStrategy.js +454 -0
  42. package/dist/strategies/DevToolsStrategy.d.ts +55 -0
  43. package/dist/strategies/DevToolsStrategy.js +314 -0
  44. package/dist/strategies/ExtensionStrategy.d.ts +66 -0
  45. package/dist/strategies/ExtensionStrategy.js +486 -0
  46. package/dist/strategies/IFrameStrategy.d.ts +49 -0
  47. package/dist/strategies/IFrameStrategy.js +255 -0
  48. package/dist/strategies/KeyboardStrategy.d.ts +35 -0
  49. package/dist/strategies/KeyboardStrategy.js +130 -0
  50. package/dist/strategies/PrintStrategy.d.ts +47 -0
  51. package/dist/strategies/PrintStrategy.js +201 -0
  52. package/dist/strategies/ScreenshotStrategy.d.ts +90 -0
  53. package/dist/strategies/ScreenshotStrategy.js +502 -0
  54. package/dist/strategies/SelectionStrategy.d.ts +49 -0
  55. package/dist/strategies/SelectionStrategy.js +216 -0
  56. package/dist/strategies/WatermarkStrategy.d.ts +56 -0
  57. package/dist/strategies/WatermarkStrategy.js +287 -0
  58. package/dist/strategies/index.d.ts +10 -0
  59. package/dist/strategies/index.js +11 -0
  60. package/dist/types/assessment.d.ts +62 -0
  61. package/dist/types/assessment.js +1 -0
  62. package/dist/types/index.d.ts +278 -0
  63. package/dist/types/index.js +17 -0
  64. package/dist/utils/DOMObserver.d.ts +68 -0
  65. package/dist/utils/DOMObserver.js +134 -0
  66. package/dist/utils/base/LoggableComponent.d.ts +44 -0
  67. package/dist/utils/base/LoggableComponent.js +56 -0
  68. package/dist/utils/detectors/AbstractDevToolsDetector.d.ts +98 -0
  69. package/dist/utils/detectors/AbstractDevToolsDetector.js +127 -0
  70. package/dist/utils/detectors/dateToStringDetector.d.ts +43 -0
  71. package/dist/utils/detectors/dateToStringDetector.js +96 -0
  72. package/dist/utils/detectors/debugLibDetector.d.ts +64 -0
  73. package/dist/utils/detectors/debugLibDetector.js +195 -0
  74. package/dist/utils/detectors/debuggerDetector.d.ts +51 -0
  75. package/dist/utils/detectors/debuggerDetector.js +211 -0
  76. package/dist/utils/detectors/defineGetterDetector.d.ts +48 -0
  77. package/dist/utils/detectors/defineGetterDetector.js +150 -0
  78. package/dist/utils/detectors/detectorInterface.d.ts +36 -0
  79. package/dist/utils/detectors/detectorInterface.js +1 -0
  80. package/dist/utils/detectors/devToolsDetectorManager.d.ts +88 -0
  81. package/dist/utils/detectors/devToolsDetectorManager.js +243 -0
  82. package/dist/utils/detectors/funcToStringDetector.d.ts +43 -0
  83. package/dist/utils/detectors/funcToStringDetector.js +90 -0
  84. package/dist/utils/detectors/regToStringDetector.d.ts +43 -0
  85. package/dist/utils/detectors/regToStringDetector.js +129 -0
  86. package/dist/utils/detectors/sizeDetector.d.ts +54 -0
  87. package/dist/utils/detectors/sizeDetector.js +134 -0
  88. package/dist/utils/detectors/timingDetector.d.ts +55 -0
  89. package/dist/utils/detectors/timingDetector.js +143 -0
  90. package/dist/utils/dom.d.ts +20 -0
  91. package/dist/utils/dom.js +83 -0
  92. package/dist/utils/environment.d.ts +29 -0
  93. package/dist/utils/environment.js +267 -0
  94. package/dist/utils/eventManager.d.ts +162 -0
  95. package/dist/utils/eventManager.js +548 -0
  96. package/dist/utils/index.d.ts +2 -0
  97. package/dist/utils/index.js +3 -0
  98. package/dist/utils/intervalManager.d.ts +91 -0
  99. package/dist/utils/intervalManager.js +221 -0
  100. package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.d.ts +41 -0
  101. package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.js +135 -0
  102. package/dist/utils/keyboardShortcutManager/keyboardShortcuts.d.ts +18 -0
  103. package/dist/utils/keyboardShortcutManager/keyboardShortcuts.js +195 -0
  104. package/dist/utils/logging/simple/Loggable.d.ts +33 -0
  105. package/dist/utils/logging/simple/Loggable.js +1 -0
  106. package/dist/utils/logging/simple/LoggingDelegate.d.ts +42 -0
  107. package/dist/utils/logging/simple/LoggingDelegate.js +53 -0
  108. package/dist/utils/logging/simple/SimpleLoggingService.d.ts +39 -0
  109. package/dist/utils/logging/simple/SimpleLoggingService.js +58 -0
  110. package/dist/utils/orientation.d.ts +15 -0
  111. package/dist/utils/orientation.js +32 -0
  112. package/dist/utils/protectedContentManager.d.ts +155 -0
  113. package/dist/utils/protectedContentManager.js +424 -0
  114. package/dist/utils/securityOverlayManager.d.ts +253 -0
  115. package/dist/utils/securityOverlayManager.js +786 -0
  116. package/dist/utils/timeoutManager.d.ts +50 -0
  117. package/dist/utils/timeoutManager.js +113 -0
  118. 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
+ }
@@ -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
+ }