From af534d11c051c109a6d5578ab87d31be7395f6a5 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 14:59:30 -0400 Subject: [PATCH 01/16] Test hybrid keystroke capture with batching. --- utils/common.js | 296 ++++++++++------------------------------- utils/common.js.backup | 234 +++++++++++++++++--------------- 2 files changed, 196 insertions(+), 334 deletions(-) diff --git a/utils/common.js b/utils/common.js index 36f2dd0..3b3d4f4 100644 --- a/utils/common.js +++ b/utils/common.js @@ -216,134 +216,6 @@ class APIClient { } } -/** - * Enhanced keylogger with memory management and performance optimization - */ -// class EnhancedKeyLogger { -// constructor(userId, platformId) { -// this.userId = userId; -// this.platformId = platformId; -// this.keyEvents = []; -// this.maxEvents = 10000; // Prevent memory leaks -// this.isActive = false; - -// // Bind methods to preserve context -// this.handleKeyDown = this.handleKeyDown.bind(this); -// this.handleKeyUp = this.handleKeyUp.bind(this); -// } - -// start() { -// if (this.isActive) return; - -// this.isActive = true; -// document.addEventListener('keydown', this.handleKeyDown); -// document.addEventListener('keyup', this.handleKeyUp); - -// this.createDownloadButton(); -// console.log('Keylogger started'); -// } - -// stop() { -// if (!this.isActive) return; - -// this.isActive = false; -// document.removeEventListener('keydown', this.handleKeyDown); -// document.removeEventListener('keyup', this.handleKeyUp); - -// const button = document.getElementById('keylogger-download-btn'); -// if (button) button.remove(); - -// console.log('Keylogger stopped'); -// } - -// handleKeyDown(event) { -// this.addKeyEvent('P', event.key); -// this.saveKeystrokes(); -// } - -// handleKeyUp(event) { -// this.addKeyEvent('R', event.key); -// this.saveKeystrokes(); -// } - -// addKeyEvent(type, key) { -// // Prevent memory leaks by limiting array size -// if (this.keyEvents.length >= this.maxEvents) { -// this.keyEvents = this.keyEvents.slice(-this.maxEvents / 2); // Keep last half -// console.warn('Keylogger array truncated to prevent memory issues'); -// } - -// this.keyEvents.push([type, key, Date.now()]); -// } - -// createDownloadButton() { -// const button = document.createElement('button'); -// button.id = 'keylogger-download-btn'; -// button.textContent = 'Download Keylog'; -// button.style.cssText = ` -// position: fixed; -// bottom: 10px; -// right: 10px; -// background: #333; -// color: white; -// border: none; -// padding: 10px 15px; -// border-radius: 5px; -// cursor: pointer; -// z-index: 10000; -// font-family: Arial, sans-serif; -// font-size: 12px; -// `; - -// button.onclick = () => this.downloadKeylog(); -// document.body.appendChild(button); -// } - -// downloadKeylog() { -// try { -// const platformLetters = { 0: 'f', 1: 'i', 2: 't' }; -// const platformLetter = platformLetters[this.platformId] || 'unknown'; -// const filename = `${platformLetter}_${this.userId}.csv`; - -// const header = [['Press or Release', 'Key', 'Time']]; -// const csvData = header.concat(this.keyEvents); -// const csvString = csvData.map(row => row.join(',')).join('\n'); - -// const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); - -// // Modern download approach -// const link = document.createElement('a'); -// const url = URL.createObjectURL(blob); - -// link.setAttribute('href', url); -// link.setAttribute('download', filename); -// link.style.visibility = 'hidden'; - -// document.body.appendChild(link); -// link.click(); -// document.body.removeChild(link); - -// // Clean up object URL -// URL.revokeObjectURL(url); - -// console.log(`Keylog downloaded: ${filename}`); - -// } catch (error) { -// console.error('Failed to download keylog:', error); -// FormValidator.showError('Failed to download keylog. Please try again.'); -// } -// } - -// getEventCount() { -// return this.keyEvents.length; -// } - -// clearEvents() { -// this.keyEvents = []; -// console.log('Keylog events cleared'); -// } -// } - /** * Navigation utilities with proper URL handling */ @@ -518,64 +390,27 @@ const PlatformSubmissionHandler = { keyEvents: [], startTime: null, isInitialized: false, - hasSubmitted: false, // Add this flag - - - /** - * Save keystrokes to sessionStorage - */ - saveKeystrokes() { - try { - const urlParams = this.getUrlParameters(); - const storageKey = `keystrokes_${urlParams.task_id}_${urlParams.platform_id}`; - - // Only limit if absolutely necessary - const maxKeystrokes = 50000; // Much higher limit - if (this.keyEvents.length > maxKeystrokes) { - console.error(`Critical: Keystroke limit reached (${this.keyEvents.length}). Data may be lost.`); - // For research, you might want to alert the user or auto-submit - alert('Maximum keystroke limit reached. Please submit your post.'); - return; - } - - sessionStorage.setItem(storageKey, JSON.stringify(this.keyEvents)); - } catch (e) { - if (e.name === 'QuotaExceededError') { - console.error('Storage quota exceeded! Cannot save keystrokes.'); - // For research integrity, this is critical - alert the user - alert('Storage limit reached. Please submit your post now to avoid data loss.'); - } - } - }, - - /** - * Load keystrokes from sessionStorage - */ - loadKeystrokes() { - const urlParams = this.getUrlParameters(); - const storageKey = `keystrokes_${urlParams.task_id}_${urlParams.platform_id}`; + hasSubmitted: false, + // High-performance keystroke capture + keyBuffer: null, + keyCodeMap: null, + keyCodeIndex: 0, + + initHighPerformanceCapture() { + // Pre-allocate typed arrays for maximum performance + this.keyBuffer = { + types: new Uint8Array(50000), // 1 byte per type + keys: new Uint16Array(50000), // 2 bytes per key code + timestamps: new Float64Array(50000), // 8 bytes per timestamp + index: 0 + }; - try { - const saved = sessionStorage.getItem(storageKey); - if (saved) { - this.keyEvents = JSON.parse(saved); - console.log(`Loaded ${this.keyEvents.length} saved keystrokes`); - } - } catch (e) { - console.error('Failed to load keystrokes:', e); - this.keyEvents = []; // Reset on error - } + // Create key code map + this.keyCodeMap = new Map(); + this.keyCodeIndex = 0; }, - /** - * Clear keystrokes from sessionStorage - */ - clearKeystrokes() { - const urlParams = this.getUrlParameters(); - const storageKey = `keystrokes_${urlParams.task_id}_${urlParams.platform_id}`; - sessionStorage.removeItem(storageKey); - console.log('Cleared saved keystrokes'); - }, + /** * Initialize the platform handler @@ -635,7 +470,7 @@ const PlatformSubmissionHandler = { this.startKeyLogger(urlParams); // Load any saved keystrokes - this.loadKeystrokes(); + // this.loadKeystrokes(); // If we have saved keystrokes but the textarea is empty, clear them // (user might have cleared the form before refresh) @@ -643,7 +478,7 @@ const PlatformSubmissionHandler = { if (inputEl && !inputEl.value.trim() && this.keyEvents.length > 0) { console.log('Text is empty but keystrokes exist - clearing keystrokes'); this.keyEvents = []; - this.clearKeystrokes(); + // this.clearKeystrokes(); } // Set up submit button @@ -811,46 +646,61 @@ const PlatformSubmissionHandler = { * Start keystroke logging */ startKeyLogger(urlParams) { - const onKeyDown = (e) => { - // CHANGE 1: Capture timestamp IMMEDIATELY - const timestamp = Date.now(); + // Initialize high-performance capture + this.initHighPerformanceCapture(); + + const captureEvent = (e, eventType) => { + // Capture timestamp with maximum precision + const timestamp = performance.now(); + const idx = this.keyBuffer.index; - // CHANGE 2: Push to array with the pre-captured timestamp - this.keyEvents.push(['P', this.replaceJsKey(e), timestamp]); + // Get or create key code + let keyCode = this.keyCodeMap.get(e.key); + if (keyCode === undefined) { + keyCode = this.keyCodeIndex++; + this.keyCodeMap.set(e.key, keyCode); + } - // CHANGE 3: Save keystrokes AFTER the critical timing capture - // Move this to after we've captured the timestamp - setTimeout(() => this.saveKeystrokes(), 0); + // Store in typed arrays (extremely fast) + this.keyBuffer.types[idx] = eventType; // 0 for press, 1 for release + this.keyBuffer.keys[idx] = keyCode; + this.keyBuffer.timestamps[idx] = timestamp; + this.keyBuffer.index++; - // Handle Enter key for multi-line support - if (e.key === "Enter" && e.target.id === this.config.textInputId) { - if (!e.shiftKey) { - e.preventDefault(); + // Handle Enter key without blocking + if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { + e.preventDefault(); + requestAnimationFrame(() => { const textarea = e.target; const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + 1; - - // Trigger any auto-resize if needed textarea.dispatchEvent(new Event('input')); - } + }); } }; + + document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); + document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); + }, - const onKeyUp = (e) => { - // CHANGE 1: Capture timestamp IMMEDIATELY - const timestamp = Date.now(); - - // CHANGE 2: Push to array with the pre-captured timestamp - this.keyEvents.push(['R', this.replaceJsKey(e), timestamp]); - - // CHANGE 3: Save keystrokes AFTER the critical timing capture - setTimeout(() => this.saveKeystrokes(), 0); - }; - - document.addEventListener('keydown', onKeyDown); - document.addEventListener('keyup', onKeyUp); + getKeystrokeData() { + const events = []; + const keyMap = Array.from(this.keyCodeMap.entries()); + + for (let i = 0; i < this.keyBuffer.index; i++) { + const keyEntry = keyMap.find(([_, code]) => code === this.keyBuffer.keys[i]); + if (keyEntry) { + events.push([ + this.keyBuffer.types[i] === 0 ? 'P' : 'R', + this.replaceJsKey({ key: keyEntry[0] }), + Math.round(this.keyBuffer.timestamps[i] + performance.timeOrigin) + ]); + } + } + + return events; }, /** @@ -936,7 +786,7 @@ const PlatformSubmissionHandler = { sessionStorage.setItem(submissionKey, 'true'); this.hasSubmitted = true; // Clear keystroke data - this.clearKeystrokes(); + // this.clearKeystrokes(); // Call after submit callback if provided if (this.config.onAfterSubmit) { @@ -957,6 +807,9 @@ const PlatformSubmissionHandler = { /** * Validate post content */ + /** + * Validate post content + */ validatePost(text) { // Always get fresh value from DOM const inputEl = document.getElementById(this.config.textInputId); @@ -974,7 +827,8 @@ const PlatformSubmissionHandler = { }; } - if (this.keyEvents.length === 0) { + // Check the buffer instead of keyEvents + if (!this.keyBuffer || this.keyBuffer.index === 0) { return { isValid: false, message: 'No keystrokes recorded! Please type something before submitting.' }; } @@ -1064,6 +918,9 @@ const PlatformSubmissionHandler = { * Build CSV blob from keystroke events */ buildCsvBlob() { + // Convert high-performance buffer to array format only when needed + this.keyEvents = this.getKeystrokeData(); + const heading = [['Press or Release', 'Key', 'Time']]; const csvString = heading .concat(this.keyEvents) @@ -1082,15 +939,6 @@ const PlatformSubmissionHandler = { const deviceInfoStr = sessionStorage.getItem('device_info'); const deviceInfo = deviceInfoStr ? JSON.parse(deviceInfoStr) : DeviceDetector.getDeviceInfo(); - // const metadata = { - // user_id: urlParams.user_id, - // platform_id: urlParams.platform_id, - // task_id: urlParams.task_id, - // start_time: this.startTime, - // end_time: endTime, - // duration_ms: endTime - this.startTime, - // platform: this.config.platform - // }; const metadata = { user_id: urlParams.user_id, platform_id: urlParams.platform_id, diff --git a/utils/common.js.backup b/utils/common.js.backup index 33eafde..36f2dd0 100644 --- a/utils/common.js.backup +++ b/utils/common.js.backup @@ -219,130 +219,130 @@ class APIClient { /** * Enhanced keylogger with memory management and performance optimization */ -class EnhancedKeyLogger { - constructor(userId, platformId) { - this.userId = userId; - this.platformId = platformId; - this.keyEvents = []; - this.maxEvents = 10000; // Prevent memory leaks - this.isActive = false; +// class EnhancedKeyLogger { +// constructor(userId, platformId) { +// this.userId = userId; +// this.platformId = platformId; +// this.keyEvents = []; +// this.maxEvents = 10000; // Prevent memory leaks +// this.isActive = false; - // Bind methods to preserve context - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleKeyUp = this.handleKeyUp.bind(this); - } +// // Bind methods to preserve context +// this.handleKeyDown = this.handleKeyDown.bind(this); +// this.handleKeyUp = this.handleKeyUp.bind(this); +// } - start() { - if (this.isActive) return; +// start() { +// if (this.isActive) return; - this.isActive = true; - document.addEventListener('keydown', this.handleKeyDown); - document.addEventListener('keyup', this.handleKeyUp); +// this.isActive = true; +// document.addEventListener('keydown', this.handleKeyDown); +// document.addEventListener('keyup', this.handleKeyUp); - this.createDownloadButton(); - console.log('Keylogger started'); - } +// this.createDownloadButton(); +// console.log('Keylogger started'); +// } - stop() { - if (!this.isActive) return; +// stop() { +// if (!this.isActive) return; - this.isActive = false; - document.removeEventListener('keydown', this.handleKeyDown); - document.removeEventListener('keyup', this.handleKeyUp); +// this.isActive = false; +// document.removeEventListener('keydown', this.handleKeyDown); +// document.removeEventListener('keyup', this.handleKeyUp); - const button = document.getElementById('keylogger-download-btn'); - if (button) button.remove(); +// const button = document.getElementById('keylogger-download-btn'); +// if (button) button.remove(); - console.log('Keylogger stopped'); - } - - handleKeyDown(event) { - this.addKeyEvent('P', event.key); - this.saveKeystrokes(); - } - - handleKeyUp(event) { - this.addKeyEvent('R', event.key); - this.saveKeystrokes(); - } - - addKeyEvent(type, key) { - // Prevent memory leaks by limiting array size - if (this.keyEvents.length >= this.maxEvents) { - this.keyEvents = this.keyEvents.slice(-this.maxEvents / 2); // Keep last half - console.warn('Keylogger array truncated to prevent memory issues'); - } +// console.log('Keylogger stopped'); +// } + +// handleKeyDown(event) { +// this.addKeyEvent('P', event.key); +// this.saveKeystrokes(); +// } + +// handleKeyUp(event) { +// this.addKeyEvent('R', event.key); +// this.saveKeystrokes(); +// } + +// addKeyEvent(type, key) { +// // Prevent memory leaks by limiting array size +// if (this.keyEvents.length >= this.maxEvents) { +// this.keyEvents = this.keyEvents.slice(-this.maxEvents / 2); // Keep last half +// console.warn('Keylogger array truncated to prevent memory issues'); +// } - this.keyEvents.push([type, key, Date.now()]); - } - - createDownloadButton() { - const button = document.createElement('button'); - button.id = 'keylogger-download-btn'; - button.textContent = 'Download Keylog'; - button.style.cssText = ` - position: fixed; - bottom: 10px; - right: 10px; - background: #333; - color: white; - border: none; - padding: 10px 15px; - border-radius: 5px; - cursor: pointer; - z-index: 10000; - font-family: Arial, sans-serif; - font-size: 12px; - `; +// this.keyEvents.push([type, key, Date.now()]); +// } + +// createDownloadButton() { +// const button = document.createElement('button'); +// button.id = 'keylogger-download-btn'; +// button.textContent = 'Download Keylog'; +// button.style.cssText = ` +// position: fixed; +// bottom: 10px; +// right: 10px; +// background: #333; +// color: white; +// border: none; +// padding: 10px 15px; +// border-radius: 5px; +// cursor: pointer; +// z-index: 10000; +// font-family: Arial, sans-serif; +// font-size: 12px; +// `; - button.onclick = () => this.downloadKeylog(); - document.body.appendChild(button); - } - - downloadKeylog() { - try { - const platformLetters = { 0: 'f', 1: 'i', 2: 't' }; - const platformLetter = platformLetters[this.platformId] || 'unknown'; - const filename = `${platformLetter}_${this.userId}.csv`; +// button.onclick = () => this.downloadKeylog(); +// document.body.appendChild(button); +// } + +// downloadKeylog() { +// try { +// const platformLetters = { 0: 'f', 1: 'i', 2: 't' }; +// const platformLetter = platformLetters[this.platformId] || 'unknown'; +// const filename = `${platformLetter}_${this.userId}.csv`; - const header = [['Press or Release', 'Key', 'Time']]; - const csvData = header.concat(this.keyEvents); - const csvString = csvData.map(row => row.join(',')).join('\n'); +// const header = [['Press or Release', 'Key', 'Time']]; +// const csvData = header.concat(this.keyEvents); +// const csvString = csvData.map(row => row.join(',')).join('\n'); - const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); +// const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); - // Modern download approach - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); +// // Modern download approach +// const link = document.createElement('a'); +// const url = URL.createObjectURL(blob); - link.setAttribute('href', url); - link.setAttribute('download', filename); - link.style.visibility = 'hidden'; +// link.setAttribute('href', url); +// link.setAttribute('download', filename); +// link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); - // Clean up object URL - URL.revokeObjectURL(url); +// // Clean up object URL +// URL.revokeObjectURL(url); - console.log(`Keylog downloaded: ${filename}`); +// console.log(`Keylog downloaded: ${filename}`); - } catch (error) { - console.error('Failed to download keylog:', error); - FormValidator.showError('Failed to download keylog. Please try again.'); - } - } - - getEventCount() { - return this.keyEvents.length; - } - - clearEvents() { - this.keyEvents = []; - console.log('Keylog events cleared'); - } -} +// } catch (error) { +// console.error('Failed to download keylog:', error); +// FormValidator.showError('Failed to download keylog. Please try again.'); +// } +// } + +// getEventCount() { +// return this.keyEvents.length; +// } + +// clearEvents() { +// this.keyEvents = []; +// console.log('Keylog events cleared'); +// } +// } /** * Navigation utilities with proper URL handling @@ -812,8 +812,15 @@ const PlatformSubmissionHandler = { */ startKeyLogger(urlParams) { const onKeyDown = (e) => { - - this.keyEvents.push(['P', this.replaceJsKey(e), Date.now()]); + // CHANGE 1: Capture timestamp IMMEDIATELY + const timestamp = Date.now(); + + // CHANGE 2: Push to array with the pre-captured timestamp + this.keyEvents.push(['P', this.replaceJsKey(e), timestamp]); + + // CHANGE 3: Save keystrokes AFTER the critical timing capture + // Move this to after we've captured the timestamp + setTimeout(() => this.saveKeystrokes(), 0); // Handle Enter key for multi-line support if (e.key === "Enter" && e.target.id === this.config.textInputId) { @@ -832,12 +839,19 @@ const PlatformSubmissionHandler = { }; const onKeyUp = (e) => { - this.keyEvents.push(['R', this.replaceJsKey(e), Date.now()]); + // CHANGE 1: Capture timestamp IMMEDIATELY + const timestamp = Date.now(); + + // CHANGE 2: Push to array with the pre-captured timestamp + this.keyEvents.push(['R', this.replaceJsKey(e), timestamp]); + + // CHANGE 3: Save keystrokes AFTER the critical timing capture + setTimeout(() => this.saveKeystrokes(), 0); }; document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); - }, + }, /** * Setup submit button handler From d5a221677492eef28d5167af50c23a39a5879a4f Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 15:37:51 -0400 Subject: [PATCH 02/16] Fix getKeystrokeData not handled. --- utils/common.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/utils/common.js b/utils/common.js index 3b3d4f4..4981cd3 100644 --- a/utils/common.js +++ b/utils/common.js @@ -692,9 +692,17 @@ const PlatformSubmissionHandler = { for (let i = 0; i < this.keyBuffer.index; i++) { const keyEntry = keyMap.find(([_, code]) => code === this.keyBuffer.keys[i]); if (keyEntry) { + const originalKey = keyEntry[0]; + + // Create a more complete fake event object for replaceJsKey + const fakeEvent = { + key: originalKey, + code: originalKey === ' ' ? 'Space' : `Key${originalKey.toUpperCase()}` + }; + events.push([ this.keyBuffer.types[i] === 0 ? 'P' : 'R', - this.replaceJsKey({ key: keyEntry[0] }), + this.replaceJsKey(fakeEvent), Math.round(this.keyBuffer.timestamps[i] + performance.timeOrigin) ]); } @@ -910,8 +918,14 @@ const PlatformSubmissionHandler = { 'CapsLock': 'Key.caps_lock' }; - if (e.code === 'Space') return 'Key.space'; - return keyMap[e.key] || e.key; + // Handle space key - check both e.code and e.key + if (e.code === 'Space' || e.key === ' ') return 'Key.space'; + + // Check if it's a mapped key + if (keyMap[e.key]) return keyMap[e.key]; + + // Return the original key + return e.key; }, /** From 22192524981575ee8cf9717603e696ac7640f0ea Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 15:46:52 -0400 Subject: [PATCH 03/16] Clean up common.js elete unused code and comments. --- utils/common.js | 48 ++---------------------------------------------- 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/utils/common.js b/utils/common.js index 4981cd3..ca2ccd6 100644 --- a/utils/common.js +++ b/utils/common.js @@ -320,7 +320,6 @@ if (typeof module !== 'undefined' && module.exports) { SecureCookieManager, FormValidator, APIClient, - EnhancedKeyLogger, NavigationManager, CONFIG }; @@ -469,9 +468,6 @@ const PlatformSubmissionHandler = { // Start keylogger this.startKeyLogger(urlParams); - // Load any saved keystrokes - // this.loadKeystrokes(); - // If we have saved keystrokes but the textarea is empty, clear them // (user might have cleared the form before refresh) const inputEl = document.getElementById(this.config.textInputId); @@ -513,41 +509,6 @@ const PlatformSubmissionHandler = { submitButton.textContent = "Already Submitted"; } }, - /** - * Save draft text to sessionStorage - */ - saveDraft() { - const inputEl = document.getElementById(this.config.textInputId); - if (inputEl && inputEl.value.trim()) { - const urlParams = this.getUrlParameters(); - const storageKey = `draft_${this.config.platform}_${urlParams.task_id}`; - sessionStorage.setItem(storageKey, inputEl.value); - console.log(`Saved draft for ${storageKey}`); - } - }, - - /** - * Restore draft text from sessionStorage - */ - restoreDraft() { - const urlParams = this.getUrlParameters(); - const storageKey = `draft_${this.config.platform}_${urlParams.task_id}`; - const savedText = sessionStorage.getItem(storageKey); - - if (savedText) { - const inputEl = document.getElementById(this.config.textInputId); - if (inputEl) { - inputEl.value = savedText; - console.log(`Restored draft for ${storageKey}`); - - // Trigger input event for auto-resize - inputEl.dispatchEvent(new Event('input')); - - // Show a message that draft was restored - this.showDraftRestoredMessage(); - } - } - }, /** * Setup visibility handler to handle back/forward navigation @@ -793,9 +754,7 @@ const PlatformSubmissionHandler = { const submissionKey = `submitted_${urlParams.user_id}_${urlParams.task_id}_${urlParams.platform_id}`; sessionStorage.setItem(submissionKey, 'true'); this.hasSubmitted = true; - // Clear keystroke data - // this.clearKeystrokes(); - + // Call after submit callback if provided if (this.config.onAfterSubmit) { this.config.onAfterSubmit(); @@ -815,9 +774,6 @@ const PlatformSubmissionHandler = { /** * Validate post content */ - /** - * Validate post content - */ validatePost(text) { // Always get fresh value from DOM const inputEl = document.getElementById(this.config.textInputId); @@ -835,7 +791,7 @@ const PlatformSubmissionHandler = { }; } - // Check the buffer instead of keyEvents + // Check the buffer instead of the text input if (!this.keyBuffer || this.keyBuffer.index === 0) { return { isValid: false, message: 'No keystrokes recorded! Please type something before submitting.' }; } From 855f72d42cf10ae3cc3640d5dbed50bbc2c669b0 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 16:41:16 -0400 Subject: [PATCH 04/16] Add wasm for keystorke timing accuracy. --- .gitignore | 27 ++- pages/fake_pages/Facebook-Clone/index.html | 7 + pages/fake_pages/instagram-clone/index.html | 7 + pages/fake_pages/twitter-clone/index.html | 6 + utils/common.js | 241 ++++++++++++++------ utils/wasm-keystrokeCapture.js | 104 +++++++++ wasm-keystroke-capture/Cargo.toml | 19 ++ wasm-keystroke-capture/src/lib.rs | 92 ++++++++ 8 files changed, 433 insertions(+), 70 deletions(-) create mode 100644 utils/wasm-keystrokeCapture.js create mode 100644 wasm-keystroke-capture/Cargo.toml create mode 100644 wasm-keystroke-capture/src/lib.rs diff --git a/.gitignore b/.gitignore index 552518b..1e7f907 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,27 @@ +# Node modules node_modules/ -package-lock.json -.netlify + +# Build outputs +dist/ +build/ + +# Rust/WASM build artifacts +wasm-keystroke-capture/target/ +wasm-keystroke-capture/pkg/ +wasm-keystroke-capture/Cargo.lock + +# OS files .DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ + +# Environment variables +.env +.env.local + +# Logs +*.log +npm-debug.log* diff --git a/pages/fake_pages/Facebook-Clone/index.html b/pages/fake_pages/Facebook-Clone/index.html index deea47a..925fda8 100644 --- a/pages/fake_pages/Facebook-Clone/index.html +++ b/pages/fake_pages/Facebook-Clone/index.html @@ -333,6 +333,13 @@

Conversation

+ + + diff --git a/pages/fake_pages/instagram-clone/index.html b/pages/fake_pages/instagram-clone/index.html index 9a16f61..cb55454 100644 --- a/pages/fake_pages/instagram-clone/index.html +++ b/pages/fake_pages/instagram-clone/index.html @@ -955,6 +955,13 @@

Suggestions for You

+ + + diff --git a/pages/fake_pages/twitter-clone/index.html b/pages/fake_pages/twitter-clone/index.html index fe28df0..21a671e 100644 --- a/pages/fake_pages/twitter-clone/index.html +++ b/pages/fake_pages/twitter-clone/index.html @@ -205,6 +205,12 @@

What's happening?

+ + diff --git a/utils/common.js b/utils/common.js index ca2ccd6..32a65a1 100644 --- a/utils/common.js +++ b/utils/common.js @@ -395,6 +395,9 @@ const PlatformSubmissionHandler = { keyCodeMap: null, keyCodeIndex: 0, + useWASM: false, // Will be set to true when WASM loads + wasmCapture: null, // Will hold the WASM capture instance + initHighPerformanceCapture() { // Pre-allocate typed arrays for maximum performance this.keyBuffer = { @@ -420,12 +423,27 @@ const PlatformSubmissionHandler = { * @param {function} config.onBeforeSubmit - Optional callback before submission * @param {function} config.onAfterSubmit - Optional callback after successful submission */ - init(config) { + async init(config) { // Check if already submitted for this task const urlParams = this.getUrlParameters(); const submissionKey = `submitted_${urlParams.user_id}_${urlParams.task_id}_${urlParams.platform_id}`; this.hasSubmitted = sessionStorage.getItem(submissionKey) === 'true'; + // Check if WASM is available (it will be loaded by the HTML page) + if (window.wasmKeystrokeManager) { + try { + await window.wasmKeystrokeManager.initialize(); + this.wasmCapture = window.wasmKeystrokeManager; + this.useWASM = true; + console.log('✅ Using WASM for high-performance keystroke capture'); + } catch (error) { + console.error('Failed to initialize WASM, falling back to JavaScript:', error); + this.useWASM = false; + } + } else { + console.log('WASM not available, using JavaScript keystroke capture'); + } + // Prevent accidental refresh window.addEventListener('beforeunload', (e) => { // Only show warning if there's unsaved text @@ -607,69 +625,142 @@ const PlatformSubmissionHandler = { * Start keystroke logging */ startKeyLogger(urlParams) { - // Initialize high-performance capture - this.initHighPerformanceCapture(); + if (this.useWASM && this.wasmCapture) { + // Use WASM for capture + console.log('Using WASM keystroke capture'); + + document.addEventListener('keydown', (e) => { + this.wasmCapture.captureKeyDown(e.key); + + // Handle Enter key + if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { + e.preventDefault(); + const textarea = e.target; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 1; + textarea.dispatchEvent(new Event('input')); + } + }); + + document.addEventListener('keyup', (e) => { + this.wasmCapture.captureKeyUp(e.key); + }); + } else { + // Fallback to original JavaScript implementation + console.log('Using JavaScript keystroke capture'); + + // Initialize high-performance capture + this.initHighPerformanceCapture(); + + const captureEvent = (e, eventType) => { + // Capture timestamp with maximum precision + const timestamp = performance.now(); + const idx = this.keyBuffer.index; + + // Get or create key code + let keyCode = this.keyCodeMap.get(e.key); + if (keyCode === undefined) { + keyCode = this.keyCodeIndex++; + this.keyCodeMap.set(e.key, keyCode); + } + + // Store in typed arrays (extremely fast) + this.keyBuffer.types[idx] = eventType; // 0 for press, 1 for release + this.keyBuffer.keys[idx] = keyCode; + this.keyBuffer.timestamps[idx] = timestamp; + this.keyBuffer.index++; + + // Handle Enter key without blocking + if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { + e.preventDefault(); + requestAnimationFrame(() => { + const textarea = e.target; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 1; + textarea.dispatchEvent(new Event('input')); + }); + } + }; + + document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); + document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); + } + }, + + // startKeyLogger(urlParams) { + // // Initialize high-performance capture + // this.initHighPerformanceCapture(); - const captureEvent = (e, eventType) => { - // Capture timestamp with maximum precision - const timestamp = performance.now(); - const idx = this.keyBuffer.index; + // const captureEvent = (e, eventType) => { + // // Capture timestamp with maximum precision + // const timestamp = performance.now(); + // const idx = this.keyBuffer.index; - // Get or create key code - let keyCode = this.keyCodeMap.get(e.key); - if (keyCode === undefined) { - keyCode = this.keyCodeIndex++; - this.keyCodeMap.set(e.key, keyCode); - } + // // Get or create key code + // let keyCode = this.keyCodeMap.get(e.key); + // if (keyCode === undefined) { + // keyCode = this.keyCodeIndex++; + // this.keyCodeMap.set(e.key, keyCode); + // } - // Store in typed arrays (extremely fast) - this.keyBuffer.types[idx] = eventType; // 0 for press, 1 for release - this.keyBuffer.keys[idx] = keyCode; - this.keyBuffer.timestamps[idx] = timestamp; - this.keyBuffer.index++; + // // Store in typed arrays (extremely fast) + // this.keyBuffer.types[idx] = eventType; // 0 for press, 1 for release + // this.keyBuffer.keys[idx] = keyCode; + // this.keyBuffer.timestamps[idx] = timestamp; + // this.keyBuffer.index++; - // Handle Enter key without blocking - if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { - e.preventDefault(); - requestAnimationFrame(() => { - const textarea = e.target; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); - textarea.selectionStart = textarea.selectionEnd = start + 1; - textarea.dispatchEvent(new Event('input')); - }); - } - }; + // // Handle Enter key without blocking + // if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { + // e.preventDefault(); + // requestAnimationFrame(() => { + // const textarea = e.target; + // const start = textarea.selectionStart; + // const end = textarea.selectionEnd; + // textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); + // textarea.selectionStart = textarea.selectionEnd = start + 1; + // textarea.dispatchEvent(new Event('input')); + // }); + // } + // }; - document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); - document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); - }, + // document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); + // document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); + // }, getKeystrokeData() { - const events = []; - const keyMap = Array.from(this.keyCodeMap.entries()); - - for (let i = 0; i < this.keyBuffer.index; i++) { - const keyEntry = keyMap.find(([_, code]) => code === this.keyBuffer.keys[i]); - if (keyEntry) { - const originalKey = keyEntry[0]; - - // Create a more complete fake event object for replaceJsKey - const fakeEvent = { - key: originalKey, - code: originalKey === ' ' ? 'Space' : `Key${originalKey.toUpperCase()}` - }; - - events.push([ - this.keyBuffer.types[i] === 0 ? 'P' : 'R', - this.replaceJsKey(fakeEvent), - Math.round(this.keyBuffer.timestamps[i] + performance.timeOrigin) - ]); + if (this.useWASM && this.wasmCapture) { + // Get data from WASM + return this.wasmCapture.getRawData(); + } else { + // Original implementation + const events = []; + const keyMap = Array.from(this.keyCodeMap.entries()); + + for (let i = 0; i < this.keyBuffer.index; i++) { + const keyEntry = keyMap.find(([_, code]) => code === this.keyBuffer.keys[i]); + if (keyEntry) { + const originalKey = keyEntry[0]; + + // Create a more complete fake event object for replaceJsKey + const fakeEvent = { + key: originalKey, + code: originalKey === ' ' ? 'Space' : `Key${originalKey.toUpperCase()}` + }; + + events.push([ + this.keyBuffer.types[i] === 0 ? 'P' : 'R', + this.replaceJsKey(fakeEvent), + Math.round(this.keyBuffer.timestamps[i] + performance.timeOrigin) + ]); + } + } + + return events; } - } - - return events; }, /** @@ -791,9 +882,16 @@ const PlatformSubmissionHandler = { }; } - // Check the buffer instead of the text input - if (!this.keyBuffer || this.keyBuffer.index === 0) { - return { isValid: false, message: 'No keystrokes recorded! Please type something before submitting.' }; + // Check keystroke data + if (this.useWASM && this.wasmCapture) { + if (this.wasmCapture.getEventCount() === 0) { + return { isValid: false, message: 'No keystrokes recorded! Please type something before submitting.' }; + } + } else { + // Check the buffer instead of the text input + if (!this.keyBuffer || this.keyBuffer.index === 0) { + return { isValid: false, message: 'No keystrokes recorded! Please type something before submitting.' }; + } } return { isValid: true }; @@ -888,15 +986,22 @@ const PlatformSubmissionHandler = { * Build CSV blob from keystroke events */ buildCsvBlob() { - // Convert high-performance buffer to array format only when needed - this.keyEvents = this.getKeystrokeData(); - - const heading = [['Press or Release', 'Key', 'Time']]; - const csvString = heading - .concat(this.keyEvents) - .map(row => row.join(',')) - .join('\n'); - return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); + if (this.useWASM && this.wasmCapture) { + // Get CSV directly from WASM + const csvString = this.wasmCapture.exportAsCSV(); + return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); + } else { + // Original implementation + // Convert high-performance buffer to array format only when needed + this.keyEvents = this.getKeystrokeData(); + + const heading = [['Press or Release', 'Key', 'Time']]; + const csvString = heading + .concat(this.keyEvents) + .map(row => row.join(',')) + .join('\n'); + return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); + } }, /** diff --git a/utils/wasm-keystrokeCapture.js b/utils/wasm-keystrokeCapture.js new file mode 100644 index 0000000..bf35291 --- /dev/null +++ b/utils/wasm-keystrokeCapture.js @@ -0,0 +1,104 @@ +// utils/wasm-keystroke.js +import init, { KeystrokeCapture } from '../wasm-keystroke-capture/pkg/keystroke_capture.js'; + +class WASMKeystrokeManager { + constructor() { + this.initialized = false; + this.capture = null; + this.keyMapping = { + 'Shift': 'Key.shift', + 'Control': 'Key.ctrl', + 'Alt': 'Key.alt', + 'Meta': 'Key.cmd', + 'Enter': 'Key.enter', + 'Backspace': 'Key.backspace', + 'Escape': 'Key.esc', + 'Tab': 'Key.tab', + 'ArrowLeft': 'Key.left', + 'ArrowRight': 'Key.right', + 'ArrowUp': 'Key.up', + 'ArrowDown': 'Key.down', + 'CapsLock': 'Key.caps_lock', + ' ': 'Key.space' + }; + } + + async initialize() { + if (this.initialized) return; + + try { + // Initialize WASM module + await init(); + + // Create capture instance with 50k capacity + this.capture = new KeystrokeCapture(50000); + this.initialized = true; + + console.log('✅ WASM keystroke capture initialized'); + } catch (error) { + console.error('❌ Failed to initialize WASM:', error); + throw error; + } + } + + mapKey(key) { + return this.keyMapping[key] || key; + } + + captureKeyDown(key) { + if (!this.initialized) { + console.warn('WASM not initialized'); + return; + } + + const mappedKey = this.mapKey(key); + try { + this.capture.capture_keystroke(mappedKey, false); + } catch (error) { + console.error('Failed to capture keydown:', error); + } + } + + captureKeyUp(key) { + if (!this.initialized) { + console.warn('WASM not initialized'); + return; + } + + const mappedKey = this.mapKey(key); + try { + this.capture.capture_keystroke(mappedKey, true); + } catch (error) { + console.error('Failed to capture keyup:', error); + } + } + + getEventCount() { + return this.capture ? this.capture.get_event_count() : 0; + } + + exportAsCSV() { + if (!this.capture) return ''; + return this.capture.export_as_csv(); + } + + getRawData() { + if (!this.capture) return []; + try { + return this.capture.get_raw_data(); + } catch (error) { + console.error('Failed to get raw data:', error); + return []; + } + } + + clear() { + if (this.capture) { + this.capture.clear(); + } + } +} + +// Export singleton instance +export const wasmKeystrokeManager = new WASMKeystrokeManager(); + diff --git a/wasm-keystroke-capture/Cargo.toml b/wasm-keystroke-capture/Cargo.toml new file mode 100644 index 0000000..d9c2c55 --- /dev/null +++ b/wasm-keystroke-capture/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "keystroke-capture" +version = "0.1.0" +authors = ["Your Name"] +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["Performance", "Window"] } +js-sys = "0.3" + +[profile.release] +# Optimize for small binary size +opt-level = "z" +lto = true + diff --git a/wasm-keystroke-capture/src/lib.rs b/wasm-keystroke-capture/src/lib.rs new file mode 100644 index 0000000..e06cfd0 --- /dev/null +++ b/wasm-keystroke-capture/src/lib.rs @@ -0,0 +1,92 @@ +use wasm_bindgen::prelude::*; +use web_sys::{Performance, Window}; + +#[wasm_bindgen] +pub struct KeystrokeCapture { + timestamps: Vec, + keys: Vec, + event_types: Vec, // 0 for press, 1 for release + capacity: usize, + performance: Performance, +} + +#[wasm_bindgen] +impl KeystrokeCapture { + #[wasm_bindgen(constructor)] + pub fn new(capacity: usize) -> Result { + // Get window and performance objects + let window = web_sys::window() + .ok_or_else(|| JsValue::from_str("No window object available"))?; + let performance = window.performance() + .ok_or_else(|| JsValue::from_str("No performance object available"))?; + + Ok(KeystrokeCapture { + timestamps: Vec::with_capacity(capacity), + keys: Vec::with_capacity(capacity), + event_types: Vec::with_capacity(capacity), + capacity, + performance, + }) + } + + #[wasm_bindgen] + pub fn capture_keystroke(&mut self, key: &str, is_release: bool) -> Result<(), JsValue> { + if self.timestamps.len() >= self.capacity { + return Err(JsValue::from_str("Capacity exceeded")); + } + + // Capture timestamp immediately in WASM context + let timestamp = self.performance.now(); + + self.timestamps.push(timestamp); + self.keys.push(key.to_string()); + self.event_types.push(if is_release { 1 } else { 0 }); + + Ok(()) + } + + #[wasm_bindgen] + pub fn get_event_count(&self) -> usize { + self.timestamps.len() + } + + #[wasm_bindgen] + pub fn export_as_csv(&self) -> String { + let mut csv = String::from("Press or Release,Key,Time\n"); + + for i in 0..self.timestamps.len() { + let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; + let timestamp = (self.timestamps[i] + 1735660000000.0) as u64; // Adjust base time + csv.push_str(&format!("{},{},{}\n", + event_type, + self.keys[i], + timestamp + )); + } + + csv + } + + #[wasm_bindgen] + pub fn clear(&mut self) { + self.timestamps.clear(); + self.keys.clear(); + self.event_types.clear(); + } + + #[wasm_bindgen] + pub fn get_raw_data(&self) -> Result { + let result = js_sys::Array::new(); + + for i in 0..self.timestamps.len() { + let entry = js_sys::Array::new(); + entry.push(&JsValue::from_str(if self.event_types[i] == 0 { "P" } else { "R" })); + entry.push(&JsValue::from_str(&self.keys[i])); + entry.push(&JsValue::from_f64(self.timestamps[i] + 1735660000000.0)); + result.push(&entry); + } + + Ok(result.into()) + } +} + From 9c337ece9457d891cce4f98fe31655ee21e11416 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 17:02:57 -0400 Subject: [PATCH 05/16] Debug messages to fake pages. --- pages/fake_pages/Facebook-Clone/index.html | 15 ++++++++++++--- pages/fake_pages/instagram-clone/index.html | 16 +++++++++++++--- pages/fake_pages/twitter-clone/index.html | 15 ++++++++++++--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/pages/fake_pages/Facebook-Clone/index.html b/pages/fake_pages/Facebook-Clone/index.html index 925fda8..4ba720b 100644 --- a/pages/fake_pages/Facebook-Clone/index.html +++ b/pages/fake_pages/Facebook-Clone/index.html @@ -335,9 +335,18 @@

Conversation

diff --git a/pages/fake_pages/instagram-clone/index.html b/pages/fake_pages/instagram-clone/index.html index cb55454..10449c6 100644 --- a/pages/fake_pages/instagram-clone/index.html +++ b/pages/fake_pages/instagram-clone/index.html @@ -956,11 +956,21 @@

Suggestions for You

+ diff --git a/pages/fake_pages/twitter-clone/index.html b/pages/fake_pages/twitter-clone/index.html index 21a671e..acb0ee3 100644 --- a/pages/fake_pages/twitter-clone/index.html +++ b/pages/fake_pages/twitter-clone/index.html @@ -207,9 +207,18 @@

What's happening?

From b9b1d9d8f2e8ba8165eae1c0ce87287f89038aba Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 17:15:56 -0400 Subject: [PATCH 06/16] fix typo in wasm-keystrokeCapture.js to wasm-keystroke.js --- utils/{wasm-keystrokeCapture.js => wasm-keystroke.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename utils/{wasm-keystrokeCapture.js => wasm-keystroke.js} (100%) diff --git a/utils/wasm-keystrokeCapture.js b/utils/wasm-keystroke.js similarity index 100% rename from utils/wasm-keystrokeCapture.js rename to utils/wasm-keystroke.js From 0abda350e07abdf3840451e891818d84455e101a Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 17:36:45 -0400 Subject: [PATCH 07/16] .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1e7f907..524f3f7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ build/ # Rust/WASM build artifacts wasm-keystroke-capture/target/ -wasm-keystroke-capture/pkg/ wasm-keystroke-capture/Cargo.lock # OS files From 90fd232e29ca52086eda3b25160396cb177aa597 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 17:48:51 -0400 Subject: [PATCH 08/16] Force add WASM pkg files --- wasm-keystroke-capture/pkg/.gitignore | 1 + .../pkg/keystroke_capture.d.ts | 53 +++ .../pkg/keystroke_capture.js | 361 ++++++++++++++++++ .../pkg/keystroke_capture_bg.wasm | Bin 0 -> 22029 bytes .../pkg/keystroke_capture_bg.wasm.d.ts | 18 + wasm-keystroke-capture/pkg/package.json | 18 + 6 files changed, 451 insertions(+) create mode 100644 wasm-keystroke-capture/pkg/.gitignore create mode 100644 wasm-keystroke-capture/pkg/keystroke_capture.d.ts create mode 100644 wasm-keystroke-capture/pkg/keystroke_capture.js create mode 100644 wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm create mode 100644 wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts create mode 100644 wasm-keystroke-capture/pkg/package.json diff --git a/wasm-keystroke-capture/pkg/.gitignore b/wasm-keystroke-capture/pkg/.gitignore new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/wasm-keystroke-capture/pkg/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/wasm-keystroke-capture/pkg/keystroke_capture.d.ts b/wasm-keystroke-capture/pkg/keystroke_capture.d.ts new file mode 100644 index 0000000..464eb25 --- /dev/null +++ b/wasm-keystroke-capture/pkg/keystroke_capture.d.ts @@ -0,0 +1,53 @@ +/* tslint:disable */ +/* eslint-disable */ +export class KeystrokeCapture { + free(): void; + constructor(capacity: number); + capture_keystroke(key: string, is_release: boolean): void; + get_event_count(): number; + export_as_csv(): string; + clear(): void; + get_raw_data(): any; +} + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_keystrokecapture_free: (a: number, b: number) => void; + readonly keystrokecapture_new: (a: number) => [number, number, number]; + readonly keystrokecapture_capture_keystroke: (a: number, b: number, c: number, d: number) => [number, number]; + readonly keystrokecapture_get_event_count: (a: number) => number; + readonly keystrokecapture_export_as_csv: (a: number) => [number, number]; + readonly keystrokecapture_clear: (a: number) => void; + readonly keystrokecapture_get_raw_data: (a: number) => [number, number, number]; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __externref_table_alloc: () => number; + readonly __wbindgen_export_2: WebAssembly.Table; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. +* +* @returns {InitOutput} +*/ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. +* +* @returns {Promise} +*/ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/wasm-keystroke-capture/pkg/keystroke_capture.js b/wasm-keystroke-capture/pkg/keystroke_capture.js new file mode 100644 index 0000000..396312a --- /dev/null +++ b/wasm-keystroke-capture/pkg/keystroke_capture.js @@ -0,0 +1,361 @@ +let wasm; + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_export_2.set(idx, obj); + return idx; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_export_2.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +const KeystrokeCaptureFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_keystrokecapture_free(ptr >>> 0, 1)); + +export class KeystrokeCapture { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + KeystrokeCaptureFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_keystrokecapture_free(ptr, 0); + } + /** + * @param {number} capacity + */ + constructor(capacity) { + const ret = wasm.keystrokecapture_new(capacity); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + this.__wbg_ptr = ret[0] >>> 0; + KeystrokeCaptureFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * @param {string} key + * @param {boolean} is_release + */ + capture_keystroke(key, is_release) { + const ptr0 = passStringToWasm0(key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.keystrokecapture_capture_keystroke(this.__wbg_ptr, ptr0, len0, is_release); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * @returns {number} + */ + get_event_count() { + const ret = wasm.keystrokecapture_get_event_count(this.__wbg_ptr); + return ret >>> 0; + } + /** + * @returns {string} + */ + export_as_csv() { + let deferred1_0; + let deferred1_1; + try { + const ret = wasm.keystrokecapture_export_as_csv(this.__wbg_ptr); + deferred1_0 = ret[0]; + deferred1_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + } + clear() { + wasm.keystrokecapture_clear(this.__wbg_ptr); + } + /** + * @returns {any} + */ + get_raw_data() { + const ret = wasm.keystrokecapture_get_raw_data(this.__wbg_ptr); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function(arg0) { + let result; + try { + result = arg0 instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_new_78feb108b6472713 = function() { + const ret = new Array(); + return ret; + }; + imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_now_d18023d54d4e5500 = function(arg0) { + const ret = arg0.now(); + return ret; + }; + imports.wbg.__wbg_performance_c185c0cdc2766575 = function(arg0) { + const ret = arg0.performance; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) { + const ret = arg0.push(arg1); + return ret; + }; + imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_2; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; + }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = arg0 === undefined; + return ret; + }; + imports.wbg.__wbindgen_number_new = function(arg0) { + const ret = arg0; + return ret; + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('keystroke_capture_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b64e84ad153b8b8574f4ef4c88f3c4d4471e63f3 GIT binary patch literal 22029 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 B@QMHc literal 0 HcmV?d00001 diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts new file mode 100644 index 0000000..b58ccff --- /dev/null +++ b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_keystrokecapture_free: (a: number, b: number) => void; +export const keystrokecapture_new: (a: number) => [number, number, number]; +export const keystrokecapture_capture_keystroke: (a: number, b: number, c: number, d: number) => [number, number]; +export const keystrokecapture_get_event_count: (a: number) => number; +export const keystrokecapture_export_as_csv: (a: number) => [number, number]; +export const keystrokecapture_clear: (a: number) => void; +export const keystrokecapture_get_raw_data: (a: number) => [number, number, number]; +export const __wbindgen_exn_store: (a: number) => void; +export const __externref_table_alloc: () => number; +export const __wbindgen_export_2: WebAssembly.Table; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/wasm-keystroke-capture/pkg/package.json b/wasm-keystroke-capture/pkg/package.json new file mode 100644 index 0000000..9d928b4 --- /dev/null +++ b/wasm-keystroke-capture/pkg/package.json @@ -0,0 +1,18 @@ +{ + "name": "keystroke-capture", + "type": "module", + "collaborators": [ + "Your Name" + ], + "version": "0.1.0", + "files": [ + "keystroke_capture_bg.wasm", + "keystroke_capture.js", + "keystroke_capture.d.ts" + ], + "main": "keystroke_capture.js", + "types": "keystroke_capture.d.ts", + "sideEffects": [ + "./snippets/*" + ] +} \ No newline at end of file From 8ae7b6f928c4df037c02514ef87834df66876eac Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 18:26:42 -0400 Subject: [PATCH 09/16] Fix CSV formatting in WASM --- utils/common.js | 40 +++++++++++------- .../pkg/keystroke_capture.d.ts | 2 + .../pkg/keystroke_capture.js | 15 +++++++ .../pkg/keystroke_capture_bg.wasm | Bin 22029 -> 40759 bytes .../pkg/keystroke_capture_bg.wasm.d.ts | 1 + wasm-keystroke-capture/src/lib.rs | 24 ++++++++++- 6 files changed, 65 insertions(+), 17 deletions(-) diff --git a/utils/common.js b/utils/common.js index 32a65a1..f0741d5 100644 --- a/utils/common.js +++ b/utils/common.js @@ -974,6 +974,7 @@ const PlatformSubmissionHandler = { // Handle space key - check both e.code and e.key if (e.code === 'Space' || e.key === ' ') return 'Key.space'; + if (e.code === ',') return 'Key.comma'; // Check if it's a mapped key if (keyMap[e.key]) return keyMap[e.key]; @@ -986,22 +987,29 @@ const PlatformSubmissionHandler = { * Build CSV blob from keystroke events */ buildCsvBlob() { - if (this.useWASM && this.wasmCapture) { - // Get CSV directly from WASM - const csvString = this.wasmCapture.exportAsCSV(); - return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); - } else { - // Original implementation - // Convert high-performance buffer to array format only when needed - this.keyEvents = this.getKeystrokeData(); - - const heading = [['Press or Release', 'Key', 'Time']]; - const csvString = heading - .concat(this.keyEvents) - .map(row => row.join(',')) - .join('\n'); - return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); - } + console.log('=== Building CSV ==='); + console.log('useWASM:', this.useWASM); + console.log('wasmCapture:', this.wasmCapture); + + if (this.useWASM && this.wasmCapture) { + console.log('✅ Using WASM export'); + // Get CSV directly from WASM + const csvString = this.wasmCapture.exportAsCSV(); + console.log('First few lines of WASM CSV:', csvString.split('\n').slice(0, 5)); + return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); + } else { + console.log('❌ Using JavaScript export'); + // Original implementation + // Convert high-performance buffer to array format only when needed + this.keyEvents = this.getKeystrokeData(); + + const heading = [['Press or Release', 'Key', 'Time']]; + const csvString = heading + .concat(this.keyEvents) + .map(row => row.join(',')) + .join('\n'); + return new Blob([csvString], { type: 'text/csv;charset=utf-8' }); + } }, /** diff --git a/wasm-keystroke-capture/pkg/keystroke_capture.d.ts b/wasm-keystroke-capture/pkg/keystroke_capture.d.ts index 464eb25..b73f683 100644 --- a/wasm-keystroke-capture/pkg/keystroke_capture.d.ts +++ b/wasm-keystroke-capture/pkg/keystroke_capture.d.ts @@ -6,6 +6,7 @@ export class KeystrokeCapture { capture_keystroke(key: string, is_release: boolean): void; get_event_count(): number; export_as_csv(): string; + get_last_10_events(): string; clear(): void; get_raw_data(): any; } @@ -19,6 +20,7 @@ export interface InitOutput { readonly keystrokecapture_capture_keystroke: (a: number, b: number, c: number, d: number) => [number, number]; readonly keystrokecapture_get_event_count: (a: number) => number; readonly keystrokecapture_export_as_csv: (a: number) => [number, number]; + readonly keystrokecapture_get_last_10_events: (a: number) => [number, number]; readonly keystrokecapture_clear: (a: number) => void; readonly keystrokecapture_get_raw_data: (a: number) => [number, number, number]; readonly __wbindgen_exn_store: (a: number) => void; diff --git a/wasm-keystroke-capture/pkg/keystroke_capture.js b/wasm-keystroke-capture/pkg/keystroke_capture.js index 396312a..0a4febe 100644 --- a/wasm-keystroke-capture/pkg/keystroke_capture.js +++ b/wasm-keystroke-capture/pkg/keystroke_capture.js @@ -162,6 +162,21 @@ export class KeystrokeCapture { wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); } } + /** + * @returns {string} + */ + get_last_10_events() { + let deferred1_0; + let deferred1_1; + try { + const ret = wasm.keystrokecapture_get_last_10_events(this.__wbg_ptr); + deferred1_0 = ret[0]; + deferred1_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + } clear() { wasm.keystrokecapture_clear(this.__wbg_ptr); } diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm index b64e84ad153b8b8574f4ef4c88f3c4d4471e63f3..77652e163b0952063234595c946d9163f940afdd 100644 GIT binary patch literal 40759 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ⅈe3AfgZF-K%QZ{Hhf* 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 q@jaL1Cy=l!HwpAuBY1q1BWOC5LasNM~-1`Fn delta 8791 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 diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts index b58ccff..a0cd8c8 100644 --- a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts +++ b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts @@ -6,6 +6,7 @@ export const keystrokecapture_new: (a: number) => [number, number, number]; export const keystrokecapture_capture_keystroke: (a: number, b: number, c: number, d: number) => [number, number]; export const keystrokecapture_get_event_count: (a: number) => number; export const keystrokecapture_export_as_csv: (a: number) => [number, number]; +export const keystrokecapture_get_last_10_events: (a: number) => [number, number]; export const keystrokecapture_clear: (a: number) => void; export const keystrokecapture_get_raw_data: (a: number) => [number, number, number]; export const __wbindgen_exn_store: (a: number) => void; diff --git a/wasm-keystroke-capture/src/lib.rs b/wasm-keystroke-capture/src/lib.rs index e06cfd0..efc43ba 100644 --- a/wasm-keystroke-capture/src/lib.rs +++ b/wasm-keystroke-capture/src/lib.rs @@ -56,7 +56,9 @@ impl KeystrokeCapture { for i in 0..self.timestamps.len() { let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; - let timestamp = (self.timestamps[i] + 1735660000000.0) as u64; // Adjust base time + let timestamp = (self.timestamps[i] + 1735660000000.0) as u64; + + // Use proper CSV formatting - no extra spaces csv.push_str(&format!("{},{},{}\n", event_type, self.keys[i], @@ -66,6 +68,26 @@ impl KeystrokeCapture { csv } + + #[wasm_bindgen] + pub fn get_last_10_events(&self) -> String { + let start = if self.timestamps.len() > 10 { + self.timestamps.len() - 10 + } else { + 0 + }; + + let mut result = String::new(); + for i in start..self.timestamps.len() { + let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; + result.push_str(&format!("{} {} {:.0}\n", + event_type, + self.keys[i], + self.timestamps[i] + )); + } + result + } #[wasm_bindgen] pub fn clear(&mut self) { From a8c7f8bf13e4f32eb92a12190b6e954b08eb3e8a Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 18:36:39 -0400 Subject: [PATCH 10/16] Fix e.code === 'Commna' and change &str to Sting in wasm function signature. --- utils/common.js | 2 +- .../pkg/keystroke_capture.d.ts | 4 +- .../pkg/keystroke_capture.js | 26 +++---- .../pkg/keystroke_capture_bg.wasm | Bin 40759 -> 40970 bytes .../pkg/keystroke_capture_bg.wasm.d.ts | 2 +- wasm-keystroke-capture/src/lib.rs | 66 +++++++++++------- 6 files changed, 57 insertions(+), 43 deletions(-) diff --git a/utils/common.js b/utils/common.js index f0741d5..632d183 100644 --- a/utils/common.js +++ b/utils/common.js @@ -974,7 +974,7 @@ const PlatformSubmissionHandler = { // Handle space key - check both e.code and e.key if (e.code === 'Space' || e.key === ' ') return 'Key.space'; - if (e.code === ',') return 'Key.comma'; + if (e.code === 'Comma' || e.key == ',' ) return 'Key.comma'; // Check if it's a mapped key if (keyMap[e.key]) return keyMap[e.key]; diff --git a/wasm-keystroke-capture/pkg/keystroke_capture.d.ts b/wasm-keystroke-capture/pkg/keystroke_capture.d.ts index b73f683..b905b57 100644 --- a/wasm-keystroke-capture/pkg/keystroke_capture.d.ts +++ b/wasm-keystroke-capture/pkg/keystroke_capture.d.ts @@ -6,9 +6,9 @@ export class KeystrokeCapture { capture_keystroke(key: string, is_release: boolean): void; get_event_count(): number; export_as_csv(): string; - get_last_10_events(): string; clear(): void; get_raw_data(): any; + get_last_10_events(): string; } export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; @@ -20,9 +20,9 @@ export interface InitOutput { readonly keystrokecapture_capture_keystroke: (a: number, b: number, c: number, d: number) => [number, number]; readonly keystrokecapture_get_event_count: (a: number) => number; readonly keystrokecapture_export_as_csv: (a: number) => [number, number]; - readonly keystrokecapture_get_last_10_events: (a: number) => [number, number]; readonly keystrokecapture_clear: (a: number) => void; readonly keystrokecapture_get_raw_data: (a: number) => [number, number, number]; + readonly keystrokecapture_get_last_10_events: (a: number) => [number, number]; readonly __wbindgen_exn_store: (a: number) => void; readonly __externref_table_alloc: () => number; readonly __wbindgen_export_2: WebAssembly.Table; diff --git a/wasm-keystroke-capture/pkg/keystroke_capture.js b/wasm-keystroke-capture/pkg/keystroke_capture.js index 0a4febe..478a2f2 100644 --- a/wasm-keystroke-capture/pkg/keystroke_capture.js +++ b/wasm-keystroke-capture/pkg/keystroke_capture.js @@ -162,6 +162,19 @@ export class KeystrokeCapture { wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); } } + clear() { + wasm.keystrokecapture_clear(this.__wbg_ptr); + } + /** + * @returns {any} + */ + get_raw_data() { + const ret = wasm.keystrokecapture_get_raw_data(this.__wbg_ptr); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } /** * @returns {string} */ @@ -177,19 +190,6 @@ export class KeystrokeCapture { wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); } } - clear() { - wasm.keystrokecapture_clear(this.__wbg_ptr); - } - /** - * @returns {any} - */ - get_raw_data() { - const ret = wasm.keystrokecapture_get_raw_data(this.__wbg_ptr); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); - } } async function __wbg_load(module, imports) { diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm index 77652e163b0952063234595c946d9163f940afdd..26aef00ee2673d8090f6c532f211280adca1234c 100644 GIT binary patch delta 7305 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 delta 7166 zcma)B4RBS*b>7{3pMKttbOjQB60q+f#1sD#So|8^3IP_j_y^E@=Q>zCA|zAy&*`$v2=SL>jxA06MF)!tRui&BOvEIgX~)!q3x*eAGea4Yty7|2 zLD^qrUMHQqv;NLg#$9EPW*cRjvL{UUlts7=O=|mQWmXL{)#R=cwnc-w?`iib4k7FL z`Uu}lt71bRW3B~B;yj_R29iYV2<~@M3-L#BD4%N7Iia$equ82aIDaCs zd$SaU5layZK*l4C^c|zUyA7CxyA+QWVuVmuEv{@t;Mlj$>6TEmq0M6%wUXBq5+uZw z9J2!IG`i}`!KC}nIS&`i^~Ti$^bFc0(0CrieQ=1NwtJ+7MiO|@2(>A(XVfvGn!F)u zyXD%5IO>g1+bwpMh*oce+HSFnM4a|Us8y}{kD;*d9_>aK5P@L=cCF&UE4q5yYX9Tj|6-B5vv58*4t7w>rV?;E0Bh+?_wGnaD8=?{$j-UzkbVi$=xO(WURO&!2=Cijzm zO&*4>6%X6wSjoIm0?ohWjSci@ zOeu-=8c{_KR4Q0F8@I!WtGg~a0ZMF{JwarBtFMt1pA`%*rg0Z-Uropr|K^4(`;5(`07`*sN1vs|UICgOOF=BlWpyx@caeBLmzUr!uY-7gk0)H-UE=j79xjE`Yd~%w$OA|w9=gxr zxX}~=+`rIUhgJ3WoNX3X7g2$vu~}KOLmVz}V_0<$x&nHd^swmIpf|ylSrwgHXhJo; z;46yPnuH)($j0^3KaUP7G2>qjhAVSK^cuReNN#Btlsv9ce-Z7!$kQ!3wqx?4>+Zxh zJ)BIDKa*El$l*PiWhMxQ%$xFATa{La1Os4bRK`KahH z!i{(?7T3!Q%hGX9oxdjp06`LvZa8ocb-jFmE+6-@_@Yh!a$FgjYS7cDEMr`zw-RcCs4DEVr())9i;49nW^5^A50qZ zwEK+LpBaqD4X0^xIXHhadEZwi_()SWCM=lpRZTtxO|(SQlx0TK z0RFaqFl7Y#(Wy779?E!lMJAPRRIAlJJg+;>z4!2GzE`iBR)tV=ep)5o|2b`H|JLJ> z466>pnL^7Abktzg)v9CDf7#~(Y=pB7kiu~y7OJ?Q6;N$j#!7jco)c3g?;N0{mnE6H zXnOgQvQ93tjsqUTd=Z+n`lm6)FX~TYkLKM1wv_m|6!j>`5P#>XlGGlPyVaM1AJe<5 zM!<{DSB>XQ`uA03n3_AYEW0kC5MN znR$d?(&J`5f%gw*{jQ)Bp_29~$PFC$QdV4D(>rFD^tt=SH%GeO&_A2~9ABqLJhH6e z>=_cUrH`ftVKY0f9)Ramvp|zTHXXv$SOkz_X3EAZI3R91Rfad*F1AcP6AMtghd3ay zPH7%UtR1S^s6H{=$Yyh8D0BvoHe>?P77b7-=9;;YxU=I{-FdIpPhvN*FGVj3aj-2F9U=xtV2^~*QRiJZB23QGt*4iX zyQVtnd`mwzw}_wD>*wy`b^61(>#>V@^Ge12SM<(#%`k&+{;XktF2{K}@NpOd`J+~E zng4x$L4PuT#w0IJM0hnpE-A>TV!}M+3J+olH?0Ro?(V6=xTZHO*a`@4FWAZ3lTR(Y z%zMA^3U=Lymn^Jgiy|3?>IGf0xPtH3%NLJ;lO~q-(XTCT=hyW7C1b>mLwfI$@#3Ej z>B~#5@k1ExU-7{YprJ^$zU#aYPwWfkn2Lh(Vjlh>E3Eld!BJ>4Rf%8aRfiZNN1i&ZZ$H~w(DW*KZ;;M! zib1-nP5SU!BnW+BZ8f66r)wAR>w4z8C9`fFgcf);M0&C>=JgQj9?Bmzz%6_bE^v|3 z2HiZJytQuRHxT25*x)!Jwlq$N?Tc3-8vT9zN5E`(ZC}Fd`PwqV>_YA7k`}^ysJev4 zpyQ$Uj)#_qj^0$)hOGxTyeDoRPu|$@b52D{%cfU(e-GX(hdc#ms3t4KzLV28|Ff^6 zmH1S}*=hTi+0w7h(+DM;jPwy#ELpv6B+tE@!rCqRbmR8liu=v_$sJ)FYtN2FF!=jB zM&NLN+A$dYfjj58MWe}LX-d)cu){`rfYo~ES~&3MJ9qKbdfToN;B#tMh3Gt`U)$9; z`O&TcJm;8CWH<2Lh&xVgBtsYMnKkvhbV^8RR^*$@oc)$%C^iS{;t~85P%852 zBZvGJ%M`_-5!bVO?R=C@K3Y^p%m(|R`~oBrpCbm4WWXRNk**X+_PPo^D$iLJ2F%&mB4IfRj!AYf z=S9zPz9Tavegm=5Foo|g^9^Y$T7x39d125s*Ik#-oOC?-!=X<8-)>n(Qk99wntW;A0eQxKjrXjv7USG*^ViP0C-LfLztNcThuz z5Wxn%$#s5F%-oI*9Bj&}%$e-o0_JQa_ki<62q0Jpz7Y>o{40v%jxg9b>__9o!xZuC zcsOX`*G61)f{Di=un@b*UUf^RFK-S#n67b*>J=GnSd5TIW z@X5)rEOZ1;S@0tlU&{d9#cFXTs5N(zpr*5hiO!F!-|hom11Jy>=oA3L9pcrz#(oV^7i41;p^m{o z&Vn#qIo#zsG&uii%mno`DEvzKlsi_1<~*b;L`?EZYl3(TQpj#7Le`m{L~ohtbux4x z!Qgn*X)0Fh455>bM?XJ~_z@Ko5p&Q?b;@MGsVB#chlMc-xIr3nU=mV~NHU#h8!%ba zF_+qsXlTz}jLc-w>F}~)Tw|g39_U>k2lO8D4G6df`bxTK4pn*3njf(U^-L%-SeW}{ zD?b%I6m{&HLfU|P1uXXpSY!>qSuz~3VkAXbf`6;$qb?$(myK$G=U0f45QB-vrg=Sn zO>u4EgG$$Gv>lkRg>J)qcmr36j<6AZaRKv`sbH}*Q{mU-#TaYDqzW+~Jy?=#chj{U zU{w%tjW_cDVKNCDlEtvRYq1PijFH7y%3_H8QBgz|qh!OC)FTc)$2;`v2aE9@)6`f0 z{$RlEz0~A&f`eEfg40oOC7FmPxxR_!9Bx{T_oq#r`t}co$a1X z$*h*|@P-(}=NX=ne#SUj`a8=V(@(W;dh`kAFo>~*c*^idJgGUIo=mF!`kW<@rFBv| zmZC2i>%{~*^oPQ^9Tp4xQO`U4(AbpNdW=lNlcMA3Ka@t-qK~x5bmQUi_*DDr!^8Lu zef#i;0%kd^5CeCy!M;pgIsnxP9X_&yzobtb8OPsA{`$yZ9=>EdYz)NS!;_6CMSqTd zh-jVr;_T%q?iMI3PLtXOnr5fQ8$tIpA3h;FLwg33E@)Y|gVEGK&74t(|UK$+(O=rTioyW@; zrl#GbMbpH_!2;>i>Oi~GT(uv7E=iOBxSQ5r953fZx~z2!-YZ*Y;QeOn4FA<2+!7P+ z1oeNl&dD1Pa^N?N4M*D-tlT;uJ%Uj(bB5QYbVPd-#VQ<`N|m1-_`&4>MlM_ zSD%mg748=;Y&&_~_)vPOz85>M?W|5B_JaQQtc?QQUY-m%Mi8 Fe*vu%N(2A^ diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts index a0cd8c8..ab25c25 100644 --- a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts +++ b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm.d.ts @@ -6,9 +6,9 @@ export const keystrokecapture_new: (a: number) => [number, number, number]; export const keystrokecapture_capture_keystroke: (a: number, b: number, c: number, d: number) => [number, number]; export const keystrokecapture_get_event_count: (a: number) => number; export const keystrokecapture_export_as_csv: (a: number) => [number, number]; -export const keystrokecapture_get_last_10_events: (a: number) => [number, number]; export const keystrokecapture_clear: (a: number) => void; export const keystrokecapture_get_raw_data: (a: number) => [number, number, number]; +export const keystrokecapture_get_last_10_events: (a: number) => [number, number]; export const __wbindgen_exn_store: (a: number) => void; export const __externref_table_alloc: () => number; export const __wbindgen_export_2: WebAssembly.Table; diff --git a/wasm-keystroke-capture/src/lib.rs b/wasm-keystroke-capture/src/lib.rs index efc43ba..5a5954e 100644 --- a/wasm-keystroke-capture/src/lib.rs +++ b/wasm-keystroke-capture/src/lib.rs @@ -30,7 +30,7 @@ impl KeystrokeCapture { } #[wasm_bindgen] - pub fn capture_keystroke(&mut self, key: &str, is_release: bool) -> Result<(), JsValue> { + pub fn capture_keystroke(&mut self, key: String, is_release: bool) -> Result<(), JsValue> { if self.timestamps.len() >= self.capacity { return Err(JsValue::from_str("Capacity exceeded")); } @@ -39,7 +39,7 @@ impl KeystrokeCapture { let timestamp = self.performance.now(); self.timestamps.push(timestamp); - self.keys.push(key.to_string()); + self.keys.push(key); // Changed: accept String directly, not &str self.event_types.push(if is_release { 1 } else { 0 }); Ok(()) @@ -58,36 +58,23 @@ impl KeystrokeCapture { let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; let timestamp = (self.timestamps[i] + 1735660000000.0) as u64; + // Handle comma in key - replace with Key.comma + let key = if self.keys[i] == "," { + "Key.comma" + } else { + &self.keys[i] + }; + // Use proper CSV formatting - no extra spaces csv.push_str(&format!("{},{},{}\n", event_type, - self.keys[i], + key, timestamp )); } csv } - - #[wasm_bindgen] - pub fn get_last_10_events(&self) -> String { - let start = if self.timestamps.len() > 10 { - self.timestamps.len() - 10 - } else { - 0 - }; - - let mut result = String::new(); - for i in start..self.timestamps.len() { - let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; - result.push_str(&format!("{} {} {:.0}\n", - event_type, - self.keys[i], - self.timestamps[i] - )); - } - result - } #[wasm_bindgen] pub fn clear(&mut self) { @@ -103,12 +90,39 @@ impl KeystrokeCapture { for i in 0..self.timestamps.len() { let entry = js_sys::Array::new(); entry.push(&JsValue::from_str(if self.event_types[i] == 0 { "P" } else { "R" })); - entry.push(&JsValue::from_str(&self.keys[i])); + + // Handle comma here too + let key = if self.keys[i] == "," { + "Key.comma" + } else { + &self.keys[i] + }; + entry.push(&JsValue::from_str(key)); + entry.push(&JsValue::from_f64(self.timestamps[i] + 1735660000000.0)); result.push(&entry); } Ok(result.into()) } -} - + + #[wasm_bindgen] + pub fn get_last_10_events(&self) -> String { + let start = if self.timestamps.len() > 10 { + self.timestamps.len() - 10 + } else { + 0 + }; + + let mut result = String::new(); + for i in start..self.timestamps.len() { + let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; + result.push_str(&format!("{} {} {:.0}\n", + event_type, + &self.keys[i], + self.timestamps[i] + )); + } + result + } +} \ No newline at end of file From f339fe9b0a9d9583f187e44b17a7b00f85e6f057 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 19:20:34 -0400 Subject: [PATCH 11/16] Fix e.key --> e.code to prevent orphaned releases caused by Shift key changes to key. --- utils/common.js | 146 +++++++++++++++++++++++----------------- utils/wasm-keystroke.js | 59 +++++++++++++--- 2 files changed, 132 insertions(+), 73 deletions(-) diff --git a/utils/common.js b/utils/common.js index 632d183..50fbc8e 100644 --- a/utils/common.js +++ b/utils/common.js @@ -625,70 +625,92 @@ const PlatformSubmissionHandler = { * Start keystroke logging */ startKeyLogger(urlParams) { - if (this.useWASM && this.wasmCapture) { - // Use WASM for capture - console.log('Using WASM keystroke capture'); - - document.addEventListener('keydown', (e) => { - this.wasmCapture.captureKeyDown(e.key); - - // Handle Enter key - if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { - e.preventDefault(); - const textarea = e.target; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); - textarea.selectionStart = textarea.selectionEnd = start + 1; - textarea.dispatchEvent(new Event('input')); - } - }); + if (this.useWASM && this.wasmCapture) { + // Use WASM for capture + console.log('Using WASM keystroke capture'); + + document.addEventListener('keydown', (e) => { + this.wasmCapture.captureKeyDown(e); + + // Handle Enter key + if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { + e.preventDefault(); + const textarea = e.target; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 1; + textarea.dispatchEvent(new Event('input')); + } + }); + + document.addEventListener('keyup', (e) => { + this.wasmCapture.captureKeyUp(e); + }); + } else { + // Fallback to JavaScript implementation with physical key tracking + console.log('Using JavaScript keystroke capture with physical key tracking'); + + // Initialize high-performance capture + this.initHighPerformanceCapture(); + + // Add physical key tracking map + this.physicalKeyMap = new Map(); + + const captureEvent = (e, eventType) => { + const timestamp = performance.now(); + const idx = this.keyBuffer.index; + const physicalCode = e.code; + + let keyToStore; + + if (eventType === 0) { // Press event + // Store the display key for this physical key + keyToStore = e.key; + this.physicalKeyMap.set(physicalCode, keyToStore); + } else { // Release event + // Get the stored key from press time + keyToStore = this.physicalKeyMap.get(physicalCode); - document.addEventListener('keyup', (e) => { - this.wasmCapture.captureKeyUp(e.key); + if (!keyToStore) { + console.warn(`No tracked press for code=${physicalCode}, using current key=${e.key}`); + keyToStore = e.key; + } else { + // Remove from tracking + this.physicalKeyMap.delete(physicalCode); + } + } + + // Get or create key code + let keyCode = this.keyCodeMap.get(keyToStore); + if (keyCode === undefined) { + keyCode = this.keyCodeIndex++; + this.keyCodeMap.set(keyToStore, keyCode); + } + + // Store in typed arrays + this.keyBuffer.types[idx] = eventType; + this.keyBuffer.keys[idx] = keyCode; + this.keyBuffer.timestamps[idx] = timestamp; + this.keyBuffer.index++; + + // Handle Enter key + if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { + e.preventDefault(); + requestAnimationFrame(() => { + const textarea = e.target; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 1; + textarea.dispatchEvent(new Event('input')); }); - } else { - // Fallback to original JavaScript implementation - console.log('Using JavaScript keystroke capture'); - - // Initialize high-performance capture - this.initHighPerformanceCapture(); - - const captureEvent = (e, eventType) => { - // Capture timestamp with maximum precision - const timestamp = performance.now(); - const idx = this.keyBuffer.index; - - // Get or create key code - let keyCode = this.keyCodeMap.get(e.key); - if (keyCode === undefined) { - keyCode = this.keyCodeIndex++; - this.keyCodeMap.set(e.key, keyCode); - } - - // Store in typed arrays (extremely fast) - this.keyBuffer.types[idx] = eventType; // 0 for press, 1 for release - this.keyBuffer.keys[idx] = keyCode; - this.keyBuffer.timestamps[idx] = timestamp; - this.keyBuffer.index++; - - // Handle Enter key without blocking - if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { - e.preventDefault(); - requestAnimationFrame(() => { - const textarea = e.target; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); - textarea.selectionStart = textarea.selectionEnd = start + 1; - textarea.dispatchEvent(new Event('input')); - }); - } - }; - - document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); - document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); - } + } + }; + + document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); + document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); + } }, // startKeyLogger(urlParams) { diff --git a/utils/wasm-keystroke.js b/utils/wasm-keystroke.js index bf35291..3598724 100644 --- a/utils/wasm-keystroke.js +++ b/utils/wasm-keystroke.js @@ -5,6 +5,8 @@ class WASMKeystrokeManager { constructor() { this.initialized = false; this.capture = null; + // Map physical keys to their display values at press time + this.keyPressMap = new Map(); this.keyMapping = { 'Shift': 'Key.shift', 'Control': 'Key.ctrl', @@ -19,7 +21,8 @@ class WASMKeystrokeManager { 'ArrowUp': 'Key.up', 'ArrowDown': 'Key.down', 'CapsLock': 'Key.caps_lock', - ' ': 'Key.space' + ' ': 'Key.space', + ',': 'Key.comma' }; } @@ -27,13 +30,9 @@ class WASMKeystrokeManager { if (this.initialized) return; try { - // Initialize WASM module await init(); - - // Create capture instance with 50k capacity this.capture = new KeystrokeCapture(50000); this.initialized = true; - console.log('✅ WASM keystroke capture initialized'); } catch (error) { console.error('❌ Failed to initialize WASM:', error); @@ -45,13 +44,22 @@ class WASMKeystrokeManager { return this.keyMapping[key] || key; } - captureKeyDown(key) { + captureKeyDown(event) { if (!this.initialized) { console.warn('WASM not initialized'); return; } - const mappedKey = this.mapKey(key); + const physicalCode = event.code; // e.g., "KeyH" + const displayKey = event.key; // e.g., "H" or "h" + const mappedKey = this.mapKey(displayKey); + + // Store the display key for this physical key + this.keyPressMap.set(physicalCode, mappedKey); + + // Debug logging + console.log(`KeyDown: code=${physicalCode}, key=${displayKey}, mapped=${mappedKey}`); + try { this.capture.capture_keystroke(mappedKey, false); } catch (error) { @@ -59,13 +67,36 @@ class WASMKeystrokeManager { } } - captureKeyUp(key) { + captureKeyUp(event) { if (!this.initialized) { console.warn('WASM not initialized'); return; } - const mappedKey = this.mapKey(key); + const physicalCode = event.code; + + // Get the stored key from when it was pressed + const mappedKey = this.keyPressMap.get(physicalCode); + + if (!mappedKey) { + // Fallback if we didn't track the press + console.warn(`No tracked press for code=${physicalCode}, using current key=${event.key}`); + const fallbackKey = this.mapKey(event.key); + + try { + this.capture.capture_keystroke(fallbackKey, true); + } catch (error) { + console.error('Failed to capture keyup:', error); + } + return; + } + + // Debug logging + console.log(`KeyUp: code=${physicalCode}, stored=${mappedKey}, current=${event.key}`); + + // Remove from tracking + this.keyPressMap.delete(physicalCode); + try { this.capture.capture_keystroke(mappedKey, true); } catch (error) { @@ -96,9 +127,15 @@ class WASMKeystrokeManager { if (this.capture) { this.capture.clear(); } + // Also clear the key press map + this.keyPressMap.clear(); + } + + // Debug method to check for unreleased keys + getUnreleasedKeys() { + return Array.from(this.keyPressMap.entries()); } } // Export singleton instance -export const wasmKeystrokeManager = new WASMKeystrokeManager(); - +export const wasmKeystrokeManager = new WASMKeystrokeManager(); \ No newline at end of file From db4d375027f5b98e202b99410abb93af4c9de4dc Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 19:45:44 -0400 Subject: [PATCH 12/16] Remove multiple instantiations of wasmKeystrokeManager because it may be causing issues. --- pages/fake_pages/Facebook-Clone/index.html | 6 ------ pages/fake_pages/instagram-clone/index.html | 7 ------- pages/fake_pages/twitter-clone/index.html | 6 ------ 3 files changed, 19 deletions(-) diff --git a/pages/fake_pages/Facebook-Clone/index.html b/pages/fake_pages/Facebook-Clone/index.html index 4ba720b..a967fe4 100644 --- a/pages/fake_pages/Facebook-Clone/index.html +++ b/pages/fake_pages/Facebook-Clone/index.html @@ -341,12 +341,6 @@

Conversation

// Debug: Check if it loaded console.log('WASM manager loaded?', window.wasmKeystrokeManager); - // Try to initialize - wasmKeystrokeManager.initialize().then(() => { - console.log('WASM initialized successfully!'); - }).catch(err => { - console.error('WASM initialization failed:', err); - }); diff --git a/pages/fake_pages/instagram-clone/index.html b/pages/fake_pages/instagram-clone/index.html index 10449c6..13adf40 100644 --- a/pages/fake_pages/instagram-clone/index.html +++ b/pages/fake_pages/instagram-clone/index.html @@ -964,13 +964,6 @@

Suggestions for You

window.wasmKeystrokeManager = wasmKeystrokeManager; // Debug: Check if it loaded console.log('WASM manager loaded?', window.wasmKeystrokeManager); - - // Try to initialize - wasmKeystrokeManager.initialize().then(() => { - console.log('WASM initialized successfully!'); - }).catch(err => { - console.error('WASM initialization failed:', err); - }); diff --git a/pages/fake_pages/twitter-clone/index.html b/pages/fake_pages/twitter-clone/index.html index acb0ee3..a9c4862 100644 --- a/pages/fake_pages/twitter-clone/index.html +++ b/pages/fake_pages/twitter-clone/index.html @@ -213,12 +213,6 @@

What's happening?

// Debug: Check if it loaded console.log('WASM manager loaded?', window.wasmKeystrokeManager); - // Try to initialize - wasmKeystrokeManager.initialize().then(() => { - console.log('WASM initialized successfully!'); - }).catch(err => { - console.error('WASM initialization failed:', err); - }); From bcc58ea4c832e053562b495ff84c0ea4b4cd0b1d Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 20:08:27 -0400 Subject: [PATCH 13/16] Testing wasm memory management issues to fix out of order PP RR. --- utils/common.js | 120 +++------ utils/wasm-keystroke.js | 236 +++++++++++++++--- .../pkg/keystroke_capture_bg.wasm | Bin 40970 -> 41189 bytes wasm-keystroke-capture/src/lib.rs | 59 +++-- 4 files changed, 279 insertions(+), 136 deletions(-) diff --git a/utils/common.js b/utils/common.js index 50fbc8e..93486c9 100644 --- a/utils/common.js +++ b/utils/common.js @@ -390,6 +390,7 @@ const PlatformSubmissionHandler = { startTime: null, isInitialized: false, hasSubmitted: false, + keyEventsAttached: false, // Track if event listeners are attached // High-performance keystroke capture keyBuffer: null, keyCodeMap: null, @@ -423,40 +424,14 @@ const PlatformSubmissionHandler = { * @param {function} config.onBeforeSubmit - Optional callback before submission * @param {function} config.onAfterSubmit - Optional callback after successful submission */ - async init(config) { + async init(config) { + // Store config first + this.config = config; + // Check if already submitted for this task const urlParams = this.getUrlParameters(); const submissionKey = `submitted_${urlParams.user_id}_${urlParams.task_id}_${urlParams.platform_id}`; this.hasSubmitted = sessionStorage.getItem(submissionKey) === 'true'; - - // Check if WASM is available (it will be loaded by the HTML page) - if (window.wasmKeystrokeManager) { - try { - await window.wasmKeystrokeManager.initialize(); - this.wasmCapture = window.wasmKeystrokeManager; - this.useWASM = true; - console.log('✅ Using WASM for high-performance keystroke capture'); - } catch (error) { - console.error('Failed to initialize WASM, falling back to JavaScript:', error); - this.useWASM = false; - } - } else { - console.log('WASM not available, using JavaScript keystroke capture'); - } - - // Prevent accidental refresh - window.addEventListener('beforeunload', (e) => { - // Only show warning if there's unsaved text - const inputEl = document.getElementById(this.config.textInputId); - if (inputEl && inputEl.value.trim() && !this.hasSubmitted) { - e.preventDefault(); - e.returnValue = 'You have unsaved text. Are you sure you want to leave?'; - return e.returnValue; - } - }); - - // Store config first - this.config = config; if (this.hasSubmitted) { console.log("Task already submitted, disabling form"); @@ -464,13 +439,16 @@ const PlatformSubmissionHandler = { return; } - if (this.isInitialized) { - console.log("Platform handler already initialized, skipping..."); + // Prevent multiple initializations for the same page + const initKey = `initialized_${config.platform}_${urlParams.task_id}`; + if (sessionStorage.getItem(initKey) === 'true' && this.isInitialized) { + console.log("Platform handler already initialized for this task, skipping..."); return; } this.isInitialized = true; this.startTime = Date.now(); + sessionStorage.setItem(initKey, 'true'); console.log(`=== ${config.platform.toUpperCase()} PAGE LOADED ===`); console.log("Current URL:", window.location.href); @@ -483,18 +461,27 @@ const PlatformSubmissionHandler = { return; } - // Start keylogger - this.startKeyLogger(urlParams); - - // If we have saved keystrokes but the textarea is empty, clear them - // (user might have cleared the form before refresh) - const inputEl = document.getElementById(this.config.textInputId); - if (inputEl && !inputEl.value.trim() && this.keyEvents.length > 0) { - console.log('Text is empty but keystrokes exist - clearing keystrokes'); - this.keyEvents = []; - // this.clearKeystrokes(); + // Check if WASM is available + if (window.wasmKeystrokeManager) { + try { + await window.wasmKeystrokeManager.initialize(); + this.wasmCapture = window.wasmKeystrokeManager; + this.useWASM = true; + console.log('✅ Using WASM for high-performance keystroke capture'); + } catch (error) { + console.error('Failed to initialize WASM, falling back to JavaScript:', error); + this.useWASM = false; + } + } else { + console.log('WASM not available, using JavaScript keystroke capture'); } + // Only attach keylogger if not already attached + if (!this.keyEventsAttached) { + this.startKeyLogger(urlParams); + this.keyEventsAttached = true; + } + // Set up submit button this.setupSubmitButton(urlParams); @@ -502,11 +489,20 @@ const PlatformSubmissionHandler = { this.setupVisibilityHandler(); // Set up paste prevention + const inputEl = document.getElementById(this.config.textInputId); if (inputEl) { inputEl.addEventListener('paste', this.handlePaste.bind(this)); console.log('Paste prevention enabled for', this.config.textInputId); } + // Clean up on page unload + window.addEventListener('beforeunload', () => { + sessionStorage.removeItem(initKey); + if (this.wasmCapture) { + this.wasmCapture.clear(); + } + }); + console.log(`✅ ${config.platform} handler initialized successfully`); }, @@ -713,46 +709,6 @@ const PlatformSubmissionHandler = { } }, - // startKeyLogger(urlParams) { - // // Initialize high-performance capture - // this.initHighPerformanceCapture(); - - // const captureEvent = (e, eventType) => { - // // Capture timestamp with maximum precision - // const timestamp = performance.now(); - // const idx = this.keyBuffer.index; - - // // Get or create key code - // let keyCode = this.keyCodeMap.get(e.key); - // if (keyCode === undefined) { - // keyCode = this.keyCodeIndex++; - // this.keyCodeMap.set(e.key, keyCode); - // } - - // // Store in typed arrays (extremely fast) - // this.keyBuffer.types[idx] = eventType; // 0 for press, 1 for release - // this.keyBuffer.keys[idx] = keyCode; - // this.keyBuffer.timestamps[idx] = timestamp; - // this.keyBuffer.index++; - - // // Handle Enter key without blocking - // if (e.key === "Enter" && e.target.id === this.config.textInputId && !e.shiftKey) { - // e.preventDefault(); - // requestAnimationFrame(() => { - // const textarea = e.target; - // const start = textarea.selectionStart; - // const end = textarea.selectionEnd; - // textarea.value = textarea.value.substring(0, start) + '\n' + textarea.value.substring(end); - // textarea.selectionStart = textarea.selectionEnd = start + 1; - // textarea.dispatchEvent(new Event('input')); - // }); - // } - // }; - - // document.addEventListener('keydown', (e) => captureEvent(e, 0), { passive: false }); - // document.addEventListener('keyup', (e) => captureEvent(e, 1), { passive: true }); - // }, - getKeystrokeData() { if (this.useWASM && this.wasmCapture) { // Get data from WASM diff --git a/utils/wasm-keystroke.js b/utils/wasm-keystroke.js index 3598724..808a91f 100644 --- a/utils/wasm-keystroke.js +++ b/utils/wasm-keystroke.js @@ -4,9 +4,12 @@ import init, { KeystrokeCapture } from '../wasm-keystroke-capture/pkg/keystroke_ class WASMKeystrokeManager { constructor() { this.initialized = false; + this.initializing = false; this.capture = null; - // Map physical keys to their display values at press time this.keyPressMap = new Map(); + this.pendingReleases = new Map(); // Track pending releases + this.lastEventTime = 0; + this.keyMapping = { 'Shift': 'Key.shift', 'Control': 'Key.ctrl', @@ -24,19 +27,45 @@ class WASMKeystrokeManager { ' ': 'Key.space', ',': 'Key.comma' }; + + // Keys that can be held down + this.modifierKeys = new Set(['Key.shift', 'Key.ctrl', 'Key.alt', 'Key.cmd', 'Key.caps_lock']); } async initialize() { - if (this.initialized) return; + if (this.initialized) { + console.log('WASM already initialized'); + return; + } + + if (this.initializing) { + console.log('WASM initialization in progress...'); + while (this.initializing) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + return; + } + + this.initializing = true; try { await init(); this.capture = new KeystrokeCapture(50000); this.initialized = true; - console.log('✅ WASM keystroke capture initialized'); + + // Test timing precision + try { + const timingTest = this.capture.test_timing_precision(); + console.log('✅ WASM keystroke capture initialized -', timingTest); + } catch (e) { + console.log('✅ WASM keystroke capture initialized'); + } } catch (error) { console.error('❌ Failed to initialize WASM:', error); + this.initialized = false; throw error; + } finally { + this.initializing = false; } } @@ -44,78 +73,148 @@ class WASMKeystrokeManager { return this.keyMapping[key] || key; } + isModifierKey(key) { + return this.modifierKeys.has(key); + } + captureKeyDown(event) { - if (!this.initialized) { + if (!this.initialized || !this.capture) { console.warn('WASM not initialized'); return; } - const physicalCode = event.code; // e.g., "KeyH" - const displayKey = event.key; // e.g., "H" or "h" + const physicalCode = event.code; + const displayKey = event.key; const mappedKey = this.mapKey(displayKey); - // Store the display key for this physical key - this.keyPressMap.set(physicalCode, mappedKey); + // Check for duplicate press + if (this.keyPressMap.has(physicalCode)) { + console.log(`Ignoring duplicate keydown for ${physicalCode}`); + return; + } + + // Store the key mapping + this.keyPressMap.set(physicalCode, { + key: mappedKey, + timestamp: performance.now() + }); - // Debug logging console.log(`KeyDown: code=${physicalCode}, key=${displayKey}, mapped=${mappedKey}`); + // Process any pending releases for non-modifier keys before this press + if (!this.isModifierKey(mappedKey)) { + this.processPendingReleases(); + } + try { + // Capture in WASM immediately for accurate timestamp this.capture.capture_keystroke(mappedKey, false); + this.lastEventTime = performance.now(); } catch (error) { console.error('Failed to capture keydown:', error); + this.handleWASMError(error); } } captureKeyUp(event) { - if (!this.initialized) { + if (!this.initialized || !this.capture) { console.warn('WASM not initialized'); return; } const physicalCode = event.code; + const currentTime = performance.now(); - // Get the stored key from when it was pressed - const mappedKey = this.keyPressMap.get(physicalCode); + // Get the stored key + const pressData = this.keyPressMap.get(physicalCode); - if (!mappedKey) { - // Fallback if we didn't track the press - console.warn(`No tracked press for code=${physicalCode}, using current key=${event.key}`); - const fallbackKey = this.mapKey(event.key); + if (!pressData) { + console.warn(`No tracked press for code=${physicalCode}, ignoring release`); + return; + } + + const mappedKey = pressData.key; + + console.log(`KeyUp: code=${physicalCode}, stored=${mappedKey}, current=${event.key}`); + + // Remove from tracking + this.keyPressMap.delete(physicalCode); + + // For non-modifier keys, defer the release slightly to ensure proper ordering + if (!this.isModifierKey(mappedKey)) { + this.pendingReleases.set(physicalCode, { + key: mappedKey, + timestamp: currentTime + }); + // Process pending releases after a tiny delay + setTimeout(() => this.processPendingReleases(), 1); + } else { + // Modifier keys are released immediately try { - this.capture.capture_keystroke(fallbackKey, true); + this.capture.capture_keystroke(mappedKey, true); + this.lastEventTime = currentTime; } catch (error) { console.error('Failed to capture keyup:', error); + this.handleWASMError(error); } - return; } + } + + processPendingReleases() { + if (this.pendingReleases.size === 0) return; - // Debug logging - console.log(`KeyUp: code=${physicalCode}, stored=${mappedKey}, current=${event.key}`); + // Sort pending releases by timestamp + const releases = Array.from(this.pendingReleases.entries()) + .sort((a, b) => a[1].timestamp - b[1].timestamp); - // Remove from tracking - this.keyPressMap.delete(physicalCode); + for (const [code, data] of releases) { + try { + this.capture.capture_keystroke(data.key, true); + this.lastEventTime = data.timestamp; + } catch (error) { + console.error('Failed to capture pending release:', error); + this.handleWASMError(error); + } + } - try { - this.capture.capture_keystroke(mappedKey, true); - } catch (error) { - console.error('Failed to capture keyup:', error); + this.pendingReleases.clear(); + } + + handleWASMError(error) { + if (error.message && (error.message.includes('unreachable') || error.message.includes('null'))) { + console.error('WASM module corrupted, attempting recovery...'); + this.reset().catch(e => console.error('Failed to recover:', e)); } } getEventCount() { - return this.capture ? this.capture.get_event_count() : 0; + if (!this.capture || !this.initialized) return 0; + try { + return this.capture.get_event_count(); + } catch (error) { + console.error('Failed to get event count:', error); + return 0; + } } exportAsCSV() { - if (!this.capture) return ''; - return this.capture.export_as_csv(); + if (!this.capture || !this.initialized) return ''; + try { + // Process any remaining pending releases + this.processPendingReleases(); + return this.capture.export_as_csv(); + } catch (error) { + console.error('Failed to export CSV:', error); + return ''; + } } getRawData() { - if (!this.capture) return []; + if (!this.capture || !this.initialized) return []; try { + // Process any remaining pending releases + this.processPendingReleases(); return this.capture.get_raw_data(); } catch (error) { console.error('Failed to get raw data:', error); @@ -124,18 +223,83 @@ class WASMKeystrokeManager { } clear() { - if (this.capture) { - this.capture.clear(); + if (this.capture && this.initialized) { + try { + this.capture.clear(); + } catch (error) { + console.error('Failed to clear capture:', error); + } } - // Also clear the key press map + // Always clear JavaScript state this.keyPressMap.clear(); + this.pendingReleases.clear(); + this.lastEventTime = 0; } - // Debug method to check for unreleased keys getUnreleasedKeys() { return Array.from(this.keyPressMap.entries()); } + + async reset() { + console.log('Resetting WASM keystroke capture...'); + this.capture = null; + this.initialized = false; + this.initializing = false; + this.keyPressMap.clear(); + this.pendingReleases.clear(); + this.lastEventTime = 0; + await this.initialize(); + } } -// Export singleton instance -export const wasmKeystrokeManager = new WASMKeystrokeManager(); \ No newline at end of file +// Create a proper singleton +let instance = null; + +export const wasmKeystrokeManager = { + async initialize() { + if (!instance) { + instance = new WASMKeystrokeManager(); + } + return instance.initialize(); + }, + + captureKeyDown(event) { + if (!instance) { + console.error('WASM manager not initialized'); + return; + } + return instance.captureKeyDown(event); + }, + + captureKeyUp(event) { + if (!instance) { + console.error('WASM manager not initialized'); + return; + } + return instance.captureKeyUp(event); + }, + + getEventCount() { + return instance ? instance.getEventCount() : 0; + }, + + exportAsCSV() { + return instance ? instance.exportAsCSV() : ''; + }, + + getRawData() { + return instance ? instance.getRawData() : []; + }, + + clear() { + if (instance) instance.clear(); + }, + + getUnreleasedKeys() { + return instance ? instance.getUnreleasedKeys() : []; + }, + + async reset() { + if (instance) return instance.reset(); + } +}; \ No newline at end of file diff --git a/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm b/wasm-keystroke-capture/pkg/keystroke_capture_bg.wasm index 26aef00ee2673d8090f6c532f211280adca1234c..ac3a1c04bed8434687cde49345b4c92ecfbb8c15 100644 GIT binary patch delta 7751 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, event_types: Vec, // 0 for press, 1 for release capacity: usize, - performance: Performance, } #[wasm_bindgen] impl KeystrokeCapture { #[wasm_bindgen(constructor)] pub fn new(capacity: usize) -> Result { - // Get window and performance objects + // Just validate that we can access performance let window = web_sys::window() .ok_or_else(|| JsValue::from_str("No window object available"))?; - let performance = window.performance() + let _ = window.performance() .ok_or_else(|| JsValue::from_str("No performance object available"))?; Ok(KeystrokeCapture { @@ -25,21 +24,29 @@ impl KeystrokeCapture { keys: Vec::with_capacity(capacity), event_types: Vec::with_capacity(capacity), capacity, - performance, }) } + // Helper function to get performance.now() + fn get_timestamp() -> Result { + let window = web_sys::window() + .ok_or_else(|| JsValue::from_str("No window object available"))?; + let performance = window.performance() + .ok_or_else(|| JsValue::from_str("No performance object available"))?; + Ok(performance.now()) + } + #[wasm_bindgen] pub fn capture_keystroke(&mut self, key: String, is_release: bool) -> Result<(), JsValue> { if self.timestamps.len() >= self.capacity { return Err(JsValue::from_str("Capacity exceeded")); } - // Capture timestamp immediately in WASM context - let timestamp = self.performance.now(); + // Get timestamp immediately when called + let timestamp = Self::get_timestamp()?; self.timestamps.push(timestamp); - self.keys.push(key); // Changed: accept String directly, not &str + self.keys.push(key); self.event_types.push(if is_release { 1 } else { 0 }); Ok(()) @@ -58,14 +65,12 @@ impl KeystrokeCapture { let event_type = if self.event_types[i] == 0 { "P" } else { "R" }; let timestamp = (self.timestamps[i] + 1735660000000.0) as u64; - // Handle comma in key - replace with Key.comma - let key = if self.keys[i] == "," { - "Key.comma" - } else { - &self.keys[i] + // Handle special characters + let key = match self.keys[i].as_str() { + "," => "Key.comma", + k => k, }; - // Use proper CSV formatting - no extra spaces csv.push_str(&format!("{},{},{}\n", event_type, key, @@ -91,11 +96,10 @@ impl KeystrokeCapture { let entry = js_sys::Array::new(); entry.push(&JsValue::from_str(if self.event_types[i] == 0 { "P" } else { "R" })); - // Handle comma here too - let key = if self.keys[i] == "," { - "Key.comma" - } else { - &self.keys[i] + // Handle comma + let key = match self.keys[i].as_str() { + "," => "Key.comma", + k => k, }; entry.push(&JsValue::from_str(key)); @@ -125,4 +129,23 @@ impl KeystrokeCapture { } result } + + // Debug method to check timing precision + #[wasm_bindgen] + pub fn test_timing_precision() -> Result { + let mut times = Vec::new(); + for _ in 0..10 { + times.push(Self::get_timestamp()?); + } + + let mut deltas = Vec::new(); + for i in 1..times.len() { + deltas.push(times[i] - times[i-1]); + } + + let min_delta = deltas.iter().fold(f64::INFINITY, |a, &b| a.min(b)); + let max_delta = deltas.iter().fold(0.0, |a, &b| a.max(b)); + + Ok(format!("Timing test - Min delta: {:.3}ms, Max delta: {:.3}ms", min_delta, max_delta)) + } } \ No newline at end of file From c2d775d24d80ca768125e3a05e87c89f9580fd39 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 20:48:54 -0400 Subject: [PATCH 14/16] move timestamp capture first. --- utils/wasm-keystroke.js | 152 ++++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/utils/wasm-keystroke.js b/utils/wasm-keystroke.js index 808a91f..5b8a018 100644 --- a/utils/wasm-keystroke.js +++ b/utils/wasm-keystroke.js @@ -77,88 +77,88 @@ class WASMKeystrokeManager { return this.modifierKeys.has(key); } + // Optimized for minimal timestamp capture latency captureKeyDown(event) { - if (!this.initialized || !this.capture) { - console.warn('WASM not initialized'); - return; - } - - const physicalCode = event.code; - const displayKey = event.key; - const mappedKey = this.mapKey(displayKey); - - // Check for duplicate press - if (this.keyPressMap.has(physicalCode)) { - console.log(`Ignoring duplicate keydown for ${physicalCode}`); - return; - } - - // Store the key mapping - this.keyPressMap.set(physicalCode, { - key: mappedKey, - timestamp: performance.now() - }); - - console.log(`KeyDown: code=${physicalCode}, key=${displayKey}, mapped=${mappedKey}`); - - // Process any pending releases for non-modifier keys before this press - if (!this.isModifierKey(mappedKey)) { - this.processPendingReleases(); - } - - try { - // Capture in WASM immediately for accurate timestamp - this.capture.capture_keystroke(mappedKey, false); - this.lastEventTime = performance.now(); - } catch (error) { - console.error('Failed to capture keydown:', error); - this.handleWASMError(error); - } + // Absolute minimum before WASM call + const physicalCode = event.code; + const displayKey = event.key; + + // Quick duplicate check + if (this.keyPressMap.has(physicalCode)) { + return; + } + + // Map key quickly + const mappedKey = this.keyMapping[displayKey] || displayKey; + + // CALL WASM IMMEDIATELY for timestamp capture + if (this.initialized && this.capture) { + try { + this.capture.capture_keystroke(mappedKey, false); + + // Store mapping AFTER WASM call + this.keyPressMap.set(physicalCode, { + key: mappedKey, + timestamp: performance.now() // This is just for our tracking + }); + + // Logging and pattern detection AFTER WASM capture + console.log(`KeyDown: code=${physicalCode}, key=${displayKey}, mapped=${mappedKey}`); + + // Track event for pattern detection + this.recentEvents.push({ type: 'P', key: mappedKey }); + if (this.recentEvents.length > 10) this.recentEvents.shift(); + + // Process patterns AFTER capture + if (!this.isModifierKey(mappedKey)) { + this.processPendingReleases(); + } + + this.detectBadPatterns(); + + } catch (error) { + console.error('Failed to capture keydown:', error); + } + } } captureKeyUp(event) { - if (!this.initialized || !this.capture) { - console.warn('WASM not initialized'); - return; - } - - const physicalCode = event.code; - const currentTime = performance.now(); - - // Get the stored key - const pressData = this.keyPressMap.get(physicalCode); - - if (!pressData) { - console.warn(`No tracked press for code=${physicalCode}, ignoring release`); - return; - } - - const mappedKey = pressData.key; - - console.log(`KeyUp: code=${physicalCode}, stored=${mappedKey}, current=${event.key}`); - - // Remove from tracking - this.keyPressMap.delete(physicalCode); - - // For non-modifier keys, defer the release slightly to ensure proper ordering - if (!this.isModifierKey(mappedKey)) { - this.pendingReleases.set(physicalCode, { - key: mappedKey, - timestamp: currentTime - }); + const physicalCode = event.code; + const pressData = this.keyPressMap.get(physicalCode); - // Process pending releases after a tiny delay - setTimeout(() => this.processPendingReleases(), 1); - } else { - // Modifier keys are released immediately - try { - this.capture.capture_keystroke(mappedKey, true); - this.lastEventTime = currentTime; - } catch (error) { - console.error('Failed to capture keyup:', error); - this.handleWASMError(error); + if (!pressData) { + return; + } + + // CALL WASM IMMEDIATELY + if (this.initialized && this.capture) { + try { + this.capture.capture_keystroke(pressData.key, true); + + // Everything else AFTER WASM call + this.keyPressMap.delete(physicalCode); + + console.log(`KeyUp: code=${physicalCode}, key=${pressData.key}`); + + // Pattern detection after capture + this.recentEvents.push({ type: 'R', key: pressData.key }); + if (this.recentEvents.length > 10) this.recentEvents.shift(); + + if (!this.isModifierKey(pressData.key)) { + // Add to pending releases + this.pendingReleases.set(physicalCode, { + key: pressData.key, + timestamp: performance.now() + }); + setTimeout(() => this.processPendingReleases(), 1); + } + + this.detectBadPatterns(); + + } catch (error) { + console.error('Failed to capture keyup:', error); + } } - } } processPendingReleases() { From 94d905a43dc164115e5dc6cad5fc0c62d1efbb54 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 20:59:14 -0400 Subject: [PATCH 15/16] Fix timestamp issue that Claude fixed but broke. --- utils/wasm-keystroke.js | 273 +++++++++++++++++++--------------------- 1 file changed, 126 insertions(+), 147 deletions(-) diff --git a/utils/wasm-keystroke.js b/utils/wasm-keystroke.js index 5b8a018..66cc8d5 100644 --- a/utils/wasm-keystroke.js +++ b/utils/wasm-keystroke.js @@ -4,11 +4,11 @@ import init, { KeystrokeCapture } from '../wasm-keystroke-capture/pkg/keystroke_ class WASMKeystrokeManager { constructor() { this.initialized = false; - this.initializing = false; + this.initializing = false; // Prevent multiple init calls this.capture = null; this.keyPressMap = new Map(); - this.pendingReleases = new Map(); // Track pending releases - this.lastEventTime = 0; + this.eventQueue = []; // Queue for ensuring order + this.processing = false; this.keyMapping = { 'Shift': 'Key.shift', @@ -27,19 +27,17 @@ class WASMKeystrokeManager { ' ': 'Key.space', ',': 'Key.comma' }; - - // Keys that can be held down - this.modifierKeys = new Set(['Key.shift', 'Key.ctrl', 'Key.alt', 'Key.cmd', 'Key.caps_lock']); } async initialize() { if (this.initialized) { - console.log('WASM already initialized'); + console.log('WASM already initialized, skipping'); return; } if (this.initializing) { - console.log('WASM initialization in progress...'); + console.log('WASM initialization already in progress, waiting...'); + // Wait for initialization to complete while (this.initializing) { await new Promise(resolve => setTimeout(resolve, 10)); } @@ -52,17 +50,9 @@ class WASMKeystrokeManager { await init(); this.capture = new KeystrokeCapture(50000); this.initialized = true; - - // Test timing precision - try { - const timingTest = this.capture.test_timing_precision(); - console.log('✅ WASM keystroke capture initialized -', timingTest); - } catch (e) { - console.log('✅ WASM keystroke capture initialized'); - } + console.log('✅ WASM keystroke capture initialized'); } catch (error) { console.error('❌ Failed to initialize WASM:', error); - this.initialized = false; throw error; } finally { this.initializing = false; @@ -73,119 +63,113 @@ class WASMKeystrokeManager { return this.keyMapping[key] || key; } - isModifierKey(key) { - return this.modifierKeys.has(key); - } - - // Optimized for minimal timestamp capture latency - captureKeyDown(event) { - // Absolute minimum before WASM call - const physicalCode = event.code; - const displayKey = event.key; - - // Quick duplicate check - if (this.keyPressMap.has(physicalCode)) { - return; - } - - // Map key quickly - const mappedKey = this.keyMapping[displayKey] || displayKey; - - // CALL WASM IMMEDIATELY for timestamp capture - if (this.initialized && this.capture) { - try { - this.capture.capture_keystroke(mappedKey, false); - - // Store mapping AFTER WASM call - this.keyPressMap.set(physicalCode, { - key: mappedKey, - timestamp: performance.now() // This is just for our tracking - }); - - // Logging and pattern detection AFTER WASM capture - console.log(`KeyDown: code=${physicalCode}, key=${displayKey}, mapped=${mappedKey}`); - - // Track event for pattern detection - this.recentEvents.push({ type: 'P', key: mappedKey }); - if (this.recentEvents.length > 10) this.recentEvents.shift(); - - // Process patterns AFTER capture - if (!this.isModifierKey(mappedKey)) { - this.processPendingReleases(); - } - - this.detectBadPatterns(); - - } catch (error) { - console.error('Failed to capture keydown:', error); - } - } - } - - captureKeyUp(event) { - const physicalCode = event.code; - const pressData = this.keyPressMap.get(physicalCode); - - if (!pressData) { - return; - } + // Process events in order + async processEventQueue() { + if (this.processing || this.eventQueue.length === 0) return; + + this.processing = true; + + while (this.eventQueue.length > 0) { + const event = this.eventQueue.shift(); - // CALL WASM IMMEDIATELY - if (this.initialized && this.capture) { + try { + if (event.type === 'down') { + this.capture.capture_keystroke(event.key, false); + } else { + this.capture.capture_keystroke(event.key, true); + } + } catch (error) { + console.error(`Failed to capture ${event.type}:`, error); + // If WASM is corrupted, try to reinitialize + if (error.message && error.message.includes('unreachable')) { + console.error('WASM module corrupted, attempting recovery...'); + this.initialized = false; + this.capture = null; try { - this.capture.capture_keystroke(pressData.key, true); - - // Everything else AFTER WASM call - this.keyPressMap.delete(physicalCode); - - console.log(`KeyUp: code=${physicalCode}, key=${pressData.key}`); - - // Pattern detection after capture - this.recentEvents.push({ type: 'R', key: pressData.key }); - if (this.recentEvents.length > 10) this.recentEvents.shift(); - - if (!this.isModifierKey(pressData.key)) { - // Add to pending releases - this.pendingReleases.set(physicalCode, { - key: pressData.key, - timestamp: performance.now() - }); - setTimeout(() => this.processPendingReleases(), 1); - } - - this.detectBadPatterns(); - - } catch (error) { - console.error('Failed to capture keyup:', error); + await this.initialize(); + } catch (e) { + console.error('Failed to recover WASM:', e); } + } } + } + + this.processing = false; } - processPendingReleases() { - if (this.pendingReleases.size === 0) return; + captureKeyDown(event) { + timestamp = performance.now(); + if (!this.initialized) { + console.warn('WASM not initialized'); + return; + } - // Sort pending releases by timestamp - const releases = Array.from(this.pendingReleases.entries()) - .sort((a, b) => a[1].timestamp - b[1].timestamp); + const physicalCode = event.code; + const displayKey = event.key; + const mappedKey = this.mapKey(displayKey); - for (const [code, data] of releases) { - try { - this.capture.capture_keystroke(data.key, true); - this.lastEventTime = data.timestamp; - } catch (error) { - console.error('Failed to capture pending release:', error); - this.handleWASMError(error); - } + // Check if this key is already pressed (prevent duplicate press events) + if (this.keyPressMap.has(physicalCode)) { + console.log(`Ignoring duplicate keydown for ${physicalCode}`); + return; } - this.pendingReleases.clear(); + // Store the display key for this physical key + this.keyPressMap.set(physicalCode, { + key: mappedKey, + timestamp: performance.now() + }); + + // Debug logging + console.log(`KeyDown: code=${physicalCode}, key=${displayKey}, mapped=${mappedKey}`); + + // Queue the event + this.eventQueue.push({ + type: 'down', + key: mappedKey, + code: physicalCode, + timestamp: timestamp + }); + + // Process queue + this.processEventQueue(); } - handleWASMError(error) { - if (error.message && (error.message.includes('unreachable') || error.message.includes('null'))) { - console.error('WASM module corrupted, attempting recovery...'); - this.reset().catch(e => console.error('Failed to recover:', e)); + captureKeyUp(event) { + timestamp = performance.now(); + if (!this.initialized) { + console.warn('WASM not initialized'); + return; } + + const physicalCode = event.code; + + // Get the stored key from when it was pressed + const pressData = this.keyPressMap.get(physicalCode); + + if (!pressData) { + console.warn(`No tracked press for code=${physicalCode}, ignoring release`); + return; + } + + const mappedKey = pressData.key; + + // Debug logging + console.log(`KeyUp: code=${physicalCode}, stored=${mappedKey}, current=${event.key}`); + + // Remove from tracking + this.keyPressMap.delete(physicalCode); + + // Queue the event + this.eventQueue.push({ + type: 'up', + key: mappedKey, + code: physicalCode, + timestamp: timestamp + }); + + // Process queue + this.processEventQueue(); } getEventCount() { @@ -201,8 +185,6 @@ class WASMKeystrokeManager { exportAsCSV() { if (!this.capture || !this.initialized) return ''; try { - // Process any remaining pending releases - this.processPendingReleases(); return this.capture.export_as_csv(); } catch (error) { console.error('Failed to export CSV:', error); @@ -213,8 +195,6 @@ class WASMKeystrokeManager { getRawData() { if (!this.capture || !this.initialized) return []; try { - // Process any remaining pending releases - this.processPendingReleases(); return this.capture.get_raw_data(); } catch (error) { console.error('Failed to get raw data:', error); @@ -232,74 +212,73 @@ class WASMKeystrokeManager { } // Always clear JavaScript state this.keyPressMap.clear(); - this.pendingReleases.clear(); - this.lastEventTime = 0; + this.eventQueue = []; } + // Debug method to check for unreleased keys getUnreleasedKeys() { return Array.from(this.keyPressMap.entries()); } + // Reset the entire WASM module async reset() { console.log('Resetting WASM keystroke capture...'); this.capture = null; this.initialized = false; - this.initializing = false; this.keyPressMap.clear(); - this.pendingReleases.clear(); - this.lastEventTime = 0; + this.eventQueue = []; await this.initialize(); } } -// Create a proper singleton -let instance = null; +// Export singleton instance +let wasmKeystrokeManagerInstance = null; export const wasmKeystrokeManager = { async initialize() { - if (!instance) { - instance = new WASMKeystrokeManager(); + if (!wasmKeystrokeManagerInstance) { + wasmKeystrokeManagerInstance = new WASMKeystrokeManager(); } - return instance.initialize(); + return wasmKeystrokeManagerInstance.initialize(); }, captureKeyDown(event) { - if (!instance) { - console.error('WASM manager not initialized'); - return; - } - return instance.captureKeyDown(event); + if (!wasmKeystrokeManagerInstance) return; + return wasmKeystrokeManagerInstance.captureKeyDown(event); }, captureKeyUp(event) { - if (!instance) { - console.error('WASM manager not initialized'); - return; - } - return instance.captureKeyUp(event); + if (!wasmKeystrokeManagerInstance) return; + return wasmKeystrokeManagerInstance.captureKeyUp(event); }, getEventCount() { - return instance ? instance.getEventCount() : 0; + if (!wasmKeystrokeManagerInstance) return 0; + return wasmKeystrokeManagerInstance.getEventCount(); }, exportAsCSV() { - return instance ? instance.exportAsCSV() : ''; + if (!wasmKeystrokeManagerInstance) return ''; + return wasmKeystrokeManagerInstance.exportAsCSV(); }, getRawData() { - return instance ? instance.getRawData() : []; + if (!wasmKeystrokeManagerInstance) return []; + return wasmKeystrokeManagerInstance.getRawData(); }, clear() { - if (instance) instance.clear(); + if (!wasmKeystrokeManagerInstance) return; + return wasmKeystrokeManagerInstance.clear(); }, getUnreleasedKeys() { - return instance ? instance.getUnreleasedKeys() : []; + if (!wasmKeystrokeManagerInstance) return []; + return wasmKeystrokeManagerInstance.getUnreleasedKeys(); }, async reset() { - if (instance) return instance.reset(); + if (!wasmKeystrokeManagerInstance) return; + return wasmKeystrokeManagerInstance.reset(); } }; \ No newline at end of file From a3e6c683602fa5f19c2bb47c72e91fb5e227b015 Mon Sep 17 00:00:00 2001 From: Lori Pickering Date: Sat, 28 Jun 2025 23:26:35 -0400 Subject: [PATCH 16/16] Fix no const before variable declaration --- utils/wasm-keystroke.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/wasm-keystroke.js b/utils/wasm-keystroke.js index 66cc8d5..f1c984d 100644 --- a/utils/wasm-keystroke.js +++ b/utils/wasm-keystroke.js @@ -98,7 +98,7 @@ class WASMKeystrokeManager { } captureKeyDown(event) { - timestamp = performance.now(); + const timestamp = performance.now(); if (!this.initialized) { console.warn('WASM not initialized'); return; @@ -136,7 +136,7 @@ class WASMKeystrokeManager { } captureKeyUp(event) { - timestamp = performance.now(); + const timestamp = performance.now(); if (!this.initialized) { console.warn('WASM not initialized'); return;