@cognior/iap-sdk 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.

Potentially problematic release.


This version of @cognior/iap-sdk might be problematic. Click here for more details.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. package/tsup.config.ts +13 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Immediate Validation Prevention
3
+ * This script runs immediately to prevent ANY browser validation tooltips
4
+ * Must be loaded BEFORE any forms are rendered
5
+ */
6
+
7
+ // Immediate global validation suppression
8
+ (function immediateValidationPrevention() {
9
+ 'use strict';
10
+
11
+ console.debug('[DAP] Immediate validation prevention activated');
12
+
13
+ // 1. Override native validation methods
14
+ if (typeof HTMLFormElement !== 'undefined') {
15
+ const originalCheckValidity = HTMLFormElement.prototype.checkValidity;
16
+ const originalReportValidity = HTMLFormElement.prototype.reportValidity;
17
+
18
+ HTMLFormElement.prototype.checkValidity = function() {
19
+ console.debug('[DAP] Form.checkValidity() intercepted');
20
+ return true; // Always return true to prevent validation UI
21
+ };
22
+
23
+ HTMLFormElement.prototype.reportValidity = function() {
24
+ console.debug('[DAP] Form.reportValidity() intercepted');
25
+ return true; // Always return true to prevent validation UI
26
+ };
27
+ }
28
+
29
+ // 2. Override input validation methods
30
+ if (typeof HTMLInputElement !== 'undefined') {
31
+ const originalCheckValidity = HTMLInputElement.prototype.checkValidity;
32
+ const originalReportValidity = HTMLInputElement.prototype.reportValidity;
33
+
34
+ HTMLInputElement.prototype.checkValidity = function() {
35
+ console.debug('[DAP] Input.checkValidity() intercepted');
36
+ return true;
37
+ };
38
+
39
+ HTMLInputElement.prototype.reportValidity = function() {
40
+ console.debug('[DAP] Input.reportValidity() intercepted');
41
+ return true;
42
+ };
43
+ }
44
+
45
+ // 3. Immediate DOM event prevention with deduplication
46
+ let lastPreventedTarget: Element | null = null;
47
+ let lastPreventedTime = 0;
48
+
49
+ const preventValidation = (event: Event) => {
50
+ const now = Date.now();
51
+ // Prevent duplicate prevention calls within 100ms for same element
52
+ if (event.target === lastPreventedTarget && now - lastPreventedTime < 100) {
53
+ return false;
54
+ }
55
+
56
+ lastPreventedTarget = event.target as Element;
57
+ lastPreventedTime = now;
58
+
59
+ console.debug('[DAP] Validation event prevented:', event.type, event.target);
60
+ event.preventDefault();
61
+ event.stopImmediatePropagation();
62
+
63
+ // Clear any validation state
64
+ const target = event.target as HTMLInputElement;
65
+ if (target && typeof target.setCustomValidity === 'function') {
66
+ target.setCustomValidity('');
67
+ }
68
+
69
+ return false;
70
+ };
71
+
72
+ // Add listeners immediately
73
+ document.addEventListener('invalid', preventValidation, { capture: true, passive: false });
74
+ document.addEventListener('submit', (event) => {
75
+ const form = event.target as HTMLFormElement;
76
+ if (form && form.tagName === 'FORM') {
77
+ // Force disable validation
78
+ form.setAttribute('novalidate', '');
79
+ form.noValidate = true;
80
+ console.debug('[DAP] Form novalidate applied during submit');
81
+ }
82
+ }, { capture: true });
83
+
84
+ // 4. CSS to hide any validation bubbles that might slip through
85
+ const style = document.createElement('style');
86
+ style.id = 'dap-validation-override';
87
+ style.textContent = `
88
+ /* Hide all browser validation UI */
89
+ input:invalid,
90
+ input:-webkit-any(invalid),
91
+ input:-moz-ui-invalid {
92
+ box-shadow: none !important;
93
+ }
94
+
95
+ /* Hide validation pseudo-elements */
96
+ input::-webkit-validation-bubble,
97
+ input::-webkit-validation-bubble-message,
98
+ input::-webkit-validation-bubble-arrow,
99
+ input::-webkit-validation-bubble-arrow-clipper {
100
+ display: none !important;
101
+ }
102
+
103
+ /* Firefox validation hiding */
104
+ input:-moz-ui-invalid {
105
+ box-shadow: none !important;
106
+ }
107
+
108
+ /* Hide any tooltips or popups */
109
+ [role="tooltip"]:not(.dap-tooltip):not(.dap-tip-bubble) {
110
+ display: none !important;
111
+ }
112
+ `;
113
+
114
+ // Insert styles immediately
115
+ if (document.head) {
116
+ document.head.appendChild(style);
117
+ } else {
118
+ // If head doesn't exist yet, wait for it
119
+ const observer = new MutationObserver(() => {
120
+ if (document.head) {
121
+ document.head.appendChild(style);
122
+ observer.disconnect();
123
+ }
124
+ });
125
+ observer.observe(document.documentElement, { childList: true, subtree: true });
126
+ }
127
+
128
+ // 5. Process existing forms immediately
129
+ const processExistingForms = () => {
130
+ const forms = document.querySelectorAll('form');
131
+ forms.forEach(form => {
132
+ form.setAttribute('novalidate', '');
133
+ (form as HTMLFormElement).noValidate = true;
134
+ console.debug('[DAP] Existing form processed:', form);
135
+ });
136
+ };
137
+
138
+ // Process immediately if DOM is ready
139
+ if (document.readyState !== 'loading') {
140
+ processExistingForms();
141
+ } else {
142
+ document.addEventListener('DOMContentLoaded', processExistingForms);
143
+ }
144
+
145
+ // 6. Monitor for new forms
146
+ const formObserver = new MutationObserver((mutations) => {
147
+ mutations.forEach((mutation) => {
148
+ mutation.addedNodes.forEach((node) => {
149
+ if (node.nodeType === Node.ELEMENT_NODE) {
150
+ const element = node as Element;
151
+
152
+ // Check if it's a form
153
+ if (element.tagName === 'FORM') {
154
+ const form = element as HTMLFormElement;
155
+ form.setAttribute('novalidate', '');
156
+ form.noValidate = true;
157
+ console.debug('[DAP] New form processed:', form);
158
+ }
159
+
160
+ // Check for forms within the element
161
+ const forms = element.querySelectorAll?.('form');
162
+ forms?.forEach(form => {
163
+ form.setAttribute('novalidate', '');
164
+ (form as HTMLFormElement).noValidate = true;
165
+ console.debug('[DAP] Nested form processed:', form);
166
+ });
167
+ }
168
+ });
169
+ });
170
+ });
171
+
172
+ // Start observing
173
+ if (document.body) {
174
+ formObserver.observe(document.body, { childList: true, subtree: true });
175
+ } else {
176
+ document.addEventListener('DOMContentLoaded', () => {
177
+ formObserver.observe(document.body, { childList: true, subtree: true });
178
+ });
179
+ }
180
+
181
+ console.debug('[DAP] Immediate validation prevention setup complete');
182
+ })();
183
+
184
+ export {}; // Make this a module
@@ -0,0 +1,50 @@
1
+ // src/utils/normalize.ts
2
+ /**
3
+ * Normalization utilities for DAP SDK
4
+ *
5
+ * This module provides central normalization functions to ensure consistent behavior across the SDK:
6
+ * 1. Trigger normalization: Converting variations like "on hover", "hover", "onhover" to standard values
7
+ * 2. Placement normalization: Handling casing variations like "Top", "TOP", "top"
8
+ *
9
+ * These utilities ensure that regardless of how data is provided from the server,
10
+ * the SDK processes it consistently.
11
+ */
12
+
13
+ /**
14
+ * Normalizes trigger strings to standard format regardless of input format
15
+ * Examples: "on hover", "hover", "onHover" -> "hover"
16
+ */
17
+ export function normalizeTrigger(t: string | undefined): "hover" | "focus" | "click" {
18
+ if (!t) return "hover";
19
+
20
+ const s = t.trim().toLowerCase();
21
+ if (s.includes("hover")) return "hover";
22
+ if (s.includes("focus")) return "focus";
23
+ if (s.includes("click")) return "click";
24
+ if (s.includes("load")) return "hover";
25
+ return "hover";
26
+ }
27
+
28
+ /**
29
+ * Normalizes placement strings to standard format regardless of casing
30
+ * Examples: "Top", "TOP", "top" -> "top"
31
+ */
32
+ export function normalizePlacement(p: string | undefined): "top" | "bottom" | "left" | "right" | "auto" {
33
+ if (!p) return "top";
34
+
35
+ const s = p.trim().toLowerCase();
36
+ if (s.startsWith("top")) return "top";
37
+ if (s.startsWith("bottom")) return "bottom";
38
+ if (s.startsWith("left")) return "left";
39
+ if (s.startsWith("right")) return "right";
40
+ if (s === "auto") return "auto";
41
+ return "top";
42
+ }
43
+
44
+ /**
45
+ * Convert server-side trigger values to client-side normalized triggers
46
+ * For backward compatibility with existing code
47
+ */
48
+ export function toTrigger(input: string | undefined): "hover" | "focus" | "click" {
49
+ return normalizeTrigger(input);
50
+ }
@@ -0,0 +1,166 @@
1
+ // src/utils/privacyManager.ts
2
+ // Provides tools for managing user consent and privacy preferences for tracking
3
+
4
+ /**
5
+ * Privacy consent levels for tracking
6
+ */
7
+ export enum TrackingConsentLevel {
8
+ NONE = 'none', // No tracking allowed
9
+ ESSENTIAL = 'essential', // Only essential tracking (errors, basic flow usage)
10
+ FUNCTIONAL = 'functional', // Functional tracking (user interactions, completion rates)
11
+ ANALYTICS = 'analytics', // Full analytics tracking (detailed user behavior)
12
+ ALL = 'all' // All tracking including external analytics integration
13
+ }
14
+
15
+ /**
16
+ * User data categories that may be collected
17
+ */
18
+ export enum UserDataCategory {
19
+ DEVICE_INFO = 'device_info', // Browser, OS, screen size
20
+ USER_ID = 'user_id', // Anonymous user identifier
21
+ INTERACTION = 'interaction', // Clicks, form inputs, etc.
22
+ TIMING = 'timing', // Time spent on pages/steps
23
+ LOCATION = 'location', // Geographic location
24
+ CONTEXT = 'context', // Page URL, referrer
25
+ CUSTOM_ATTRIBUTES = 'custom_attributes' // Any custom attributes
26
+ }
27
+
28
+ // Storage key for privacy preferences
29
+ const PRIVACY_PREFS_KEY = 'dap_privacy_preferences';
30
+
31
+ /**
32
+ * User privacy preferences
33
+ */
34
+ interface PrivacyPreferences {
35
+ consentLevel: TrackingConsentLevel;
36
+ allowedDataCategories: UserDataCategory[];
37
+ lastUpdated: number; // timestamp
38
+ expiresAt: number; // timestamp
39
+ hasExplicitConsent: boolean;
40
+ }
41
+
42
+ // Default preferences - minimal tracking only
43
+ const DEFAULT_PREFERENCES: PrivacyPreferences = {
44
+ consentLevel: TrackingConsentLevel.ESSENTIAL,
45
+ allowedDataCategories: [
46
+ UserDataCategory.DEVICE_INFO,
47
+ UserDataCategory.USER_ID
48
+ ],
49
+ lastUpdated: Date.now(),
50
+ expiresAt: Date.now() + (180 * 24 * 60 * 60 * 1000), // 180 days
51
+ hasExplicitConsent: false
52
+ };
53
+
54
+ /**
55
+ * Get the current privacy preferences
56
+ * @returns The current privacy preferences
57
+ */
58
+ export function getPrivacyPreferences(): PrivacyPreferences {
59
+ try {
60
+ const storedPrefs = localStorage.getItem(PRIVACY_PREFS_KEY);
61
+ if (!storedPrefs) {
62
+ return DEFAULT_PREFERENCES;
63
+ }
64
+
65
+ const parsedPrefs = JSON.parse(storedPrefs);
66
+
67
+ // Check if preferences have expired
68
+ if (parsedPrefs.expiresAt < Date.now()) {
69
+ // Reset to default if expired
70
+ return DEFAULT_PREFERENCES;
71
+ }
72
+
73
+ return parsedPrefs;
74
+ } catch (error) {
75
+ console.error('[DAP] Error reading privacy preferences:', error);
76
+ return DEFAULT_PREFERENCES;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Set privacy preferences
82
+ * @param preferences The privacy preferences to set
83
+ */
84
+ export function setPrivacyPreferences(preferences: Partial<PrivacyPreferences>): void {
85
+ try {
86
+ const currentPrefs = getPrivacyPreferences();
87
+ const updatedPrefs = {
88
+ ...currentPrefs,
89
+ ...preferences,
90
+ lastUpdated: Date.now(),
91
+ hasExplicitConsent: true
92
+ };
93
+
94
+ localStorage.setItem(PRIVACY_PREFS_KEY, JSON.stringify(updatedPrefs));
95
+ } catch (error) {
96
+ console.error('[DAP] Error saving privacy preferences:', error);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if a specific data category can be collected
102
+ * @param category The data category to check
103
+ * @returns Whether the data category can be collected
104
+ */
105
+ export function canCollectDataCategory(category: UserDataCategory): boolean {
106
+ const prefs = getPrivacyPreferences();
107
+ return prefs.allowedDataCategories.includes(category);
108
+ }
109
+
110
+ /**
111
+ * Check if a specific consent level is granted
112
+ * @param level The consent level to check
113
+ * @returns Whether the consent level is granted
114
+ */
115
+ export function hasConsentLevel(level: TrackingConsentLevel): boolean {
116
+ const prefs = getPrivacyPreferences();
117
+ const levels = [
118
+ TrackingConsentLevel.NONE,
119
+ TrackingConsentLevel.ESSENTIAL,
120
+ TrackingConsentLevel.FUNCTIONAL,
121
+ TrackingConsentLevel.ANALYTICS,
122
+ TrackingConsentLevel.ALL
123
+ ];
124
+
125
+ const currentLevelIndex = levels.indexOf(prefs.consentLevel);
126
+ const requestedLevelIndex = levels.indexOf(level);
127
+
128
+ return currentLevelIndex >= requestedLevelIndex;
129
+ }
130
+
131
+ /**
132
+ * Reset privacy preferences to default
133
+ */
134
+ export function resetPrivacyPreferences(): void {
135
+ localStorage.removeItem(PRIVACY_PREFS_KEY);
136
+ }
137
+
138
+ /**
139
+ * Get a sanitized version of tracking data based on privacy preferences
140
+ * @param data The tracking data to sanitize
141
+ * @returns Sanitized tracking data
142
+ */
143
+ export function sanitizeTrackingData(data: any): any {
144
+ const prefs = getPrivacyPreferences();
145
+ const result = { ...data };
146
+
147
+ // Remove user ID if not allowed
148
+ if (!canCollectDataCategory(UserDataCategory.USER_ID) && result.clientInfo) {
149
+ result.clientInfo = { ...result.clientInfo, userId: undefined };
150
+ }
151
+
152
+ // Remove interaction data if not allowed
153
+ if (!canCollectDataCategory(UserDataCategory.INTERACTION)) {
154
+ result.elementSelector = undefined;
155
+ result.elementContext = undefined;
156
+ result.interactionDepth = undefined;
157
+ }
158
+
159
+ // Remove timing data if not allowed
160
+ if (!canCollectDataCategory(UserDataCategory.TIMING)) {
161
+ result.timeOnStep = undefined;
162
+ result.totalFlowTime = undefined;
163
+ }
164
+
165
+ return result;
166
+ }
@@ -0,0 +1,199 @@
1
+ // src/utils/ruleEvaluator.ts
2
+ // Rule evaluation engine for dynamic flow switching
3
+
4
+ import type {
5
+ ConditionRuleBlock,
6
+ RuleCondition,
7
+ ConditionOperator,
8
+ LogicalOperator,
9
+ ConditionValueType
10
+ } from '../experiences/types';
11
+
12
+ /**
13
+ * Evaluates a single condition against a given value
14
+ */
15
+ export function evaluateCondition(condition: RuleCondition, inputValue: string | number | boolean): boolean {
16
+ try {
17
+ let targetValue = inputValue;
18
+ let conditionValue = condition.value;
19
+
20
+ // Type coercion based on valueType
21
+ if (condition.valueType === 'Number') {
22
+ targetValue = typeof inputValue === 'string' ? parseFloat(inputValue) : Number(inputValue);
23
+ conditionValue = Number(condition.value);
24
+
25
+ if (isNaN(targetValue as number) || isNaN(conditionValue as number)) {
26
+ console.warn(`[DAP] Invalid number comparison: ${inputValue} vs ${condition.value}`);
27
+ return false;
28
+ }
29
+ } else if (condition.valueType === 'Boolean') {
30
+ targetValue = typeof inputValue === 'string' ?
31
+ inputValue.toLowerCase() === 'true' : Boolean(inputValue);
32
+ conditionValue = typeof condition.value === 'string' ?
33
+ condition.value.toLowerCase() === 'true' : Boolean(condition.value);
34
+ } else {
35
+ // String comparison - convert both to strings
36
+ targetValue = String(inputValue);
37
+ conditionValue = String(condition.value);
38
+ }
39
+
40
+ switch (condition.operator) {
41
+ case 'Equals':
42
+ return targetValue === conditionValue;
43
+
44
+ case 'NotEquals':
45
+ return targetValue !== conditionValue;
46
+
47
+ case 'Contains':
48
+ return String(targetValue).toLowerCase().includes(String(conditionValue).toLowerCase());
49
+
50
+ case 'NotContains':
51
+ return !String(targetValue).toLowerCase().includes(String(conditionValue).toLowerCase());
52
+
53
+ case 'GreaterThan':
54
+ if (condition.valueType === 'Number') {
55
+ return (targetValue as number) > (conditionValue as number);
56
+ }
57
+ // For strings, compare lexicographically
58
+ return String(targetValue) > String(conditionValue);
59
+
60
+ case 'LessThan':
61
+ if (condition.valueType === 'Number') {
62
+ return (targetValue as number) < (conditionValue as number);
63
+ }
64
+ // For strings, compare lexicographically
65
+ return String(targetValue) < String(conditionValue);
66
+
67
+ default:
68
+ console.warn(`[DAP] Unknown condition operator: ${condition.operator}`);
69
+ return false;
70
+ }
71
+ } catch (error) {
72
+ console.error(`[DAP] Error evaluating condition:`, error, condition);
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Evaluates a rule block with multiple conditions
79
+ */
80
+ export function evaluateRuleBlock(ruleBlock: ConditionRuleBlock, inputValue: string | number | boolean): boolean {
81
+ try {
82
+ if (!ruleBlock.conditions || ruleBlock.conditions.length === 0) {
83
+ console.warn(`[DAP] Rule block ${ruleBlock.ruleBlockId} has no conditions`);
84
+ return false;
85
+ }
86
+
87
+ const results = ruleBlock.conditions.map(condition => evaluateCondition(condition, inputValue));
88
+
89
+ if (ruleBlock.logicalOperator === 'And') {
90
+ return results.every(result => result === true);
91
+ } else if (ruleBlock.logicalOperator === 'Or') {
92
+ return results.some(result => result === true);
93
+ } else {
94
+ console.warn(`[DAP] Unknown logical operator: ${ruleBlock.logicalOperator}`);
95
+ return false;
96
+ }
97
+ } catch (error) {
98
+ console.error(`[DAP] Error evaluating rule block:`, error, ruleBlock);
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Evaluates multiple rule blocks and returns the first matching nextFlowId
105
+ */
106
+ export function evaluateRules(ruleBlocks: ConditionRuleBlock[], inputValue: string | number | boolean): string | null {
107
+ try {
108
+ for (const ruleBlock of ruleBlocks) {
109
+ if (evaluateRuleBlock(ruleBlock, inputValue)) {
110
+ console.debug(`[DAP] Rule block ${ruleBlock.ruleBlockId} matched, nextFlowId: ${ruleBlock.nextFlowId}`);
111
+ return ruleBlock.nextFlowId;
112
+ }
113
+ }
114
+
115
+ console.debug(`[DAP] No rule blocks matched for value: ${inputValue}`);
116
+ return null;
117
+ } catch (error) {
118
+ console.error(`[DAP] Error evaluating rules:`, error);
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Gets the current value from a DOM element based on its type
125
+ */
126
+ export function getElementValue(element: HTMLElement): string | number | boolean {
127
+ try {
128
+ if (element instanceof HTMLInputElement) {
129
+ switch (element.type) {
130
+ case 'checkbox':
131
+ case 'radio':
132
+ return element.checked;
133
+ case 'number':
134
+ return element.valueAsNumber || 0;
135
+ default:
136
+ return element.value;
137
+ }
138
+ } else if (element instanceof HTMLSelectElement) {
139
+ return element.value;
140
+ } else if (element instanceof HTMLTextAreaElement) {
141
+ return element.value;
142
+ } else {
143
+ // For other elements, return text content
144
+ return element.textContent || element.innerText || '';
145
+ }
146
+ } catch (error) {
147
+ console.error(`[DAP] Error getting element value:`, error);
148
+ return '';
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Creates an input event listener for rule evaluation with comprehensive error handling
154
+ */
155
+ export function createRuleEvaluationListener(
156
+ ruleBlocks: ConditionRuleBlock[],
157
+ onRuleMatch: (nextFlowId: string) => void
158
+ ): (event: Event) => void {
159
+ return (event: Event) => {
160
+ try {
161
+ const target = event.target as HTMLElement;
162
+ if (!target) {
163
+ console.warn('[DAP] Rule evaluation: No target element');
164
+ return;
165
+ }
166
+
167
+ // Validate rule blocks
168
+ if (!Array.isArray(ruleBlocks) || ruleBlocks.length === 0) {
169
+ console.warn('[DAP] Rule evaluation: No valid rule blocks');
170
+ return;
171
+ }
172
+
173
+ // Validate callback
174
+ if (typeof onRuleMatch !== 'function') {
175
+ console.error('[DAP] Rule evaluation: Invalid callback function');
176
+ return;
177
+ }
178
+
179
+ const inputValue = getElementValue(target);
180
+ console.debug(`[DAP] Evaluating rules for value:`, inputValue);
181
+
182
+ const nextFlowId = evaluateRules(ruleBlocks, inputValue);
183
+
184
+ if (nextFlowId) {
185
+ console.debug(`[DAP] Rule evaluation triggered flow transition to: ${nextFlowId}`);
186
+
187
+ try {
188
+ onRuleMatch(nextFlowId);
189
+ } catch (callbackError) {
190
+ console.error(`[DAP] Error in rule match callback:`, callbackError);
191
+ }
192
+ } else {
193
+ console.debug(`[DAP] No rules matched for value:`, inputValue);
194
+ }
195
+ } catch (error) {
196
+ console.error(`[DAP] Error in rule evaluation listener:`, error);
197
+ }
198
+ };
199
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Minimal allowlist-based HTML sanitizer.
3
+ * - Keeps only allowed tags/attributes
4
+ * - Validates href/src as http(s)
5
+ * - No dependency on dom.iterable (index loops only)
6
+ */
7
+
8
+ export function sanitizeHtml(unsafe: string): string {
9
+ const tmp = document.createElement("div");
10
+ tmp.innerHTML = unsafe || "";
11
+
12
+ const elements = tmp.querySelectorAll("*");
13
+ for (let i = 0; i < elements.length; i++) {
14
+ const el = elements[i] as HTMLElement;
15
+ const name = el.nodeName.toLowerCase();
16
+
17
+ if (!ALLOW.has(name)) {
18
+ const text = document.createTextNode(el.textContent || "");
19
+ const parent = el.parentNode;
20
+ if (parent) parent.replaceChild(text, el);
21
+ continue;
22
+ }
23
+
24
+ const attrs = el.attributes;
25
+ for (let j = attrs.length - 1; j >= 0; j--) {
26
+ const attr = attrs[j];
27
+ const an = attr.name.toLowerCase();
28
+ const av = attr.value;
29
+
30
+ if (!ATTR_ALLOW.has(an)) {
31
+ el.removeAttribute(attr.name);
32
+ continue;
33
+ }
34
+
35
+ if (an === "href" || an === "src") {
36
+ if (!isSafeHttpUrl(av)) {
37
+ el.removeAttribute(attr.name);
38
+ continue;
39
+ }
40
+ if (an === "href" && isHttpUrl(av)) {
41
+ if (!el.getAttribute("rel")) el.setAttribute("rel", "noopener noreferrer");
42
+ if (!el.getAttribute("target")) el.setAttribute("target", "_blank");
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ return tmp.innerHTML;
49
+ }
50
+
51
+ const ALLOW = new Set<string>([
52
+ "b","strong","i","em","u",
53
+ "span","p","br","ul","ol","li",
54
+ "a","code","pre","small","div",
55
+ // for DOCX previews (Mammoth)
56
+ "h1","h2","h3","h4","h5","h6",
57
+ "table","thead","tbody","tr","td","th"
58
+ ]);
59
+
60
+ const ATTR_ALLOW = new Set<string>([
61
+ "href","target","rel","class","style",
62
+ "src","alt","title","aria-label","colspan","rowspan","scope"
63
+ ]);
64
+
65
+ export function isSafeHttpUrl(u?: string | null): boolean {
66
+ if (!u) return false;
67
+ try {
68
+ const url = new URL(u, location.origin);
69
+ return url.protocol === "http:" || url.protocol === "https:";
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ function isHttpUrl(u: string): boolean {
75
+ try {
76
+ const url = new URL(u, location.origin);
77
+ return url.protocol === "http:" || url.protocol === "https:";
78
+ } catch { return false; }
79
+ }