refactor: migrate extension to TypeScript and add Material-UI components
							
								
								
									
										21
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| BROWSER ?= firefox | ||||
|  | ||||
| .PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client build-wasm-app | ||||
| .PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client build-wasm-app build-hero-vault-extension | ||||
|  | ||||
| test-browser-all: test-browser-kvstore test-browser-vault test-browser-evm-client | ||||
|  | ||||
| @@ -25,18 +25,7 @@ test-browser-evm-client: | ||||
| build-wasm-app: | ||||
| 	cd wasm_app && wasm-pack build --target web | ||||
|  | ||||
| # Build everything: wasm, copy, then extension | ||||
| build-extension-all: build-wasm-app | ||||
| 	cd extension && npm run build | ||||
|  | ||||
| # Build everything: wasm, copy, then extension | ||||
| build-vault-browser-ext: | ||||
| 	cd wasm_app && wasm-pack build --target web --out-dir ../vault_browser_ext/wasm_app/pkg | ||||
| 	cp vault_browser_ext/wasm_app/pkg/wasm_app.js vault_browser_ext/public/wasm/ | ||||
| 	cp vault_browser_ext/wasm_app/pkg/wasm_app_bg.wasm vault_browser_ext/public/wasm/ | ||||
| 	cd vault_browser_ext && npm install && npm run build | ||||
| 	cp vault_browser_ext/manifest.json vault_browser_ext/dist/ | ||||
| 	cp vault_browser_ext/*.png vault_browser_ext/dist/ | ||||
| 	mkdir -p vault_browser_ext/dist/src | ||||
| 	cp vault_browser_ext/sandbox.html vault_browser_ext/dist/ | ||||
| 	cp vault_browser_ext/sandbox.js vault_browser_ext/dist/ | ||||
| # Build Hero Vault extension: wasm, copy, then extension | ||||
| build-hero-vault-extension: | ||||
| 	cd wasm_app && wasm-pack build --target web | ||||
| 	cd hero_vault_extension && npm run build | ||||
							
								
								
									
										48
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| #!/bin/bash | ||||
| # Main build script for Hero Vault Extension | ||||
| # This script handles the complete build process in one step | ||||
|  | ||||
| set -e  # Exit on any error | ||||
|  | ||||
| # Colors for better readability | ||||
| GREEN="\033[0;32m" | ||||
| BLUE="\033[0;34m" | ||||
| RESET="\033[0m" | ||||
|  | ||||
| echo -e "${BLUE}=== Building Hero Vault Extension ===${RESET}" | ||||
|  | ||||
| # Step 1: Build the WASM package | ||||
| echo -e "${BLUE}Building WASM package...${RESET}" | ||||
| cd "$(dirname "$0")/wasm_app" || exit 1 | ||||
| wasm-pack build --target web | ||||
| echo -e "${GREEN}✓ WASM build successful!${RESET}" | ||||
|  | ||||
| # Step 2: Build the frontend extension | ||||
| echo -e "${BLUE}Building frontend extension...${RESET}" | ||||
| cd ../hero_vault_extension || exit 1 | ||||
|  | ||||
| # Copy WASM files to the extension's public directory | ||||
| echo "Copying WASM files..." | ||||
| mkdir -p public/wasm | ||||
| cp ../wasm_app/pkg/wasm_app* public/wasm/ | ||||
| cp ../wasm_app/pkg/*.d.ts public/wasm/ | ||||
| cp ../wasm_app/pkg/package.json public/wasm/ | ||||
|  | ||||
| # Build the extension without TypeScript checking | ||||
| echo "Building extension..." | ||||
| export NO_TYPECHECK=true | ||||
| npm run build | ||||
|  | ||||
| # Ensure the background script is properly built | ||||
| echo "Building background script..." | ||||
| node scripts/build-background.js | ||||
| echo -e "${GREEN}✓ Frontend build successful!${RESET}" | ||||
|  | ||||
| echo -e "${GREEN}=== Build Complete ===${RESET}" | ||||
| echo "Extension is ready in: $(pwd)/dist" | ||||
| echo "" | ||||
| echo -e "${BLUE}To load the extension in Chrome:${RESET}" | ||||
| echo "1. Go to chrome://extensions/" | ||||
| echo "2. Enable Developer mode (toggle in top-right)" | ||||
| echo "3. Click 'Load unpacked'" | ||||
| echo "4. Select the 'dist' directory: $(pwd)/dist" | ||||
| @@ -1,35 +0,0 @@ | ||||
| # Modular Vault Browser Extension | ||||
|  | ||||
| A cross-browser (Manifest V3) extension for secure cryptographic operations and Rhai scripting, powered by Rust/WASM. | ||||
|  | ||||
| ## Features | ||||
| - Session/keypair management | ||||
| - Cryptographic signing, encryption, and EVM actions | ||||
| - Secure WASM integration (signing only accessible from extension scripts) | ||||
| - React-based popup UI with dark mode | ||||
| - Future: WebSocket integration for remote scripting | ||||
|  | ||||
| ## Structure | ||||
| - `manifest.json`: Extension manifest (MV3, Chrome/Firefox) | ||||
| - `popup/`: React UI for user interaction | ||||
| - `background/`: Service worker for session, keypair, and WASM logic | ||||
| - `assets/`: Icons and static assets | ||||
|  | ||||
| ## Dev Workflow | ||||
| 1. Build Rust WASM: `wasm-pack build --target web --out-dir ../extension/wasm` | ||||
| 2. Install JS deps: `npm install` (from `extension/`) | ||||
| 3. Build popup: `npm run build` | ||||
| 4. Load `/extension` as an unpacked extension in your browser | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Security | ||||
| - WASM cryptographic APIs are only accessible from extension scripts (not content scripts or web pages). | ||||
| - All sensitive actions require explicit user approval. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## TODO | ||||
| - Implement background logic for session/keypair | ||||
| - Integrate popup UI with WASM APIs | ||||
| - Add WebSocket support (Phase 2) | ||||
| @@ -1,81 +0,0 @@ | ||||
| // Background service worker for Modular Vault Extension | ||||
| // Handles state persistence between popup sessions | ||||
|  | ||||
| console.log('Background service worker started'); | ||||
|  | ||||
| // Store session state locally for quicker access | ||||
| let sessionState = { | ||||
|   currentKeyspace: null, | ||||
|   keypairs: [], | ||||
|   selectedKeypair: null | ||||
| }; | ||||
|  | ||||
| // Initialize state from storage | ||||
| chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']) | ||||
|   .then(state => { | ||||
|     sessionState = { | ||||
|       currentKeyspace: state.currentKeyspace || null, | ||||
|       keypairs: state.keypairs || [], | ||||
|       selectedKeypair: state.selectedKeypair || null | ||||
|     }; | ||||
|     console.log('Session state loaded from storage:', sessionState); | ||||
|   }) | ||||
|   .catch(error => { | ||||
|     console.error('Failed to load session state:', error); | ||||
|   }); | ||||
|  | ||||
| // Handle messages from the popup | ||||
| chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|   console.log('Background received message:', message.action, message.type || ''); | ||||
|    | ||||
|   // Update session state | ||||
|   if (message.action === 'update_session') { | ||||
|     try { | ||||
|       const { type, data } = message; | ||||
|        | ||||
|       // Update our local state | ||||
|       if (type === 'keyspace') { | ||||
|         sessionState.currentKeyspace = data; | ||||
|       } else if (type === 'keypair_selected') { | ||||
|         sessionState.selectedKeypair = data; | ||||
|       } else if (type === 'keypair_added') { | ||||
|         sessionState.keypairs = [...sessionState.keypairs, data]; | ||||
|       } else if (type === 'keypairs_loaded') { | ||||
|         // Replace the entire keypair list with what came from the vault | ||||
|         console.log('Updating keypairs from vault:', data); | ||||
|         sessionState.keypairs = data; | ||||
|       } else if (type === 'session_locked') { | ||||
|         // When locking, we don't need to maintain keypairs in memory anymore | ||||
|         // since they'll be reloaded from the vault when unlocking | ||||
|         sessionState = { | ||||
|           currentKeyspace: null, | ||||
|           keypairs: [], // Clear keypairs from memory since they're in the vault | ||||
|           selectedKeypair: null | ||||
|         }; | ||||
|       } | ||||
|        | ||||
|       // Persist to storage | ||||
|       chrome.storage.local.set(sessionState) | ||||
|         .then(() => { | ||||
|           console.log('Updated session state in storage:', sessionState); | ||||
|           sendResponse({ success: true }); | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           console.error('Failed to persist session state:', error); | ||||
|           sendResponse({ success: false, error: error.message }); | ||||
|         }); | ||||
|        | ||||
|       return true; // Keep connection open for async response | ||||
|     } catch (error) { | ||||
|       console.error('Error in update_session message handler:', error); | ||||
|       sendResponse({ success: false, error: error.message }); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Get session state | ||||
|   if (message.action === 'get_session') { | ||||
|     sendResponse(sessionState); | ||||
|     return false; // No async response needed | ||||
|   } | ||||
| }); | ||||
| @@ -1,84 +0,0 @@ | ||||
| // Simple build script for browser extension | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
|  | ||||
| // Paths | ||||
| const sourceDir = __dirname; | ||||
| const distDir = path.join(sourceDir, 'dist'); | ||||
|  | ||||
| // Make sure the dist directory exists | ||||
| if (!fs.existsSync(distDir)) { | ||||
|   fs.mkdirSync(distDir, { recursive: true }); | ||||
| } | ||||
|  | ||||
| // Helper function to copy a file | ||||
| function copyFile(src, dest) { | ||||
|   // Create destination directory if it doesn't exist | ||||
|   const destDir = path.dirname(dest); | ||||
|   if (!fs.existsSync(destDir)) { | ||||
|     fs.mkdirSync(destDir, { recursive: true }); | ||||
|   } | ||||
|    | ||||
|   // Copy the file | ||||
|   fs.copyFileSync(src, dest); | ||||
|   console.log(`Copied: ${path.relative(sourceDir, src)} -> ${path.relative(sourceDir, dest)}`); | ||||
| } | ||||
|  | ||||
| // Helper function to copy an entire directory | ||||
| function copyDir(src, dest) { | ||||
|   // Create destination directory | ||||
|   if (!fs.existsSync(dest)) { | ||||
|     fs.mkdirSync(dest, { recursive: true }); | ||||
|   } | ||||
|    | ||||
|   // Get list of files | ||||
|   const files = fs.readdirSync(src); | ||||
|    | ||||
|   // Copy each file | ||||
|   for (const file of files) { | ||||
|     const srcPath = path.join(src, file); | ||||
|     const destPath = path.join(dest, file); | ||||
|      | ||||
|     const stat = fs.statSync(srcPath); | ||||
|      | ||||
|     if (stat.isDirectory()) { | ||||
|       // Recursively copy directories | ||||
|       copyDir(srcPath, destPath); | ||||
|     } else { | ||||
|       // Copy file | ||||
|       copyFile(srcPath, destPath); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Copy manifest | ||||
| copyFile( | ||||
|   path.join(sourceDir, 'manifest.json'), | ||||
|   path.join(distDir, 'manifest.json') | ||||
| ); | ||||
|  | ||||
| // Copy assets | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'assets'), | ||||
|   path.join(distDir, 'assets') | ||||
| ); | ||||
|  | ||||
| // Copy popup files | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'popup'), | ||||
|   path.join(distDir, 'popup') | ||||
| ); | ||||
|  | ||||
| // Copy background script | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'background'), | ||||
|   path.join(distDir, 'background') | ||||
| ); | ||||
|  | ||||
| // Copy WebAssembly files | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'wasm'), | ||||
|   path.join(distDir, 'wasm') | ||||
| ); | ||||
|  | ||||
| console.log('Build complete! Extension files copied to dist directory.'); | ||||
							
								
								
									
										70
									
								
								extension/dist/assets/popup.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								extension/dist/assets/wasm_app_bg.wasm
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										81
									
								
								extension/dist/background/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,81 +0,0 @@ | ||||
| // Background service worker for Modular Vault Extension | ||||
| // Handles state persistence between popup sessions | ||||
|  | ||||
| console.log('Background service worker started'); | ||||
|  | ||||
| // Store session state locally for quicker access | ||||
| let sessionState = { | ||||
|   currentKeyspace: null, | ||||
|   keypairs: [], | ||||
|   selectedKeypair: null | ||||
| }; | ||||
|  | ||||
| // Initialize state from storage | ||||
| chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']) | ||||
|   .then(state => { | ||||
|     sessionState = { | ||||
|       currentKeyspace: state.currentKeyspace || null, | ||||
|       keypairs: state.keypairs || [], | ||||
|       selectedKeypair: state.selectedKeypair || null | ||||
|     }; | ||||
|     console.log('Session state loaded from storage:', sessionState); | ||||
|   }) | ||||
|   .catch(error => { | ||||
|     console.error('Failed to load session state:', error); | ||||
|   }); | ||||
|  | ||||
| // Handle messages from the popup | ||||
| chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|   console.log('Background received message:', message.action, message.type || ''); | ||||
|    | ||||
|   // Update session state | ||||
|   if (message.action === 'update_session') { | ||||
|     try { | ||||
|       const { type, data } = message; | ||||
|        | ||||
|       // Update our local state | ||||
|       if (type === 'keyspace') { | ||||
|         sessionState.currentKeyspace = data; | ||||
|       } else if (type === 'keypair_selected') { | ||||
|         sessionState.selectedKeypair = data; | ||||
|       } else if (type === 'keypair_added') { | ||||
|         sessionState.keypairs = [...sessionState.keypairs, data]; | ||||
|       } else if (type === 'keypairs_loaded') { | ||||
|         // Replace the entire keypair list with what came from the vault | ||||
|         console.log('Updating keypairs from vault:', data); | ||||
|         sessionState.keypairs = data; | ||||
|       } else if (type === 'session_locked') { | ||||
|         // When locking, we don't need to maintain keypairs in memory anymore | ||||
|         // since they'll be reloaded from the vault when unlocking | ||||
|         sessionState = { | ||||
|           currentKeyspace: null, | ||||
|           keypairs: [], // Clear keypairs from memory since they're in the vault | ||||
|           selectedKeypair: null | ||||
|         }; | ||||
|       } | ||||
|        | ||||
|       // Persist to storage | ||||
|       chrome.storage.local.set(sessionState) | ||||
|         .then(() => { | ||||
|           console.log('Updated session state in storage:', sessionState); | ||||
|           sendResponse({ success: true }); | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           console.error('Failed to persist session state:', error); | ||||
|           sendResponse({ success: false, error: error.message }); | ||||
|         }); | ||||
|        | ||||
|       return true; // Keep connection open for async response | ||||
|     } catch (error) { | ||||
|       console.error('Error in update_session message handler:', error); | ||||
|       sendResponse({ success: false, error: error.message }); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Get session state | ||||
|   if (message.action === 'get_session') { | ||||
|     sendResponse(sessionState); | ||||
|     return false; // No async response needed | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										36
									
								
								extension/dist/manifest.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,36 +0,0 @@ | ||||
| { | ||||
|   "manifest_version": 3, | ||||
|   "name": "Modular Vault Extension", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Cross-browser modular vault for cryptographic operations and scripting.", | ||||
|   "action": { | ||||
|     "default_popup": "popup/index.html", | ||||
|     "default_icon": { | ||||
|       "16": "assets/icon-16.png", | ||||
|       "32": "assets/icon-32.png", | ||||
|       "48": "assets/icon-48.png", | ||||
|       "128": "assets/icon-128.png" | ||||
|     } | ||||
|   }, | ||||
|   "background": { | ||||
|     "service_worker": "background/index.js", | ||||
|     "type": "module" | ||||
|   }, | ||||
|   "permissions": [ | ||||
|     "storage", | ||||
|     "scripting" | ||||
|   ], | ||||
|   "host_permissions": [], | ||||
|   "icons": { | ||||
|     "16": "assets/icon-16.png", | ||||
|     "32": "assets/icon-32.png", | ||||
|     "48": "assets/icon-48.png", | ||||
|     "128": "assets/icon-128.png" | ||||
|   }, | ||||
|   "web_accessible_resources": [ | ||||
|     { | ||||
|       "resources": ["wasm/*.wasm", "wasm/*.js"], | ||||
|       "matches": ["<all_urls>"] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										117
									
								
								extension/dist/popup/popup.css
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,117 +0,0 @@ | ||||
| /* Basic styles for the extension popup */ | ||||
| body { | ||||
|   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   width: 350px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   font-size: 18px; | ||||
|   margin: 0 0 15px 0; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
|   padding-bottom: 10px; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|   font-size: 16px; | ||||
|   margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .form-section { | ||||
|   margin-bottom: 20px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 8px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| .form-group { | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| label { | ||||
|   display: block; | ||||
|   margin-bottom: 5px; | ||||
|   font-size: 13px; | ||||
|   color: #9aa0a6; | ||||
| } | ||||
|  | ||||
| input, textarea { | ||||
|   width: 100%; | ||||
|   padding: 8px; | ||||
|   border: 1px solid #3c4043; | ||||
|   border-radius: 4px; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| textarea { | ||||
|   min-height: 60px; | ||||
|   resize: vertical; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   background-color: #8ab4f8; | ||||
|   color: #202124; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   padding: 8px 16px; | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.3s; | ||||
| } | ||||
|  | ||||
| button:hover { | ||||
|   background-color: #669df6; | ||||
| } | ||||
|  | ||||
| button.small { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 12px; | ||||
| } | ||||
|  | ||||
| .button-group { | ||||
|   display: flex; | ||||
|   gap: 10px; | ||||
| } | ||||
|  | ||||
| .status { | ||||
|   margin: 10px 0; | ||||
|   padding: 8px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 4px; | ||||
|   font-size: 13px; | ||||
| } | ||||
|  | ||||
| .list { | ||||
|   margin-top: 10px; | ||||
|   max-height: 150px; | ||||
|   overflow-y: auto; | ||||
| } | ||||
|  | ||||
| .list-item { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 8px; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
| } | ||||
|  | ||||
| .list-item.selected { | ||||
|   background-color: rgba(138, 180, 248, 0.1); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .session-info { | ||||
|   margin-top: 15px; | ||||
| } | ||||
							
								
								
									
										765
									
								
								extension/dist/wasm/wasm_app.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,765 +0,0 @@ | ||||
| import * as __wbg_star0 from 'env'; | ||||
|  | ||||
| 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 getArrayU8FromWasm0(ptr, len) { | ||||
|     ptr = ptr >>> 0; | ||||
|     return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); | ||||
| } | ||||
|  | ||||
| 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; | ||||
| } | ||||
|  | ||||
| let cachedDataViewMemory0 = null; | ||||
|  | ||||
| function getDataViewMemory0() { | ||||
|     if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { | ||||
|         cachedDataViewMemory0 = new DataView(wasm.memory.buffer); | ||||
|     } | ||||
|     return cachedDataViewMemory0; | ||||
| } | ||||
|  | ||||
| const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') | ||||
|     ? { register: () => {}, unregister: () => {} } | ||||
|     : new FinalizationRegistry(state => { | ||||
|     wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b) | ||||
| }); | ||||
|  | ||||
| function makeMutClosure(arg0, arg1, dtor, f) { | ||||
|     const state = { a: arg0, b: arg1, cnt: 1, dtor }; | ||||
|     const real = (...args) => { | ||||
|         // First up with a closure we increment the internal reference | ||||
|         // count. This ensures that the Rust closure environment won't | ||||
|         // be deallocated while we're invoking it. | ||||
|         state.cnt++; | ||||
|         const a = state.a; | ||||
|         state.a = 0; | ||||
|         try { | ||||
|             return f(a, state.b, ...args); | ||||
|         } finally { | ||||
|             if (--state.cnt === 0) { | ||||
|                 wasm.__wbindgen_export_5.get(state.dtor)(a, state.b); | ||||
|                 CLOSURE_DTORS.unregister(state); | ||||
|             } else { | ||||
|                 state.a = a; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     real.original = state; | ||||
|     CLOSURE_DTORS.register(real, state, state); | ||||
|     return real; | ||||
| } | ||||
|  | ||||
| function debugString(val) { | ||||
|     // primitive types | ||||
|     const type = typeof val; | ||||
|     if (type == 'number' || type == 'boolean' || val == null) { | ||||
|         return  `${val}`; | ||||
|     } | ||||
|     if (type == 'string') { | ||||
|         return `"${val}"`; | ||||
|     } | ||||
|     if (type == 'symbol') { | ||||
|         const description = val.description; | ||||
|         if (description == null) { | ||||
|             return 'Symbol'; | ||||
|         } else { | ||||
|             return `Symbol(${description})`; | ||||
|         } | ||||
|     } | ||||
|     if (type == 'function') { | ||||
|         const name = val.name; | ||||
|         if (typeof name == 'string' && name.length > 0) { | ||||
|             return `Function(${name})`; | ||||
|         } else { | ||||
|             return 'Function'; | ||||
|         } | ||||
|     } | ||||
|     // objects | ||||
|     if (Array.isArray(val)) { | ||||
|         const length = val.length; | ||||
|         let debug = '['; | ||||
|         if (length > 0) { | ||||
|             debug += debugString(val[0]); | ||||
|         } | ||||
|         for(let i = 1; i < length; i++) { | ||||
|             debug += ', ' + debugString(val[i]); | ||||
|         } | ||||
|         debug += ']'; | ||||
|         return debug; | ||||
|     } | ||||
|     // Test for built-in | ||||
|     const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); | ||||
|     let className; | ||||
|     if (builtInMatches && builtInMatches.length > 1) { | ||||
|         className = builtInMatches[1]; | ||||
|     } else { | ||||
|         // Failed to match the standard '[object ClassName]' | ||||
|         return toString.call(val); | ||||
|     } | ||||
|     if (className == 'Object') { | ||||
|         // we're a user defined class or Object | ||||
|         // JSON.stringify avoids problems with cycles, and is generally much | ||||
|         // easier than looping through ownProperties of `val`. | ||||
|         try { | ||||
|             return 'Object(' + JSON.stringify(val) + ')'; | ||||
|         } catch (_) { | ||||
|             return 'Object'; | ||||
|         } | ||||
|     } | ||||
|     // errors | ||||
|     if (val instanceof Error) { | ||||
|         return `${val.name}: ${val.message}\n${val.stack}`; | ||||
|     } | ||||
|     // TODO we could test for more things here, like `Set`s and `Map`s. | ||||
|     return className; | ||||
| } | ||||
| /** | ||||
|  * Initialize the scripting environment (must be called before run_rhai) | ||||
|  */ | ||||
| export function init_rhai_env() { | ||||
|     wasm.init_rhai_env(); | ||||
| } | ||||
|  | ||||
| function takeFromExternrefTable0(idx) { | ||||
|     const value = wasm.__wbindgen_export_2.get(idx); | ||||
|     wasm.__externref_table_dealloc(idx); | ||||
|     return value; | ||||
| } | ||||
| /** | ||||
|  * Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
|  * @param {string} script | ||||
|  * @returns {any} | ||||
|  */ | ||||
| export function run_rhai(script) { | ||||
|     const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.run_rhai(ptr0, len0); | ||||
|     if (ret[2]) { | ||||
|         throw takeFromExternrefTable0(ret[1]); | ||||
|     } | ||||
|     return takeFromExternrefTable0(ret[0]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Initialize session with keyspace and password | ||||
|  * @param {string} keyspace | ||||
|  * @param {string} password | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export function init_session(keyspace, password) { | ||||
|     const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.init_session(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lock the session (zeroize password and session) | ||||
|  */ | ||||
| export function lock_session() { | ||||
|     wasm.lock_session(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all keypairs from the current session | ||||
|  * Returns an array of keypair objects with id, type, and metadata | ||||
|  * Select keypair for the session | ||||
|  * @param {string} key_id | ||||
|  */ | ||||
| export function select_keypair(key_id) { | ||||
|     const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.select_keypair(ptr0, len0); | ||||
|     if (ret[1]) { | ||||
|         throw takeFromExternrefTable0(ret[0]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * List keypairs in the current session's keyspace | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function list_keypairs() { | ||||
|     const ret = wasm.list_keypairs(); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Add a keypair to the current keyspace | ||||
|  * @param {string | null} [key_type] | ||||
|  * @param {string | null} [metadata] | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function add_keypair(key_type, metadata) { | ||||
|     var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len0 = WASM_VECTOR_LEN; | ||||
|     var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.add_keypair(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function passArray8ToWasm0(arg, malloc) { | ||||
|     const ptr = malloc(arg.length * 1, 1) >>> 0; | ||||
|     getUint8ArrayMemory0().set(arg, ptr / 1); | ||||
|     WASM_VECTOR_LEN = arg.length; | ||||
|     return ptr; | ||||
| } | ||||
| /** | ||||
|  * Sign message with current session | ||||
|  * @param {Uint8Array} message | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function sign(message) { | ||||
|     const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.sign(ptr0, len0); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_32(arg0, arg1, arg2) { | ||||
|     wasm.closure77_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_35(arg0, arg1, arg2) { | ||||
|     wasm.closure126_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_38(arg0, arg1, arg2) { | ||||
|     wasm.closure188_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_123(arg0, arg1, arg2, arg3) { | ||||
|     wasm.closure213_externref_shim(arg0, arg1, arg2, arg3); | ||||
| } | ||||
|  | ||||
| const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"]; | ||||
|  | ||||
| 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_buffer_609cc3eee51ed158 = function(arg0) { | ||||
|         const ret = arg0.buffer; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.call(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.call(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { | ||||
|         const ret = arg0.crypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) { | ||||
|         console.error(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.error; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) { | ||||
|         globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.getRandomValues(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) { | ||||
|         const ret = arg1[arg2 >>> 0]; | ||||
|         var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         var len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = Reflect.get(arg0, arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.get(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBDatabase; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBFactory; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBOpenDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) { | ||||
|         const ret = arg0.length; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { | ||||
|         const ret = arg0.msCrypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) { | ||||
|         try { | ||||
|             var state0 = {a: arg0, b: arg1}; | ||||
|             var cb0 = (arg0, arg1) => { | ||||
|                 const a = state0.a; | ||||
|                 state0.a = 0; | ||||
|                 try { | ||||
|                     return __wbg_adapter_123(a, state0.b, arg0, arg1); | ||||
|                 } finally { | ||||
|                     state0.a = a; | ||||
|                 } | ||||
|             }; | ||||
|             const ret = new Promise(cb0); | ||||
|             return ret; | ||||
|         } finally { | ||||
|             state0.a = state0.b = 0; | ||||
|         } | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_405e22f390576ce2 = function() { | ||||
|         const ret = new Object(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_78feb108b6472713 = function() { | ||||
|         const ret = new Array(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) { | ||||
|         const ret = new Function(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) { | ||||
|         const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { | ||||
|         const ret = arg0.node; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) { | ||||
|         const ret = arg0.objectStoreNames; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) { | ||||
|         const ret = arg0.process; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) { | ||||
|         const ret = arg0.push(arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.put(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.put(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) { | ||||
|         queueMicrotask(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) { | ||||
|         const ret = arg0.queueMicrotask; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.randomFillSync(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { | ||||
|         const ret = module.require; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) { | ||||
|         const ret = Promise.resolve(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.result; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) { | ||||
|         arg0.set(arg1, arg2 >>> 0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) { | ||||
|         arg0.onerror = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) { | ||||
|         arg0.onsuccess = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) { | ||||
|         arg0.onupgradeneeded = arg1; | ||||
|     }; | ||||
|     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.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) { | ||||
|         const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) { | ||||
|         const ret = arg0.target; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) { | ||||
|         const ret = arg0.then(arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { | ||||
|         const ret = arg0.versions; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_cb_drop = function(arg0) { | ||||
|         const obj = arg0.original; | ||||
|         if (obj.cnt-- == 1) { | ||||
|             obj.a = 0; | ||||
|             return true; | ||||
|         } | ||||
|         const ret = false; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { | ||||
|         const ret = debugString(arg1); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     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_function = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'function'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_null = function(arg0) { | ||||
|         const ret = arg0 === null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_object = function(arg0) { | ||||
|         const val = arg0; | ||||
|         const ret = typeof(val) === 'object' && val !== null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_string = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'string'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_undefined = function(arg0) { | ||||
|         const ret = arg0 === undefined; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { | ||||
|         const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { | ||||
|         const obj = arg1; | ||||
|         const ret = JSON.stringify(obj === undefined ? null : obj); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_memory = function() { | ||||
|         const ret = wasm.memory; | ||||
|         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)); | ||||
|     }; | ||||
|     imports['env'] = __wbg_star0; | ||||
|  | ||||
|     return imports; | ||||
| } | ||||
|  | ||||
| function __wbg_init_memory(imports, memory) { | ||||
|  | ||||
| } | ||||
|  | ||||
| function __wbg_finalize_init(instance, module) { | ||||
|     wasm = instance.exports; | ||||
|     __wbg_init.__wbindgen_wasm_module = module; | ||||
|     cachedDataViewMemory0 = null; | ||||
|     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('wasm_app_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; | ||||
							
								
								
									
										
											BIN
										
									
								
								extension/dist/wasm/wasm_app_bg.wasm
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,36 +0,0 @@ | ||||
| { | ||||
|   "manifest_version": 3, | ||||
|   "name": "Modular Vault Extension", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Cross-browser modular vault for cryptographic operations and scripting.", | ||||
|   "action": { | ||||
|     "default_popup": "popup/index.html", | ||||
|     "default_icon": { | ||||
|       "16": "assets/icon-16.png", | ||||
|       "32": "assets/icon-32.png", | ||||
|       "48": "assets/icon-48.png", | ||||
|       "128": "assets/icon-128.png" | ||||
|     } | ||||
|   }, | ||||
|   "background": { | ||||
|     "service_worker": "background/index.js", | ||||
|     "type": "module" | ||||
|   }, | ||||
|   "permissions": [ | ||||
|     "storage", | ||||
|     "scripting" | ||||
|   ], | ||||
|   "host_permissions": [], | ||||
|   "icons": { | ||||
|     "16": "assets/icon-16.png", | ||||
|     "32": "assets/icon-32.png", | ||||
|     "48": "assets/icon-48.png", | ||||
|     "128": "assets/icon-128.png" | ||||
|   }, | ||||
|   "web_accessible_resources": [ | ||||
|     { | ||||
|       "resources": ["wasm/*.wasm", "wasm/*.js"], | ||||
|       "matches": ["<all_urls>"] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										1474
									
								
								extension/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,21 +0,0 @@ | ||||
| { | ||||
|   "name": "modular-vault-extension", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Cross-browser modular vault extension with secure WASM integration and React UI.", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "vite --mode development", | ||||
|     "build": "vite build", | ||||
|     "build:ext": "node build.js" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@vitejs/plugin-react": "^4.4.1", | ||||
|     "react": "^18.3.1", | ||||
|     "react-dom": "^18.3.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "vite": "^4.5.0", | ||||
|     "vite-plugin-top-level-await": "^1.4.0", | ||||
|     "vite-plugin-wasm": "^3.4.1" | ||||
|   } | ||||
| } | ||||
| @@ -1,219 +0,0 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import KeyspaceManager from './KeyspaceManager'; | ||||
| import KeypairManager from './KeypairManager'; | ||||
| import SignMessage from './SignMessage'; | ||||
| import * as wasmHelper from './WasmHelper'; | ||||
|  | ||||
| function App() { | ||||
|   const [wasmState, setWasmState] = useState({ | ||||
|     loading: false, | ||||
|     initialized: false, | ||||
|     error: null | ||||
|   }); | ||||
|   const [locked, setLocked] = useState(true); | ||||
|   const [keyspaces, setKeyspaces] = useState([]); | ||||
|   const [currentKeyspace, setCurrentKeyspace] = useState(''); | ||||
|   const [keypairs, setKeypairs] = useState([]); // [{id, label, publicKey}] | ||||
|   const [selectedKeypair, setSelectedKeypair] = useState(''); | ||||
|   const [signature, setSignature] = useState(''); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [status, setStatus] = useState(''); | ||||
|  | ||||
|   // Load WebAssembly on component mount | ||||
|   useEffect(() => { | ||||
|     async function initWasm() { | ||||
|       try { | ||||
|         setStatus('Loading WebAssembly module...'); | ||||
|         await wasmHelper.loadWasmModule(); | ||||
|         setWasmState(wasmHelper.getWasmState()); | ||||
|         setStatus('WebAssembly module loaded'); | ||||
|         // Load session state | ||||
|         await refreshStatus(); | ||||
|       } catch (error) { | ||||
|         console.error('Failed to load WebAssembly:', error); | ||||
|         setStatus('Error loading WebAssembly: ' + (error.message || 'Unknown error')); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     initWasm(); | ||||
|   }, []); | ||||
|  | ||||
|   // Fetch status from background on mount | ||||
|   async function refreshStatus() { | ||||
|     const state = await wasmHelper.getSessionState(); | ||||
|     setCurrentKeyspace(state.currentKeyspace || ''); | ||||
|     setKeypairs(state.keypairs || []); | ||||
|     setSelectedKeypair(state.selectedKeypair || ''); | ||||
|     setLocked(!state.currentKeyspace); | ||||
|      | ||||
|     // For demo: collect all keyspaces from storage | ||||
|     if (state.keypairs && state.keypairs.length > 0) { | ||||
|       setKeyspaces([state.currentKeyspace]); | ||||
|     } else { | ||||
|       setKeyspaces([state.currentKeyspace].filter(Boolean)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Session unlock/create | ||||
|   const handleUnlock = async (keyspace, password) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Unlocking...'); | ||||
|     try { | ||||
|       await wasmHelper.initSession(keyspace, password); | ||||
|       setCurrentKeyspace(keyspace); | ||||
|       setLocked(false); | ||||
|       setStatus('Session unlocked!'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Unlock failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleCreateKeyspace = async (keyspace, password) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Creating keyspace...'); | ||||
|     try { | ||||
|       await wasmHelper.initSession(keyspace, password); | ||||
|       setCurrentKeyspace(keyspace); | ||||
|       setLocked(false); | ||||
|       setStatus('Keyspace created and unlocked!'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Create failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleLock = async () => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Locking...'); | ||||
|     try { | ||||
|       await wasmHelper.lockSession(); | ||||
|       setLocked(true); | ||||
|       setCurrentKeyspace(''); | ||||
|       setKeypairs([]); | ||||
|       setSelectedKeypair(''); | ||||
|       setStatus('Session locked.'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Lock failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleSelectKeypair = async (id) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Selecting keypair...'); | ||||
|     try { | ||||
|       await wasmHelper.selectKeypair(id); | ||||
|       setSelectedKeypair(id); | ||||
|       setStatus('Keypair selected.'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Select failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleCreateKeypair = async () => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Creating keypair...'); | ||||
|     try { | ||||
|       const keyId = await wasmHelper.addKeypair(); | ||||
|       setStatus('Keypair created. ID: ' + keyId); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Create failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleSign = async (message) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Signing message...'); | ||||
|     try { | ||||
|       if (!selectedKeypair) { | ||||
|         throw new Error('No keypair selected'); | ||||
|       } | ||||
|       const sig = await wasmHelper.sign(message); | ||||
|       setSignature(sig); | ||||
|       setStatus('Message signed!'); | ||||
|     } catch (e) { | ||||
|       setStatus('Signing failed: ' + e); | ||||
|       setSignature(''); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="App"> | ||||
|       <h1>Modular Vault Extension</h1> | ||||
|       {wasmState.error && ( | ||||
|         <div className="error"> | ||||
|           WebAssembly Error: {wasmState.error} | ||||
|         </div> | ||||
|       )} | ||||
|       <KeyspaceManager | ||||
|         keyspaces={keyspaces} | ||||
|         onUnlock={handleUnlock} | ||||
|         onCreate={handleCreateKeyspace} | ||||
|         locked={locked} | ||||
|         onLock={handleLock} | ||||
|         currentKeyspace={currentKeyspace} | ||||
|       /> | ||||
|       {!locked && ( | ||||
|         <> | ||||
|           <KeypairManager | ||||
|             keypairs={keypairs} | ||||
|             onSelect={handleSelectKeypair} | ||||
|             onCreate={handleCreateKeypair} | ||||
|             selectedKeypair={selectedKeypair} | ||||
|           /> | ||||
|           {selectedKeypair && ( | ||||
|             <SignMessage | ||||
|               onSign={handleSign} | ||||
|               signature={signature} | ||||
|               loading={loading} | ||||
|             /> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
|       <div className="status" style={{marginTop: '1rem', minHeight: 24}}> | ||||
|         {status} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
| @@ -1,30 +0,0 @@ | ||||
| import React, { useState } from 'react'; | ||||
|  | ||||
| export default function KeypairManager({ keypairs, onSelect, onCreate, selectedKeypair }) { | ||||
|   const [creating, setCreating] = useState(false); | ||||
|  | ||||
|   return ( | ||||
|     <div className="keypair-manager"> | ||||
|       <label>Keypair:</label> | ||||
|       <select value={selectedKeypair || ''} onChange={e => onSelect(e.target.value)}> | ||||
|         <option value="" disabled>Select keypair</option> | ||||
|         {keypairs.map(kp => ( | ||||
|           <option key={kp.id} value={kp.id}>{kp.label}</option> | ||||
|         ))} | ||||
|       </select> | ||||
|       <button onClick={() => setCreating(true)} style={{marginLeft: 8}}>Create New</button> | ||||
|       {creating && ( | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <button onClick={() => { onCreate(); setCreating(false); }}>Create Secp256k1 Keypair</button> | ||||
|           <button onClick={() => setCreating(false)} style={{marginLeft: 8}}>Cancel</button> | ||||
|         </div> | ||||
|       )} | ||||
|       {selectedKeypair && ( | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <span>Public Key: <code>{keypairs.find(kp => kp.id === selectedKeypair)?.publicKey}</code></span> | ||||
|           <button onClick={() => navigator.clipboard.writeText(keypairs.find(kp => kp.id === selectedKeypair)?.publicKey)} style={{marginLeft: 8}}>Copy</button> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
| import React, { useState } from 'react'; | ||||
|  | ||||
| export default function KeyspaceManager({ keyspaces, onUnlock, onCreate, locked, onLock, currentKeyspace }) { | ||||
|   const [selected, setSelected] = useState(keyspaces[0] || ''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [newKeyspace, setNewKeyspace] = useState(''); | ||||
|  | ||||
|   if (locked) { | ||||
|     return ( | ||||
|       <div className="keyspace-manager"> | ||||
|         <label>Keyspace:</label> | ||||
|         <select value={selected} onChange={e => setSelected(e.target.value)}> | ||||
|           {keyspaces.map(k => <option key={k} value={k}>{k}</option>)} | ||||
|         </select> | ||||
|         <button onClick={() => onUnlock(selected, password)} disabled={!selected || !password}>Unlock</button> | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <input placeholder="New keyspace name" value={newKeyspace} onChange={e => setNewKeyspace(e.target.value)} /> | ||||
|           <input placeholder="Password" type="password" value={password} onChange={e => setPassword(e.target.value)} /> | ||||
|           <button onClick={() => onCreate(newKeyspace, password)} disabled={!newKeyspace || !password}>Create</button> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|   return ( | ||||
|     <div className="keyspace-manager"> | ||||
|       <span>Keyspace: <b>{currentKeyspace}</b></span> | ||||
|       <button onClick={onLock} style={{marginLeft: 8}}>Lock Session</button> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| import React, { useState } from 'react'; | ||||
|  | ||||
| export default function SignMessage({ onSign, signature, loading }) { | ||||
|   const [message, setMessage] = useState(''); | ||||
|  | ||||
|   return ( | ||||
|     <div className="sign-message"> | ||||
|       <label>Message to sign:</label> | ||||
|       <input | ||||
|         type="text" | ||||
|         placeholder="Enter plaintext message" | ||||
|         value={message} | ||||
|         onChange={e => setMessage(e.target.value)} | ||||
|         style={{width: '100%', marginBottom: 8}} | ||||
|       /> | ||||
|       <button onClick={() => onSign(message)} disabled={!message || loading}> | ||||
|         {loading ? 'Signing...' : 'Sign'} | ||||
|       </button> | ||||
|       {signature && ( | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <span>Signature: <code>{signature}</code></span> | ||||
|           <button onClick={() => navigator.clipboard.writeText(signature)} style={{marginLeft: 8}}>Copy</button> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @@ -1,623 +0,0 @@ | ||||
| import init, * as wasmModuleImport from '@wasm/wasm_app.js'; | ||||
|  | ||||
| /** | ||||
|  * Browser extension-friendly WebAssembly loader and helper functions | ||||
|  * This handles loading the WebAssembly module without relying on ES modules | ||||
|  */ | ||||
|  | ||||
| // Global reference to the loaded WebAssembly module | ||||
| let wasmModule = null; | ||||
|  | ||||
| // Initialization state | ||||
| const state = { | ||||
|   loading: false, | ||||
|   initialized: false, | ||||
|   error: null | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Load the WebAssembly module | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export async function loadWasmModule() { | ||||
|   if (state.initialized || state.loading) { | ||||
|     return; | ||||
|   } | ||||
|   state.loading = true; | ||||
|   try { | ||||
|     await init(); | ||||
|     window.wasm_app = wasmModuleImport; | ||||
|  | ||||
|     // Debug logging for available functions in the WebAssembly module | ||||
|     console.log('Available WebAssembly functions:'); | ||||
|     console.log('init_rhai_env:', typeof window.init_rhai_env, typeof (window.wasm_app && window.wasm_app.init_rhai_env)); | ||||
|     console.log('init_session:', typeof window.init_session, typeof (window.wasm_app && window.wasm_app.init_session)); | ||||
|     console.log('lock_session:', typeof window.lock_session, typeof (window.wasm_app && window.wasm_app.lock_session)); | ||||
|     console.log('add_keypair:', typeof window.add_keypair, typeof (window.wasm_app && window.wasm_app.add_keypair)); | ||||
|     console.log('select_keypair:', typeof window.select_keypair, typeof (window.wasm_app && window.wasm_app.select_keypair)); | ||||
|     console.log('sign:', typeof window.sign, typeof (window.wasm_app && window.wasm_app.sign)); | ||||
|     console.log('run_rhai:', typeof window.run_rhai, typeof (window.wasm_app && window.wasm_app.run_rhai)); | ||||
|     console.log('list_keypairs:', typeof window.list_keypairs, typeof (window.wasm_app && window.wasm_app.list_keypairs)); | ||||
|      | ||||
|     // Store reference to all the exported functions | ||||
|     wasmModule = { | ||||
|       init_rhai_env: window.init_rhai_env || (window.wasm_app && window.wasm_app.init_rhai_env), | ||||
|       init_session: window.init_session || (window.wasm_app && window.wasm_app.init_session), | ||||
|       lock_session: window.lock_session || (window.wasm_app && window.wasm_app.lock_session), | ||||
|       add_keypair: window.add_keypair || (window.wasm_app && window.wasm_app.add_keypair), | ||||
|       select_keypair: window.select_keypair || (window.wasm_app && window.wasm_app.select_keypair), | ||||
|       sign: window.sign || (window.wasm_app && window.wasm_app.sign), | ||||
|       run_rhai: window.run_rhai || (window.wasm_app && window.wasm_app.run_rhai), | ||||
|       list_keypairs: window.list_keypairs || (window.wasm_app && window.wasm_app.list_keypairs), | ||||
|       list_keypairs_debug: window.list_keypairs_debug || (window.wasm_app && window.wasm_app.list_keypairs_debug), | ||||
|       check_indexeddb: window.check_indexeddb || (window.wasm_app && window.wasm_app.check_indexeddb) | ||||
|     }; | ||||
|  | ||||
|     // Log what was actually registered | ||||
|     console.log('Registered WebAssembly module functions:'); | ||||
|     for (const [key, value] of Object.entries(wasmModule)) { | ||||
|       console.log(`${key}: ${typeof value}`, value ? 'Available' : 'Missing'); | ||||
|     } | ||||
|  | ||||
|     // Initialize the WASM environment | ||||
|     if (typeof wasmModule.init_rhai_env === 'function') { | ||||
|       wasmModule.init_rhai_env(); | ||||
|     } | ||||
|     state.initialized = true; | ||||
|     console.log('WASM module loaded and initialized successfully'); | ||||
|   } catch (error) { | ||||
|     console.error('Failed to load WASM module:', error); | ||||
|     state.error = error.message || 'Unknown error loading WebAssembly module'; | ||||
|   } finally { | ||||
|     state.loading = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the current state of the WebAssembly module | ||||
|  * @returns {{loading: boolean, initialized: boolean, error: string|null}} | ||||
|  */ | ||||
| export function getWasmState() { | ||||
|   return { ...state }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the WebAssembly module | ||||
|  * @returns {object|null} The WebAssembly module or null if not loaded | ||||
|  */ | ||||
| export function getWasmModule() { | ||||
|   return wasmModule; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Debug function to check the vault state | ||||
|  * @returns {Promise<object>} State information | ||||
|  */ | ||||
| export async function debugVaultState() { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('🔍 Debugging vault state...'); | ||||
|      | ||||
|     // Check if we have a valid session using Rhai script | ||||
|     const sessionCheck = ` | ||||
|       let has_session = vault::has_active_session(); | ||||
|       let keyspace = ""; | ||||
|       if has_session { | ||||
|           keyspace = vault::get_current_keyspace(); | ||||
|       } | ||||
|        | ||||
|       // Return info about the session | ||||
|       { | ||||
|           "has_session": has_session, | ||||
|           "keyspace": keyspace | ||||
|       } | ||||
|     `; | ||||
|      | ||||
|     console.log('Checking session status...'); | ||||
|     const sessionStatus = await module.run_rhai(sessionCheck); | ||||
|     console.log('Session status:', sessionStatus); | ||||
|      | ||||
|     // Get keypair info if we have a session | ||||
|     if (sessionStatus && sessionStatus.has_session) { | ||||
|       const keypairsScript = ` | ||||
|         // Get all keypairs for the current keyspace | ||||
|         let keypairs = vault::list_keypairs(); | ||||
|          | ||||
|         // Add diagnostic information | ||||
|         let diagnostic = { | ||||
|           "keypair_count": keypairs.len(), | ||||
|           "keyspace": vault::get_current_keyspace(), | ||||
|           "keypairs": keypairs | ||||
|         }; | ||||
|          | ||||
|         diagnostic | ||||
|       `; | ||||
|        | ||||
|       console.log('Fetching keypair details...'); | ||||
|       const keypairDiagnostic = await module.run_rhai(keypairsScript); | ||||
|       console.log('Keypair diagnostic:', keypairDiagnostic); | ||||
|        | ||||
|       return keypairDiagnostic; | ||||
|     } | ||||
|      | ||||
|     return sessionStatus; | ||||
|   } catch (error) { | ||||
|     console.error('Error in debug function:', error); | ||||
|     return { error: error.toString() }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get keypairs from the vault | ||||
|  * @returns {Promise<Array>} List of keypairs | ||||
|  */ | ||||
| export async function getKeypairsFromVault() { | ||||
|   console.log('==============================================='); | ||||
|   console.log('Starting getKeypairsFromVault...'); | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     console.error('WebAssembly module not loaded!'); | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|   console.log('WebAssembly module:', module); | ||||
|   console.log('Module functions available:', Object.keys(module)); | ||||
|    | ||||
|   // Check if IndexedDB is available and working | ||||
|   const isIndexedDBAvailable = await checkIndexedDBAvailability(); | ||||
|   if (!isIndexedDBAvailable) { | ||||
|     console.warn('IndexedDB is not available or not working properly'); | ||||
|     // We'll continue, but this is likely why keypairs aren't persisting | ||||
|   } | ||||
|    | ||||
|   // Force re-initialization of the current session if needed | ||||
|   try { | ||||
|     // This checks if we have the debug function available | ||||
|     if (typeof module.list_keypairs_debug === 'function') { | ||||
|       console.log('Using debug function to diagnose keypair loading issues...'); | ||||
|       const debugResult = await module.list_keypairs_debug(); | ||||
|       console.log('Debug keypair listing result:', debugResult); | ||||
|       if (Array.isArray(debugResult) && debugResult.length > 0) { | ||||
|         console.log('Debug function returned keypairs:', debugResult); | ||||
|         // If debug function worked but regular function doesn't, use its result | ||||
|         return debugResult; | ||||
|       } else { | ||||
|         console.log('Debug function did not return keypairs, continuing with normal flow...'); | ||||
|       } | ||||
|     } | ||||
|   } catch (err) { | ||||
|     console.error('Error in debug function:', err); | ||||
|     // Continue with normal flow even if the debug function fails | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('-----------------------------------------------'); | ||||
|     console.log('Running diagnostics to check vault state...'); | ||||
|     // Run diagnostic first to log vault state | ||||
|     await debugVaultState(); | ||||
|     console.log('Diagnostics complete'); | ||||
|     console.log('-----------------------------------------------'); | ||||
|      | ||||
|     console.log('Checking if list_keypairs function is available:', typeof module.list_keypairs); | ||||
|     for (const key in module) { | ||||
|       console.log(`Module function: ${key} = ${typeof module[key]}`); | ||||
|     } | ||||
|     if (typeof module.list_keypairs !== 'function') { | ||||
|       console.error('list_keypairs function is not available in the WebAssembly module!'); | ||||
|       console.log('Available functions:', Object.keys(module)); | ||||
|       // Fall back to Rhai script | ||||
|       console.log('Falling back to using Rhai script for listing keypairs...'); | ||||
|       const script = ` | ||||
|         // Get all keypairs from the current keyspace | ||||
|         let keypairs = vault::list_keypairs(); | ||||
|         keypairs | ||||
|       `; | ||||
|       const keypairList = await module.run_rhai(script); | ||||
|       console.log('Retrieved keypairs from vault using Rhai:', keypairList); | ||||
|       return keypairList; | ||||
|     } | ||||
|      | ||||
|     console.log('Calling WebAssembly list_keypairs function...'); | ||||
|     // Use the direct list_keypairs function from WebAssembly instead of Rhai script | ||||
|     const keypairList = await module.list_keypairs(); | ||||
|     console.log('Retrieved keypairs from vault:', keypairList); | ||||
|      | ||||
|     console.log('Raw keypair list type:', typeof keypairList); | ||||
|     console.log('Is array?', Array.isArray(keypairList)); | ||||
|     console.log('Raw keypair list:', keypairList); | ||||
|      | ||||
|     // Format keypairs for UI | ||||
|     const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => { | ||||
|       // Parse metadata if available | ||||
|       let metadata = {}; | ||||
|       if (kp.metadata) { | ||||
|         try { | ||||
|           if (typeof kp.metadata === 'string') { | ||||
|             metadata = JSON.parse(kp.metadata); | ||||
|           } else { | ||||
|             metadata = kp.metadata; | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse keypair metadata:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         id: kp.id, | ||||
|         label: metadata.label || `Key-${kp.id.substring(0, 4)}` | ||||
|       }; | ||||
|     }) : []; | ||||
|      | ||||
|     console.log('Formatted keypairs for UI:', formattedKeypairs); | ||||
|      | ||||
|     // Update background service worker | ||||
|     return new Promise((resolve) => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: formattedKeypairs | ||||
|       }, (response) => { | ||||
|         console.log('Background response to keypairs update:', response); | ||||
|         resolve(formattedKeypairs); | ||||
|       }); | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error('Error fetching keypairs from vault:', error); | ||||
|     return []; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if IndexedDB is available and working | ||||
|  * @returns {Promise<boolean>} True if IndexedDB is working | ||||
|  */ | ||||
| export async function checkIndexedDBAvailability() { | ||||
|   console.log('Checking IndexedDB availability...'); | ||||
|    | ||||
|   // First check if IndexedDB is available in the browser | ||||
|   if (!window.indexedDB) { | ||||
|     console.error('IndexedDB is not available in this browser'); | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   const module = getWasmModule(); | ||||
|   if (!module || typeof module.check_indexeddb !== 'function') { | ||||
|     console.error('WebAssembly module or check_indexeddb function not available'); | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     const result = await module.check_indexeddb(); | ||||
|     console.log('IndexedDB check result:', result); | ||||
|     return true; | ||||
|   } catch (error) { | ||||
|     console.error('IndexedDB check failed:', error); | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Initialize a session with the given keyspace and password | ||||
|  * @param {string} keyspace  | ||||
|  * @param {string} password  | ||||
|  * @returns {Promise<Array>} List of keypairs after initialization | ||||
|  */ | ||||
| export async function initSession(keyspace, password) { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log(`Initializing session for keyspace: ${keyspace}`); | ||||
|      | ||||
|     // Check if IndexedDB is working | ||||
|     const isIndexedDBAvailable = await checkIndexedDBAvailability(); | ||||
|     if (!isIndexedDBAvailable) { | ||||
|       console.warn('IndexedDB is not available or not working properly. Keypairs might not persist.'); | ||||
|       // Continue anyway as we might fall back to memory storage | ||||
|     } | ||||
|      | ||||
|     // Initialize the session using the WASM module | ||||
|     await module.init_session(keyspace, password); | ||||
|     console.log('Session initialized successfully'); | ||||
|      | ||||
|     // Check if we have stored keypairs for this keyspace in Chrome storage | ||||
|     const storedKeypairs = await new Promise(resolve => { | ||||
|       chrome.storage.local.get([`keypairs:${keyspace}`], result => { | ||||
|         resolve(result[`keypairs:${keyspace}`] || []); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     console.log(`Found ${storedKeypairs.length} stored keypairs for keyspace ${keyspace}`); | ||||
|      | ||||
|     // Import stored keypairs into the WebAssembly session if they don't exist already | ||||
|     if (storedKeypairs.length > 0) { | ||||
|       console.log('Importing stored keypairs into WebAssembly session...'); | ||||
|        | ||||
|       // First get current keypairs from the vault directly | ||||
|       const wasmKeypairs = await module.list_keypairs(); | ||||
|       console.log('Current keypairs in WebAssembly vault:', wasmKeypairs); | ||||
|        | ||||
|       // Get the IDs of existing keypairs in the vault | ||||
|       const existingIds = new Set(wasmKeypairs.map(kp => kp.id)); | ||||
|        | ||||
|       // Import keypairs that don't already exist in the vault | ||||
|       for (const keypair of storedKeypairs) { | ||||
|         if (!existingIds.has(keypair.id)) { | ||||
|           console.log(`Importing keypair ${keypair.id} into WebAssembly vault...`); | ||||
|            | ||||
|           // Create metadata for the keypair | ||||
|           const metadata = JSON.stringify({ | ||||
|             label: keypair.label || `Key-${keypair.id.substring(0, 8)}`, | ||||
|             imported: true, | ||||
|             importDate: new Date().toISOString() | ||||
|           }); | ||||
|            | ||||
|           // For adding existing keypairs, we'd normally need the private key | ||||
|           // Since we can't retrieve it, we'll create a new one with the same label | ||||
|           // This is a placeholder - in a real implementation, you'd need to use the actual keys | ||||
|           try { | ||||
|             const keyType = keypair.type || 'Secp256k1'; | ||||
|             await module.add_keypair(keyType, metadata); | ||||
|             console.log(`Created keypair of type ${keyType} with label ${keypair.label}`); | ||||
|           } catch (err) { | ||||
|             console.warn(`Failed to import keypair ${keypair.id}:`, err); | ||||
|             // Continue with other keypairs even if one fails | ||||
|           } | ||||
|         } else { | ||||
|           console.log(`Keypair ${keypair.id} already exists in vault, skipping import`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Initialize session using WASM (await the async function) | ||||
|     await module.init_session(keyspace, password); | ||||
|  | ||||
|     // Get keypairs from the vault after session is ready | ||||
|     const currentKeypairs = await getKeypairsFromVault(); | ||||
|  | ||||
|     // Update keypairs in background service worker | ||||
|     await new Promise(resolve => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: currentKeypairs | ||||
|       }, response => { | ||||
|         console.log('Updated keypairs in background service worker'); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     return currentKeypairs; | ||||
|   } catch (error) { | ||||
|     console.error('Failed to initialize session:', error); | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lock the current session | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export async function lockSession() { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('Locking session...'); | ||||
|      | ||||
|     // First run diagnostics to see what we have before locking | ||||
|     await debugVaultState(); | ||||
|      | ||||
|     // Call the WASM lock_session function | ||||
|     module.lock_session(); | ||||
|     console.log('Session locked in WebAssembly module'); | ||||
|      | ||||
|     // Update session state in background | ||||
|     await new Promise((resolve, reject) => { | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'session_locked' | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           console.log('Background service worker updated for locked session'); | ||||
|           resolve(); | ||||
|         } else { | ||||
|           console.error('Failed to update session state in background:', response?.error); | ||||
|           reject(new Error(response?.error || 'Failed to update session state')); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Verify session is locked properly | ||||
|     const sessionStatus = await debugVaultState(); | ||||
|     console.log('Session status after locking:', sessionStatus); | ||||
|   } catch (error) { | ||||
|     console.error('Error locking session:', error); | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Add a new keypair | ||||
|  * @param {string} keyType The type of key to create (default: 'Secp256k1') | ||||
|  * @param {string} label Optional custom label for the keypair | ||||
|  * @returns {Promise<{id: string, label: string}>} The created keypair info | ||||
|  */ | ||||
| export async function addKeypair(keyType = 'Secp256k1', label = null) { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     // Get current keyspace | ||||
|     const sessionState = await getSessionState(); | ||||
|     const keyspace = sessionState.currentKeyspace; | ||||
|     if (!keyspace) { | ||||
|       throw new Error('No active keyspace'); | ||||
|     } | ||||
|      | ||||
|     // Generate default label if not provided | ||||
|     const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`; | ||||
|      | ||||
|     // Create metadata JSON | ||||
|     const metadata = JSON.stringify({ | ||||
|       label: keyLabel, | ||||
|       created: new Date().toISOString(), | ||||
|       type: keyType | ||||
|     }); | ||||
|      | ||||
|     console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`); | ||||
|     console.log('Keypair metadata:', metadata); | ||||
|      | ||||
|     // Call the WASM add_keypair function with metadata | ||||
|     // This will add the keypair to the WebAssembly vault | ||||
|     const keyId = await module.add_keypair(keyType, metadata); | ||||
|     console.log(`Keypair created with ID: ${keyId} in WebAssembly vault`); | ||||
|      | ||||
|     // Create keypair object for UI and storage | ||||
|     const newKeypair = {  | ||||
|       id: keyId,  | ||||
|       label: keyLabel, | ||||
|       type: keyType, | ||||
|       created: new Date().toISOString() | ||||
|     }; | ||||
|      | ||||
|     // Get the latest keypairs from the WebAssembly vault to ensure consistency | ||||
|     const vaultKeypairs = await module.list_keypairs(); | ||||
|     console.log('Current keypairs in vault after addition:', vaultKeypairs); | ||||
|      | ||||
|     // Format the vault keypairs for storage | ||||
|     const formattedVaultKeypairs = vaultKeypairs.map(kp => { | ||||
|       // Parse metadata if available | ||||
|       let metadata = {}; | ||||
|       if (kp.metadata) { | ||||
|         try { | ||||
|           if (typeof kp.metadata === 'string') { | ||||
|             metadata = JSON.parse(kp.metadata); | ||||
|           } else { | ||||
|             metadata = kp.metadata; | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse keypair metadata:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         id: kp.id, | ||||
|         label: metadata.label || `Key-${kp.id.substring(0, 8)}`, | ||||
|         type: kp.type || 'Secp256k1', | ||||
|         created: metadata.created || new Date().toISOString() | ||||
|       }; | ||||
|     }); | ||||
|      | ||||
|     // Save the formatted keypairs to Chrome storage | ||||
|     await new Promise(resolve => { | ||||
|       chrome.storage.local.set({ [`keypairs:${keyspace}`]: formattedVaultKeypairs }, () => { | ||||
|         console.log(`Saved ${formattedVaultKeypairs.length} keypairs to Chrome storage for keyspace ${keyspace}`); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Update session state in background with the new keypair information | ||||
|     await new Promise((resolve, reject) => { | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_added',  | ||||
|         data: newKeypair  | ||||
|       }, async (response) => { | ||||
|         if (response && response.success) { | ||||
|           console.log('Background service worker updated with new keypair'); | ||||
|           resolve(newKeypair); | ||||
|         } else { | ||||
|           const error = response?.error || 'Failed to update session state'; | ||||
|           console.error('Error updating background state:', error); | ||||
|           reject(new Error(error)); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Also update the complete keypair list in background with the current vault state | ||||
|     await new Promise(resolve => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: formattedVaultKeypairs | ||||
|       }, () => { | ||||
|         console.log('Updated complete keypair list in background with vault state'); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     return newKeypair; | ||||
|   } catch (error) { | ||||
|     console.error('Error adding keypair:', error); | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Select a keypair | ||||
|  * @param {string} keyId The ID of the keypair to select | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export async function selectKeypair(keyId) { | ||||
|   if (!wasmModule || !wasmModule.select_keypair) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   // Call the WASM select_keypair function | ||||
|   await wasmModule.select_keypair(keyId); | ||||
|    | ||||
|   // Update session state in background | ||||
|   await new Promise((resolve, reject) => { | ||||
|     chrome.runtime.sendMessage({  | ||||
|       action: 'update_session',  | ||||
|       type: 'keypair_selected',  | ||||
|       data: keyId | ||||
|     }, (response) => { | ||||
|       if (response && response.success) { | ||||
|         resolve(); | ||||
|       } else { | ||||
|         reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sign a message with the selected keypair | ||||
|  * @param {string} message The message to sign | ||||
|  * @returns {Promise<string>} The signature as a hex string | ||||
|  */ | ||||
| export async function sign(message) { | ||||
|   if (!wasmModule || !wasmModule.sign) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   // Convert message to Uint8Array | ||||
|   const encoder = new TextEncoder(); | ||||
|   const messageBytes = encoder.encode(message); | ||||
|    | ||||
|   // Call the WASM sign function | ||||
|   return await wasmModule.sign(messageBytes); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the current session state | ||||
|  * @returns {Promise<{currentKeyspace: string|null, keypairs: Array, selectedKeypair: string|null}>} | ||||
|  */ | ||||
| export async function getSessionState() { | ||||
|   return new Promise((resolve) => { | ||||
|     chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { | ||||
|       resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -1,88 +0,0 @@ | ||||
| import React, { useState, useEffect, createContext, useContext } from 'react'; | ||||
|  | ||||
| // Create a context to share the WASM module across components | ||||
| export const WasmContext = createContext(null); | ||||
|  | ||||
| // Hook to access WASM module | ||||
| export function useWasm() { | ||||
|   return useContext(WasmContext); | ||||
| } | ||||
|  | ||||
| // Component that loads and initializes the WASM module | ||||
| export function WasmProvider({ children }) { | ||||
|   const [wasmModule, setWasmModule] = useState(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState(null); | ||||
|    | ||||
|   useEffect(() => { | ||||
|     async function loadWasm() { | ||||
|       try { | ||||
|         setLoading(true); | ||||
|          | ||||
|         // Instead of using dynamic imports which require correct MIME types, | ||||
|         // we'll use fetch to load the JavaScript file as text and eval it | ||||
|         const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js'); | ||||
|         console.log('Loading WASM JS from:', wasmJsPath); | ||||
|          | ||||
|         // Load the JavaScript file | ||||
|         const jsResponse = await fetch(wasmJsPath); | ||||
|         if (!jsResponse.ok) { | ||||
|           throw new Error(`Failed to load WASM JS: ${jsResponse.status} ${jsResponse.statusText}`); | ||||
|         } | ||||
|          | ||||
|         // Get the JavaScript code as text | ||||
|         const jsCode = await jsResponse.text(); | ||||
|          | ||||
|         // Create a function to execute the code in an isolated scope | ||||
|         let wasmModuleExports = {}; | ||||
|         const moduleFunction = new Function('exports', jsCode + '\nreturn { initSync, default: __wbg_init, init_rhai_env, init_session, lock_session, add_keypair, select_keypair, sign, run_rhai };'); | ||||
|          | ||||
|         // Execute the function to get the exports | ||||
|         const wasmModule = moduleFunction(wasmModuleExports); | ||||
|          | ||||
|         // Initialize WASM with the binary | ||||
|         const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); | ||||
|         console.log('Initializing WASM with binary:', wasmBinaryPath); | ||||
|          | ||||
|         const binaryResponse = await fetch(wasmBinaryPath); | ||||
|         if (!binaryResponse.ok) { | ||||
|           throw new Error(`Failed to load WASM binary: ${binaryResponse.status} ${binaryResponse.statusText}`); | ||||
|         } | ||||
|          | ||||
|         const wasmBinary = await binaryResponse.arrayBuffer(); | ||||
|          | ||||
|         // Initialize the WASM module | ||||
|         await wasmModule.default(wasmBinary); | ||||
|          | ||||
|         // Initialize the WASM environment | ||||
|         if (typeof wasmModule.init_rhai_env === 'function') { | ||||
|           wasmModule.init_rhai_env(); | ||||
|         } | ||||
|          | ||||
|         console.log('WASM module loaded successfully'); | ||||
|         setWasmModule(wasmModule); | ||||
|         setLoading(false); | ||||
|       } catch (error) { | ||||
|         console.error('Failed to load WASM module:', error); | ||||
|         setError(error.message || 'Failed to load WebAssembly module'); | ||||
|         setLoading(false); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     loadWasm(); | ||||
|   }, []); | ||||
|    | ||||
|   if (loading) { | ||||
|     return <div className="wasm-loading">Loading WebAssembly module...</div>; | ||||
|   } | ||||
|    | ||||
|   if (error) { | ||||
|     return <div className="wasm-error">Error: {error}</div>; | ||||
|   } | ||||
|    | ||||
|   return ( | ||||
|     <WasmContext.Provider value={wasmModule}> | ||||
|       {children} | ||||
|     </WasmContext.Provider> | ||||
|   ); | ||||
| } | ||||
| @@ -1,88 +0,0 @@ | ||||
| /** | ||||
|  * Debug helper for WebAssembly Vault with Rhai scripts | ||||
|  */ | ||||
|  | ||||
| // Helper to try various Rhai scripts for debugging | ||||
| export const RHAI_SCRIPTS = { | ||||
|   // Check if there's an active session | ||||
|   CHECK_SESSION: ` | ||||
|     let has_session = false; | ||||
|     let current_keyspace = ""; | ||||
|      | ||||
|     // Try to access functions expected to exist in the vault namespace | ||||
|     if (isdef(vault) && isdef(vault::has_active_session)) { | ||||
|       has_session = vault::has_active_session(); | ||||
|       if (has_session && isdef(vault::get_current_keyspace)) { | ||||
|         current_keyspace = vault::get_current_keyspace(); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     { | ||||
|       "has_session": has_session, | ||||
|       "keyspace": current_keyspace, | ||||
|       "available_functions": [ | ||||
|         isdef(vault::list_keypairs) ? "list_keypairs" : null, | ||||
|         isdef(vault::add_keypair) ? "add_keypair" : null, | ||||
|         isdef(vault::has_active_session) ? "has_active_session" : null, | ||||
|         isdef(vault::get_current_keyspace) ? "get_current_keyspace" : null | ||||
|       ] | ||||
|     } | ||||
|   `, | ||||
|    | ||||
|   // Explicitly get keypairs for the current keyspace using session data | ||||
|   LIST_KEYPAIRS: ` | ||||
|     let result = {"error": "Not initialized"}; | ||||
|      | ||||
|     if (isdef(vault) && isdef(vault::has_active_session) && vault::has_active_session()) { | ||||
|       let keyspace = vault::get_current_keyspace(); | ||||
|        | ||||
|       // Try to list the keypairs from the current session | ||||
|       if (isdef(vault::get_keypairs_from_session)) { | ||||
|         result = { | ||||
|           "keyspace": keyspace, | ||||
|           "keypairs": vault::get_keypairs_from_session() | ||||
|         }; | ||||
|       } else { | ||||
|         result = { | ||||
|           "error": "vault::get_keypairs_from_session is not defined", | ||||
|           "keyspace": keyspace | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     result | ||||
|   `, | ||||
|    | ||||
|   // Use Rhai to inspect the Vault storage directly (for advanced debugging) | ||||
|   INSPECT_VAULT_STORAGE: ` | ||||
|     let result = {"error": "Not accessible"}; | ||||
|      | ||||
|     if (isdef(vault) && isdef(vault::inspect_storage)) { | ||||
|       result = vault::inspect_storage(); | ||||
|     } | ||||
|      | ||||
|     result | ||||
|   ` | ||||
| }; | ||||
|  | ||||
| // Run all debug scripts and collect results | ||||
| export async function runDiagnostics(wasmModule) { | ||||
|   if (!wasmModule || !wasmModule.run_rhai) { | ||||
|     throw new Error('WebAssembly module not loaded or run_rhai not available'); | ||||
|   } | ||||
|  | ||||
|   const results = {}; | ||||
|    | ||||
|   for (const [name, script] of Object.entries(RHAI_SCRIPTS)) { | ||||
|     try { | ||||
|       console.log(`Running Rhai diagnostic script: ${name}`); | ||||
|       results[name] = await wasmModule.run_rhai(script); | ||||
|       console.log(`Result from ${name}:`, results[name]); | ||||
|     } catch (error) { | ||||
|       console.error(`Error running script ${name}:`, error); | ||||
|       results[name] = { error: error.toString() }; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return results; | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { createRoot } from 'react-dom/client'; | ||||
| import App from './App'; | ||||
| import './style.css'; | ||||
|  | ||||
| // Render the React app | ||||
| const root = createRoot(document.getElementById('root')); | ||||
| root.render(<App />); | ||||
| @@ -1,8 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { createRoot } from 'react-dom/client'; | ||||
| import App from './App'; | ||||
| import './style.css'; | ||||
|  | ||||
| // Render the React app | ||||
| const root = createRoot(document.getElementById('root')); | ||||
| root.render(<App />); | ||||
| @@ -1,117 +0,0 @@ | ||||
| /* Basic styles for the extension popup */ | ||||
| body { | ||||
|   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   width: 350px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   font-size: 18px; | ||||
|   margin: 0 0 15px 0; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
|   padding-bottom: 10px; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|   font-size: 16px; | ||||
|   margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .form-section { | ||||
|   margin-bottom: 20px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 8px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| .form-group { | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| label { | ||||
|   display: block; | ||||
|   margin-bottom: 5px; | ||||
|   font-size: 13px; | ||||
|   color: #9aa0a6; | ||||
| } | ||||
|  | ||||
| input, textarea { | ||||
|   width: 100%; | ||||
|   padding: 8px; | ||||
|   border: 1px solid #3c4043; | ||||
|   border-radius: 4px; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| textarea { | ||||
|   min-height: 60px; | ||||
|   resize: vertical; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   background-color: #8ab4f8; | ||||
|   color: #202124; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   padding: 8px 16px; | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.3s; | ||||
| } | ||||
|  | ||||
| button:hover { | ||||
|   background-color: #669df6; | ||||
| } | ||||
|  | ||||
| button.small { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 12px; | ||||
| } | ||||
|  | ||||
| .button-group { | ||||
|   display: flex; | ||||
|   gap: 10px; | ||||
| } | ||||
|  | ||||
| .status { | ||||
|   margin: 10px 0; | ||||
|   padding: 8px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 4px; | ||||
|   font-size: 13px; | ||||
| } | ||||
|  | ||||
| .list { | ||||
|   margin-top: 10px; | ||||
|   max-height: 150px; | ||||
|   overflow-y: auto; | ||||
| } | ||||
|  | ||||
| .list-item { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 8px; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
| } | ||||
|  | ||||
| .list-item.selected { | ||||
|   background-color: rgba(138, 180, 248, 0.1); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .session-info { | ||||
|   margin-top: 15px; | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| body { | ||||
|   margin: 0; | ||||
|   font-family: 'Inter', Arial, sans-serif; | ||||
|   background: #181c20; | ||||
|   color: #f3f6fa; | ||||
| } | ||||
|  | ||||
| .App { | ||||
|   padding: 1.5rem; | ||||
|   min-width: 320px; | ||||
|   max-width: 400px; | ||||
|   background: #23272e; | ||||
|   border-radius: 12px; | ||||
|   box-shadow: 0 4px 24px rgba(0,0,0,0.2); | ||||
| } | ||||
| h1 { | ||||
|   font-size: 1.5rem; | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| p { | ||||
|   color: #b0bac9; | ||||
|   margin-bottom: 1.5rem; | ||||
| } | ||||
| .status { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| @@ -1,317 +0,0 @@ | ||||
| // WebAssembly API functions for accessing WASM operations directly | ||||
| // and synchronizing state with background service worker | ||||
|  | ||||
| // Get session state from the background service worker | ||||
| export function getStatus() { | ||||
|   return new Promise((resolve) => { | ||||
|     chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { | ||||
|       resolve(response); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Debug function to examine vault state using Rhai scripts | ||||
| export async function debugVaultState(wasmModule) { | ||||
|   if (!wasmModule) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('🔍 Debugging vault state...'); | ||||
|      | ||||
|     // First check if we have a valid session | ||||
|     const sessionCheck = ` | ||||
|       let has_session = vault::has_active_session(); | ||||
|       let keyspace = ""; | ||||
|       if has_session { | ||||
|           keyspace = vault::get_current_keyspace(); | ||||
|       } | ||||
|        | ||||
|       // Return info about the session | ||||
|       { | ||||
|           "has_session": has_session, | ||||
|           "keyspace": keyspace | ||||
|       } | ||||
|     `; | ||||
|      | ||||
|     console.log('Checking session status...'); | ||||
|     const sessionStatus = await wasmModule.run_rhai(sessionCheck); | ||||
|     console.log('Session status:', sessionStatus); | ||||
|      | ||||
|     // Only try to get keypairs if we have an active session | ||||
|     if (sessionStatus && sessionStatus.has_session) { | ||||
|       // Get information about all keypairs | ||||
|       const keypairsScript = ` | ||||
|         // Get all keypairs for the current keyspace | ||||
|         let keypairs = vault::list_keypairs(); | ||||
|          | ||||
|         // Add more diagnostic information | ||||
|         let diagnostic = { | ||||
|           "keypair_count": keypairs.len(), | ||||
|           "keyspace": vault::get_current_keyspace(), | ||||
|           "keypairs": keypairs | ||||
|         }; | ||||
|          | ||||
|         diagnostic | ||||
|       `; | ||||
|        | ||||
|       console.log('Fetching keypair details...'); | ||||
|       const keypairDiagnostic = await wasmModule.run_rhai(keypairsScript); | ||||
|       console.log('Keypair diagnostic:', keypairDiagnostic); | ||||
|        | ||||
|       return keypairDiagnostic; | ||||
|     } else { | ||||
|       console.log('No active session, cannot fetch keypairs'); | ||||
|       return { error: 'No active session' }; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Error in debug function:', error); | ||||
|     return { error: error.toString() }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Fetch all keypairs from the WebAssembly vault | ||||
| export async function getKeypairsFromVault(wasmModule) { | ||||
|   if (!wasmModule) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     // First run diagnostics for debugging | ||||
|     await debugVaultState(wasmModule); | ||||
|      | ||||
|     console.log('Calling list_keypairs WebAssembly binding...'); | ||||
|      | ||||
|     // Use our new direct WebAssembly binding instead of Rhai script | ||||
|     const keypairList = await wasmModule.list_keypairs(); | ||||
|     console.log('Retrieved keypairs from vault:', keypairList); | ||||
|      | ||||
|     // Transform the keypairs into the expected format | ||||
|     // The WebAssembly binding returns an array of objects with id, type, and metadata | ||||
|     const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => { | ||||
|       // Parse metadata if it's a string | ||||
|       let metadata = {}; | ||||
|       if (kp.metadata) { | ||||
|         try { | ||||
|           if (typeof kp.metadata === 'string') { | ||||
|             metadata = JSON.parse(kp.metadata); | ||||
|           } else { | ||||
|             metadata = kp.metadata; | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse keypair metadata:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         id: kp.id, | ||||
|         label: metadata.label || `${kp.type}-Key-${kp.id.substring(0, 4)}` | ||||
|       }; | ||||
|     }) : []; | ||||
|      | ||||
|     console.log('Formatted keypairs:', formattedKeypairs); | ||||
|      | ||||
|     // Update the keypairs in the background service worker | ||||
|     return new Promise((resolve) => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: formattedKeypairs | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           console.log('Successfully updated keypairs in background'); | ||||
|           resolve(formattedKeypairs); | ||||
|         } else { | ||||
|           console.error('Failed to update keypairs in background:', response?.error); | ||||
|           resolve([]); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error('Error fetching keypairs from vault:', error); | ||||
|     return []; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Initialize session with the WASM module | ||||
| export function initSession(wasmModule, keyspace, password) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM init_session function | ||||
|       console.log(`Initializing session for keyspace: ${keyspace}`); | ||||
|       await wasmModule.init_session(keyspace, password); | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keyspace',  | ||||
|         data: keyspace  | ||||
|       }, async (response) => { | ||||
|         if (response && response.success) { | ||||
|           try { | ||||
|             // After successful session initialization, fetch keypairs from the vault | ||||
|             console.log('Session initialized, fetching keypairs from vault...'); | ||||
|             const keypairs = await getKeypairsFromVault(wasmModule); | ||||
|             console.log('Keypairs loaded:', keypairs); | ||||
|             resolve(keypairs); | ||||
|           } catch (fetchError) { | ||||
|             console.error('Error fetching keypairs:', fetchError); | ||||
|             // Even if fetching keypairs fails, the session is initialized | ||||
|             resolve([]); | ||||
|           } | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Session initialization error:', error); | ||||
|       reject(error.message || 'Failed to initialize session'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Lock the session using the WASM module | ||||
| export function lockSession(wasmModule) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM lock_session function | ||||
|       wasmModule.lock_session(); | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'session_locked' | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           resolve(); | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       reject(error.message || 'Failed to lock session'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Add a keypair using the WASM module | ||||
| export function addKeypair(wasmModule, keyType = 'Secp256k1', label = null) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Create a default label if none provided | ||||
|       const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`; | ||||
|        | ||||
|       // Create metadata JSON for the keypair | ||||
|       const metadata = JSON.stringify({ | ||||
|         label: keyLabel, | ||||
|         created: new Date().toISOString(), | ||||
|         type: keyType | ||||
|       }); | ||||
|        | ||||
|       console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`); | ||||
|        | ||||
|       // Call the WASM add_keypair function with metadata | ||||
|       const keyId = await wasmModule.add_keypair(keyType, metadata); | ||||
|       console.log(`Keypair created with ID: ${keyId}`); | ||||
|        | ||||
|       // Create keypair object with ID and label | ||||
|       const newKeypair = {  | ||||
|         id: keyId,  | ||||
|         label: keyLabel  | ||||
|       }; | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_added',  | ||||
|         data: newKeypair  | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           // After adding a keypair, refresh the whole list from the vault | ||||
|           getKeypairsFromVault(wasmModule) | ||||
|             .then(() => { | ||||
|               console.log('Keypair list refreshed from vault'); | ||||
|               resolve(keyId); | ||||
|             }) | ||||
|             .catch(refreshError => { | ||||
|               console.warn('Error refreshing keypair list:', refreshError); | ||||
|               // Still resolve with the key ID since the key was created | ||||
|               resolve(keyId); | ||||
|             }); | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Error adding keypair:', error); | ||||
|       reject(error.message || 'Failed to add keypair'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Select a keypair using the WASM module | ||||
| export function selectKeypair(wasmModule, keyId) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM select_keypair function | ||||
|       await wasmModule.select_keypair(keyId); | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_selected',  | ||||
|         data: keyId  | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           resolve(); | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       reject(error.message || 'Failed to select keypair'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Sign a message using the WASM module | ||||
| export function sign(wasmModule, message) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Convert message to Uint8Array for WASM | ||||
|       const encoder = new TextEncoder(); | ||||
|       const messageBytes = encoder.encode(message); | ||||
|        | ||||
|       // Call the WASM sign function | ||||
|       const signature = await wasmModule.sign(messageBytes); | ||||
|       resolve(signature); | ||||
|     } catch (error) { | ||||
|       reject(error.message || 'Failed to sign message'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| @@ -1,102 +0,0 @@ | ||||
| // Background service worker for Modular Vault Extension | ||||
| // Handles session, keypair, and WASM logic | ||||
|  | ||||
| // We need to use dynamic imports for service workers in MV3 | ||||
| let wasmModule; | ||||
| let init; | ||||
| let wasm; | ||||
| let wasmReady = false; | ||||
|  | ||||
| // Initialize WASM on startup with dynamic import | ||||
| async function loadWasm() { | ||||
|   try { | ||||
|     // Using importScripts for service worker | ||||
|     const wasmUrl = chrome.runtime.getURL('wasm/wasm_app.js'); | ||||
|     wasmModule = await import(wasmUrl); | ||||
|     init = wasmModule.default; | ||||
|     wasm = wasmModule; | ||||
|      | ||||
|     // Initialize WASM with explicit WASM file path | ||||
|     await init(chrome.runtime.getURL('wasm/wasm_app_bg.wasm')); | ||||
|     wasmReady = true; | ||||
|     console.log('WASM initialized in background'); | ||||
|   } catch (error) { | ||||
|     console.error('Failed to initialize WASM:', error); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Start loading WASM | ||||
| loadWasm(); | ||||
|  | ||||
| chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { | ||||
|   if (!wasmReady) { | ||||
|     sendResponse({ error: 'WASM not ready' }); | ||||
|     return true; | ||||
|   } | ||||
|   // Session unlock/create | ||||
|   if (request.action === 'init_session') { | ||||
|     try { | ||||
|       const result = await wasm.init_session(request.keyspace, request.password); | ||||
|       // Persist current session info | ||||
|       await chrome.storage.local.set({ currentKeyspace: request.keyspace }); | ||||
|       sendResponse({ ok: true }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Lock session | ||||
|   if (request.action === 'lock_session') { | ||||
|     try { | ||||
|       wasm.lock_session(); | ||||
|       await chrome.storage.local.set({ currentKeyspace: null }); | ||||
|       sendResponse({ ok: true }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Add keypair | ||||
|   if (request.action === 'add_keypair') { | ||||
|     try { | ||||
|       const keyId = await wasm.add_keypair('Secp256k1', null); | ||||
|       let keypairs = (await chrome.storage.local.get(['keypairs'])).keypairs || []; | ||||
|       keypairs.push({ id: keyId, label: `Secp256k1-${keypairs.length + 1}` }); | ||||
|       await chrome.storage.local.set({ keypairs }); | ||||
|       sendResponse({ keyId }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Select keypair | ||||
|   if (request.action === 'select_keypair') { | ||||
|     try { | ||||
|       await wasm.select_keypair(request.keyId); | ||||
|       await chrome.storage.local.set({ selectedKeypair: request.keyId }); | ||||
|       sendResponse({ ok: true }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Sign | ||||
|   if (request.action === 'sign') { | ||||
|     try { | ||||
|       // Convert plaintext to Uint8Array | ||||
|       const encoder = new TextEncoder(); | ||||
|       const msgBytes = encoder.encode(request.message); | ||||
|       const signature = await wasm.sign(msgBytes); | ||||
|       sendResponse({ signature }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Query status | ||||
|   if (request.action === 'get_status') { | ||||
|     const { currentKeyspace, keypairs, selectedKeypair } = await chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']); | ||||
|     sendResponse({ currentKeyspace, keypairs: keypairs || [], selectedKeypair }); | ||||
|     return true; | ||||
|   } | ||||
| }); | ||||
| @@ -1,765 +0,0 @@ | ||||
| import * as __wbg_star0 from 'env'; | ||||
|  | ||||
| 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 getArrayU8FromWasm0(ptr, len) { | ||||
|     ptr = ptr >>> 0; | ||||
|     return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); | ||||
| } | ||||
|  | ||||
| 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; | ||||
| } | ||||
|  | ||||
| let cachedDataViewMemory0 = null; | ||||
|  | ||||
| function getDataViewMemory0() { | ||||
|     if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { | ||||
|         cachedDataViewMemory0 = new DataView(wasm.memory.buffer); | ||||
|     } | ||||
|     return cachedDataViewMemory0; | ||||
| } | ||||
|  | ||||
| const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') | ||||
|     ? { register: () => {}, unregister: () => {} } | ||||
|     : new FinalizationRegistry(state => { | ||||
|     wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b) | ||||
| }); | ||||
|  | ||||
| function makeMutClosure(arg0, arg1, dtor, f) { | ||||
|     const state = { a: arg0, b: arg1, cnt: 1, dtor }; | ||||
|     const real = (...args) => { | ||||
|         // First up with a closure we increment the internal reference | ||||
|         // count. This ensures that the Rust closure environment won't | ||||
|         // be deallocated while we're invoking it. | ||||
|         state.cnt++; | ||||
|         const a = state.a; | ||||
|         state.a = 0; | ||||
|         try { | ||||
|             return f(a, state.b, ...args); | ||||
|         } finally { | ||||
|             if (--state.cnt === 0) { | ||||
|                 wasm.__wbindgen_export_5.get(state.dtor)(a, state.b); | ||||
|                 CLOSURE_DTORS.unregister(state); | ||||
|             } else { | ||||
|                 state.a = a; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     real.original = state; | ||||
|     CLOSURE_DTORS.register(real, state, state); | ||||
|     return real; | ||||
| } | ||||
|  | ||||
| function debugString(val) { | ||||
|     // primitive types | ||||
|     const type = typeof val; | ||||
|     if (type == 'number' || type == 'boolean' || val == null) { | ||||
|         return  `${val}`; | ||||
|     } | ||||
|     if (type == 'string') { | ||||
|         return `"${val}"`; | ||||
|     } | ||||
|     if (type == 'symbol') { | ||||
|         const description = val.description; | ||||
|         if (description == null) { | ||||
|             return 'Symbol'; | ||||
|         } else { | ||||
|             return `Symbol(${description})`; | ||||
|         } | ||||
|     } | ||||
|     if (type == 'function') { | ||||
|         const name = val.name; | ||||
|         if (typeof name == 'string' && name.length > 0) { | ||||
|             return `Function(${name})`; | ||||
|         } else { | ||||
|             return 'Function'; | ||||
|         } | ||||
|     } | ||||
|     // objects | ||||
|     if (Array.isArray(val)) { | ||||
|         const length = val.length; | ||||
|         let debug = '['; | ||||
|         if (length > 0) { | ||||
|             debug += debugString(val[0]); | ||||
|         } | ||||
|         for(let i = 1; i < length; i++) { | ||||
|             debug += ', ' + debugString(val[i]); | ||||
|         } | ||||
|         debug += ']'; | ||||
|         return debug; | ||||
|     } | ||||
|     // Test for built-in | ||||
|     const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); | ||||
|     let className; | ||||
|     if (builtInMatches && builtInMatches.length > 1) { | ||||
|         className = builtInMatches[1]; | ||||
|     } else { | ||||
|         // Failed to match the standard '[object ClassName]' | ||||
|         return toString.call(val); | ||||
|     } | ||||
|     if (className == 'Object') { | ||||
|         // we're a user defined class or Object | ||||
|         // JSON.stringify avoids problems with cycles, and is generally much | ||||
|         // easier than looping through ownProperties of `val`. | ||||
|         try { | ||||
|             return 'Object(' + JSON.stringify(val) + ')'; | ||||
|         } catch (_) { | ||||
|             return 'Object'; | ||||
|         } | ||||
|     } | ||||
|     // errors | ||||
|     if (val instanceof Error) { | ||||
|         return `${val.name}: ${val.message}\n${val.stack}`; | ||||
|     } | ||||
|     // TODO we could test for more things here, like `Set`s and `Map`s. | ||||
|     return className; | ||||
| } | ||||
| /** | ||||
|  * Initialize the scripting environment (must be called before run_rhai) | ||||
|  */ | ||||
| export function init_rhai_env() { | ||||
|     wasm.init_rhai_env(); | ||||
| } | ||||
|  | ||||
| function takeFromExternrefTable0(idx) { | ||||
|     const value = wasm.__wbindgen_export_2.get(idx); | ||||
|     wasm.__externref_table_dealloc(idx); | ||||
|     return value; | ||||
| } | ||||
| /** | ||||
|  * Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
|  * @param {string} script | ||||
|  * @returns {any} | ||||
|  */ | ||||
| export function run_rhai(script) { | ||||
|     const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.run_rhai(ptr0, len0); | ||||
|     if (ret[2]) { | ||||
|         throw takeFromExternrefTable0(ret[1]); | ||||
|     } | ||||
|     return takeFromExternrefTable0(ret[0]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Initialize session with keyspace and password | ||||
|  * @param {string} keyspace | ||||
|  * @param {string} password | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export function init_session(keyspace, password) { | ||||
|     const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.init_session(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lock the session (zeroize password and session) | ||||
|  */ | ||||
| export function lock_session() { | ||||
|     wasm.lock_session(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all keypairs from the current session | ||||
|  * Returns an array of keypair objects with id, type, and metadata | ||||
|  * Select keypair for the session | ||||
|  * @param {string} key_id | ||||
|  */ | ||||
| export function select_keypair(key_id) { | ||||
|     const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.select_keypair(ptr0, len0); | ||||
|     if (ret[1]) { | ||||
|         throw takeFromExternrefTable0(ret[0]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * List keypairs in the current session's keyspace | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function list_keypairs() { | ||||
|     const ret = wasm.list_keypairs(); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Add a keypair to the current keyspace | ||||
|  * @param {string | null} [key_type] | ||||
|  * @param {string | null} [metadata] | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function add_keypair(key_type, metadata) { | ||||
|     var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len0 = WASM_VECTOR_LEN; | ||||
|     var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.add_keypair(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function passArray8ToWasm0(arg, malloc) { | ||||
|     const ptr = malloc(arg.length * 1, 1) >>> 0; | ||||
|     getUint8ArrayMemory0().set(arg, ptr / 1); | ||||
|     WASM_VECTOR_LEN = arg.length; | ||||
|     return ptr; | ||||
| } | ||||
| /** | ||||
|  * Sign message with current session | ||||
|  * @param {Uint8Array} message | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function sign(message) { | ||||
|     const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.sign(ptr0, len0); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_32(arg0, arg1, arg2) { | ||||
|     wasm.closure77_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_35(arg0, arg1, arg2) { | ||||
|     wasm.closure126_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_38(arg0, arg1, arg2) { | ||||
|     wasm.closure188_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_123(arg0, arg1, arg2, arg3) { | ||||
|     wasm.closure213_externref_shim(arg0, arg1, arg2, arg3); | ||||
| } | ||||
|  | ||||
| const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"]; | ||||
|  | ||||
| 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_buffer_609cc3eee51ed158 = function(arg0) { | ||||
|         const ret = arg0.buffer; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.call(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.call(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { | ||||
|         const ret = arg0.crypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) { | ||||
|         console.error(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.error; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) { | ||||
|         globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.getRandomValues(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) { | ||||
|         const ret = arg1[arg2 >>> 0]; | ||||
|         var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         var len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = Reflect.get(arg0, arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.get(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBDatabase; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBFactory; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBOpenDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) { | ||||
|         const ret = arg0.length; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { | ||||
|         const ret = arg0.msCrypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) { | ||||
|         try { | ||||
|             var state0 = {a: arg0, b: arg1}; | ||||
|             var cb0 = (arg0, arg1) => { | ||||
|                 const a = state0.a; | ||||
|                 state0.a = 0; | ||||
|                 try { | ||||
|                     return __wbg_adapter_123(a, state0.b, arg0, arg1); | ||||
|                 } finally { | ||||
|                     state0.a = a; | ||||
|                 } | ||||
|             }; | ||||
|             const ret = new Promise(cb0); | ||||
|             return ret; | ||||
|         } finally { | ||||
|             state0.a = state0.b = 0; | ||||
|         } | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_405e22f390576ce2 = function() { | ||||
|         const ret = new Object(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_78feb108b6472713 = function() { | ||||
|         const ret = new Array(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) { | ||||
|         const ret = new Function(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) { | ||||
|         const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { | ||||
|         const ret = arg0.node; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) { | ||||
|         const ret = arg0.objectStoreNames; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) { | ||||
|         const ret = arg0.process; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) { | ||||
|         const ret = arg0.push(arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.put(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.put(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) { | ||||
|         queueMicrotask(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) { | ||||
|         const ret = arg0.queueMicrotask; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.randomFillSync(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { | ||||
|         const ret = module.require; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) { | ||||
|         const ret = Promise.resolve(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.result; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) { | ||||
|         arg0.set(arg1, arg2 >>> 0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) { | ||||
|         arg0.onerror = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) { | ||||
|         arg0.onsuccess = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) { | ||||
|         arg0.onupgradeneeded = arg1; | ||||
|     }; | ||||
|     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.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) { | ||||
|         const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) { | ||||
|         const ret = arg0.target; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) { | ||||
|         const ret = arg0.then(arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { | ||||
|         const ret = arg0.versions; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_cb_drop = function(arg0) { | ||||
|         const obj = arg0.original; | ||||
|         if (obj.cnt-- == 1) { | ||||
|             obj.a = 0; | ||||
|             return true; | ||||
|         } | ||||
|         const ret = false; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { | ||||
|         const ret = debugString(arg1); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     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_function = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'function'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_null = function(arg0) { | ||||
|         const ret = arg0 === null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_object = function(arg0) { | ||||
|         const val = arg0; | ||||
|         const ret = typeof(val) === 'object' && val !== null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_string = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'string'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_undefined = function(arg0) { | ||||
|         const ret = arg0 === undefined; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { | ||||
|         const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { | ||||
|         const obj = arg1; | ||||
|         const ret = JSON.stringify(obj === undefined ? null : obj); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_memory = function() { | ||||
|         const ret = wasm.memory; | ||||
|         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)); | ||||
|     }; | ||||
|     imports['env'] = __wbg_star0; | ||||
|  | ||||
|     return imports; | ||||
| } | ||||
|  | ||||
| function __wbg_init_memory(imports, memory) { | ||||
|  | ||||
| } | ||||
|  | ||||
| function __wbg_finalize_init(instance, module) { | ||||
|     wasm = instance.exports; | ||||
|     __wbg_init.__wbindgen_wasm_module = module; | ||||
|     cachedDataViewMemory0 = null; | ||||
|     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('wasm_app_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; | ||||
| @@ -1,122 +0,0 @@ | ||||
| import { defineConfig } from 'vite'; | ||||
| import react from '@vitejs/plugin-react'; | ||||
| import wasm from 'vite-plugin-wasm'; | ||||
| import topLevelAwait from 'vite-plugin-top-level-await'; | ||||
| import { resolve } from 'path'; | ||||
| import fs from 'fs'; | ||||
| import { Plugin } from 'vite'; | ||||
|  | ||||
| // Custom plugin to copy extension files directly to the dist directory | ||||
| const copyExtensionFiles = () => { | ||||
|   return { | ||||
|     name: 'copy-extension-files', | ||||
|     closeBundle() { | ||||
|       // Create the wasm directory in dist if it doesn't exist | ||||
|       const wasmDistDir = resolve(__dirname, 'dist/wasm'); | ||||
|       if (!fs.existsSync(wasmDistDir)) { | ||||
|         fs.mkdirSync(wasmDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Copy the wasm.js file | ||||
|       const wasmJsSource = resolve(__dirname, 'wasm/wasm_app.js'); | ||||
|       const wasmJsDest = resolve(wasmDistDir, 'wasm_app.js'); | ||||
|       fs.copyFileSync(wasmJsSource, wasmJsDest); | ||||
|        | ||||
|       // Copy the wasm binary file from the pkg output | ||||
|       const wasmBinSource = resolve(__dirname, '../wasm_app/pkg/wasm_app_bg.wasm'); | ||||
|       const wasmBinDest = resolve(wasmDistDir, 'wasm_app_bg.wasm'); | ||||
|       fs.copyFileSync(wasmBinSource, wasmBinDest); | ||||
|  | ||||
|       // Create background directory and copy the background script | ||||
|       const bgDistDir = resolve(__dirname, 'dist/background'); | ||||
|       if (!fs.existsSync(bgDistDir)) { | ||||
|         fs.mkdirSync(bgDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       const bgSource = resolve(__dirname, 'background/index.js'); | ||||
|       const bgDest = resolve(bgDistDir, 'index.js'); | ||||
|       fs.copyFileSync(bgSource, bgDest); | ||||
|        | ||||
|       // Create popup directory and copy the popup files | ||||
|       const popupDistDir = resolve(__dirname, 'dist/popup'); | ||||
|       if (!fs.existsSync(popupDistDir)) { | ||||
|         fs.mkdirSync(popupDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|  | ||||
|  | ||||
|       // Copy CSS file | ||||
|       const cssSource = resolve(__dirname, 'popup/popup.css'); | ||||
|       const cssDest = resolve(popupDistDir, 'popup.css'); | ||||
|       fs.copyFileSync(cssSource, cssDest); | ||||
|        | ||||
|       // Also copy the manifest.json file | ||||
|       const manifestSource = resolve(__dirname, 'manifest.json'); | ||||
|       const manifestDest = resolve(__dirname, 'dist/manifest.json'); | ||||
|       fs.copyFileSync(manifestSource, manifestDest); | ||||
|        | ||||
|       // Copy assets directory | ||||
|       const assetsDistDir = resolve(__dirname, 'dist/assets'); | ||||
|       if (!fs.existsSync(assetsDistDir)) { | ||||
|         fs.mkdirSync(assetsDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Copy icon files | ||||
|       const iconSizes = [16, 32, 48, 128]; | ||||
|       iconSizes.forEach(size => { | ||||
|         const iconSource = resolve(__dirname, `assets/icon-${size}.png`); | ||||
|         const iconDest = resolve(assetsDistDir, `icon-${size}.png`); | ||||
|         if (fs.existsSync(iconSource)) { | ||||
|           fs.copyFileSync(iconSource, iconDest); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|  | ||||
|       console.log('Extension files copied to dist directory'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| }; | ||||
|  | ||||
| import path from 'path'; | ||||
|  | ||||
| export default defineConfig({ | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       '@wasm': path.resolve(__dirname, '../wasm_app/pkg') | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   plugins: [ | ||||
|     react(), | ||||
|     wasm(), | ||||
|     topLevelAwait(), | ||||
|     copyExtensionFiles() | ||||
|   ], | ||||
|   build: { | ||||
|     outDir: 'dist', | ||||
|     emptyOutDir: true, | ||||
|     // Simplify the build output for browser extension | ||||
|     rollupOptions: { | ||||
|       input: { | ||||
|         popup: resolve(__dirname, 'popup/index.html') | ||||
|       }, | ||||
|       output: { | ||||
|         // Use a simpler output format without hash values | ||||
|         entryFileNames: 'assets/[name].js', | ||||
|         chunkFileNames: 'assets/[name]-[hash].js', | ||||
|         assetFileNames: 'assets/[name].[ext]', | ||||
|         // Make sure output is compatible with browser extensions | ||||
|         format: 'iife', | ||||
|         // Don't generate separate code-split chunks | ||||
|         manualChunks: undefined | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   // Provide a simple dev server config | ||||
|   server: { | ||||
|     fs: { | ||||
|       allow: ['../'] | ||||
|     } | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										88
									
								
								hero_vault_extension/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | ||||
| # SAL Modular Cryptographic Browser Extension | ||||
|  | ||||
| A modern, secure browser extension for interacting with the SAL modular Rust cryptographic stack, enabling key management, cryptographic operations, and secure Rhai script execution. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| ### Session & Key Management | ||||
| - Create and unlock encrypted keyspaces with password protection | ||||
| - Create, select, and manage multiple keypairs (Ed25519, Secp256k1) | ||||
| - Clear session state visualization and management | ||||
|  | ||||
| ### Cryptographic Operations | ||||
| - Sign and verify messages using selected keypair | ||||
| - Encrypt and decrypt messages using asymmetric cryptography | ||||
| - Support for symmetric encryption using password-derived keys | ||||
|  | ||||
| ### Scripting (Rhai) | ||||
| - Execute Rhai scripts securely within the extension | ||||
| - Explicit user approval for all script executions | ||||
| - Script history and audit trail | ||||
|  | ||||
| ### WebSocket Integration | ||||
| - Connect to WebSocket servers using keypair's public key | ||||
| - Receive, review, and approve/reject incoming scripts | ||||
| - Support for both local and remote script execution | ||||
|  | ||||
| ### Security | ||||
| - Dark mode UI with modern, responsive design | ||||
| - Session auto-lock after configurable inactivity period | ||||
| - Explicit user approval for all sensitive operations | ||||
| - No persistent storage of passwords or private keys in plaintext | ||||
|  | ||||
| ## Architecture | ||||
|  | ||||
| The extension is built with a modern tech stack: | ||||
|  | ||||
| - **Frontend**: React with TypeScript, Material-UI | ||||
| - **State Management**: Zustand | ||||
| - **Backend**: WebAssembly (WASM) modules compiled from Rust | ||||
| - **Storage**: Chrome extension storage API with encryption | ||||
| - **Networking**: WebSocket for server communication | ||||
|  | ||||
| ## Development Setup | ||||
|  | ||||
| 1. Install dependencies: | ||||
|    ``` | ||||
|    cd sal_extension | ||||
|    npm install | ||||
|    ``` | ||||
|  | ||||
| 2. Build the extension: | ||||
|    ``` | ||||
|    npm run build | ||||
|    ``` | ||||
|  | ||||
| 3. Load the extension in Chrome/Edge: | ||||
|    - Navigate to `chrome://extensions/` | ||||
|    - Enable "Developer mode" | ||||
|    - Click "Load unpacked" and select the `dist` directory | ||||
|  | ||||
| 4. For development with hot-reload: | ||||
|    ``` | ||||
|    npm run watch | ||||
|    ``` | ||||
|  | ||||
| ## Integration with WASM | ||||
|  | ||||
| The extension uses WebAssembly modules compiled from Rust to perform cryptographic operations securely. The WASM modules are loaded in the extension's background script and provide a secure API for the frontend. | ||||
|  | ||||
| Key WASM functions exposed: | ||||
| - `init_session` - Unlock a keyspace with password | ||||
| - `create_keyspace` - Create a new keyspace | ||||
| - `add_keypair` - Create a new keypair | ||||
| - `select_keypair` - Select a keypair for use | ||||
| - `sign` - Sign a message with the selected keypair | ||||
| - `run_rhai` - Execute a Rhai script securely | ||||
|  | ||||
| ## Security Considerations | ||||
|  | ||||
| - The extension follows the principle of least privilege | ||||
| - All sensitive operations require explicit user approval | ||||
| - Passwords are never stored persistently, only kept in memory during an active session | ||||
| - Session state is automatically cleared when the extension is locked | ||||
| - WebSocket connections are authenticated using the user's public key | ||||
|  | ||||
| ## License | ||||
|  | ||||
| [MIT License](LICENSE) | ||||
							
								
								
									
										1
									
								
								hero_vault_extension/dist/assets/index-11057528.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| :root{font-family:Roboto,system-ui,sans-serif;line-height:1.5;font-weight:400;color-scheme:dark}body{margin:0;min-width:360px;min-height:520px;overflow-x:hidden}#root{width:100%;height:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.3)} | ||||
							
								
								
									
										205
									
								
								hero_vault_extension/dist/assets/index-b58c7e43.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								hero_vault_extension/dist/assets/simple-background.ts-e63275e1.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| console.log("Background script initialized");let i=!1,e=null;chrome.runtime.onMessage.addListener((o,l,r)=>{if(console.log("Background received message:",o.type),o.type==="SESSION_STATUS")return r({active:i}),!0;if(o.type==="SESSION_UNLOCK")return i=!0,r({success:!0}),!0;if(o.type==="SESSION_LOCK")return i=!1,e&&(e.close(),e=null),r({success:!0}),!0;if(o.type==="CONNECT_WEBSOCKET"&&o.serverUrl&&o.publicKey){try{e&&e.close(),e=new WebSocket(o.serverUrl),e.onopen=()=>{console.log("WebSocket connection established"),e&&e.send(JSON.stringify({type:"IDENTIFY",publicKey:o.publicKey}))},e.onmessage=c=>{try{const t=JSON.parse(c.data);console.log("WebSocket message received:",t),chrome.runtime.sendMessage({type:"WEBSOCKET_MESSAGE",data:t}).catch(n=>{console.error("Failed to forward WebSocket message:",n)})}catch(t){console.error("Failed to parse WebSocket message:",t)}},e.onerror=c=>{console.error("WebSocket error:",c)},e.onclose=()=>{console.log("WebSocket connection closed"),e=null},r({success:!0})}catch(c){console.error("Failed to connect to WebSocket:",c),r({success:!1,error:c.message})}return!0}return o.type==="DISCONNECT_WEBSOCKET"?(e?(e.close(),e=null,r({success:!0})):r({success:!1,error:"No active WebSocket connection"}),!0):!1});chrome.notifications&&chrome.notifications.onClicked&&chrome.notifications.onClicked.addListener(o=>{chrome.action.openPopup()}); | ||||
							
								
								
									
										2
									
								
								hero_vault_extension/dist/assets/wasm_app-bd9134aa.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										61
									
								
								hero_vault_extension/dist/background.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,61 @@ | ||||
|  | ||||
|       // Background Service Worker for SAL Modular Cryptographic Extension | ||||
|       // This is a simplified version that only handles messaging | ||||
|        | ||||
|       console.log('Background script initialized'); | ||||
|        | ||||
|       // Store active WebSocket connection | ||||
|       let activeWebSocket = null; | ||||
|       let sessionActive = false; | ||||
|        | ||||
|       // Listen for messages from popup or content scripts | ||||
|       chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|         console.log('Background received message:', message.type); | ||||
|          | ||||
|         if (message.type === 'SESSION_STATUS') { | ||||
|           sendResponse({ active: sessionActive }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'SESSION_UNLOCK') { | ||||
|           sessionActive = true; | ||||
|           sendResponse({ success: true }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'SESSION_LOCK') { | ||||
|           sessionActive = false; | ||||
|           if (activeWebSocket) { | ||||
|             activeWebSocket.close(); | ||||
|             activeWebSocket = null; | ||||
|           } | ||||
|           sendResponse({ success: true }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'CONNECT_WEBSOCKET') { | ||||
|           // Simplified WebSocket handling | ||||
|           sendResponse({ success: true }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'DISCONNECT_WEBSOCKET') { | ||||
|           if (activeWebSocket) { | ||||
|             activeWebSocket.close(); | ||||
|             activeWebSocket = null; | ||||
|             sendResponse({ success: true }); | ||||
|           } else { | ||||
|             sendResponse({ success: false, error: 'No active WebSocket connection' }); | ||||
|           } | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         return false; | ||||
|       }); | ||||
|        | ||||
|       // Initialize notification setup | ||||
|       chrome.notifications.onClicked.addListener((notificationId) => { | ||||
|         // Open the extension popup when a notification is clicked | ||||
|         chrome.action.openPopup(); | ||||
|       }); | ||||
|      | ||||
| Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB | 
| Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B | 
| Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 712 B | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| @@ -3,12 +3,12 @@ | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Modular Vault Extension</title> | ||||
|     <link rel="stylesheet" href="popup.css"> | ||||
|     <title>Hero Vault</title> | ||||
|     <script type="module" crossorigin src="/assets/index-b58c7e43.js"></script> | ||||
|     <link rel="stylesheet" href="/assets/index-11057528.css"> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
| 
 | ||||
|     <script type="module" src="./main.jsx"></script> | ||||
|      | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										26
									
								
								hero_vault_extension/dist/manifest.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | ||||
| { | ||||
|   "manifest_version": 3, | ||||
|   "name": "Hero Vault", | ||||
|   "version": "1.0.0", | ||||
|   "description": "A secure browser extension for cryptographic operations and Rhai script execution", | ||||
|   "action": { | ||||
|     "default_popup": "index.html", | ||||
|     "default_title": "Hero Vault" | ||||
|   }, | ||||
|   "icons": { | ||||
|     "16": "icons/icon-16.png", | ||||
|     "48": "icons/icon-48.png", | ||||
|     "128": "icons/icon-128.png" | ||||
|   }, | ||||
|   "permissions": [ | ||||
|     "storage", | ||||
|     "unlimitedStorage" | ||||
|   ], | ||||
|   "background": { | ||||
|     "service_worker": "service-worker-loader.js", | ||||
|     "type": "module" | ||||
|   }, | ||||
|   "content_security_policy": { | ||||
|     "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1
									
								
								hero_vault_extension/dist/service-worker-loader.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| import './assets/simple-background.ts-e63275e1.js'; | ||||
| @@ -3,13 +3,10 @@ | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Modular Vault Extension</title> | ||||
|      | ||||
|     <script type="module" crossorigin src="/assets/popup.js"></script> | ||||
|     <title>Hero Vault</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
| 
 | ||||
|      | ||||
|     <script type="module" src="/src/main.tsx"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										4862
									
								
								hero_vault_extension/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										42
									
								
								hero_vault_extension/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| { | ||||
|   "name": "hero-vault-extension", | ||||
|   "version": "1.0.0", | ||||
|   "description": "Hero Vault - A secure browser extension for cryptographic operations", | ||||
|   "scripts": { | ||||
|     "dev": "node scripts/copy-wasm.js && vite", | ||||
|     "build": "node scripts/copy-wasm.js && ([ \"$NO_TYPECHECK\" = \"true\" ] || tsc) && vite build", | ||||
|     "watch": "node scripts/copy-wasm.js && tsc && vite build --watch", | ||||
|     "preview": "vite preview", | ||||
|     "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | ||||
|     "format": "prettier --write \"src/**/*.{ts,tsx,css,scss}\"", | ||||
|     "copy-wasm": "node scripts/copy-wasm.js" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@emotion/react": "^11.11.1", | ||||
|     "@emotion/styled": "^11.11.0", | ||||
|     "@mui/icons-material": "^5.14.3", | ||||
|     "@mui/material": "^5.14.3", | ||||
|     "react": "^18.2.0", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-router-dom": "^6.14.2", | ||||
|     "zustand": "^4.4.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@crxjs/vite-plugin": "^2.0.0-beta.18", | ||||
|     "@types/chrome": "^0.0.243", | ||||
|     "@types/node": "^20.4.5", | ||||
|     "@types/react": "^18.2.15", | ||||
|     "@types/react-dom": "^18.2.7", | ||||
|     "@typescript-eslint/eslint-plugin": "^6.0.0", | ||||
|     "@typescript-eslint/parser": "^6.0.0", | ||||
|     "@vitejs/plugin-react": "^4.0.3", | ||||
|     "esbuild": "^0.25.4", | ||||
|     "eslint": "^8.45.0", | ||||
|     "eslint-plugin-react-hooks": "^4.6.0", | ||||
|     "eslint-plugin-react-refresh": "^0.4.3", | ||||
|     "prettier": "^3.0.0", | ||||
|     "sass": "^1.64.1", | ||||
|     "typescript": "^5.0.2", | ||||
|     "vite": "^4.4.5" | ||||
|   } | ||||
| } | ||||
| Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB | 
| Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B | 
| Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 712 B | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										26
									
								
								hero_vault_extension/public/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | ||||
| { | ||||
|   "manifest_version": 3, | ||||
|   "name": "Hero Vault", | ||||
|   "version": "1.0.0", | ||||
|   "description": "A secure browser extension for cryptographic operations and Rhai script execution", | ||||
|   "action": { | ||||
|     "default_popup": "index.html", | ||||
|     "default_title": "Hero Vault" | ||||
|   }, | ||||
|   "icons": { | ||||
|     "16": "icons/icon-16.png", | ||||
|     "48": "icons/icon-48.png", | ||||
|     "128": "icons/icon-128.png" | ||||
|   }, | ||||
|   "permissions": [ | ||||
|     "storage", | ||||
|     "unlimitedStorage" | ||||
|   ], | ||||
|   "background": { | ||||
|     "service_worker": "src/background/simple-background.ts", | ||||
|     "type": "module" | ||||
|   }, | ||||
|   "content_security_policy": { | ||||
|     "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										85
									
								
								hero_vault_extension/scripts/build-background.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,85 @@ | ||||
| /** | ||||
|  * Script to build the background script for the extension | ||||
|  */ | ||||
| const { build } = require('esbuild'); | ||||
| const { resolve } = require('path'); | ||||
| const fs = require('fs'); | ||||
|  | ||||
| async function buildBackground() { | ||||
|   try { | ||||
|     console.log('Building background script...'); | ||||
|      | ||||
|     // First, create a simplified background script that doesn't import WASM | ||||
|     const backgroundContent = ` | ||||
|       // Background Service Worker for SAL Modular Cryptographic Extension | ||||
|       // This is a simplified version that only handles messaging | ||||
|        | ||||
|       console.log('Background script initialized'); | ||||
|        | ||||
|       // Store active WebSocket connection | ||||
|       let activeWebSocket = null; | ||||
|       let sessionActive = false; | ||||
|        | ||||
|       // Listen for messages from popup or content scripts | ||||
|       chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|         console.log('Background received message:', message.type); | ||||
|          | ||||
|         if (message.type === 'SESSION_STATUS') { | ||||
|           sendResponse({ active: sessionActive }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'SESSION_UNLOCK') { | ||||
|           sessionActive = true; | ||||
|           sendResponse({ success: true }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'SESSION_LOCK') { | ||||
|           sessionActive = false; | ||||
|           if (activeWebSocket) { | ||||
|             activeWebSocket.close(); | ||||
|             activeWebSocket = null; | ||||
|           } | ||||
|           sendResponse({ success: true }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'CONNECT_WEBSOCKET') { | ||||
|           // Simplified WebSocket handling | ||||
|           sendResponse({ success: true }); | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'DISCONNECT_WEBSOCKET') { | ||||
|           if (activeWebSocket) { | ||||
|             activeWebSocket.close(); | ||||
|             activeWebSocket = null; | ||||
|             sendResponse({ success: true }); | ||||
|           } else { | ||||
|             sendResponse({ success: false, error: 'No active WebSocket connection' }); | ||||
|           } | ||||
|           return true; | ||||
|         } | ||||
|          | ||||
|         return false; | ||||
|       }); | ||||
|        | ||||
|       // Initialize notification setup | ||||
|       chrome.notifications.onClicked.addListener((notificationId) => { | ||||
|         // Open the extension popup when a notification is clicked | ||||
|         chrome.action.openPopup(); | ||||
|       }); | ||||
|     `; | ||||
|      | ||||
|     // Write the simplified background script to a temporary file | ||||
|     fs.writeFileSync(resolve(__dirname, '../dist/background.js'), backgroundContent); | ||||
|      | ||||
|     console.log('Background script built successfully!'); | ||||
|   } catch (error) { | ||||
|     console.error('Error building background script:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
| } | ||||
|  | ||||
| buildBackground(); | ||||
							
								
								
									
										33
									
								
								hero_vault_extension/scripts/copy-wasm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| /** | ||||
|  * Script to copy WASM files from wasm_app/pkg to the extension build directory | ||||
|  */ | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
|  | ||||
| // Source and destination paths | ||||
| const sourceDir = path.resolve(__dirname, '../../wasm_app/pkg'); | ||||
| const destDir = path.resolve(__dirname, '../public/wasm'); | ||||
|  | ||||
| // Create destination directory if it doesn't exist | ||||
| if (!fs.existsSync(destDir)) { | ||||
|   fs.mkdirSync(destDir, { recursive: true }); | ||||
|   console.log(`Created directory: ${destDir}`); | ||||
| } | ||||
|  | ||||
| // Copy all files from source to destination | ||||
| try { | ||||
|   const files = fs.readdirSync(sourceDir); | ||||
|    | ||||
|   files.forEach(file => { | ||||
|     const sourcePath = path.join(sourceDir, file); | ||||
|     const destPath = path.join(destDir, file); | ||||
|      | ||||
|     fs.copyFileSync(sourcePath, destPath); | ||||
|     console.log(`Copied: ${file}`); | ||||
|   }); | ||||
|    | ||||
|   console.log('WASM files copied successfully!'); | ||||
| } catch (error) { | ||||
|   console.error('Error copying WASM files:', error); | ||||
|   process.exit(1); | ||||
| } | ||||
							
								
								
									
										127
									
								
								hero_vault_extension/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { Box, Container, Paper } from '@mui/material'; | ||||
| import { Routes, Route, HashRouter } from 'react-router-dom'; | ||||
|  | ||||
| // Import pages | ||||
| import HomePage from './pages/HomePage'; | ||||
| import SessionPage from './pages/SessionPage'; | ||||
| import KeypairPage from './pages/KeypairPage'; | ||||
| import ScriptPage from './pages/ScriptPage'; | ||||
| import SettingsPage from './pages/SettingsPage'; | ||||
| import WebSocketPage from './pages/WebSocketPage'; | ||||
| import CryptoPage from './pages/CryptoPage'; | ||||
|  | ||||
| // Import components | ||||
| import Header from './components/Header'; | ||||
| import Navigation from './components/Navigation'; | ||||
|  | ||||
| // Import session state management | ||||
| import { useSessionStore } from './store/sessionStore'; | ||||
|  | ||||
| function App() { | ||||
|   const { checkSessionStatus, initWasm } = useSessionStore(); | ||||
|   const [isLoading, setIsLoading] = useState(true); | ||||
|   const [wasmError, setWasmError] = useState<string | null>(null); | ||||
|  | ||||
|   // Initialize WASM and check session status on mount | ||||
|   useEffect(() => { | ||||
|     const initializeApp = async () => { | ||||
|       try { | ||||
|         // First initialize WASM module | ||||
|         const wasmInitialized = await initWasm(); | ||||
|          | ||||
|         if (!wasmInitialized) { | ||||
|           throw new Error('Failed to initialize WASM module'); | ||||
|         } | ||||
|          | ||||
|         // Then check session status | ||||
|         await checkSessionStatus(); | ||||
|       } catch (error) { | ||||
|         console.error('Initialization error:', error); | ||||
|         setWasmError((error as Error).message || 'Failed to initialize the extension'); | ||||
|       } finally { | ||||
|         setIsLoading(false); | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     initializeApp(); | ||||
|   }, [checkSessionStatus, initWasm]); | ||||
|  | ||||
|   if (isLoading) { | ||||
|     return ( | ||||
|       <Box | ||||
|         sx={{ | ||||
|           display: 'flex', | ||||
|           justifyContent: 'center', | ||||
|           alignItems: 'center', | ||||
|           height: '100vh', | ||||
|         }} | ||||
|       > | ||||
|         Loading... | ||||
|       </Box> | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   if (wasmError) { | ||||
|     return ( | ||||
|       <Box | ||||
|         sx={{ | ||||
|           display: 'flex', | ||||
|           flexDirection: 'column', | ||||
|           justifyContent: 'center', | ||||
|           alignItems: 'center', | ||||
|           height: '100vh', | ||||
|           p: 3, | ||||
|           textAlign: 'center', | ||||
|         }} | ||||
|       > | ||||
|         <Paper sx={{ p: 3, maxWidth: 400 }}> | ||||
|           <h6 style={{ color: 'red', marginBottom: '8px' }}> | ||||
|             WASM Module Failed to Initialize | ||||
|           </h6> | ||||
|           <p style={{ marginBottom: '16px' }}> | ||||
|             The WASM module could not be loaded. Please try reloading the extension. | ||||
|           </p> | ||||
|           <p style={{ fontSize: '0.875rem', color: 'gray' }}> | ||||
|             Error: {wasmError} Please contact support if the problem persists. | ||||
|           </p> | ||||
|         </Paper> | ||||
|       </Box> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <HashRouter> | ||||
|       <Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}> | ||||
|         <Header /> | ||||
|          | ||||
|         <Container component="main" sx={{ flexGrow: 1, overflow: 'auto', py: 2 }}> | ||||
|           <Paper  | ||||
|             elevation={3}  | ||||
|             sx={{  | ||||
|               p: 2,  | ||||
|               height: '100%',  | ||||
|               display: 'flex',  | ||||
|               flexDirection: 'column', | ||||
|               overflow: 'hidden' | ||||
|             }} | ||||
|           > | ||||
|             <Routes> | ||||
|               <Route path="/" element={<HomePage />} /> | ||||
|               <Route path="/session" element={<SessionPage />} /> | ||||
|               <Route path="/keypair" element={<KeypairPage />} /> | ||||
|               <Route path="/crypto" element={<CryptoPage />} /> | ||||
|               <Route path="/script" element={<ScriptPage />} /> | ||||
|               <Route path="/websocket" element={<WebSocketPage />} /> | ||||
|               <Route path="/settings" element={<SettingsPage />} /> | ||||
|             </Routes> | ||||
|           </Paper> | ||||
|         </Container> | ||||
|          | ||||
|         <Navigation /> | ||||
|       </Box> | ||||
|     </HashRouter> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
							
								
								
									
										145
									
								
								hero_vault_extension/src/background/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,145 @@ | ||||
| /** | ||||
|  * Background Service Worker for Hero Vault Extension | ||||
|  *  | ||||
|  * Responsibilities: | ||||
|  * - Maintain WebSocket connections | ||||
|  * - Handle incoming script requests | ||||
|  * - Manage session state when popup is closed | ||||
|  * - Provide messaging interface for popup/content scripts | ||||
|  * - Initialize WASM module when extension starts | ||||
|  */ | ||||
|  | ||||
| // Import WASM helper functions | ||||
| import { initWasm } from '../wasm/wasmHelper'; | ||||
|  | ||||
| // Initialize WASM module when service worker starts | ||||
| initWasm().catch(error => { | ||||
|   console.error('Failed to initialize WASM module:', error); | ||||
| }); | ||||
|  | ||||
| // Store active WebSocket connection | ||||
| let activeWebSocket: WebSocket | null = null; | ||||
| let sessionActive = false; | ||||
|  | ||||
| // Listen for messages from popup or content scripts | ||||
| chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { | ||||
|   if (message.type === 'SESSION_STATUS') { | ||||
|     sendResponse({ active: sessionActive }); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'SESSION_UNLOCK') { | ||||
|     sessionActive = true; | ||||
|     sendResponse({ success: true }); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'SESSION_LOCK') { | ||||
|     sessionActive = false; | ||||
|     if (activeWebSocket) { | ||||
|       activeWebSocket.close(); | ||||
|       activeWebSocket = null; | ||||
|     } | ||||
|     sendResponse({ success: true }); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) { | ||||
|     connectToWebSocket(message.serverUrl, message.publicKey) | ||||
|       .then(success => sendResponse({ success })) | ||||
|       .catch(error => sendResponse({ success: false, error: error.message })); | ||||
|     return true; // Indicates we'll respond asynchronously | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'DISCONNECT_WEBSOCKET') { | ||||
|     if (activeWebSocket) { | ||||
|       activeWebSocket.close(); | ||||
|       activeWebSocket = null; | ||||
|       sendResponse({ success: true }); | ||||
|     } else { | ||||
|       sendResponse({ success: false, error: 'No active WebSocket connection' }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Connect to a WebSocket server with the user's public key | ||||
|  */ | ||||
| async function connectToWebSocket(serverUrl: string, publicKey: string): Promise<boolean> { | ||||
|   if (activeWebSocket) { | ||||
|     activeWebSocket.close(); | ||||
|   } | ||||
|    | ||||
|   return new Promise((resolve, reject) => { | ||||
|     try { | ||||
|       const ws = new WebSocket(serverUrl); | ||||
|        | ||||
|       ws.onopen = () => { | ||||
|         // Send authentication message with public key | ||||
|         ws.send(JSON.stringify({ | ||||
|           type: 'AUTH', | ||||
|           publicKey | ||||
|         })); | ||||
|          | ||||
|         activeWebSocket = ws; | ||||
|         resolve(true); | ||||
|       }; | ||||
|        | ||||
|       ws.onerror = (error) => { | ||||
|         console.error('WebSocket error:', error); | ||||
|         reject(new Error('Failed to connect to WebSocket server')); | ||||
|       }; | ||||
|        | ||||
|       ws.onclose = () => { | ||||
|         activeWebSocket = null; | ||||
|         console.log('WebSocket connection closed'); | ||||
|       }; | ||||
|        | ||||
|       ws.onmessage = async (event) => { | ||||
|         try { | ||||
|           const data = JSON.parse(event.data); | ||||
|            | ||||
|           // Handle incoming script requests | ||||
|           if (data.type === 'SCRIPT_REQUEST') { | ||||
|             // Notify the user of the script request | ||||
|             chrome.notifications.create({ | ||||
|               type: 'basic', | ||||
|               iconUrl: 'icons/icon128.png', | ||||
|               title: 'Script Request', | ||||
|               message: `Received script request: ${data.title || 'Untitled Script'}`, | ||||
|               priority: 2 | ||||
|             }); | ||||
|              | ||||
|             // Store the script request for the popup to handle | ||||
|             await chrome.storage.local.set({ | ||||
|               pendingScripts: [ | ||||
|                 ...(await chrome.storage.local.get('pendingScripts')).pendingScripts || [], | ||||
|                 { | ||||
|                   id: data.id, | ||||
|                   title: data.title || 'Untitled Script', | ||||
|                   description: data.description || '', | ||||
|                   script: data.script, | ||||
|                   tags: data.tags || [], | ||||
|                   timestamp: Date.now() | ||||
|                 } | ||||
|               ] | ||||
|             }); | ||||
|           } | ||||
|         } catch (error) { | ||||
|           console.error('Error processing WebSocket message:', error); | ||||
|         } | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       reject(error); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Initialize notification setup | ||||
| chrome.notifications.onClicked.addListener((_notificationId) => { | ||||
|   // Open the extension popup when a notification is clicked | ||||
|   chrome.action.openPopup(); | ||||
| }); | ||||
|  | ||||
| console.log('Hero Vault Extension background service worker initialized'); | ||||
							
								
								
									
										115
									
								
								hero_vault_extension/src/background/simple-background.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,115 @@ | ||||
| /** | ||||
|  * Simplified Background Service Worker for Hero Vault Extension | ||||
|  *  | ||||
|  * This is a version that doesn't use WASM to avoid service worker limitations | ||||
|  * with dynamic imports. It only handles basic messaging between components. | ||||
|  */ | ||||
|  | ||||
| console.log('Background script initialized'); | ||||
|  | ||||
| // Store session state | ||||
| let sessionActive = false; | ||||
| let activeWebSocket: WebSocket | null = null; | ||||
|  | ||||
| // Listen for messages from popup or content scripts | ||||
| chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|   console.log('Background received message:', message.type); | ||||
|    | ||||
|   if (message.type === 'SESSION_STATUS') { | ||||
|     sendResponse({ active: sessionActive }); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'SESSION_UNLOCK') { | ||||
|     sessionActive = true; | ||||
|     sendResponse({ success: true }); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'SESSION_LOCK') { | ||||
|     sessionActive = false; | ||||
|     if (activeWebSocket) { | ||||
|       activeWebSocket.close(); | ||||
|       activeWebSocket = null; | ||||
|     } | ||||
|     sendResponse({ success: true }); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) { | ||||
|     // Simplified WebSocket handling | ||||
|     try { | ||||
|       if (activeWebSocket) { | ||||
|         activeWebSocket.close(); | ||||
|       } | ||||
|        | ||||
|       activeWebSocket = new WebSocket(message.serverUrl); | ||||
|        | ||||
|       activeWebSocket.onopen = () => { | ||||
|         console.log('WebSocket connection established'); | ||||
|         // Send public key to identify this client | ||||
|         if (activeWebSocket) { | ||||
|           activeWebSocket.send(JSON.stringify({ | ||||
|             type: 'IDENTIFY', | ||||
|             publicKey: message.publicKey | ||||
|           })); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       activeWebSocket.onmessage = (event) => { | ||||
|         try { | ||||
|           const data = JSON.parse(event.data); | ||||
|           console.log('WebSocket message received:', data); | ||||
|            | ||||
|           // Forward message to popup | ||||
|           chrome.runtime.sendMessage({ | ||||
|             type: 'WEBSOCKET_MESSAGE', | ||||
|             data | ||||
|           }).catch(error => { | ||||
|             console.error('Failed to forward WebSocket message:', error); | ||||
|           }); | ||||
|         } catch (error) { | ||||
|           console.error('Failed to parse WebSocket message:', error); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       activeWebSocket.onerror = (error) => { | ||||
|         console.error('WebSocket error:', error); | ||||
|       }; | ||||
|        | ||||
|       activeWebSocket.onclose = () => { | ||||
|         console.log('WebSocket connection closed'); | ||||
|         activeWebSocket = null; | ||||
|       }; | ||||
|        | ||||
|       sendResponse({ success: true }); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to connect to WebSocket:', error); | ||||
|       sendResponse({ success: false, error: error.message }); | ||||
|     } | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   if (message.type === 'DISCONNECT_WEBSOCKET') { | ||||
|     if (activeWebSocket) { | ||||
|       activeWebSocket.close(); | ||||
|       activeWebSocket = null; | ||||
|       sendResponse({ success: true }); | ||||
|     } else { | ||||
|       sendResponse({ success: false, error: 'No active WebSocket connection' }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   // If we don't handle the message, return false | ||||
|   return false; | ||||
| }); | ||||
|  | ||||
| // Handle notifications if available | ||||
| if (chrome.notifications && chrome.notifications.onClicked) { | ||||
|   chrome.notifications.onClicked.addListener((notificationId) => { | ||||
|     // Open the extension popup when a notification is clicked | ||||
|     chrome.action.openPopup(); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										97
									
								
								hero_vault_extension/src/components/Header.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,97 @@ | ||||
| import { AppBar, Toolbar, Typography, IconButton, Box, Chip } from '@mui/material'; | ||||
| import LockIcon from '@mui/icons-material/Lock'; | ||||
| import LockOpenIcon from '@mui/icons-material/LockOpen'; | ||||
| import SignalWifiStatusbar4BarIcon from '@mui/icons-material/SignalWifiStatusbar4Bar'; | ||||
| import SignalWifiOffIcon from '@mui/icons-material/SignalWifiOff'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
|  | ||||
| const Header = () => { | ||||
|   const {  | ||||
|     isSessionUnlocked,  | ||||
|     currentKeyspace,  | ||||
|     currentKeypair,  | ||||
|     isWebSocketConnected, | ||||
|     lockSession  | ||||
|   } = useSessionStore(); | ||||
|  | ||||
|   const handleLockClick = async () => { | ||||
|     if (isSessionUnlocked) { | ||||
|       await lockSession(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AppBar position="static" color="primary" elevation={0}> | ||||
|       <Toolbar> | ||||
|         <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> | ||||
|           Hero Vault | ||||
|         </Typography> | ||||
|          | ||||
|         <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}> | ||||
|           {/* WebSocket connection status */} | ||||
|           {isWebSocketConnected ? ( | ||||
|             <Chip  | ||||
|               icon={<SignalWifiStatusbar4BarIcon fontSize="small" />}  | ||||
|               label="Connected"  | ||||
|               size="small"  | ||||
|               color="success"  | ||||
|               variant="outlined" | ||||
|             /> | ||||
|           ) : ( | ||||
|             <Chip  | ||||
|               icon={<SignalWifiOffIcon fontSize="small" />}  | ||||
|               label="Offline"  | ||||
|               size="small"  | ||||
|               color="default"  | ||||
|               variant="outlined"  | ||||
|             /> | ||||
|           )} | ||||
|            | ||||
|           {/* Session status */} | ||||
|           {isSessionUnlocked ? ( | ||||
|             <Chip  | ||||
|               icon={<LockOpenIcon fontSize="small" />}  | ||||
|               label={currentKeyspace || 'Unlocked'}  | ||||
|               size="small"  | ||||
|               color="primary"  | ||||
|               variant="outlined" | ||||
|             /> | ||||
|           ) : ( | ||||
|             <Chip  | ||||
|               icon={<LockIcon fontSize="small" />}  | ||||
|               label="Locked"  | ||||
|               size="small"  | ||||
|               color="error"  | ||||
|               variant="outlined"  | ||||
|             /> | ||||
|           )} | ||||
|            | ||||
|           {/* Current keypair */} | ||||
|           {isSessionUnlocked && currentKeypair && ( | ||||
|             <Chip  | ||||
|               label={currentKeypair.name || currentKeypair.id}  | ||||
|               size="small"  | ||||
|               color="secondary"  | ||||
|               variant="outlined" | ||||
|             /> | ||||
|           )} | ||||
|            | ||||
|           {/* Lock button */} | ||||
|           {isSessionUnlocked && ( | ||||
|             <IconButton  | ||||
|               edge="end"  | ||||
|               color="inherit"  | ||||
|               onClick={handleLockClick}  | ||||
|               size="small" | ||||
|               aria-label="lock session" | ||||
|             > | ||||
|               <LockIcon /> | ||||
|             </IconButton> | ||||
|           )} | ||||
|         </Box> | ||||
|       </Toolbar> | ||||
|     </AppBar> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Header; | ||||
							
								
								
									
										130
									
								
								hero_vault_extension/src/components/Navigation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,130 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import { BottomNavigation, BottomNavigationAction, Paper, Box, IconButton, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material'; | ||||
| import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||||
| import { useNavigate, useLocation } from 'react-router-dom'; | ||||
| import HomeIcon from '@mui/icons-material/Home'; | ||||
| import VpnKeyIcon from '@mui/icons-material/VpnKey'; | ||||
| import CodeIcon from '@mui/icons-material/Code'; | ||||
| import SettingsIcon from '@mui/icons-material/Settings'; | ||||
| import WifiIcon from '@mui/icons-material/Wifi'; | ||||
| import LockIcon from '@mui/icons-material/Lock'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
|  | ||||
| const Navigation = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const location = useLocation(); | ||||
|   const { isSessionUnlocked } = useSessionStore(); | ||||
|    | ||||
|   // Get current path without leading slash | ||||
|   const currentPath = location.pathname.substring(1) || 'home'; | ||||
|  | ||||
|   // State for the more menu | ||||
|   const [moreAnchorEl, setMoreAnchorEl] = useState<null | HTMLElement>(null); | ||||
|   const isMoreMenuOpen = Boolean(moreAnchorEl); | ||||
|  | ||||
|   const handleMoreClick = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||||
|     setMoreAnchorEl(event.currentTarget); | ||||
|   }; | ||||
|  | ||||
|   const handleMoreClose = () => { | ||||
|     setMoreAnchorEl(null); | ||||
|   }; | ||||
|  | ||||
|   const handleNavigation = (path: string) => { | ||||
|     navigate(`/${path === 'home' ? '' : path}`); | ||||
|     handleMoreClose(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Paper  | ||||
|       sx={{ position: 'static', bottom: 0, left: 0, right: 0 }}  | ||||
|       elevation={3} | ||||
|     > | ||||
|       <Box sx={{ display: 'flex', width: '100%' }}> | ||||
|         <BottomNavigation | ||||
|           showLabels | ||||
|           value={currentPath} | ||||
|           onChange={(_, newValue) => { | ||||
|             navigate(`/${newValue === 'home' ? '' : newValue}`); | ||||
|           }} | ||||
|           sx={{ flexGrow: 1 }} | ||||
|         > | ||||
|           <BottomNavigationAction  | ||||
|             label="Home"  | ||||
|             value="home"  | ||||
|             icon={<HomeIcon />}  | ||||
|           /> | ||||
|            | ||||
|           <BottomNavigationAction  | ||||
|             label="Keys"  | ||||
|             value="keypair"  | ||||
|             icon={<VpnKeyIcon />}  | ||||
|             disabled={!isSessionUnlocked} | ||||
|           /> | ||||
|            | ||||
|           <BottomNavigationAction  | ||||
|             label="Crypto"  | ||||
|             value="crypto"  | ||||
|             icon={<LockIcon />}  | ||||
|             disabled={!isSessionUnlocked} | ||||
|           /> | ||||
|            | ||||
|           <BottomNavigationAction  | ||||
|             label="More"  | ||||
|             value="more"  | ||||
|             icon={<MoreVertIcon />}  | ||||
|             onClick={handleMoreClick} | ||||
|           /> | ||||
|         </BottomNavigation> | ||||
|  | ||||
|         <Menu | ||||
|           anchorEl={moreAnchorEl} | ||||
|           open={isMoreMenuOpen} | ||||
|           onClose={handleMoreClose} | ||||
|           anchorOrigin={{ | ||||
|             vertical: 'top', | ||||
|             horizontal: 'right', | ||||
|           }} | ||||
|           transformOrigin={{ | ||||
|             vertical: 'bottom', | ||||
|             horizontal: 'right', | ||||
|           }} | ||||
|         > | ||||
|           <MenuItem  | ||||
|             onClick={() => handleNavigation('script')}  | ||||
|             disabled={!isSessionUnlocked} | ||||
|             selected={currentPath === 'script'} | ||||
|           > | ||||
|             <ListItemIcon> | ||||
|               <CodeIcon fontSize="small" /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>Scripts</ListItemText> | ||||
|           </MenuItem> | ||||
|            | ||||
|           <MenuItem  | ||||
|             onClick={() => handleNavigation('websocket')}  | ||||
|             disabled={!isSessionUnlocked} | ||||
|             selected={currentPath === 'websocket'} | ||||
|           > | ||||
|             <ListItemIcon> | ||||
|               <WifiIcon fontSize="small" /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>WebSocket</ListItemText> | ||||
|           </MenuItem> | ||||
|            | ||||
|           <MenuItem  | ||||
|             onClick={() => handleNavigation('settings')}  | ||||
|             selected={currentPath === 'settings'} | ||||
|           > | ||||
|             <ListItemIcon> | ||||
|               <SettingsIcon fontSize="small" /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>Settings</ListItemText> | ||||
|           </MenuItem> | ||||
|         </Menu> | ||||
|       </Box> | ||||
|     </Paper> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Navigation; | ||||
							
								
								
									
										38
									
								
								hero_vault_extension/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | ||||
| :root { | ||||
|   font-family: 'Roboto', system-ui, sans-serif; | ||||
|   line-height: 1.5; | ||||
|   font-weight: 400; | ||||
|   color-scheme: dark; | ||||
| } | ||||
|  | ||||
| body { | ||||
|   margin: 0; | ||||
|   min-width: 360px; | ||||
|   min-height: 520px; | ||||
|   overflow-x: hidden; | ||||
| } | ||||
|  | ||||
| #root { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| /* Scrollbar styling */ | ||||
| ::-webkit-scrollbar { | ||||
|   width: 6px; | ||||
|   height: 6px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-track { | ||||
|   background: rgba(255, 255, 255, 0.05); | ||||
|   border-radius: 3px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb { | ||||
|   background: rgba(255, 255, 255, 0.2); | ||||
|   border-radius: 3px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb:hover { | ||||
|   background: rgba(255, 255, 255, 0.3); | ||||
| } | ||||
							
								
								
									
										64
									
								
								hero_vault_extension/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import { ThemeProvider, createTheme } from '@mui/material/styles'; | ||||
| import CssBaseline from '@mui/material/CssBaseline'; | ||||
| import App from './App'; | ||||
| import './index.css'; | ||||
|  | ||||
| // Create a dark theme for the extension | ||||
| const darkTheme = createTheme({ | ||||
|   palette: { | ||||
|     mode: 'dark', | ||||
|     primary: { | ||||
|       main: '#6200ee', | ||||
|     }, | ||||
|     secondary: { | ||||
|       main: '#03dac6', | ||||
|     }, | ||||
|     background: { | ||||
|       default: '#121212', | ||||
|       paper: '#1e1e1e', | ||||
|     }, | ||||
|   }, | ||||
|   typography: { | ||||
|     fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', | ||||
|     h1: { | ||||
|       fontSize: '1.5rem', | ||||
|       fontWeight: 600, | ||||
|     }, | ||||
|     h2: { | ||||
|       fontSize: '1.25rem', | ||||
|       fontWeight: 600, | ||||
|     }, | ||||
|     h3: { | ||||
|       fontSize: '1.125rem', | ||||
|       fontWeight: 600, | ||||
|     }, | ||||
|   }, | ||||
|   components: { | ||||
|     MuiButton: { | ||||
|       styleOverrides: { | ||||
|         root: { | ||||
|           borderRadius: 8, | ||||
|           textTransform: 'none', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     MuiPaper: { | ||||
|       styleOverrides: { | ||||
|         root: { | ||||
|           borderRadius: 8, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | ||||
|   <React.StrictMode> | ||||
|     <ThemeProvider theme={darkTheme}> | ||||
|       <CssBaseline /> | ||||
|       <App /> | ||||
|     </ThemeProvider> | ||||
|   </React.StrictMode> | ||||
| ); | ||||
							
								
								
									
										392
									
								
								hero_vault_extension/src/pages/CryptoPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,392 @@ | ||||
| /** | ||||
|  * Cryptographic Operations Page | ||||
|  *  | ||||
|  * This page provides a UI for: | ||||
|  * - Encrypting/decrypting data using the keyspace's symmetric cipher | ||||
|  * - Signing/verifying messages using the selected keypair | ||||
|  */ | ||||
|  | ||||
| import { useState, useEffect } from 'react'; | ||||
| import type { SyntheticEvent } from '../types'; | ||||
| import { | ||||
|   Box, | ||||
|   Typography, | ||||
|   TextField, | ||||
|   Button, | ||||
|   Paper, | ||||
|   Tabs, | ||||
|   Tab, | ||||
|   CircularProgress, | ||||
|   Alert, | ||||
|   Divider, | ||||
|   IconButton, | ||||
|   Tooltip, | ||||
| } from '@mui/material'; | ||||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
| import { useCryptoStore } from '../store/cryptoStore'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| const CryptoPage = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const { isSessionUnlocked, currentKeypair } = useSessionStore(); | ||||
|   const { | ||||
|     encryptData, | ||||
|     decryptData, | ||||
|     signMessage, | ||||
|     verifySignature, | ||||
|     isEncrypting, | ||||
|     isDecrypting, | ||||
|     isSigning, | ||||
|     isVerifying, | ||||
|     error, | ||||
|     clearError | ||||
|   } = useCryptoStore(); | ||||
|    | ||||
|   const [activeTab, setActiveTab] = useState(0); | ||||
|   const [copySuccess, setCopySuccess] = useState<string | null>(null); | ||||
|    | ||||
|   // Encryption state | ||||
|   const [plaintext, setPlaintext] = useState(''); | ||||
|   const [encryptedData, setEncryptedData] = useState(''); | ||||
|    | ||||
|   // Decryption state | ||||
|   const [ciphertext, setCiphertext] = useState(''); | ||||
|   const [decryptedData, setDecryptedData] = useState(''); | ||||
|    | ||||
|   // Signing state | ||||
|   const [messageToSign, setMessageToSign] = useState(''); | ||||
|   const [signature, setSignature] = useState(''); | ||||
|    | ||||
|   // Verification state | ||||
|   const [messageToVerify, setMessageToVerify] = useState(''); | ||||
|   const [signatureToVerify, setSignatureToVerify] = useState(''); | ||||
|   const [isVerified, setIsVerified] = useState<boolean | null>(null); | ||||
|    | ||||
|   // Redirect if not unlocked | ||||
|   useEffect(() => { | ||||
|     if (!isSessionUnlocked) { | ||||
|       navigate('/'); | ||||
|     } | ||||
|   }, [isSessionUnlocked, navigate]); | ||||
|    | ||||
|   const handleTabChange = (_event: React.SyntheticEvent<Element, Event>, newValue: number) => { | ||||
|     setActiveTab(newValue); | ||||
|     clearError(); | ||||
|     setCopySuccess(null); | ||||
|   }; | ||||
|    | ||||
|   const handleEncrypt = async () => { | ||||
|     try { | ||||
|       const result = await encryptData(plaintext); | ||||
|       setEncryptedData(result); | ||||
|     } catch (err) { | ||||
|       // Error is already handled in the store | ||||
|     } | ||||
|   }; | ||||
|    | ||||
|   const handleDecrypt = async () => { | ||||
|     try { | ||||
|       const result = await decryptData(ciphertext); | ||||
|       setDecryptedData(result); | ||||
|     } catch (err) { | ||||
|       // Error is already handled in the store | ||||
|     } | ||||
|   }; | ||||
|    | ||||
|   const handleSign = async () => { | ||||
|     try { | ||||
|       const result = await signMessage(messageToSign); | ||||
|       setSignature(result); | ||||
|     } catch (err) { | ||||
|       // Error is already handled in the store | ||||
|     } | ||||
|   }; | ||||
|    | ||||
|   const handleVerify = async () => { | ||||
|     try { | ||||
|       const result = await verifySignature(messageToVerify, signatureToVerify); | ||||
|       setIsVerified(result); | ||||
|     } catch (err) { | ||||
|       setIsVerified(false); | ||||
|       // Error is already handled in the store | ||||
|     } | ||||
|   }; | ||||
|    | ||||
|   const copyToClipboard = (text: string, label: string) => { | ||||
|     navigator.clipboard.writeText(text).then( | ||||
|       () => { | ||||
|         setCopySuccess(`${label} copied to clipboard!`); | ||||
|         setTimeout(() => setCopySuccess(null), 2000); | ||||
|       }, | ||||
|       () => { | ||||
|         setCopySuccess('Failed to copy!'); | ||||
|       } | ||||
|     ); | ||||
|   }; | ||||
|    | ||||
|   if (!isSessionUnlocked) { | ||||
|     return null; // Will redirect via useEffect | ||||
|   } | ||||
|    | ||||
|   return ( | ||||
|     <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | ||||
|       <Typography variant="h6" sx={{ mb: 2 }}>Cryptographic Operations</Typography> | ||||
|        | ||||
|       {error && ( | ||||
|         <Alert severity="error" sx={{ mb: 2 }}> | ||||
|           {error} | ||||
|         </Alert> | ||||
|       )} | ||||
|        | ||||
|       {copySuccess && ( | ||||
|         <Alert severity="success" sx={{ mb: 2 }}> | ||||
|           {copySuccess} | ||||
|         </Alert> | ||||
|       )} | ||||
|        | ||||
|       <Paper sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> | ||||
|         {/* Tabs with smaller width and scrollable */} | ||||
|         <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> | ||||
|           <Tabs  | ||||
|             value={activeTab}  | ||||
|             onChange={handleTabChange} | ||||
|             variant="scrollable" | ||||
|             scrollButtons="auto" | ||||
|             allowScrollButtonsMobile | ||||
|             sx={{ minHeight: '48px' }} | ||||
|           > | ||||
|             <Tab label="Encrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} /> | ||||
|             <Tab label="Decrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} /> | ||||
|             <Tab label="Sign" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} /> | ||||
|             <Tab label="Verify" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} /> | ||||
|           </Tabs> | ||||
|         </Box> | ||||
|          | ||||
|         {/* Content area with proper scrolling */} | ||||
|         <Box sx={{ p: 2, flexGrow: 1, overflow: 'auto', height: 'calc(100% - 48px)' }}> | ||||
|           {/* Encryption Tab */} | ||||
|           {activeTab === 0 && ( | ||||
|             <Box> | ||||
|               <Typography variant="subtitle1" gutterBottom>Encrypt Data</Typography> | ||||
|               <Typography variant="body2" color="text.secondary" paragraph> | ||||
|                 Data will be encrypted using ChaCha20-Poly1305 with a key derived from your keyspace password. | ||||
|               </Typography> | ||||
|                | ||||
|               <TextField | ||||
|                 label="Data to Encrypt" | ||||
|                 multiline | ||||
|                 rows={4} | ||||
|                 fullWidth | ||||
|                 value={plaintext} | ||||
|                 onChange={(e) => setPlaintext(e.target.value)} | ||||
|                 margin="normal" | ||||
|               /> | ||||
|                | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 onClick={handleEncrypt} | ||||
|                 disabled={!plaintext || isEncrypting} | ||||
|                 sx={{ mt: 2 }} | ||||
|               > | ||||
|                 {isEncrypting ? <CircularProgress size={24} /> : 'Encrypt'} | ||||
|               </Button> | ||||
|                | ||||
|               {encryptedData && ( | ||||
|                 <Box sx={{ mt: 3 }}> | ||||
|                   <Divider sx={{ my: 2 }} /> | ||||
|                   <Typography variant="subtitle1">Encrypted Result</Typography> | ||||
|                   <Box sx={{ position: 'relative' }}> | ||||
|                     <TextField | ||||
|                       label="Encrypted Data (Base64)" | ||||
|                       multiline | ||||
|                       rows={4} | ||||
|                       fullWidth | ||||
|                       value={encryptedData} | ||||
|                       InputProps={{ readOnly: true }} | ||||
|                       margin="normal" | ||||
|                     /> | ||||
|                     <Tooltip title="Copy to clipboard"> | ||||
|                       <IconButton  | ||||
|                         sx={{ position: 'absolute', top: 8, right: 8 }} | ||||
|                         onClick={() => copyToClipboard(encryptedData, 'Encrypted data')} | ||||
|                       > | ||||
|                         <ContentCopyIcon fontSize="small" /> | ||||
|                       </IconButton> | ||||
|                     </Tooltip> | ||||
|                   </Box> | ||||
|                 </Box> | ||||
|               )} | ||||
|             </Box> | ||||
|           )} | ||||
|            | ||||
|           {/* Decryption Tab */} | ||||
|           {activeTab === 1 && ( | ||||
|             <Box> | ||||
|               <Typography variant="subtitle1" gutterBottom>Decrypt Data</Typography> | ||||
|               <Typography variant="body2" color="text.secondary" paragraph> | ||||
|                 Paste encrypted data (in Base64 format) to decrypt it using your keyspace password. | ||||
|               </Typography> | ||||
|                | ||||
|               <TextField | ||||
|                 label="Encrypted Data (Base64)" | ||||
|                 multiline | ||||
|                 rows={4} | ||||
|                 fullWidth | ||||
|                 value={ciphertext} | ||||
|                 onChange={(e) => setCiphertext(e.target.value)} | ||||
|                 margin="normal" | ||||
|               /> | ||||
|                | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 onClick={handleDecrypt} | ||||
|                 disabled={!ciphertext || isDecrypting} | ||||
|                 sx={{ mt: 2 }} | ||||
|               > | ||||
|                 {isDecrypting ? <CircularProgress size={24} /> : 'Decrypt'} | ||||
|               </Button> | ||||
|                | ||||
|               {decryptedData && ( | ||||
|                 <Box sx={{ mt: 3 }}> | ||||
|                   <Divider sx={{ my: 2 }} /> | ||||
|                   <Typography variant="subtitle1">Decrypted Result</Typography> | ||||
|                   <Box sx={{ position: 'relative' }}> | ||||
|                     <TextField | ||||
|                       label="Decrypted Data" | ||||
|                       multiline | ||||
|                       rows={4} | ||||
|                       fullWidth | ||||
|                       value={decryptedData} | ||||
|                       InputProps={{ readOnly: true }} | ||||
|                       margin="normal" | ||||
|                     /> | ||||
|                     <Tooltip title="Copy to clipboard"> | ||||
|                       <IconButton  | ||||
|                         sx={{ position: 'absolute', top: 8, right: 8 }} | ||||
|                         onClick={() => copyToClipboard(decryptedData, 'Decrypted data')} | ||||
|                       > | ||||
|                         <ContentCopyIcon fontSize="small" /> | ||||
|                       </IconButton> | ||||
|                     </Tooltip> | ||||
|                   </Box> | ||||
|                 </Box> | ||||
|               )} | ||||
|             </Box> | ||||
|           )} | ||||
|            | ||||
|           {/* Signing Tab */} | ||||
|           {activeTab === 2 && ( | ||||
|             <Box> | ||||
|               <Typography variant="subtitle1" gutterBottom>Sign Message</Typography> | ||||
|                | ||||
|               {!currentKeypair ? ( | ||||
|                 <Alert severity="warning" sx={{ mb: 2 }}> | ||||
|                   Please select a keypair from the Keypair page before signing messages. | ||||
|                 </Alert> | ||||
|               ) : ( | ||||
|                 <Alert severity="info" sx={{ mb: 2 }}> | ||||
|                   Signing with keypair: {currentKeypair.name || currentKeypair.id.substring(0, 8)}... | ||||
|                 </Alert> | ||||
|               )} | ||||
|                | ||||
|               <TextField | ||||
|                 label="Message to Sign" | ||||
|                 multiline | ||||
|                 rows={4} | ||||
|                 fullWidth | ||||
|                 value={messageToSign} | ||||
|                 onChange={(e) => setMessageToSign(e.target.value)} | ||||
|                 margin="normal" | ||||
|                 disabled={!currentKeypair} | ||||
|               /> | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 onClick={handleSign} | ||||
|                 disabled={!messageToSign || !currentKeypair || isSigning} | ||||
|                 sx={{ mt: 2 }} | ||||
|               > | ||||
|                 {isSigning ? <CircularProgress size={24} /> : 'Sign Message'} | ||||
|               </Button> | ||||
|                | ||||
|               {signature && ( | ||||
|                 <Box sx={{ mt: 3 }}> | ||||
|                   <Divider sx={{ my: 2 }} /> | ||||
|                   <Typography variant="subtitle1">Signature</Typography> | ||||
|                   <Box sx={{ position: 'relative' }}> | ||||
|                     <TextField | ||||
|                       label="Signature (Hex)" | ||||
|                       multiline | ||||
|                       rows={4} | ||||
|                       fullWidth | ||||
|                       value={signature} | ||||
|                       InputProps={{ readOnly: true }} | ||||
|                       margin="normal" | ||||
|                     /> | ||||
|                     <Tooltip title="Copy to clipboard"> | ||||
|                       <IconButton  | ||||
|                         sx={{ position: 'absolute', top: 8, right: 8 }} | ||||
|                         onClick={() => copyToClipboard(signature, 'Signature')} | ||||
|                       > | ||||
|                         <ContentCopyIcon fontSize="small" /> | ||||
|                       </IconButton> | ||||
|                     </Tooltip> | ||||
|                   </Box> | ||||
|                 </Box> | ||||
|               )} | ||||
|             </Box> | ||||
|           )} | ||||
|            | ||||
|           {/* Verification Tab */} | ||||
|           {activeTab === 3 && ( | ||||
|             <Box> | ||||
|               <Typography variant="subtitle1" gutterBottom>Verify Signature</Typography> | ||||
|               <Typography variant="body2" color="text.secondary" paragraph> | ||||
|                 Verify that a message was signed by the currently selected keypair. | ||||
|               </Typography> | ||||
|                | ||||
|               <TextField | ||||
|                 label="Message" | ||||
|                 multiline | ||||
|                 rows={4} | ||||
|                 fullWidth | ||||
|                 value={messageToVerify} | ||||
|                 onChange={(e) => setMessageToVerify(e.target.value)} | ||||
|                 margin="normal" | ||||
|               /> | ||||
|               <TextField | ||||
|                 label="Signature (Hex)" | ||||
|                 multiline | ||||
|                 rows={2} | ||||
|                 fullWidth | ||||
|                 value={signatureToVerify} | ||||
|                 onChange={(e) => setSignatureToVerify(e.target.value)} | ||||
|                 margin="normal" | ||||
|               /> | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 onClick={handleVerify} | ||||
|                 disabled={!messageToVerify || !signatureToVerify || isVerifying} | ||||
|                 sx={{ mt: 2 }} | ||||
|               > | ||||
|                 {isVerifying ? <CircularProgress size={24} /> : 'Verify Signature'} | ||||
|               </Button> | ||||
|                | ||||
|               {isVerified !== null && ( | ||||
|                 <Box sx={{ mt: 3 }}> | ||||
|                   <Alert severity={isVerified ? "success" : "error"}> | ||||
|                     {isVerified  | ||||
|                       ? "Signature is valid! The message was signed by the expected keypair."  | ||||
|                       : "Invalid signature. The message may have been tampered with or signed by a different keypair."} | ||||
|                   </Alert> | ||||
|                 </Box> | ||||
|               )} | ||||
|             </Box> | ||||
|           )} | ||||
|         </Box> | ||||
|       </Paper> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default CryptoPage; | ||||
							
								
								
									
										155
									
								
								hero_vault_extension/src/pages/HomePage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,155 @@ | ||||
| import { useState } from 'react'; | ||||
| import {  | ||||
|   Box,  | ||||
|   Typography,  | ||||
|   Button,  | ||||
|   TextField,  | ||||
|   Card,  | ||||
|   CardContent,  | ||||
|   Stack, | ||||
|   Alert, | ||||
|   CircularProgress | ||||
| } from '@mui/material'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
|  | ||||
| const HomePage = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const { isSessionUnlocked, unlockSession, createKeyspace } = useSessionStore(); | ||||
|    | ||||
|   const [keyspace, setKeyspace] = useState<string>(''); | ||||
|   const [password, setPassword] = useState<string>(''); | ||||
|   const [isLoading, setIsLoading] = useState<boolean>(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [mode, setMode] = useState<'unlock' | 'create'>('unlock'); | ||||
|  | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setError(null); | ||||
|     setIsLoading(true); | ||||
|      | ||||
|     try { | ||||
|       let success = false; | ||||
|        | ||||
|       if (mode === 'unlock') { | ||||
|         success = await unlockSession(keyspace, password); | ||||
|       } else { | ||||
|         success = await createKeyspace(keyspace, password); | ||||
|       } | ||||
|        | ||||
|       if (success) { | ||||
|         // Navigate to keypair page on success | ||||
|         navigate('/keypair'); | ||||
|       } else { | ||||
|         setError(mode === 'unlock'  | ||||
|           ? 'Failed to unlock keyspace. Check your password and try again.'  | ||||
|           : 'Failed to create keyspace. Please try again.'); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       setError((err as Error).message || 'An unexpected error occurred'); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (isSessionUnlocked) { | ||||
|     return ( | ||||
|       <Box sx={{ textAlign: 'center', py: 4 }}> | ||||
|         <Typography variant="h5" gutterBottom> | ||||
|           Welcome to Hero Vault | ||||
|         </Typography> | ||||
|         <Typography variant="body1" color="text.secondary" paragraph> | ||||
|           Your session is unlocked. You can now use the extension features. | ||||
|         </Typography> | ||||
|         <Stack direction="row" spacing={2} justifyContent="center" mt={3}> | ||||
|           <Button  | ||||
|             variant="contained"  | ||||
|             color="primary" | ||||
|             onClick={() => navigate('/keypair')} | ||||
|           > | ||||
|             Manage Keys | ||||
|           </Button> | ||||
|           <Button  | ||||
|             variant="outlined"  | ||||
|             color="secondary" | ||||
|             onClick={() => navigate('/script')} | ||||
|           > | ||||
|             Run Scripts | ||||
|           </Button> | ||||
|         </Stack> | ||||
|       </Box> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ maxWidth: 400, mx: 'auto', py: 2 }}> | ||||
|       <Typography variant="h5" align="center" gutterBottom> | ||||
|         Hero Vault | ||||
|       </Typography> | ||||
|        | ||||
|       <Card variant="outlined" sx={{ mt: 3 }}> | ||||
|         <CardContent> | ||||
|           <Typography variant="h6" gutterBottom> | ||||
|             {mode === 'unlock' ? 'Unlock Keyspace' : 'Create New Keyspace'} | ||||
|           </Typography> | ||||
|            | ||||
|           {error && ( | ||||
|             <Alert severity="error" sx={{ mb: 2 }}> | ||||
|               {error} | ||||
|             </Alert> | ||||
|           )} | ||||
|            | ||||
|           <form onSubmit={handleSubmit}> | ||||
|             <TextField | ||||
|               label="Keyspace Name" | ||||
|               value={keyspace} | ||||
|               onChange={(e) => setKeyspace(e.target.value)} | ||||
|               fullWidth | ||||
|               margin="normal" | ||||
|               required | ||||
|               disabled={isLoading} | ||||
|             /> | ||||
|              | ||||
|             <TextField | ||||
|               label="Password" | ||||
|               type="password" | ||||
|               value={password} | ||||
|               onChange={(e) => setPassword(e.target.value)} | ||||
|               fullWidth | ||||
|               margin="normal" | ||||
|               required | ||||
|               disabled={isLoading} | ||||
|             /> | ||||
|              | ||||
|             <Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}> | ||||
|               <Button | ||||
|                 variant="text" | ||||
|                 onClick={() => setMode(mode === 'unlock' ? 'create' : 'unlock')} | ||||
|                 disabled={isLoading} | ||||
|               > | ||||
|                 {mode === 'unlock' ? 'Create New Keyspace' : 'Unlock Existing'} | ||||
|               </Button> | ||||
|                | ||||
|               <Button | ||||
|                 type="submit" | ||||
|                 variant="contained" | ||||
|                 color="primary" | ||||
|                 disabled={isLoading || !keyspace || !password} | ||||
|               > | ||||
|                 {isLoading ? ( | ||||
|                   <CircularProgress size={24} color="inherit" /> | ||||
|                 ) : mode === 'unlock' ? ( | ||||
|                   'Unlock' | ||||
|                 ) : ( | ||||
|                   'Create' | ||||
|                 )} | ||||
|               </Button> | ||||
|             </Box> | ||||
|           </form> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default HomePage; | ||||
							
								
								
									
										242
									
								
								hero_vault_extension/src/pages/KeypairPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,242 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   Typography, | ||||
|   Button, | ||||
|   List, | ||||
|   ListItem, | ||||
|   ListItemText, | ||||
|   ListItemSecondaryAction, | ||||
|   IconButton, | ||||
|   Divider, | ||||
|   Dialog, | ||||
|   DialogTitle, | ||||
|   DialogContent, | ||||
|   DialogActions, | ||||
|   TextField, | ||||
|   FormControl, | ||||
|   InputLabel, | ||||
|   Select, | ||||
|   MenuItem, | ||||
|   CircularProgress, | ||||
|   Paper, | ||||
|   Alert, | ||||
|   Chip | ||||
| } from '@mui/material'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | ||||
| import CheckIcon from '@mui/icons-material/Check'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| const KeypairPage = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const { | ||||
|     isSessionUnlocked, | ||||
|     availableKeypairs, | ||||
|     currentKeypair, | ||||
|     listKeypairs, | ||||
|     selectKeypair, | ||||
|     createKeypair | ||||
|   } = useSessionStore(); | ||||
|  | ||||
|   const [isLoading, setIsLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [createDialogOpen, setCreateDialogOpen] = useState(false); | ||||
|   const [newKeypairName, setNewKeypairName] = useState(''); | ||||
|   const [newKeypairType, setNewKeypairType] = useState('Secp256k1'); | ||||
|   const [newKeypairDescription, setNewKeypairDescription] = useState(''); | ||||
|   const [isCreating, setIsCreating] = useState(false); | ||||
|  | ||||
|   // Redirect if not unlocked | ||||
|   useEffect(() => { | ||||
|     if (!isSessionUnlocked) { | ||||
|       navigate('/'); | ||||
|     } | ||||
|   }, [isSessionUnlocked, navigate]); | ||||
|  | ||||
|   // Load keypairs on mount | ||||
|   useEffect(() => { | ||||
|     const loadKeypairs = async () => { | ||||
|       try { | ||||
|         setIsLoading(true); | ||||
|         await listKeypairs(); | ||||
|       } catch (err) { | ||||
|         setError((err as Error).message || 'Failed to load keypairs'); | ||||
|       } finally { | ||||
|         setIsLoading(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (isSessionUnlocked) { | ||||
|       loadKeypairs(); | ||||
|     } | ||||
|   }, [isSessionUnlocked, listKeypairs]); | ||||
|  | ||||
|   const handleSelectKeypair = async (keypairId: string) => { | ||||
|     try { | ||||
|       setIsLoading(true); | ||||
|       await selectKeypair(keypairId); | ||||
|     } catch (err) { | ||||
|       setError((err as Error).message || 'Failed to select keypair'); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleCreateKeypair = async () => { | ||||
|     try { | ||||
|       setIsCreating(true); | ||||
|       setError(null); | ||||
|        | ||||
|       await createKeypair(newKeypairType, { | ||||
|         name: newKeypairName, | ||||
|         description: newKeypairDescription | ||||
|       }); | ||||
|        | ||||
|       setCreateDialogOpen(false); | ||||
|       setNewKeypairName(''); | ||||
|       setNewKeypairDescription(''); | ||||
|        | ||||
|       // Refresh the list | ||||
|       await listKeypairs(); | ||||
|     } catch (err) { | ||||
|       setError((err as Error).message || 'Failed to create keypair'); | ||||
|     } finally { | ||||
|       setIsCreating(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (!isSessionUnlocked) { | ||||
|     return null; // Will redirect via useEffect | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | ||||
|       <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | ||||
|         <Typography variant="h6">Keypair Management</Typography> | ||||
|         <Button | ||||
|           variant="contained" | ||||
|           startIcon={<AddIcon />} | ||||
|           onClick={() => setCreateDialogOpen(true)} | ||||
|           disabled={isLoading} | ||||
|         > | ||||
|           Create New | ||||
|         </Button> | ||||
|       </Box> | ||||
|  | ||||
|       {error && ( | ||||
|         <Alert severity="error" sx={{ mb: 2 }}> | ||||
|           {error} | ||||
|         </Alert> | ||||
|       )} | ||||
|  | ||||
|       {isLoading ? ( | ||||
|         <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> | ||||
|           <CircularProgress /> | ||||
|         </Box> | ||||
|       ) : availableKeypairs.length === 0 ? ( | ||||
|         <Paper sx={{ p: 3, textAlign: 'center' }}> | ||||
|           <Typography variant="body1" color="text.secondary"> | ||||
|             No keypairs found. Create your first keypair to get started. | ||||
|           </Typography> | ||||
|         </Paper> | ||||
|       ) : ( | ||||
|         <Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}> | ||||
|           <List disablePadding> | ||||
|             {availableKeypairs.map((keypair: any, index: number) => ( | ||||
|               <Box key={keypair.id}> | ||||
|                 {index > 0 && <Divider />} | ||||
|                 <ListItem | ||||
|                   button | ||||
|                   selected={currentKeypair?.id === keypair.id} | ||||
|                   onClick={() => handleSelectKeypair(keypair.id)} | ||||
|                 > | ||||
|                   <ListItemText | ||||
|                     primary={ | ||||
|                       <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | ||||
|                         {keypair.name || keypair.id} | ||||
|                         <Chip | ||||
|                           label={keypair.type} | ||||
|                           size="small" | ||||
|                           color="primary" | ||||
|                           variant="outlined" | ||||
|                         /> | ||||
|                       </Box> | ||||
|                     } | ||||
|                     secondary={ | ||||
|                       <Typography variant="body2" color="text.secondary"> | ||||
|                         {keypair.description || 'No description'} | ||||
|                         <br /> | ||||
|                         Created: {new Date(keypair.createdAt).toLocaleString()} | ||||
|                       </Typography> | ||||
|                     } | ||||
|                   /> | ||||
|                   <ListItemSecondaryAction> | ||||
|                     {currentKeypair?.id === keypair.id && ( | ||||
|                       <IconButton edge="end" disabled> | ||||
|                         <CheckIcon color="success" /> | ||||
|                       </IconButton> | ||||
|                     )} | ||||
|                   </ListItemSecondaryAction> | ||||
|                 </ListItem> | ||||
|               </Box> | ||||
|             ))} | ||||
|           </List> | ||||
|         </Paper> | ||||
|       )} | ||||
|  | ||||
|       {/* Create Keypair Dialog */} | ||||
|       <Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth> | ||||
|         <DialogTitle>Create New Keypair</DialogTitle> | ||||
|         <DialogContent> | ||||
|           <TextField | ||||
|             label="Name" | ||||
|             value={newKeypairName} | ||||
|             onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairName(e.target.value)} | ||||
|             fullWidth | ||||
|             margin="normal" | ||||
|             disabled={isCreating} | ||||
|           /> | ||||
|  | ||||
|           <FormControl fullWidth margin="normal"> | ||||
|             <InputLabel>Type</InputLabel> | ||||
|             <Select | ||||
|               value={newKeypairType} | ||||
|               onChange={(e) => setNewKeypairType(e.target.value)} | ||||
|               disabled={isCreating} | ||||
|             > | ||||
|               <MenuItem value="Ed25519">Ed25519</MenuItem> | ||||
|               <MenuItem value="Secp256k1">Secp256k1 (Ethereum)</MenuItem> | ||||
|             </Select> | ||||
|           </FormControl> | ||||
|  | ||||
|           <TextField | ||||
|             label="Description" | ||||
|             value={newKeypairDescription} | ||||
|             onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairDescription(e.target.value)} | ||||
|             fullWidth | ||||
|             margin="normal" | ||||
|             multiline | ||||
|             rows={2} | ||||
|             disabled={isCreating} | ||||
|           /> | ||||
|         </DialogContent> | ||||
|         <DialogActions> | ||||
|           <Button onClick={() => setCreateDialogOpen(false)} disabled={isCreating}> | ||||
|             Cancel | ||||
|           </Button> | ||||
|           <Button | ||||
|             onClick={handleCreateKeypair} | ||||
|             color="primary" | ||||
|             variant="contained" | ||||
|             disabled={isCreating || !newKeypairName} | ||||
|           > | ||||
|             {isCreating ? <CircularProgress size={24} /> : 'Create'} | ||||
|           </Button> | ||||
|         </DialogActions> | ||||
|       </Dialog> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default KeypairPage; | ||||
							
								
								
									
										557
									
								
								hero_vault_extension/src/pages/ScriptPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,557 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getChromeApi } from '../utils/chromeApi'; | ||||
| import { | ||||
|   Box, | ||||
|   Typography, | ||||
|   Button, | ||||
|   TextField, | ||||
|   Paper, | ||||
|   Alert, | ||||
|   CircularProgress, | ||||
|   Divider, | ||||
|   Tabs, | ||||
|   Tab, | ||||
|   List, | ||||
|   ListItem, | ||||
|   ListItemText, | ||||
|   ListItemSecondaryAction, | ||||
|   IconButton, | ||||
|   Dialog, | ||||
|   DialogTitle, | ||||
|   DialogContent, | ||||
|   DialogActions, | ||||
|   Chip | ||||
| } from '@mui/material'; | ||||
| import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | ||||
| import VisibilityIcon from '@mui/icons-material/Visibility'; | ||||
| // DeleteIcon removed as it's not used | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
|  | ||||
| interface ScriptResult { | ||||
|   id: string; | ||||
|   timestamp: number; | ||||
|   script: string; | ||||
|   result: string; | ||||
|   success: boolean; | ||||
| } | ||||
|  | ||||
| interface PendingScript { | ||||
|   id: string; | ||||
|   title: string; | ||||
|   description: string; | ||||
|   script: string; | ||||
|   tags: string[]; | ||||
|   timestamp: number; | ||||
| } | ||||
|  | ||||
| const ScriptPage = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const { isSessionUnlocked, currentKeypair } = useSessionStore(); | ||||
|    | ||||
|   const [tabValue, setTabValue] = useState<number>(0); | ||||
|   const [scriptInput, setScriptInput] = useState<string>(''); | ||||
|   const [isExecuting, setIsExecuting] = useState<boolean>(false); | ||||
|   const [executionResult, setExecutionResult] = useState<string | null>(null); | ||||
|   const [executionSuccess, setExecutionSuccess] = useState<boolean | null>(null); | ||||
|   const [scriptResults, setScriptResults] = useState<ScriptResult[]>([]); | ||||
|   const [pendingScripts, setPendingScripts] = useState<PendingScript[]>([]); | ||||
|   const [selectedPendingScript, setSelectedPendingScript] = useState<PendingScript | null>(null); | ||||
|   const [scriptDialogOpen, setScriptDialogOpen] = useState<boolean>(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   // Redirect if not unlocked | ||||
|   useEffect(() => { | ||||
|     if (!isSessionUnlocked) { | ||||
|       navigate('/'); | ||||
|     } | ||||
|   }, [isSessionUnlocked, navigate]); | ||||
|  | ||||
|   // Load pending scripts from storage | ||||
|   useEffect(() => { | ||||
|     const loadPendingScripts = async () => { | ||||
|       try { | ||||
|         const chromeApi = getChromeApi(); | ||||
|         const data = await chromeApi.storage.local.get('pendingScripts'); | ||||
|         if (data.pendingScripts) { | ||||
|           setPendingScripts(data.pendingScripts); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error('Failed to load pending scripts:', err); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (isSessionUnlocked) { | ||||
|       loadPendingScripts(); | ||||
|     } | ||||
|   }, [isSessionUnlocked]); | ||||
|  | ||||
|   // Load script history from storage | ||||
|   useEffect(() => { | ||||
|     const loadScriptResults = async () => { | ||||
|       try { | ||||
|         const chromeApi = getChromeApi(); | ||||
|         const data = await chromeApi.storage.local.get('scriptResults'); | ||||
|         if (data.scriptResults) { | ||||
|           setScriptResults(data.scriptResults); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error('Failed to load script results:', err); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (isSessionUnlocked) { | ||||
|       loadScriptResults(); | ||||
|     } | ||||
|   }, [isSessionUnlocked]); | ||||
|  | ||||
|   const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { | ||||
|     setTabValue(newValue); | ||||
|   }; | ||||
|  | ||||
|   const handleExecuteScript = async () => { | ||||
|     if (!scriptInput.trim()) return; | ||||
|      | ||||
|     setIsExecuting(true); | ||||
|     setError(null); | ||||
|     setExecutionResult(null); | ||||
|     setExecutionSuccess(null); | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM run_rhai function via our store | ||||
|       const result = await useSessionStore.getState().executeScript(scriptInput); | ||||
|        | ||||
|       setExecutionResult(result); | ||||
|       setExecutionSuccess(true); | ||||
|        | ||||
|       // Save to history | ||||
|       const newResult: ScriptResult = { | ||||
|         id: `script-${Date.now()}`, | ||||
|         timestamp: Date.now(), | ||||
|         script: scriptInput, | ||||
|         result, | ||||
|         success: true | ||||
|       }; | ||||
|        | ||||
|       const updatedResults = [newResult, ...scriptResults].slice(0, 20); // Keep last 20 | ||||
|       setScriptResults(updatedResults); | ||||
|        | ||||
|       // Save to storage | ||||
|       const chromeApi = getChromeApi(); | ||||
|       await chromeApi.storage.local.set({ scriptResults: updatedResults }); | ||||
|     } catch (err) { | ||||
|       setError((err as Error).message || 'Failed to execute script'); | ||||
|       setExecutionSuccess(false); | ||||
|       setExecutionResult('Execution failed'); | ||||
|     } finally { | ||||
|       setIsExecuting(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleViewPendingScript = (script: PendingScript) => { | ||||
|     setSelectedPendingScript(script); | ||||
|     setScriptDialogOpen(true); | ||||
|   }; | ||||
|  | ||||
|   const handleApprovePendingScript = async () => { | ||||
|     if (!selectedPendingScript) return; | ||||
|      | ||||
|     setScriptDialogOpen(false); | ||||
|     setScriptInput(selectedPendingScript.script); | ||||
|     setTabValue(0); // Switch to execute tab | ||||
|      | ||||
|     // Remove from pending list | ||||
|     const updatedPendingScripts = pendingScripts.filter( | ||||
|       script => script.id !== selectedPendingScript.id | ||||
|     ); | ||||
|      | ||||
|     setPendingScripts(updatedPendingScripts); | ||||
|     const chromeApi = getChromeApi(); | ||||
|     await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts }); | ||||
|     setSelectedPendingScript(null); | ||||
|   }; | ||||
|  | ||||
|   const handleRejectPendingScript = async () => { | ||||
|     if (!selectedPendingScript) return; | ||||
|      | ||||
|     // Remove from pending list | ||||
|     const updatedPendingScripts = pendingScripts.filter( | ||||
|       script => script.id !== selectedPendingScript.id | ||||
|     ); | ||||
|      | ||||
|     setPendingScripts(updatedPendingScripts); | ||||
|     const chromeApi = getChromeApi(); | ||||
|     await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts }); | ||||
|      | ||||
|     setScriptDialogOpen(false); | ||||
|     setSelectedPendingScript(null); | ||||
|   }; | ||||
|  | ||||
|   const handleClearHistory = async () => { | ||||
|     setScriptResults([]); | ||||
|     const chromeApi = getChromeApi(); | ||||
|     await chromeApi.storage.local.set({ scriptResults: [] }); | ||||
|   }; | ||||
|  | ||||
|   if (!isSessionUnlocked) { | ||||
|     return null; // Will redirect via useEffect | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> | ||||
|       <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> | ||||
|         <Tabs  | ||||
|           value={tabValue}  | ||||
|           onChange={handleTabChange}  | ||||
|           aria-label="script tabs" | ||||
|           variant="scrollable" | ||||
|           scrollButtons="auto" | ||||
|           allowScrollButtonsMobile | ||||
|           sx={{ minHeight: '48px' }} | ||||
|         > | ||||
|           <Tab label="Execute" sx={{ minHeight: '48px', py: 0 }} /> | ||||
|           <Tab  | ||||
|             label={ | ||||
|               <Box sx={{ display: 'flex', alignItems: 'center' }}> | ||||
|                 Pending | ||||
|                 {pendingScripts.length > 0 && ( | ||||
|                   <Chip  | ||||
|                     label={pendingScripts.length}  | ||||
|                     size="small"  | ||||
|                     color="primary"  | ||||
|                     sx={{ ml: 1 }}  | ||||
|                   /> | ||||
|                 )} | ||||
|               </Box> | ||||
|             } | ||||
|             sx={{ minHeight: '48px', py: 0 }} | ||||
|           /> | ||||
|           <Tab label="History" sx={{ minHeight: '48px', py: 0 }} /> | ||||
|         </Tabs> | ||||
|       </Box> | ||||
|  | ||||
|       {/* Execute Tab */} | ||||
|       {tabValue === 0 && ( | ||||
|         <Box sx={{  | ||||
|           p: 2,  | ||||
|           flexGrow: 1,  | ||||
|           display: 'flex',  | ||||
|           flexDirection: 'column', | ||||
|           overflow: 'hidden', | ||||
|           height: 'calc(100% - 48px)' // Subtract tab height | ||||
|         }}> | ||||
|           <Box sx={{  | ||||
|             display: 'flex',  | ||||
|             flexDirection: 'column', | ||||
|             overflow: 'auto', | ||||
|             height: '100%', | ||||
|             pb: 2 // Add padding at bottom for scrolling | ||||
|           }}> | ||||
|             {!currentKeypair && ( | ||||
|               <Alert severity="warning" sx={{ mb: 2 }}> | ||||
|                 No keypair selected. Select a keypair to enable script execution with signing capabilities. | ||||
|               </Alert> | ||||
|             )} | ||||
|              | ||||
|             {error && ( | ||||
|               <Alert severity="error" sx={{ mb: 2 }}> | ||||
|                 {error} | ||||
|               </Alert> | ||||
|             )} | ||||
|              | ||||
|             <TextField | ||||
|               label="Rhai Script" | ||||
|               multiline | ||||
|               rows={6} // Reduced from 8 to leave more space for results | ||||
|               value={scriptInput} | ||||
|               onChange={(e) => setScriptInput(e.target.value)} | ||||
|               fullWidth | ||||
|               variant="outlined" | ||||
|               placeholder="Enter your Rhai script here..." | ||||
|               sx={{ mb: 2 }} | ||||
|               disabled={isExecuting} | ||||
|             /> | ||||
|              | ||||
|             <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 color="primary" | ||||
|                 startIcon={<PlayArrowIcon />} | ||||
|                 onClick={handleExecuteScript} | ||||
|                 disabled={isExecuting || !scriptInput.trim()} | ||||
|               > | ||||
|                 {isExecuting ? <CircularProgress size={24} /> : 'Execute'} | ||||
|               </Button> | ||||
|             </Box> | ||||
|              | ||||
|             {executionResult && ( | ||||
|               <Paper  | ||||
|                 variant="outlined"  | ||||
|                 sx={{  | ||||
|                   p: 2,  | ||||
|                   bgcolor: executionSuccess ? 'success.dark' : 'error.dark', | ||||
|                   color: 'white', | ||||
|                   overflowY: 'auto', | ||||
|                   mb: 2, // Add margin at bottom | ||||
|                   minHeight: '100px', // Ensure minimum height for visibility | ||||
|                   maxHeight: '200px' // Limit maximum height | ||||
|                 }} | ||||
|               > | ||||
|                 <Typography variant="subtitle2" gutterBottom> | ||||
|                   Execution Result: | ||||
|                 </Typography> | ||||
|                 <Typography  | ||||
|                   variant="body2"  | ||||
|                   component="pre"  | ||||
|                   sx={{  | ||||
|                     whiteSpace: 'pre-wrap',  | ||||
|                     wordBreak: 'break-word', | ||||
|                     fontFamily: 'monospace' | ||||
|                   }} | ||||
|                 > | ||||
|                   {executionResult} | ||||
|                 </Typography> | ||||
|               </Paper> | ||||
|             )} | ||||
|           </Box> | ||||
|         </Box> | ||||
|       )} | ||||
|  | ||||
|       {/* Pending Scripts Tab */} | ||||
|       {tabValue === 1 && ( | ||||
|         <Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}> | ||||
|           {pendingScripts.length === 0 ? ( | ||||
|             <Paper sx={{ p: 3, textAlign: 'center' }}> | ||||
|               <Typography variant="body1" color="text.secondary"> | ||||
|                 No pending scripts. Incoming scripts from connected WebSocket servers will appear here. | ||||
|               </Typography> | ||||
|             </Paper> | ||||
|           ) : ( | ||||
|             <Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}> | ||||
|               <List disablePadding> | ||||
|                 {pendingScripts.map((script, index) => ( | ||||
|                   <Box key={script.id}> | ||||
|                     {index > 0 && <Divider />} | ||||
|                     <ListItem> | ||||
|                       <ListItemText | ||||
|                         primary={script.title} | ||||
|                         secondary={ | ||||
|                           <> | ||||
|                             <Typography variant="body2" color="text.secondary"> | ||||
|                               {script.description || 'No description'} | ||||
|                             </Typography> | ||||
|                             <Box sx={{ mt: 0.5 }}> | ||||
|                               {script.tags.map(tag => ( | ||||
|                                 <Chip | ||||
|                                   key={tag} | ||||
|                                   label={tag} | ||||
|                                   size="small" | ||||
|                                   color={tag === 'remote' ? 'secondary' : 'primary'} | ||||
|                                   variant="outlined" | ||||
|                                   sx={{ mr: 0.5 }} | ||||
|                                 /> | ||||
|                               ))} | ||||
|                             </Box> | ||||
|                           </> | ||||
|                         } | ||||
|                       /> | ||||
|                       <ListItemSecondaryAction> | ||||
|                         <IconButton  | ||||
|                           edge="end"  | ||||
|                           onClick={() => handleViewPendingScript(script)} | ||||
|                           aria-label="view script" | ||||
|                         > | ||||
|                           <VisibilityIcon /> | ||||
|                         </IconButton> | ||||
|                       </ListItemSecondaryAction> | ||||
|                     </ListItem> | ||||
|                   </Box> | ||||
|                 ))} | ||||
|               </List> | ||||
|             </Paper> | ||||
|           )} | ||||
|         </Box> | ||||
|       )} | ||||
|  | ||||
|       {/* History Tab */} | ||||
|       {tabValue === 2 && ( | ||||
|         <Box sx={{  | ||||
|           p: 2,  | ||||
|           flexGrow: 1,  | ||||
|           display: 'flex',  | ||||
|           flexDirection: 'column', | ||||
|           overflow: 'hidden', | ||||
|           height: 'calc(100% - 48px)' // Subtract tab height | ||||
|         }}> | ||||
|           <Box sx={{  | ||||
|             display: 'flex',  | ||||
|             flexDirection: 'column', | ||||
|             overflow: 'auto', | ||||
|             height: '100%', | ||||
|             pb: 2 // Add padding at bottom for scrolling | ||||
|           }}> | ||||
|             <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> | ||||
|               <Button | ||||
|                 variant="outlined" | ||||
|                 color="error" | ||||
|                 size="small" | ||||
|                 onClick={handleClearHistory} | ||||
|                 disabled={scriptResults.length === 0} | ||||
|               > | ||||
|                 Clear History | ||||
|               </Button> | ||||
|             </Box> | ||||
|            | ||||
|           {scriptResults.length === 0 ? ( | ||||
|             <Paper sx={{ p: 3, textAlign: 'center' }}> | ||||
|               <Typography variant="body1" color="text.secondary"> | ||||
|                 No script execution history yet. | ||||
|               </Typography> | ||||
|             </Paper> | ||||
|           ) : ( | ||||
|             <Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}> | ||||
|               <List disablePadding> | ||||
|                 {scriptResults.map((result, index) => ( | ||||
|                   <Box key={result.id}> | ||||
|                     {index > 0 && <Divider />} | ||||
|                     <ListItem> | ||||
|                       <ListItemText | ||||
|                         primary={ | ||||
|                           <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | ||||
|                             <Typography variant="subtitle2"> | ||||
|                               {new Date(result.timestamp).toLocaleString()} | ||||
|                             </Typography> | ||||
|                             <Chip | ||||
|                               label={result.success ? 'Success' : 'Failed'} | ||||
|                               size="small" | ||||
|                               color={result.success ? 'success' : 'error'} | ||||
|                               variant="outlined" | ||||
|                             /> | ||||
|                           </Box> | ||||
|                         } | ||||
|                         secondary={ | ||||
|                           <Typography  | ||||
|                             variant="body2"  | ||||
|                             color="text.secondary" | ||||
|                             sx={{  | ||||
|                               whiteSpace: 'nowrap',  | ||||
|                               overflow: 'hidden',  | ||||
|                               textOverflow: 'ellipsis', | ||||
|                               maxWidth: '280px' | ||||
|                             }} | ||||
|                           > | ||||
|                             {result.script} | ||||
|                           </Typography> | ||||
|                         } | ||||
|                       /> | ||||
|                       <ListItemSecondaryAction> | ||||
|                         <IconButton  | ||||
|                           edge="end"  | ||||
|                           onClick={() => { | ||||
|                             setScriptInput(result.script); | ||||
|                             setTabValue(0); | ||||
|                           }} | ||||
|                           aria-label="reuse script" | ||||
|                         > | ||||
|                           <PlayArrowIcon /> | ||||
|                         </IconButton> | ||||
|                       </ListItemSecondaryAction> | ||||
|                     </ListItem> | ||||
|                   </Box> | ||||
|                 ))} | ||||
|               </List> | ||||
|             </Paper> | ||||
|           )} | ||||
|           </Box> | ||||
|         </Box> | ||||
|       )} | ||||
|  | ||||
|       {/* Pending Script Dialog */} | ||||
|       <Dialog  | ||||
|         open={scriptDialogOpen}  | ||||
|         onClose={() => setScriptDialogOpen(false)} | ||||
|         maxWidth="md" | ||||
|         fullWidth | ||||
|       > | ||||
|         <DialogTitle> | ||||
|           {selectedPendingScript?.title || 'Script Details'} | ||||
|         </DialogTitle> | ||||
|         <DialogContent> | ||||
|           {selectedPendingScript && ( | ||||
|             <> | ||||
|               <Typography variant="subtitle2" gutterBottom> | ||||
|                 Description: | ||||
|               </Typography> | ||||
|               <Typography variant="body2" paragraph> | ||||
|                 {selectedPendingScript.description || 'No description provided'} | ||||
|               </Typography> | ||||
|                | ||||
|               <Box sx={{ mb: 2 }}> | ||||
|                 {selectedPendingScript.tags.map(tag => ( | ||||
|                   <Chip | ||||
|                     key={tag} | ||||
|                     label={tag} | ||||
|                     size="small" | ||||
|                     color={tag === 'remote' ? 'secondary' : 'primary'} | ||||
|                     sx={{ mr: 0.5 }} | ||||
|                   /> | ||||
|                 ))} | ||||
|               </Box> | ||||
|                | ||||
|               <Typography variant="subtitle2" gutterBottom> | ||||
|                 Script Content: | ||||
|               </Typography> | ||||
|               <Paper  | ||||
|                 variant="outlined"  | ||||
|                 sx={{  | ||||
|                   p: 2,  | ||||
|                   bgcolor: 'background.paper', | ||||
|                   maxHeight: '300px', | ||||
|                   overflow: 'auto' | ||||
|                 }} | ||||
|               > | ||||
|                 <Typography  | ||||
|                   variant="body2"  | ||||
|                   component="pre"  | ||||
|                   sx={{  | ||||
|                     whiteSpace: 'pre-wrap',  | ||||
|                     wordBreak: 'break-word', | ||||
|                     fontFamily: 'monospace' | ||||
|                   }} | ||||
|                 > | ||||
|                   {selectedPendingScript.script} | ||||
|                 </Typography> | ||||
|               </Paper> | ||||
|                | ||||
|               <Alert severity="warning" sx={{ mt: 2 }}> | ||||
|                 <Typography variant="body2"> | ||||
|                   {selectedPendingScript.tags.includes('remote')  | ||||
|                     ? 'This is a remote script. If approved, your signature will be sent to the server and the script may execute remotely.' | ||||
|                     : 'This script will execute locally in your browser extension if approved.'} | ||||
|                 </Typography> | ||||
|               </Alert> | ||||
|             </> | ||||
|           )} | ||||
|         </DialogContent> | ||||
|         <DialogActions> | ||||
|           <Button  | ||||
|             onClick={handleRejectPendingScript}  | ||||
|             color="error" | ||||
|             variant="outlined" | ||||
|           > | ||||
|             Reject | ||||
|           </Button> | ||||
|           <Button  | ||||
|             onClick={handleApprovePendingScript}  | ||||
|             color="primary" | ||||
|             variant="contained" | ||||
|           > | ||||
|             Approve | ||||
|           </Button> | ||||
|         </DialogActions> | ||||
|       </Dialog> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ScriptPage; | ||||
							
								
								
									
										191
									
								
								hero_vault_extension/src/pages/SessionPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,191 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   Typography, | ||||
|   Button, | ||||
|   Paper, | ||||
|   Alert, | ||||
|   CircularProgress, | ||||
|   List, | ||||
|   ListItem, | ||||
|   ListItemText, | ||||
|   Divider, | ||||
|   Card, | ||||
|   CardContent, | ||||
|   Grid | ||||
| } from '@mui/material'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
| import LockIcon from '@mui/icons-material/Lock'; | ||||
| import SecurityIcon from '@mui/icons-material/Security'; | ||||
| // HistoryIcon removed as it's not used | ||||
|  | ||||
| interface SessionActivity { | ||||
|   id: string; | ||||
|   action: string; | ||||
|   timestamp: number; | ||||
|   details?: string; | ||||
| } | ||||
|  | ||||
| const SessionPage = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const {  | ||||
|     isSessionUnlocked,  | ||||
|     currentKeyspace,  | ||||
|     currentKeypair, | ||||
|     lockSession | ||||
|   } = useSessionStore(); | ||||
|    | ||||
|   const [sessionActivities, setSessionActivities] = useState<SessionActivity[]>([]); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|  | ||||
|   // Redirect if not unlocked | ||||
|   useEffect(() => { | ||||
|     if (!isSessionUnlocked) { | ||||
|       navigate('/'); | ||||
|     } | ||||
|   }, [isSessionUnlocked, navigate]); | ||||
|  | ||||
|   // Load session activities from storage | ||||
|   useEffect(() => { | ||||
|     const loadSessionActivities = async () => { | ||||
|       try { | ||||
|         setIsLoading(true); | ||||
|         const data = await chrome.storage.local.get('sessionActivities'); | ||||
|         if (data.sessionActivities) { | ||||
|           setSessionActivities(data.sessionActivities); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error('Failed to load session activities:', err); | ||||
|       } finally { | ||||
|         setIsLoading(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (isSessionUnlocked) { | ||||
|       loadSessionActivities(); | ||||
|     } | ||||
|   }, [isSessionUnlocked]); | ||||
|  | ||||
|   const handleLockSession = async () => { | ||||
|     try { | ||||
|       await lockSession(); | ||||
|       navigate('/'); | ||||
|     } catch (err) { | ||||
|       console.error('Failed to lock session:', err); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (!isSessionUnlocked) { | ||||
|     return null; // Will redirect via useEffect | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         Session Management | ||||
|       </Typography> | ||||
|        | ||||
|       <Grid container spacing={2} sx={{ mb: 3 }}> | ||||
|         <Grid item xs={12} sm={6}> | ||||
|           <Card variant="outlined"> | ||||
|             <CardContent> | ||||
|               <Typography color="text.secondary" gutterBottom> | ||||
|                 Current Keyspace | ||||
|               </Typography> | ||||
|               <Typography variant="h5" component="div"> | ||||
|                 {currentKeyspace || 'None'} | ||||
|               </Typography> | ||||
|             </CardContent> | ||||
|           </Card> | ||||
|         </Grid> | ||||
|          | ||||
|         <Grid item xs={12} sm={6}> | ||||
|           <Card variant="outlined"> | ||||
|             <CardContent> | ||||
|               <Typography color="text.secondary" gutterBottom> | ||||
|                 Selected Keypair | ||||
|               </Typography> | ||||
|               <Typography variant="h5" component="div"> | ||||
|                 {currentKeypair?.name || currentKeypair?.id || 'None'} | ||||
|               </Typography> | ||||
|               {currentKeypair && ( | ||||
|                 <Typography variant="body2" color="text.secondary"> | ||||
|                   Type: {currentKeypair.type} | ||||
|                 </Typography> | ||||
|               )} | ||||
|             </CardContent> | ||||
|           </Card> | ||||
|         </Grid> | ||||
|       </Grid> | ||||
|        | ||||
|       <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}> | ||||
|         <Typography variant="subtitle1"> | ||||
|           Session Activity | ||||
|         </Typography> | ||||
|          | ||||
|         <Button | ||||
|           variant="outlined" | ||||
|           color="error" | ||||
|           startIcon={<LockIcon />} | ||||
|           onClick={handleLockSession} | ||||
|         > | ||||
|           Lock Session | ||||
|         </Button> | ||||
|       </Box> | ||||
|        | ||||
|       {isLoading ? ( | ||||
|         <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> | ||||
|           <CircularProgress /> | ||||
|         </Box> | ||||
|       ) : sessionActivities.length === 0 ? ( | ||||
|         <Paper sx={{ p: 3, textAlign: 'center' }}> | ||||
|           <Typography variant="body1" color="text.secondary"> | ||||
|             No session activity recorded yet. | ||||
|           </Typography> | ||||
|         </Paper> | ||||
|       ) : ( | ||||
|         <Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}> | ||||
|           <List disablePadding> | ||||
|             {sessionActivities.map((activity, index) => ( | ||||
|               <Box key={activity.id}> | ||||
|                 {index > 0 && <Divider />} | ||||
|                 <ListItem> | ||||
|                   <ListItemText | ||||
|                     primary={ | ||||
|                       <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | ||||
|                         <Typography variant="subtitle2"> | ||||
|                           {activity.action} | ||||
|                         </Typography> | ||||
|                       </Box> | ||||
|                     } | ||||
|                     secondary={ | ||||
|                       <> | ||||
|                         <Typography variant="body2" color="text.secondary"> | ||||
|                           {new Date(activity.timestamp).toLocaleString()} | ||||
|                         </Typography> | ||||
|                         {activity.details && ( | ||||
|                           <Typography variant="body2" color="text.secondary"> | ||||
|                             {activity.details} | ||||
|                           </Typography> | ||||
|                         )} | ||||
|                       </> | ||||
|                     } | ||||
|                   /> | ||||
|                 </ListItem> | ||||
|               </Box> | ||||
|             ))} | ||||
|           </List> | ||||
|         </Paper> | ||||
|       )} | ||||
|        | ||||
|       <Box sx={{ mt: 3 }}> | ||||
|         <Alert severity="info" icon={<SecurityIcon />}> | ||||
|           Your session is active. All cryptographic operations and script executions require explicit approval. | ||||
|         </Alert> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SessionPage; | ||||
							
								
								
									
										246
									
								
								hero_vault_extension/src/pages/SettingsPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,246 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   Typography, | ||||
|   Switch, | ||||
|   // FormControlLabel removed as it's not used | ||||
|   Divider, | ||||
|   Paper, | ||||
|   List, | ||||
|   ListItem, | ||||
|   ListItemText, | ||||
|   Button, | ||||
|   Dialog, | ||||
|   DialogTitle, | ||||
|   DialogContent, | ||||
|   DialogActions, | ||||
|   TextField, | ||||
|   Alert, | ||||
|   Snackbar | ||||
| } from '@mui/material'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| import InfoIcon from '@mui/icons-material/Info'; | ||||
|  | ||||
| interface Settings { | ||||
|   darkMode: boolean; | ||||
|   autoLockTimeout: number; // minutes | ||||
|   confirmCryptoOperations: boolean; | ||||
|   showScriptNotifications: boolean; | ||||
| } | ||||
|  | ||||
| const SettingsPage = () => { | ||||
|   const [settings, setSettings] = useState<Settings>({ | ||||
|     darkMode: true, | ||||
|     autoLockTimeout: 15, | ||||
|     confirmCryptoOperations: true, | ||||
|     showScriptNotifications: true | ||||
|   }); | ||||
|    | ||||
|   const [clearDataDialogOpen, setClearDataDialogOpen] = useState(false); | ||||
|   const [confirmText, setConfirmText] = useState(''); | ||||
|   const [snackbarOpen, setSnackbarOpen] = useState(false); | ||||
|   const [snackbarMessage, setSnackbarMessage] = useState(''); | ||||
|  | ||||
|   // Load settings from storage | ||||
|   useEffect(() => { | ||||
|     const loadSettings = async () => { | ||||
|       try { | ||||
|         const data = await chrome.storage.local.get('settings'); | ||||
|         if (data.settings) { | ||||
|           setSettings(data.settings); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error('Failed to load settings:', err); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     loadSettings(); | ||||
|   }, []); | ||||
|  | ||||
|   // Save settings when changed | ||||
|   const handleSettingChange = (key: keyof Settings, value: boolean | number) => { | ||||
|     const updatedSettings = { ...settings, [key]: value }; | ||||
|     setSettings(updatedSettings); | ||||
|      | ||||
|     // Save to storage | ||||
|     chrome.storage.local.set({ settings: updatedSettings }) | ||||
|       .then(() => { | ||||
|         setSnackbarMessage('Settings saved'); | ||||
|         setSnackbarOpen(true); | ||||
|       }) | ||||
|       .catch(err => { | ||||
|         console.error('Failed to save settings:', err); | ||||
|         setSnackbarMessage('Failed to save settings'); | ||||
|         setSnackbarOpen(true); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   const handleClearAllData = () => { | ||||
|     if (confirmText !== 'CLEAR ALL DATA') { | ||||
|       setSnackbarMessage('Please type the confirmation text exactly'); | ||||
|       setSnackbarOpen(true); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Clear all extension data | ||||
|     chrome.storage.local.clear() | ||||
|       .then(() => { | ||||
|         setSnackbarMessage('All data cleared successfully'); | ||||
|         setSnackbarOpen(true); | ||||
|         setClearDataDialogOpen(false); | ||||
|         setConfirmText(''); | ||||
|          | ||||
|         // Reset settings to defaults | ||||
|         setSettings({ | ||||
|           darkMode: true, | ||||
|           autoLockTimeout: 15, | ||||
|           confirmCryptoOperations: true, | ||||
|           showScriptNotifications: true | ||||
|         }); | ||||
|       }) | ||||
|       .catch(err => { | ||||
|         console.error('Failed to clear data:', err); | ||||
|         setSnackbarMessage('Failed to clear data'); | ||||
|         setSnackbarOpen(true); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         Settings | ||||
|       </Typography> | ||||
|        | ||||
|       <Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}> | ||||
|         <List disablePadding> | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="Dark Mode" | ||||
|               secondary="Use dark theme for the extension" | ||||
|             /> | ||||
|             <Switch | ||||
|               edge="end" | ||||
|               checked={settings.darkMode} | ||||
|               onChange={(e) => handleSettingChange('darkMode', e.target.checked)} | ||||
|             /> | ||||
|           </ListItem> | ||||
|            | ||||
|           <Divider /> | ||||
|            | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="Auto-Lock Timeout" | ||||
|               secondary={`Automatically lock session after ${settings.autoLockTimeout} minutes of inactivity`} | ||||
|             /> | ||||
|             <Box sx={{ width: 120 }}> | ||||
|               <TextField | ||||
|                 type="number" | ||||
|                 size="small" | ||||
|                 value={settings.autoLockTimeout} | ||||
|                 onChange={(e) => { | ||||
|                   const value = parseInt(e.target.value); | ||||
|                   if (!isNaN(value) && value >= 1) { | ||||
|                     handleSettingChange('autoLockTimeout', value); | ||||
|                   } | ||||
|                 }} | ||||
|                 InputProps={{ inputProps: { min: 1, max: 60 } }} | ||||
|               /> | ||||
|             </Box> | ||||
|           </ListItem> | ||||
|            | ||||
|           <Divider /> | ||||
|            | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="Confirm Cryptographic Operations" | ||||
|               secondary="Always ask for confirmation before signing or encrypting" | ||||
|             /> | ||||
|             <Switch | ||||
|               edge="end" | ||||
|               checked={settings.confirmCryptoOperations} | ||||
|               onChange={(e) => handleSettingChange('confirmCryptoOperations', e.target.checked)} | ||||
|             /> | ||||
|           </ListItem> | ||||
|            | ||||
|           <Divider /> | ||||
|            | ||||
|           <ListItem> | ||||
|             <ListItemText | ||||
|               primary="Script Notifications" | ||||
|               secondary="Show notifications when new scripts are received" | ||||
|             /> | ||||
|             <Switch | ||||
|               edge="end" | ||||
|               checked={settings.showScriptNotifications} | ||||
|               onChange={(e) => handleSettingChange('showScriptNotifications', e.target.checked)} | ||||
|             /> | ||||
|           </ListItem> | ||||
|         </List> | ||||
|       </Paper> | ||||
|        | ||||
|       <Box sx={{ mt: 3 }}> | ||||
|         <Alert  | ||||
|           severity="info"  | ||||
|           icon={<InfoIcon />} | ||||
|           sx={{ mb: 2 }} | ||||
|         > | ||||
|           <Typography variant="body2"> | ||||
|             The extension stores all cryptographic keys in encrypted form. Your password is never stored and is only kept in memory while the session is unlocked. | ||||
|           </Typography> | ||||
|         </Alert> | ||||
|          | ||||
|         <Button | ||||
|           variant="outlined" | ||||
|           color="error" | ||||
|           startIcon={<DeleteIcon />} | ||||
|           onClick={() => setClearDataDialogOpen(true)} | ||||
|           fullWidth | ||||
|         > | ||||
|           Clear All Data | ||||
|         </Button> | ||||
|       </Box> | ||||
|        | ||||
|       {/* Clear Data Confirmation Dialog */} | ||||
|       <Dialog open={clearDataDialogOpen} onClose={() => setClearDataDialogOpen(false)}> | ||||
|         <DialogTitle>Clear All Extension Data</DialogTitle> | ||||
|         <DialogContent> | ||||
|           <Typography variant="body1" paragraph> | ||||
|             This will permanently delete all your keyspaces, keypairs, and settings. This action cannot be undone. | ||||
|           </Typography> | ||||
|           <Typography variant="body2" color="error" paragraph> | ||||
|             Type "CLEAR ALL DATA" to confirm: | ||||
|           </Typography> | ||||
|           <TextField | ||||
|             value={confirmText} | ||||
|             onChange={(e) => setConfirmText(e.target.value)} | ||||
|             fullWidth | ||||
|             variant="outlined" | ||||
|             placeholder="CLEAR ALL DATA" | ||||
|           /> | ||||
|         </DialogContent> | ||||
|         <DialogActions> | ||||
|           <Button onClick={() => setClearDataDialogOpen(false)}> | ||||
|             Cancel | ||||
|           </Button> | ||||
|           <Button  | ||||
|             onClick={handleClearAllData}  | ||||
|             color="error" | ||||
|             disabled={confirmText !== 'CLEAR ALL DATA'} | ||||
|           > | ||||
|             Clear All Data | ||||
|           </Button> | ||||
|         </DialogActions> | ||||
|       </Dialog> | ||||
|        | ||||
|       {/* Snackbar for notifications */} | ||||
|       <Snackbar | ||||
|         open={snackbarOpen} | ||||
|         autoHideDuration={3000} | ||||
|         onClose={() => setSnackbarOpen(false)} | ||||
|         message={snackbarMessage} | ||||
|       /> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SettingsPage; | ||||
							
								
								
									
										248
									
								
								hero_vault_extension/src/pages/WebSocketPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,248 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { | ||||
|   Box, | ||||
|   Typography, | ||||
|   Button, | ||||
|   TextField, | ||||
|   Paper, | ||||
|   Alert, | ||||
|   CircularProgress, | ||||
|   List, | ||||
|   ListItem, | ||||
|   ListItemText, | ||||
|   Divider, | ||||
|   Chip | ||||
| } from '@mui/material'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { useSessionStore } from '../store/sessionStore'; | ||||
|  | ||||
| interface ConnectionHistory { | ||||
|   id: string; | ||||
|   url: string; | ||||
|   timestamp: number; | ||||
|   status: 'connected' | 'disconnected'; | ||||
| } | ||||
|  | ||||
| const WebSocketPage = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const {  | ||||
|     isSessionUnlocked,  | ||||
|     currentKeypair,  | ||||
|     isWebSocketConnected,  | ||||
|     webSocketUrl, | ||||
|     connectWebSocket, | ||||
|     disconnectWebSocket | ||||
|   } = useSessionStore(); | ||||
|    | ||||
|   const [serverUrl, setServerUrl] = useState(''); | ||||
|   const [isConnecting, setIsConnecting] = useState(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [connectionHistory, setConnectionHistory] = useState<ConnectionHistory[]>([]); | ||||
|  | ||||
|   // Redirect if not unlocked | ||||
|   useEffect(() => { | ||||
|     if (!isSessionUnlocked) { | ||||
|       navigate('/'); | ||||
|     } | ||||
|   }, [isSessionUnlocked, navigate]); | ||||
|  | ||||
|   // Load connection history from storage | ||||
|   useEffect(() => { | ||||
|     const loadConnectionHistory = async () => { | ||||
|       try { | ||||
|         const data = await chrome.storage.local.get('connectionHistory'); | ||||
|         if (data.connectionHistory) { | ||||
|           setConnectionHistory(data.connectionHistory); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error('Failed to load connection history:', err); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (isSessionUnlocked) { | ||||
|       loadConnectionHistory(); | ||||
|     } | ||||
|   }, [isSessionUnlocked]); | ||||
|  | ||||
|   const handleConnect = async () => { | ||||
|     if (!serverUrl.trim() || !currentKeypair) return; | ||||
|      | ||||
|     setIsConnecting(true); | ||||
|     setError(null); | ||||
|      | ||||
|     try { | ||||
|       const success = await connectWebSocket(serverUrl); | ||||
|        | ||||
|       if (success) { | ||||
|         // Add to connection history | ||||
|         const newConnection: ConnectionHistory = { | ||||
|           id: `conn-${Date.now()}`, | ||||
|           url: serverUrl, | ||||
|           timestamp: Date.now(), | ||||
|           status: 'connected' | ||||
|         }; | ||||
|          | ||||
|         const updatedHistory = [newConnection, ...connectionHistory].slice(0, 10); // Keep last 10 | ||||
|         setConnectionHistory(updatedHistory); | ||||
|          | ||||
|         // Save to storage | ||||
|         await chrome.storage.local.set({ connectionHistory: updatedHistory }); | ||||
|       } else { | ||||
|         throw new Error('Failed to connect to WebSocket server'); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       setError((err as Error).message || 'Failed to connect to WebSocket server'); | ||||
|     } finally { | ||||
|       setIsConnecting(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDisconnect = async () => { | ||||
|     try { | ||||
|       const success = await disconnectWebSocket(); | ||||
|        | ||||
|       if (success && webSocketUrl) { | ||||
|         // Update connection history | ||||
|         const updatedHistory = connectionHistory.map(conn =>  | ||||
|           conn.url === webSocketUrl && conn.status === 'connected' | ||||
|             ? { ...conn, status: 'disconnected' } | ||||
|             : conn | ||||
|         ); | ||||
|          | ||||
|         setConnectionHistory(updatedHistory); | ||||
|          | ||||
|         // Save to storage | ||||
|         await chrome.storage.local.set({ connectionHistory: updatedHistory }); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       setError((err as Error).message || 'Failed to disconnect from WebSocket server'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleQuickConnect = (url: string) => { | ||||
|     setServerUrl(url); | ||||
|     // Don't auto-connect to avoid unexpected connections | ||||
|   }; | ||||
|  | ||||
|   if (!isSessionUnlocked) { | ||||
|     return null; // Will redirect via useEffect | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         WebSocket Connection | ||||
|       </Typography> | ||||
|        | ||||
|       {!currentKeypair && ( | ||||
|         <Alert severity="warning" sx={{ mb: 2 }}> | ||||
|           No keypair selected. Select a keypair before connecting to a WebSocket server. | ||||
|         </Alert> | ||||
|       )} | ||||
|        | ||||
|       {error && ( | ||||
|         <Alert severity="error" sx={{ mb: 2 }}> | ||||
|           {error} | ||||
|         </Alert> | ||||
|       )} | ||||
|        | ||||
|       <Paper variant="outlined" sx={{ p: 2, mb: 2 }}> | ||||
|         <Box sx={{ mb: 2 }}> | ||||
|           <Typography variant="subtitle2" gutterBottom> | ||||
|             Connection Status: | ||||
|           </Typography> | ||||
|           <Chip | ||||
|             label={isWebSocketConnected ? 'Connected' : 'Disconnected'} | ||||
|             color={isWebSocketConnected ? 'success' : 'default'} | ||||
|             variant="outlined" | ||||
|           /> | ||||
|           {isWebSocketConnected && webSocketUrl && ( | ||||
|             <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | ||||
|               Connected to: {webSocketUrl} | ||||
|             </Typography> | ||||
|           )} | ||||
|         </Box> | ||||
|          | ||||
|         <Box sx={{ display: 'flex', gap: 1 }}> | ||||
|           <TextField | ||||
|             label="WebSocket Server URL" | ||||
|             placeholder="wss://example.com/ws" | ||||
|             value={serverUrl} | ||||
|             onChange={(e) => setServerUrl(e.target.value)} | ||||
|             fullWidth | ||||
|             disabled={isConnecting || isWebSocketConnected || !currentKeypair} | ||||
|           /> | ||||
|            | ||||
|           {isWebSocketConnected ? ( | ||||
|             <Button | ||||
|               variant="outlined" | ||||
|               color="error" | ||||
|               onClick={handleDisconnect} | ||||
|             > | ||||
|               Disconnect | ||||
|             </Button> | ||||
|           ) : ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               color="primary" | ||||
|               onClick={handleConnect} | ||||
|               disabled={isConnecting || !serverUrl.trim() || !currentKeypair} | ||||
|             > | ||||
|               {isConnecting ? <CircularProgress size={24} /> : 'Connect'} | ||||
|             </Button> | ||||
|           )} | ||||
|         </Box> | ||||
|       </Paper> | ||||
|        | ||||
|       <Typography variant="subtitle1" gutterBottom> | ||||
|         Connection History | ||||
|       </Typography> | ||||
|        | ||||
|       {connectionHistory.length === 0 ? ( | ||||
|         <Paper sx={{ p: 3, textAlign: 'center' }}> | ||||
|           <Typography variant="body1" color="text.secondary"> | ||||
|             No connection history yet. | ||||
|           </Typography> | ||||
|         </Paper> | ||||
|       ) : ( | ||||
|         <Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}> | ||||
|           <List disablePadding> | ||||
|             {connectionHistory.map((conn, index) => ( | ||||
|               <Box key={conn.id}> | ||||
|                 {index > 0 && <Divider />} | ||||
|                 <ListItem  | ||||
|                   button  | ||||
|                   onClick={() => handleQuickConnect(conn.url)} | ||||
|                   disabled={isWebSocketConnected} | ||||
|                 > | ||||
|                   <ListItemText | ||||
|                     primary={ | ||||
|                       <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | ||||
|                         <Typography variant="subtitle2"> | ||||
|                           {conn.url} | ||||
|                         </Typography> | ||||
|                         <Chip | ||||
|                           label={conn.status} | ||||
|                           size="small" | ||||
|                           color={conn.status === 'connected' ? 'success' : 'default'} | ||||
|                           variant="outlined" | ||||
|                         /> | ||||
|                       </Box> | ||||
|                     } | ||||
|                     secondary={ | ||||
|                       <Typography variant="body2" color="text.secondary"> | ||||
|                         {new Date(conn.timestamp).toLocaleString()} | ||||
|                       </Typography> | ||||
|                     } | ||||
|                   /> | ||||
|                 </ListItem> | ||||
|               </Box> | ||||
|             ))} | ||||
|           </List> | ||||
|         </Paper> | ||||
|       )} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default WebSocketPage; | ||||
							
								
								
									
										144
									
								
								hero_vault_extension/src/store/cryptoStore.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,144 @@ | ||||
| /** | ||||
|  * Crypto Store for Hero Vault Extension | ||||
|  *  | ||||
|  * This store manages cryptographic operations such as: | ||||
|  * - Encryption/decryption using the keyspace's symmetric cipher | ||||
|  * - Signing/verification using the selected keypair | ||||
|  */ | ||||
|  | ||||
| import { create } from 'zustand'; | ||||
| import { getWasmModule, stringToUint8Array, uint8ArrayToString } from '../wasm/wasmHelper'; | ||||
|  | ||||
| // Helper functions for Unicode-safe base64 encoding/decoding | ||||
| function base64Encode(data: Uint8Array): string { | ||||
|   // Convert binary data to a string that only uses the low 8 bits of each character | ||||
|   const binaryString = Array.from(data) | ||||
|     .map(byte => String.fromCharCode(byte)) | ||||
|     .join(''); | ||||
|    | ||||
|   // Use btoa on the binary string | ||||
|   return btoa(binaryString); | ||||
| } | ||||
|  | ||||
| function base64Decode(base64: string): Uint8Array { | ||||
|   // Decode base64 to binary string | ||||
|   const binaryString = atob(base64); | ||||
|    | ||||
|   // Convert binary string to Uint8Array | ||||
|   const bytes = new Uint8Array(binaryString.length); | ||||
|   for (let i = 0; i < binaryString.length; i++) { | ||||
|     bytes[i] = binaryString.charCodeAt(i); | ||||
|   } | ||||
|    | ||||
|   return bytes; | ||||
| } | ||||
|  | ||||
| interface CryptoState { | ||||
|   // State | ||||
|   isEncrypting: boolean; | ||||
|   isDecrypting: boolean; | ||||
|   isSigning: boolean; | ||||
|   isVerifying: boolean; | ||||
|   error: string | null; | ||||
|    | ||||
|   // Actions | ||||
|   encryptData: (data: string) => Promise<string>; | ||||
|   decryptData: (encrypted: string) => Promise<string>; | ||||
|   signMessage: (message: string) => Promise<string>; | ||||
|   verifySignature: (message: string, signature: string) => Promise<boolean>; | ||||
|   clearError: () => void; | ||||
| } | ||||
|  | ||||
| export const useCryptoStore = create<CryptoState>()((set, get) => ({ | ||||
|   isEncrypting: false, | ||||
|   isDecrypting: false, | ||||
|   isSigning: false, | ||||
|   isVerifying: false, | ||||
|   error: null, | ||||
|    | ||||
|   encryptData: async (data: string) => { | ||||
|     try { | ||||
|       set({ isEncrypting: true, error: null }); | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Convert input to Uint8Array | ||||
|       const dataBytes = stringToUint8Array(data); | ||||
|        | ||||
|       // Encrypt the data | ||||
|       const encrypted = await wasmModule.encrypt_data(dataBytes); | ||||
|        | ||||
|       // Convert result to base64 for storage/display using our Unicode-safe function | ||||
|       const encryptedBase64 = base64Encode(encrypted); | ||||
|        | ||||
|       return encryptedBase64; | ||||
|     } catch (error) { | ||||
|       set({ error: (error as Error).message || 'Failed to encrypt data' }); | ||||
|       throw error; | ||||
|     } finally { | ||||
|       set({ isEncrypting: false }); | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   decryptData: async (encrypted: string) => { | ||||
|     try { | ||||
|       set({ isDecrypting: true, error: null }); | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Convert input from base64 using our Unicode-safe function | ||||
|       const encryptedBytes = base64Decode(encrypted); | ||||
|        | ||||
|       // Decrypt the data | ||||
|       const decrypted = await wasmModule.decrypt_data(encryptedBytes); | ||||
|        | ||||
|       // Convert result to string | ||||
|       return uint8ArrayToString(decrypted); | ||||
|     } catch (error) { | ||||
|       set({ error: (error as Error).message || 'Failed to decrypt data' }); | ||||
|       throw error; | ||||
|     } finally { | ||||
|       set({ isDecrypting: false }); | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   signMessage: async (message: string) => { | ||||
|     try { | ||||
|       set({ isSigning: true, error: null }); | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Convert message to Uint8Array | ||||
|       const messageBytes = stringToUint8Array(message); | ||||
|        | ||||
|       // Sign the message | ||||
|       const signature = await wasmModule.sign(messageBytes); | ||||
|        | ||||
|       return signature; | ||||
|     } catch (error) { | ||||
|       set({ error: (error as Error).message || 'Failed to sign message' }); | ||||
|       throw error; | ||||
|     } finally { | ||||
|       set({ isSigning: false }); | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   verifySignature: async (message: string, signature: string) => { | ||||
|     try { | ||||
|       set({ isVerifying: true, error: null }); | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Convert inputs | ||||
|       const messageBytes = stringToUint8Array(message); | ||||
|        | ||||
|       // Verify the signature | ||||
|       const isValid = await wasmModule.verify(messageBytes, signature); | ||||
|        | ||||
|       return isValid; | ||||
|     } catch (error) { | ||||
|       set({ error: (error as Error).message || 'Failed to verify signature' }); | ||||
|       throw error; | ||||
|     } finally { | ||||
|       set({ isVerifying: false }); | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   clearError: () => set({ error: null }) | ||||
| })); | ||||
							
								
								
									
										416
									
								
								hero_vault_extension/src/store/sessionStore.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,416 @@ | ||||
| import { create } from 'zustand'; | ||||
| import { getWasmModule, stringToUint8Array } from '../wasm/wasmHelper'; | ||||
| import { getChromeApi } from '../utils/chromeApi'; | ||||
|  | ||||
| // Import Chrome types | ||||
| /// <reference types="chrome" /> | ||||
|  | ||||
| interface KeypairMetadata { | ||||
|   id: string; | ||||
|   type: string; | ||||
|   name?: string; | ||||
|   description?: string; | ||||
|   createdAt: number; | ||||
| } | ||||
|  | ||||
| interface SessionState { | ||||
|   isSessionUnlocked: boolean; | ||||
|   currentKeyspace: string | null; | ||||
|   currentKeypair: KeypairMetadata | null; | ||||
|   availableKeypairs: KeypairMetadata[]; | ||||
|   isWebSocketConnected: boolean; | ||||
|   webSocketUrl: string | null; | ||||
|   isWasmLoaded: boolean; | ||||
|    | ||||
|   // Actions | ||||
|   initWasm: () => Promise<boolean>; | ||||
|   checkSessionStatus: () => Promise<boolean>; | ||||
|   unlockSession: (keyspace: string, password: string) => Promise<boolean>; | ||||
|   lockSession: () => Promise<boolean>; | ||||
|   createKeyspace: (keyspace: string, password: string) => Promise<boolean>; | ||||
|   listKeypairs: () => Promise<KeypairMetadata[]>; | ||||
|   selectKeypair: (keypairId: string) => Promise<boolean>; | ||||
|   createKeypair: (type: string, metadata?: Record<string, any>) => Promise<string>; | ||||
|   connectWebSocket: (url: string) => Promise<boolean>; | ||||
|   disconnectWebSocket: () => Promise<boolean>; | ||||
|   executeScript: (script: string) => Promise<string>; | ||||
|   signMessage: (message: string) => Promise<string>; | ||||
| } | ||||
|  | ||||
| // Create the store | ||||
| export const useSessionStore = create<SessionState>((set: any, get: any) => ({ | ||||
|   isSessionUnlocked: false, | ||||
|   currentKeyspace: null, | ||||
|   currentKeypair: null, | ||||
|   availableKeypairs: [], | ||||
|   isWebSocketConnected: false, | ||||
|   webSocketUrl: null, | ||||
|   isWasmLoaded: false, | ||||
|    | ||||
|   // Initialize WASM module | ||||
|   initWasm: async () => { | ||||
|     try { | ||||
|       set({ isWasmLoaded: true }); | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to initialize WASM module:', error); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Check if a session is currently active | ||||
|   checkSessionStatus: async () => { | ||||
|     try { | ||||
|       // First check with the background service worker | ||||
|       const chromeApi = getChromeApi(); | ||||
|       const response = await chromeApi.runtime.sendMessage({ type: 'SESSION_STATUS' }); | ||||
|        | ||||
|       if (response && response.active) { | ||||
|         // If session is active in the background, check with WASM | ||||
|         try { | ||||
|           const wasmModule = await getWasmModule(); | ||||
|           const isUnlocked = wasmModule.is_unlocked(); | ||||
|            | ||||
|           if (isUnlocked) { | ||||
|             // Get current keypair metadata if available | ||||
|             try { | ||||
|               const keypairMetadata = await wasmModule.current_keypair_metadata(); | ||||
|               const parsedMetadata = JSON.parse(keypairMetadata); | ||||
|                | ||||
|               set({  | ||||
|                 isSessionUnlocked: true, | ||||
|                 currentKeypair: parsedMetadata | ||||
|               }); | ||||
|                | ||||
|               // Load keypairs | ||||
|               await get().listKeypairs(); | ||||
|             } catch (e) { | ||||
|               // No keypair selected, but session is unlocked | ||||
|               set({ isSessionUnlocked: true }); | ||||
|             } | ||||
|             return true; | ||||
|           } | ||||
|         } catch (wasmError) { | ||||
|           console.error('WASM error checking session status:', wasmError); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       set({ isSessionUnlocked: false }); | ||||
|       return false; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to check session status:', error); | ||||
|       set({ isSessionUnlocked: false }); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Unlock a session with keyspace and password | ||||
|   unlockSession: async (keyspace: string, password: string) => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Call the WASM init_session function | ||||
|       await wasmModule.init_session(keyspace, password); | ||||
|        | ||||
|       // Initialize Rhai environment | ||||
|       wasmModule.init_rhai_env(); | ||||
|        | ||||
|       // Notify background service worker | ||||
|       const chromeApi = getChromeApi(); | ||||
|       await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' }); | ||||
|        | ||||
|       set({  | ||||
|         isSessionUnlocked: true, | ||||
|         currentKeyspace: keyspace, | ||||
|         currentKeypair: null | ||||
|       }); | ||||
|        | ||||
|       // Load keypairs after unlocking | ||||
|       const keypairs = await get().listKeypairs(); | ||||
|       set({ availableKeypairs: keypairs }); | ||||
|        | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to unlock session:', error); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Lock the current session | ||||
|   lockSession: async () => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Call the WASM lock_session function | ||||
|       wasmModule.lock_session(); | ||||
|        | ||||
|       // Notify background service worker | ||||
|       const chromeApi = getChromeApi(); | ||||
|       await chromeApi.runtime.sendMessage({ type: 'SESSION_LOCK' }); | ||||
|        | ||||
|       set({ | ||||
|         isSessionUnlocked: false, | ||||
|         currentKeyspace: null, | ||||
|         currentKeypair: null, | ||||
|         availableKeypairs: [], | ||||
|         isWebSocketConnected: false, | ||||
|         webSocketUrl: null | ||||
|       }); | ||||
|        | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to lock session:', error); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Create a new keyspace | ||||
|   createKeyspace: async (keyspace: string, password: string) => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Call the WASM create_keyspace function | ||||
|       await wasmModule.create_keyspace(keyspace, password); | ||||
|        | ||||
|       // Initialize Rhai environment | ||||
|       wasmModule.init_rhai_env(); | ||||
|        | ||||
|       // Notify background service worker | ||||
|       const chromeApi = getChromeApi(); | ||||
|       await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' }); | ||||
|        | ||||
|       set({  | ||||
|         isSessionUnlocked: true, | ||||
|         currentKeyspace: keyspace, | ||||
|         currentKeypair: null, | ||||
|         availableKeypairs: [] | ||||
|       }); | ||||
|        | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to create keyspace:', error); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // List all keypairs in the current keyspace | ||||
|   listKeypairs: async () => { | ||||
|     try { | ||||
|       console.log('Listing keypairs from WASM module'); | ||||
|       const wasmModule = await getWasmModule(); | ||||
|       console.log('WASM module loaded, calling list_keypairs'); | ||||
|        | ||||
|       // Call the WASM list_keypairs function | ||||
|       let keypairsJson; | ||||
|       try { | ||||
|         keypairsJson = await wasmModule.list_keypairs(); | ||||
|         console.log('Raw keypairs JSON from WASM:', keypairsJson); | ||||
|       } catch (listError) { | ||||
|         console.error('Error calling list_keypairs:', listError); | ||||
|         throw new Error(`Failed to list keypairs: ${listError.message || listError}`); | ||||
|       } | ||||
|        | ||||
|       let keypairs; | ||||
|       try { | ||||
|         keypairs = JSON.parse(keypairsJson); | ||||
|         console.log('Parsed keypairs object:', keypairs); | ||||
|       } catch (parseError) { | ||||
|         console.error('Error parsing keypairs JSON:', parseError); | ||||
|         throw new Error(`Failed to parse keypairs JSON: ${parseError.message}`); | ||||
|       } | ||||
|        | ||||
|       // Transform the keypairs to our expected format | ||||
|       const formattedKeypairs: KeypairMetadata[] = keypairs.map((keypair: any, index: number) => { | ||||
|         console.log(`Processing keypair at index ${index}:`, keypair); | ||||
|         return { | ||||
|           id: keypair.id, // Use the actual keypair ID from the WASM module | ||||
|           type: keypair.key_type || 'Unknown', | ||||
|           name: keypair.metadata?.name, | ||||
|           description: keypair.metadata?.description, | ||||
|           createdAt: keypair.metadata?.created_at || Date.now() | ||||
|         }; | ||||
|       }); | ||||
|        | ||||
|       console.log('Formatted keypairs for UI:', formattedKeypairs); | ||||
|       set({ availableKeypairs: formattedKeypairs }); | ||||
|       return formattedKeypairs; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to list keypairs:', error); | ||||
|       return []; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Select a keypair for use | ||||
|   selectKeypair: async (keypairId: string) => { | ||||
|     try { | ||||
|       console.log('Selecting keypair with ID:', keypairId); | ||||
|        | ||||
|       // First, let's log the available keypairs to see what we have | ||||
|       const { availableKeypairs } = get(); | ||||
|       console.log('Available keypairs:', JSON.stringify(availableKeypairs)); | ||||
|        | ||||
|       const wasmModule = await getWasmModule(); | ||||
|       console.log('WASM module loaded, attempting to select keypair'); | ||||
|        | ||||
|       try { | ||||
|         // Call the WASM select_keypair function | ||||
|         await wasmModule.select_keypair(keypairId); | ||||
|         console.log('Successfully selected keypair in WASM'); | ||||
|       } catch (selectError) { | ||||
|         console.error('Error in WASM select_keypair:', selectError); | ||||
|         throw new Error(`select_keypair error: ${selectError.message || selectError}`); | ||||
|       } | ||||
|        | ||||
|       // Find the keypair in our availableKeypairs list | ||||
|       const selectedKeypair = availableKeypairs.find((kp: KeypairMetadata) => kp.id === keypairId); | ||||
|        | ||||
|       if (selectedKeypair) { | ||||
|         console.log('Found keypair in available list, setting as current'); | ||||
|         set({ currentKeypair: selectedKeypair }); | ||||
|       } else { | ||||
|         console.log('Keypair not found in available list, creating new entry from available data'); | ||||
|         // If not found in our list (rare case), create a new entry with what we know | ||||
|         // Since we can't get metadata from WASM, use what we have from the keypair list | ||||
|         const matchingKeypair = availableKeypairs.find(k => k.id === keypairId); | ||||
|          | ||||
|         if (matchingKeypair) { | ||||
|           set({ currentKeypair: matchingKeypair }); | ||||
|         } else { | ||||
|           // Last resort: create a minimal keypair entry | ||||
|           const newKeypair: KeypairMetadata = { | ||||
|             id: keypairId, | ||||
|             type: 'Unknown', | ||||
|             name: `Keypair ${keypairId.substring(0, 8)}...`, | ||||
|             createdAt: Date.now() | ||||
|           }; | ||||
|            | ||||
|           set({ currentKeypair: newKeypair }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to select keypair:', error); | ||||
|       throw error; // Re-throw to show error in UI | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Create a new keypair | ||||
|   createKeypair: async (type: string, metadata?: Record<string, any>) => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Format metadata for WASM | ||||
|       const metadataJson = metadata ? JSON.stringify({ | ||||
|         name: metadata.name, | ||||
|         description: metadata.description, | ||||
|         created_at: Date.now() | ||||
|       }) : undefined; | ||||
|        | ||||
|       // Call the WASM add_keypair function | ||||
|       const keypairId = await wasmModule.add_keypair(type, metadataJson); | ||||
|        | ||||
|       // Refresh the keypair list | ||||
|       await get().listKeypairs(); | ||||
|        | ||||
|       return keypairId; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to create keypair:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Connect to a WebSocket server | ||||
|   connectWebSocket: async (url: string) => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|       const { currentKeypair } = get(); | ||||
|        | ||||
|       if (!currentKeypair) { | ||||
|         throw new Error('No keypair selected'); | ||||
|       } | ||||
|        | ||||
|       // Get the public key from WASM | ||||
|       const publicKeyArray = await wasmModule.current_keypair_public_key(); | ||||
|       const publicKeyHex = Array.from(publicKeyArray) | ||||
|         .map(b => b.toString(16).padStart(2, '0')) | ||||
|         .join(''); | ||||
|        | ||||
|       // Connect to WebSocket via background service worker | ||||
|       const chromeApi = getChromeApi(); | ||||
|       const response = await chromeApi.runtime.sendMessage({ | ||||
|         type: 'CONNECT_WEBSOCKET', | ||||
|         serverUrl: url, | ||||
|         publicKey: publicKeyHex | ||||
|       }); | ||||
|        | ||||
|       if (response && response.success) { | ||||
|         set({ | ||||
|           isWebSocketConnected: true, | ||||
|           webSocketUrl: url | ||||
|         }); | ||||
|         return true; | ||||
|       } else { | ||||
|         throw new Error(response?.error || 'Failed to connect to WebSocket server'); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Failed to connect to WebSocket:', error); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Disconnect from WebSocket server | ||||
|   disconnectWebSocket: async () => { | ||||
|     try { | ||||
|       // Disconnect via background service worker | ||||
|       const chromeApi = getChromeApi(); | ||||
|       const response = await chromeApi.runtime.sendMessage({ | ||||
|         type: 'DISCONNECT_WEBSOCKET' | ||||
|       }); | ||||
|        | ||||
|       if (response && response.success) { | ||||
|         set({ | ||||
|           isWebSocketConnected: false, | ||||
|           webSocketUrl: null | ||||
|         }); | ||||
|         return true; | ||||
|       } else { | ||||
|         throw new Error(response?.error || 'Failed to disconnect from WebSocket server'); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Failed to disconnect from WebSocket:', error); | ||||
|       return false; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Execute a Rhai script | ||||
|   executeScript: async (script: string) => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Call the WASM run_rhai function | ||||
|       const result = await wasmModule.run_rhai(script); | ||||
|       return result; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to execute script:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   }, | ||||
|    | ||||
|   // Sign a message with the current keypair | ||||
|   signMessage: async (message: string) => { | ||||
|     try { | ||||
|       const wasmModule = await getWasmModule(); | ||||
|        | ||||
|       // Convert message to Uint8Array | ||||
|       const messageBytes = stringToUint8Array(message); | ||||
|        | ||||
|       // Call the WASM sign function | ||||
|       const signature = await wasmModule.sign(messageBytes); | ||||
|       return signature; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to sign message:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
| })); | ||||
							
								
								
									
										45
									
								
								hero_vault_extension/src/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | ||||
| /** | ||||
|  * Common TypeScript types for the Hero Vault Extension | ||||
|  */ | ||||
|  | ||||
| // React types | ||||
| export type SyntheticEvent<T = Element, E = Event> = React.BaseSyntheticEvent<E, EventTarget & T, EventTarget>; | ||||
|  | ||||
| // Session types | ||||
| export interface SessionActivity { | ||||
|   timestamp: number; | ||||
|   action: string; | ||||
|   details?: string; | ||||
| } | ||||
|  | ||||
| // Script types | ||||
| export interface ScriptResult { | ||||
|   id: string; | ||||
|   script: string; | ||||
|   result: string; | ||||
|   timestamp: number; | ||||
|   success: boolean; | ||||
| } | ||||
|  | ||||
| export interface PendingScript { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   script: string; | ||||
| } | ||||
|  | ||||
| // WebSocket types | ||||
| export interface ConnectionHistory { | ||||
|   id: string; | ||||
|   url: string; | ||||
|   timestamp: number; | ||||
|   status: 'connected' | 'disconnected' | 'error'; | ||||
|   message?: string; | ||||
| } | ||||
|  | ||||
| // Settings types | ||||
| export interface Settings { | ||||
|   darkMode: boolean; | ||||
|   autoLockTimeout: number; | ||||
|   defaultKeyType: string; | ||||
|   showScriptNotifications: boolean; | ||||
| } | ||||
							
								
								
									
										5
									
								
								hero_vault_extension/src/types/chrome.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| /// <reference types="chrome" /> | ||||
|  | ||||
| // This file provides type declarations for Chrome extension APIs | ||||
| // It's needed because we're using the Chrome extension API in a TypeScript project | ||||
| // The actual implementation is provided by the browser at runtime | ||||
							
								
								
									
										14
									
								
								hero_vault_extension/src/types/declarations.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| // Type declarations for modules without type definitions | ||||
|  | ||||
| // React and Material UI | ||||
| declare module 'react'; | ||||
| declare module 'react-dom'; | ||||
| declare module 'react-router-dom'; | ||||
| declare module '@mui/material'; | ||||
| declare module '@mui/material/*'; | ||||
| declare module '@mui/icons-material/*'; | ||||
|  | ||||
| // Project modules | ||||
| declare module './pages/*'; | ||||
| declare module './components/*'; | ||||
| declare module './store/*'; | ||||
							
								
								
									
										16
									
								
								hero_vault_extension/src/types/wasm.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| declare module '*/wasm_app.js' { | ||||
|   export default function init(): Promise<void>; | ||||
|   export function init_session(keyspace: string, password: string): Promise<void>; | ||||
|   export function create_keyspace(keyspace: string, password: string): Promise<void>; | ||||
|   export function lock_session(): void; | ||||
|   export function is_unlocked(): boolean; | ||||
|   export function add_keypair(key_type: string | undefined, metadata: string | undefined): Promise<string>; | ||||
|   export function list_keypairs(): Promise<string>; | ||||
|   export function select_keypair(key_id: string): Promise<void>; | ||||
|   export function current_keypair_metadata(): Promise<any>; | ||||
|   export function current_keypair_public_key(): Promise<Uint8Array>; | ||||
|   export function sign(message: Uint8Array): Promise<string>; | ||||
|   export function verify(signature: string, message: Uint8Array): Promise<boolean>; | ||||
|   export function init_rhai_env(): void; | ||||
|   export function run_rhai(script: string): Promise<string>; | ||||
| } | ||||
							
								
								
									
										103
									
								
								hero_vault_extension/src/utils/chromeApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,103 @@ | ||||
| /** | ||||
|  * Chrome API utilities for Hero Vault Extension | ||||
|  *  | ||||
|  * This module provides Chrome API detection and mocks for development mode | ||||
|  */ | ||||
|  | ||||
| // Check if we're running in a Chrome extension environment | ||||
| export const isExtensionEnvironment = (): boolean => { | ||||
|   return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.id; | ||||
| }; | ||||
|  | ||||
| // Mock storage for development mode | ||||
| const mockStorage: Record<string, any> = { | ||||
|   // Initialize with some default values for script storage | ||||
|   pendingScripts: [], | ||||
|   scriptResults: [] | ||||
| }; | ||||
|  | ||||
| // Mock Chrome API for development mode | ||||
| export const getChromeApi = () => { | ||||
|   // If we're in a Chrome extension environment, return the real Chrome API | ||||
|   if (isExtensionEnvironment()) { | ||||
|     return chrome; | ||||
|   } | ||||
|  | ||||
|   // Otherwise, return a mock implementation | ||||
|   return { | ||||
|     runtime: { | ||||
|       sendMessage: (message: any): Promise<any> => { | ||||
|         console.log('Mock sendMessage called with:', message); | ||||
|          | ||||
|         // Mock responses based on message type | ||||
|         if (message.type === 'SESSION_STATUS') { | ||||
|           return Promise.resolve({ active: false }); | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'CREATE_KEYSPACE') { | ||||
|           mockStorage['currentKeyspace'] = message.keyspace; | ||||
|           return Promise.resolve({ success: true }); | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'UNLOCK_SESSION') { | ||||
|           mockStorage['currentKeyspace'] = message.keyspace; | ||||
|           return Promise.resolve({ success: true }); | ||||
|         } | ||||
|          | ||||
|         if (message.type === 'LOCK_SESSION') { | ||||
|           delete mockStorage['currentKeyspace']; | ||||
|           return Promise.resolve({ success: true }); | ||||
|         } | ||||
|          | ||||
|         return Promise.resolve({ success: false }); | ||||
|       }, | ||||
|       getURL: (path: string): string => { | ||||
|         return path; | ||||
|       } | ||||
|     }, | ||||
|     storage: { | ||||
|       local: { | ||||
|         get: (keys: string | string[] | object): Promise<Record<string, any>> => { | ||||
|           console.log('Mock storage.local.get called with:', keys); | ||||
|            | ||||
|           if (typeof keys === 'string') { | ||||
|             // Handle specific script storage keys | ||||
|             if (keys === 'pendingScripts' && !mockStorage[keys]) { | ||||
|               mockStorage[keys] = []; | ||||
|             } | ||||
|             if (keys === 'scriptResults' && !mockStorage[keys]) { | ||||
|               mockStorage[keys] = []; | ||||
|             } | ||||
|             return Promise.resolve({ [keys]: mockStorage[keys] }); | ||||
|           } | ||||
|            | ||||
|           if (Array.isArray(keys)) { | ||||
|             const result: Record<string, any> = {}; | ||||
|             keys.forEach(key => { | ||||
|               // Handle specific script storage keys | ||||
|               if (key === 'pendingScripts' && !mockStorage[key]) { | ||||
|                 mockStorage[key] = []; | ||||
|               } | ||||
|               if (key === 'scriptResults' && !mockStorage[key]) { | ||||
|                 mockStorage[key] = []; | ||||
|               } | ||||
|               result[key] = mockStorage[key]; | ||||
|             }); | ||||
|             return Promise.resolve(result); | ||||
|           } | ||||
|            | ||||
|           return Promise.resolve(mockStorage); | ||||
|         }, | ||||
|         set: (items: Record<string, any>): Promise<void> => { | ||||
|           console.log('Mock storage.local.set called with:', items); | ||||
|            | ||||
|           Object.keys(items).forEach(key => { | ||||
|             mockStorage[key] = items[key]; | ||||
|           }); | ||||
|            | ||||
|           return Promise.resolve(); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } as typeof chrome; | ||||
| }; | ||||
							
								
								
									
										139
									
								
								hero_vault_extension/src/wasm/wasmHelper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,139 @@ | ||||
| /** | ||||
|  * WASM Helper for Hero Vault Extension | ||||
|  *  | ||||
|  * This module handles loading and initializing the WASM module, | ||||
|  * and provides a typed interface to the WASM functions. | ||||
|  */ | ||||
|  | ||||
| // Import types for TypeScript | ||||
| interface WasmModule { | ||||
|   // Session management | ||||
|   init_session: (keyspace: string, password: string) => Promise<void>; | ||||
|   create_keyspace: (keyspace: string, password: string) => Promise<void>; | ||||
|   lock_session: () => void; | ||||
|   is_unlocked: () => boolean; | ||||
|    | ||||
|   // Keypair management | ||||
|   add_keypair: (key_type: string | undefined, metadata: string | undefined) => Promise<string>; | ||||
|   list_keypairs: () => Promise<string>; | ||||
|   select_keypair: (key_id: string) => Promise<void>; | ||||
|   current_keypair_metadata: () => Promise<any>; | ||||
|   current_keypair_public_key: () => Promise<Uint8Array>; | ||||
|    | ||||
|   // Cryptographic operations | ||||
|   sign: (message: Uint8Array) => Promise<string>; | ||||
|   verify: (message: Uint8Array, signature: string) => Promise<boolean>; | ||||
|   encrypt_data: (data: Uint8Array) => Promise<Uint8Array>; | ||||
|   decrypt_data: (encrypted: Uint8Array) => Promise<Uint8Array>; | ||||
|    | ||||
|   // Rhai scripting | ||||
|   init_rhai_env: () => void; | ||||
|   run_rhai: (script: string) => Promise<string>; | ||||
| } | ||||
|  | ||||
| // Global reference to the WASM module | ||||
| let wasmModule: WasmModule | null = null; | ||||
| let isInitializing = false; | ||||
| let initPromise: Promise<void> | null = null; | ||||
|  | ||||
| /** | ||||
|  * Initialize the WASM module | ||||
|  * This should be called before any other WASM functions | ||||
|  */ | ||||
| export const initWasm = async (): Promise<void> => { | ||||
|   if (wasmModule) { | ||||
|     return Promise.resolve(); // Already initialized | ||||
|   } | ||||
|    | ||||
|   if (isInitializing && initPromise) { | ||||
|     return initPromise; // Already initializing | ||||
|   } | ||||
|    | ||||
|   isInitializing = true; | ||||
|    | ||||
|   initPromise = new Promise<void>(async (resolve, reject) => { | ||||
|     try { | ||||
|       try { | ||||
|         // Import the WASM module | ||||
|         // Use a relative path that will be resolved by Vite during build | ||||
|         const wasmImport = await import('../../public/wasm/wasm_app.js'); | ||||
|          | ||||
|         // Initialize the WASM module | ||||
|         await wasmImport.default(); | ||||
|          | ||||
|         // Store the WASM module globally | ||||
|         wasmModule = wasmImport as unknown as WasmModule; | ||||
|          | ||||
|         console.log('WASM module initialized successfully'); | ||||
|         resolve(); | ||||
|       } catch (error) { | ||||
|         console.error('Failed to initialize WASM module:', error); | ||||
|         reject(error); | ||||
|       } | ||||
|        | ||||
|     } finally { | ||||
|       isInitializing = false; | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   return initPromise; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Get the WASM module | ||||
|  * This will initialize the module if it hasn't been initialized yet | ||||
|  */ | ||||
| export const getWasmModule = async (): Promise<WasmModule> => { | ||||
|   if (!wasmModule) { | ||||
|     await initWasm(); | ||||
|   } | ||||
|    | ||||
|   if (!wasmModule) { | ||||
|     throw new Error('WASM module failed to initialize'); | ||||
|   } | ||||
|    | ||||
|   return wasmModule; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Check if the WASM module is initialized | ||||
|  */ | ||||
| export const isWasmInitialized = (): boolean => { | ||||
|   return wasmModule !== null; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Helper to convert string to Uint8Array | ||||
|  */ | ||||
| export const stringToUint8Array = (str: string): Uint8Array => { | ||||
|   const encoder = new TextEncoder(); | ||||
|   return encoder.encode(str); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Helper to convert Uint8Array to string | ||||
|  */ | ||||
| export const uint8ArrayToString = (array: Uint8Array): string => { | ||||
|   const decoder = new TextDecoder(); | ||||
|   return decoder.decode(array); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Helper to convert hex string to Uint8Array | ||||
|  */ | ||||
| export const hexToUint8Array = (hex: string): Uint8Array => { | ||||
|   const bytes = new Uint8Array(hex.length / 2); | ||||
|   for (let i = 0; i < hex.length; i += 2) { | ||||
|     bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); | ||||
|   } | ||||
|   return bytes; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Helper to convert Uint8Array to hex string | ||||
|  */ | ||||
| export const uint8ArrayToHex = (array: Uint8Array): string => { | ||||
|   return Array.from(array) | ||||
|     .map(b => b.toString(16).padStart(2, '0')) | ||||
|     .join(''); | ||||
| }; | ||||
							
								
								
									
										30
									
								
								hero_vault_extension/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "ES2020", | ||||
|     "useDefineForClassFields": true, | ||||
|     "lib": ["ES2020", "DOM", "DOM.Iterable"], | ||||
|     "module": "ESNext", | ||||
|     "skipLibCheck": true, | ||||
|     "moduleResolution": "bundler", | ||||
|     "allowImportingTsExtensions": true, | ||||
|     "resolveJsonModule": true, | ||||
|     "isolatedModules": true, | ||||
|     "noEmit": true, | ||||
|     "jsx": "react-jsx", | ||||
|     "strict": false, | ||||
|     "noImplicitAny": false, | ||||
|     "noUnusedLocals": false, | ||||
|     "noUnusedParameters": false, | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     }, | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "esModuleInterop": true, | ||||
|     "typeRoots": ["./node_modules/@types", "./src/types"], | ||||
|     "jsxImportSource": "react" | ||||
|   }, | ||||
|   "include": ["src"], | ||||
|   "references": [{ "path": "./tsconfig.node.json" }] | ||||
| } | ||||
							
								
								
									
										10
									
								
								hero_vault_extension/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "composite": true, | ||||
|     "skipLibCheck": true, | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "bundler", | ||||
|     "allowSyntheticDefaultImports": true | ||||
|   }, | ||||
|   "include": ["vite.config.ts"] | ||||
| } | ||||
							
								
								
									
										33
									
								
								hero_vault_extension/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| import { defineConfig } from 'vite'; | ||||
| import react from '@vitejs/plugin-react'; | ||||
| import { crx } from '@crxjs/vite-plugin'; | ||||
| import { resolve } from 'path'; | ||||
| import { readFileSync } from 'fs'; | ||||
| import fs from 'fs'; | ||||
|  | ||||
| const manifest = JSON.parse( | ||||
|   readFileSync('public/manifest.json', 'utf-8') | ||||
| ); | ||||
|  | ||||
| export default defineConfig({ | ||||
|   plugins: [ | ||||
|     react(), | ||||
|     crx({ manifest }), | ||||
|   ], | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       '@': resolve(__dirname, 'src'), | ||||
|     }, | ||||
|   }, | ||||
|   build: { | ||||
|     outDir: 'dist', | ||||
|     emptyOutDir: true, | ||||
|     rollupOptions: { | ||||
|       input: { | ||||
|         index: resolve(__dirname, 'index.html'), | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   // Copy WASM files to the dist directory | ||||
|   publicDir: 'public', | ||||
| }); | ||||
							
								
								
									
										526
									
								
								vault/src/lib.rs
									
									
									
									
									
								
							
							
						
						| @@ -1,34 +1,32 @@ | ||||
| //! vault: Cryptographic keyspace and operations | ||||
|  | ||||
|  | ||||
| //! vault: Cryptographic keyspace and operations | ||||
|  | ||||
| pub mod data; | ||||
| pub use crate::data::{KeyEntry, KeyMetadata, KeyType}; | ||||
| pub use crate::session::SessionManager; | ||||
| pub use crate::data::{KeyType, KeyMetadata, KeyEntry}; | ||||
| mod error; | ||||
| mod crypto; | ||||
| mod error; | ||||
| pub mod rhai_bindings; | ||||
| mod rhai_sync_helpers; | ||||
| pub mod session; | ||||
| mod utils; | ||||
| mod rhai_sync_helpers; | ||||
| pub mod rhai_bindings; | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| pub mod session_singleton; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| pub mod wasm_helpers; | ||||
|  | ||||
|  | ||||
| pub use kvstore::traits::KVStore; | ||||
| use crate::crypto::kdf; | ||||
| use crate::crypto::random_salt; | ||||
| use data::*; | ||||
| use error::VaultError; | ||||
| use crate::crypto::random_salt; | ||||
| use crate::crypto::kdf; | ||||
| pub use kvstore::traits::KVStore; | ||||
|  | ||||
| use crate::crypto::cipher::{encrypt_chacha20, decrypt_chacha20}; | ||||
| use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20}; | ||||
| use signature::SignatureEncoding; | ||||
| // TEMP: File-based debug logger for crypto troubleshooting | ||||
| use log::{debug}; | ||||
| use log::debug; | ||||
|  | ||||
| /// Vault: Cryptographic keyspace and operations | ||||
| pub struct Vault<S: KVStore> { | ||||
| @@ -43,8 +41,7 @@ fn encrypt_with_nonce_prepended(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, | ||||
|     let nonce = random_salt(12); | ||||
|     debug!("nonce: {}", hex::encode(&nonce)); | ||||
|     // Always use ChaCha20Poly1305 for encryption | ||||
|     let ct = encrypt_chacha20(key, plaintext, &nonce) | ||||
|         .map_err(|e| VaultError::Crypto(e))?; | ||||
|     let ct = encrypt_chacha20(key, plaintext, &nonce).map_err(|e| VaultError::Crypto(e))?; | ||||
|     debug!("ct: {}", hex::encode(&ct)); | ||||
|     debug!("key: {}", hex::encode(key)); | ||||
|     let mut blob = nonce.clone(); | ||||
| @@ -60,17 +57,28 @@ impl<S: KVStore> Vault<S> { | ||||
|  | ||||
|     /// Create a new keyspace with the given name, password, and options. | ||||
|     /// Create a new keyspace with the given name and password. Always uses PBKDF2 and ChaCha20Poly1305. | ||||
| pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> { | ||||
|     pub async fn create_keyspace( | ||||
|         &mut self, | ||||
|         name: &str, | ||||
|         password: &[u8], | ||||
|         tags: Option<Vec<String>>, | ||||
|     ) -> Result<(), VaultError> { | ||||
|         // Check if keyspace already exists | ||||
|         if self.storage.get(name).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?.is_some() { | ||||
|         if self | ||||
|             .storage | ||||
|             .get(name) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))? | ||||
|             .is_some() | ||||
|         { | ||||
|             debug!("keyspace '{}' already exists", name); | ||||
|             return Err(VaultError::Crypto("Keyspace already exists".to_string())); | ||||
|         } | ||||
|         debug!("entry: name={}", name); | ||||
|         use crate::crypto::{random_salt, kdf}; | ||||
|         use crate::data::{KeyspaceMetadata, KeyspaceData}; | ||||
|         use crate::crypto::{kdf, random_salt}; | ||||
|         use crate::data::{KeyspaceData, KeyspaceMetadata}; | ||||
|         use serde_json; | ||||
|          | ||||
|  | ||||
|         // 1. Generate salt | ||||
|         let salt = random_salt(16); | ||||
|         debug!("salt: {:?}", salt); | ||||
| @@ -112,7 +120,10 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio | ||||
|                 return Err(VaultError::Serialization(e.to_string())); | ||||
|             } | ||||
|         }; | ||||
|         self.storage.set(name, &meta_bytes).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         self.storage | ||||
|             .set(name, &meta_bytes) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         debug!("success"); | ||||
|         Ok(()) | ||||
|     } | ||||
| @@ -121,10 +132,19 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio | ||||
|     pub async fn list_keyspaces(&self) -> Result<Vec<KeyspaceMetadata>, VaultError> { | ||||
|         use serde_json; | ||||
|         // 1. List all keys in kvstore | ||||
|         let keys = self.storage.keys().await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         let keys = self | ||||
|             .storage | ||||
|             .keys() | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         let mut keyspaces = Vec::new(); | ||||
|         for key in keys { | ||||
|             if let Some(bytes) = self.storage.get(&key).await.map_err(|e| VaultError::Storage(format!("{e:?}")))? { | ||||
|             if let Some(bytes) = self | ||||
|                 .storage | ||||
|                 .get(&key) | ||||
|                 .await | ||||
|                 .map_err(|e| VaultError::Storage(format!("{e:?}")))? | ||||
|             { | ||||
|                 if let Ok(meta) = serde_json::from_slice::<KeyspaceMetadata>(&bytes) { | ||||
|                     keyspaces.push(meta); | ||||
|                 } | ||||
| @@ -136,31 +156,42 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio | ||||
|     /// Unlock a keyspace by name and password, returning the decrypted data | ||||
|     /// Unlock a keyspace by name and password, returning the decrypted data | ||||
|     /// Always uses PBKDF2 and ChaCha20Poly1305. | ||||
|     pub async fn unlock_keyspace(&self, name: &str, password: &[u8]) -> Result<KeyspaceData, VaultError> { | ||||
|     pub async fn unlock_keyspace( | ||||
|         &self, | ||||
|         name: &str, | ||||
|         password: &[u8], | ||||
|     ) -> Result<KeyspaceData, VaultError> { | ||||
|         debug!("unlock_keyspace entry: name={}", name); | ||||
|         // use crate::crypto::kdf; // removed if not needed | ||||
|         use serde_json; | ||||
|         // 1. Fetch keyspace metadata | ||||
|         let meta_bytes = self.storage.get(name).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         let meta_bytes = self | ||||
|             .storage | ||||
|             .get(name) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         let meta_bytes = meta_bytes.ok_or(VaultError::KeyspaceNotFound(name.to_string()))?; | ||||
|         let metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes).map_err(|e| VaultError::Serialization(e.to_string()))?; | ||||
|         let metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes) | ||||
|             .map_err(|e| VaultError::Serialization(e.to_string()))?; | ||||
|         if metadata.salt.len() != 16 { | ||||
|             debug!("salt length {} != 16", metadata.salt.len()); | ||||
|             return Err(VaultError::Crypto("Salt length must be 16 bytes".to_string())); | ||||
|             return Err(VaultError::Crypto( | ||||
|                 "Salt length must be 16 bytes".to_string(), | ||||
|             )); | ||||
|         } | ||||
|         // 2. Derive key | ||||
|         let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); | ||||
|         debug!("derived key: {} bytes", key.len()); | ||||
|      | ||||
|  | ||||
|         let ciphertext = &metadata.encrypted_blob; | ||||
|         if ciphertext.len() < 12 { | ||||
|             debug!("ciphertext too short: {}", ciphertext.len()); | ||||
|             return Err(VaultError::Crypto("Ciphertext too short".to_string())); | ||||
|         } | ||||
|      | ||||
|  | ||||
|         let (nonce, ct) = ciphertext.split_at(12); | ||||
| debug!("nonce: {}", hex::encode(nonce)); | ||||
| let plaintext = decrypt_chacha20(&key, ct, nonce).map_err(VaultError::Crypto)?; | ||||
|         debug!("nonce: {}", hex::encode(nonce)); | ||||
|         let plaintext = decrypt_chacha20(&key, ct, nonce).map_err(VaultError::Crypto)?; | ||||
|         debug!("plaintext decrypted: {} bytes", plaintext.len()); | ||||
|         // 4. Deserialize keyspace data | ||||
|         let keyspace_data: KeyspaceData = match serde_json::from_slice(&plaintext) { | ||||
| @@ -184,8 +215,14 @@ let plaintext = decrypt_chacha20(&key, ct, nonce).map_err(VaultError::Crypto)?; | ||||
|  | ||||
|     /// Add a new keypair to a keyspace (generates and stores a new keypair) | ||||
|     /// Add a new keypair to a keyspace (generates and stores a new keypair) | ||||
| /// If key_type is None, defaults to Secp256k1. | ||||
| pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: Option<KeyType>, metadata: Option<KeyMetadata>) -> Result<String, VaultError> { | ||||
|     /// If key_type is None, defaults to Secp256k1. | ||||
|     pub async fn add_keypair( | ||||
|         &mut self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         key_type: Option<KeyType>, | ||||
|         metadata: Option<KeyMetadata>, | ||||
|     ) -> Result<String, VaultError> { | ||||
|         use crate::data::KeyEntry; | ||||
|         use rand_core::OsRng; | ||||
|         use rand_core::RngCore; | ||||
| @@ -194,7 +231,7 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O | ||||
|         let mut data = self.unlock_keyspace(keyspace, password).await?; | ||||
|         // 2. Generate keypair | ||||
|         let key_type = key_type.unwrap_or(KeyType::Secp256k1); | ||||
|     let (private_key, public_key, id) = match key_type { | ||||
|         let (private_key, public_key, id) = match key_type { | ||||
|             KeyType::Ed25519 => { | ||||
|                 use ed25519_dalek::{SigningKey, VerifyingKey}; | ||||
|                 let mut bytes = [0u8; 32]; | ||||
| @@ -205,7 +242,7 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O | ||||
|                 let pub_bytes = verifying.to_bytes().to_vec(); | ||||
|                 let id = hex::encode(&pub_bytes); | ||||
|                 (priv_bytes, pub_bytes, id) | ||||
|             }, | ||||
|             } | ||||
|             KeyType::Secp256k1 => { | ||||
|                 use k256::ecdsa::SigningKey; | ||||
|  | ||||
| @@ -215,7 +252,7 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O | ||||
|                 let pub_bytes = pk.to_encoded_point(false).as_bytes().to_vec(); | ||||
|                 let id = hex::encode(&pub_bytes); | ||||
|                 (priv_bytes, pub_bytes, id) | ||||
|             }, | ||||
|             } | ||||
|         }; | ||||
|         // 3. Add to keypairs | ||||
|         let entry = KeyEntry { | ||||
| @@ -232,190 +269,291 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O | ||||
|     } | ||||
|  | ||||
|     /// Remove a keypair by id from a keyspace | ||||
|     pub async fn remove_keypair(&mut self, keyspace: &str, password: &[u8], key_id: &str) -> Result<(), VaultError> { | ||||
|     pub async fn remove_keypair( | ||||
|         &mut self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         key_id: &str, | ||||
|     ) -> Result<(), VaultError> { | ||||
|         let mut data = self.unlock_keyspace(keyspace, password).await?; | ||||
|         data.keypairs.retain(|k| k.id != key_id); | ||||
|         self.save_keyspace(keyspace, password, &data).await | ||||
|     } | ||||
|  | ||||
|     /// List all keypairs in a keyspace (public info only) | ||||
|     pub async fn list_keypairs(&self, keyspace: &str, password: &[u8]) -> Result<Vec<(String, KeyType)>, VaultError> { | ||||
|     pub async fn list_keypairs( | ||||
|         &self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|     ) -> Result<Vec<(String, KeyType)>, VaultError> { | ||||
|         let data = self.unlock_keyspace(keyspace, password).await?; | ||||
|         Ok(data.keypairs.iter().map(|k| (k.id.clone(), k.key_type.clone())).collect()) | ||||
|         Ok(data | ||||
|             .keypairs | ||||
|             .iter() | ||||
|             .map(|k| (k.id.clone(), k.key_type.clone())) | ||||
|             .collect()) | ||||
|     } | ||||
|  | ||||
|     /// Export a keypair's private and public key by id | ||||
|     pub async fn export_keypair(&self, keyspace: &str, password: &[u8], key_id: &str) -> Result<(Vec<u8>, Vec<u8>), VaultError> { | ||||
|     pub async fn export_keypair( | ||||
|         &self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         key_id: &str, | ||||
|     ) -> Result<(Vec<u8>, Vec<u8>), VaultError> { | ||||
|         let data = self.unlock_keyspace(keyspace, password).await?; | ||||
|         let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?; | ||||
|         let key = data | ||||
|             .keypairs | ||||
|             .iter() | ||||
|             .find(|k| k.id == key_id) | ||||
|             .ok_or(VaultError::KeyNotFound(key_id.to_string()))?; | ||||
|         Ok((key.private_key.clone(), key.public_key.clone())) | ||||
|     } | ||||
|  | ||||
|     /// Save the updated keyspace data (helper) | ||||
|     async fn save_keyspace(&mut self, keyspace: &str, password: &[u8], data: &KeyspaceData) -> Result<(), VaultError> { | ||||
|     async fn save_keyspace( | ||||
|         &mut self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         data: &KeyspaceData, | ||||
|     ) -> Result<(), VaultError> { | ||||
|         debug!("save_keyspace entry: keyspace={}", keyspace); | ||||
|         use crate::crypto::kdf; | ||||
|         use serde_json; | ||||
|  | ||||
|     let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|     debug!("got meta_bytes: {}", meta_bytes.as_ref().map(|v| v.len()).unwrap_or(0)); | ||||
|     let meta_bytes = meta_bytes.ok_or(VaultError::KeyspaceNotFound(keyspace.to_string()))?; | ||||
|     let mut metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes).map_err(|e| VaultError::Serialization(e.to_string()))?; | ||||
|     debug!("metadata: salt={:?}", metadata.salt); | ||||
|     if metadata.salt.len() != 16 { | ||||
|         debug!("salt length {} != 16", metadata.salt.len()); | ||||
|         return Err(VaultError::Crypto("Salt length must be 16 bytes".to_string())); | ||||
|         let meta_bytes = self | ||||
|             .storage | ||||
|             .get(keyspace) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         debug!( | ||||
|             "got meta_bytes: {}", | ||||
|             meta_bytes.as_ref().map(|v| v.len()).unwrap_or(0) | ||||
|         ); | ||||
|         let meta_bytes = meta_bytes.ok_or(VaultError::KeyspaceNotFound(keyspace.to_string()))?; | ||||
|         let mut metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes) | ||||
|             .map_err(|e| VaultError::Serialization(e.to_string()))?; | ||||
|         debug!("metadata: salt={:?}", metadata.salt); | ||||
|         if metadata.salt.len() != 16 { | ||||
|             debug!("salt length {} != 16", metadata.salt.len()); | ||||
|             return Err(VaultError::Crypto( | ||||
|                 "Salt length must be 16 bytes".to_string(), | ||||
|             )); | ||||
|         } | ||||
|         // 2. Derive key | ||||
|         let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); | ||||
|         debug!("derived key: {} bytes", key.len()); | ||||
|         // 3. Serialize plaintext | ||||
|         let plaintext = match serde_json::to_vec(data) { | ||||
|             Ok(val) => val, | ||||
|             Err(e) => { | ||||
|                 debug!("serde_json data error: {}", e); | ||||
|                 return Err(VaultError::Serialization(e.to_string())); | ||||
|             } | ||||
|         }; | ||||
|         debug!("plaintext serialized: {} bytes", plaintext.len()); | ||||
|         // 4. Generate nonce | ||||
|         let nonce = random_salt(12); | ||||
|         debug!("nonce: {}", hex::encode(&nonce)); | ||||
|         // 5. Encrypt | ||||
|         let encrypted_blob = encrypt_with_nonce_prepended(&key, &plaintext)?; | ||||
|         debug!("encrypted_blob: {} bytes", encrypted_blob.len()); | ||||
|         // 6. Store new encrypted blob | ||||
|         metadata.encrypted_blob = encrypted_blob; | ||||
|         let meta_bytes = match serde_json::to_vec(&metadata) { | ||||
|             Ok(val) => val, | ||||
|             Err(e) => { | ||||
|                 debug!("serde_json metadata error: {}", e); | ||||
|                 return Err(VaultError::Serialization(e.to_string())); | ||||
|             } | ||||
|         }; | ||||
|         self.storage | ||||
|             .set(keyspace, &meta_bytes) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         debug!("success"); | ||||
|         Ok(()) | ||||
|     } | ||||
|     // 2. Derive key | ||||
|     let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); | ||||
|     debug!("derived key: {} bytes", key.len()); | ||||
|     // 3. Serialize plaintext | ||||
|     let plaintext = match serde_json::to_vec(data) { | ||||
|         Ok(val) => val, | ||||
|         Err(e) => { | ||||
|             debug!("serde_json data error: {}", e); | ||||
|             return Err(VaultError::Serialization(e.to_string())); | ||||
|         } | ||||
|     }; | ||||
|     debug!("plaintext serialized: {} bytes", plaintext.len()); | ||||
|     // 4. Generate nonce | ||||
|     let nonce = random_salt(12); | ||||
|     debug!("nonce: {}", hex::encode(&nonce)); | ||||
|     // 5. Encrypt | ||||
|     let encrypted_blob = encrypt_with_nonce_prepended(&key, &plaintext)?; | ||||
|     debug!("encrypted_blob: {} bytes", encrypted_blob.len()); | ||||
|     // 6. Store new encrypted blob | ||||
|     metadata.encrypted_blob = encrypted_blob; | ||||
|     let meta_bytes = match serde_json::to_vec(&metadata) { | ||||
|         Ok(val) => val, | ||||
|         Err(e) => { | ||||
|             debug!("serde_json metadata error: {}", e); | ||||
|             return Err(VaultError::Serialization(e.to_string())); | ||||
|         } | ||||
|     }; | ||||
|     self.storage.set(keyspace, &meta_bytes).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|     debug!("success"); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Sign a message with a stored keypair in a keyspace | ||||
| /// | ||||
| /// # Arguments | ||||
| /// * `keyspace` - Keyspace name | ||||
| /// * `password` - Keyspace password | ||||
| /// * `key_id` - Keypair ID | ||||
| /// * `message` - Message to sign | ||||
| pub async fn sign(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|     let data = self.unlock_keyspace(keyspace, password).await?; | ||||
|     let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?; | ||||
|     match key.key_type { | ||||
|         KeyType::Ed25519 => { | ||||
|             use ed25519_dalek::{SigningKey, Signer}; | ||||
|             let signing = SigningKey::from_bytes(&key.private_key.clone().try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 private key length".to_string()))?); | ||||
|             let sig = signing.sign(message); | ||||
|             Ok(sig.to_bytes().to_vec()) | ||||
|         } | ||||
|         KeyType::Secp256k1 => { | ||||
|             use k256::ecdsa::{SigningKey, signature::Signer}; | ||||
|             let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| VaultError::Crypto("Invalid secp256k1 private key length".to_string()))?; | ||||
|             let sk = SigningKey::from_bytes(arr.into()).map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|             let sig: k256::ecdsa::DerSignature = sk.sign(message); | ||||
|             Ok(sig.to_vec()) | ||||
|     /// Sign a message with a stored keypair in a keyspace | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `keyspace` - Keyspace name | ||||
|     /// * `password` - Keyspace password | ||||
|     /// * `key_id` - Keypair ID | ||||
|     /// * `message` - Message to sign | ||||
|     pub async fn sign( | ||||
|         &self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         key_id: &str, | ||||
|         message: &[u8], | ||||
|     ) -> Result<Vec<u8>, VaultError> { | ||||
|         let data = self.unlock_keyspace(keyspace, password).await?; | ||||
|         let key = data | ||||
|             .keypairs | ||||
|             .iter() | ||||
|             .find(|k| k.id == key_id) | ||||
|             .ok_or(VaultError::KeyNotFound(key_id.to_string()))?; | ||||
|         match key.key_type { | ||||
|             KeyType::Ed25519 => { | ||||
|                 use ed25519_dalek::{Signer, SigningKey}; | ||||
|                 let signing = | ||||
|                     SigningKey::from_bytes(&key.private_key.clone().try_into().map_err(|_| { | ||||
|                         VaultError::Crypto("Invalid Ed25519 private key length".to_string()) | ||||
|                     })?); | ||||
|                 let sig = signing.sign(message); | ||||
|                 Ok(sig.to_bytes().to_vec()) | ||||
|             } | ||||
|             KeyType::Secp256k1 => { | ||||
|                 use k256::ecdsa::{signature::Signer, SigningKey}; | ||||
|                 let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| { | ||||
|                     VaultError::Crypto("Invalid secp256k1 private key length".to_string()) | ||||
|                 })?; | ||||
|                 let sk = SigningKey::from_bytes(arr.into()) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 let sig: k256::ecdsa::DerSignature = sk.sign(message); | ||||
|                 Ok(sig.to_vec()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Verify a signature with a stored keypair in a keyspace | ||||
| /// | ||||
| /// # Arguments | ||||
| /// * `keyspace` - Keyspace name | ||||
| /// * `password` - Keyspace password | ||||
| /// * `key_id` - Keypair ID | ||||
| /// * `message` - Message that was signed | ||||
| /// * `signature` - Signature to verify | ||||
| pub async fn verify(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8], signature: &[u8]) -> Result<bool, VaultError> { | ||||
|     let data = self.unlock_keyspace(keyspace, password).await?; | ||||
|     let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?; | ||||
|     match key.key_type { | ||||
|         KeyType::Ed25519 => { | ||||
|             use ed25519_dalek::{VerifyingKey, Signature, Verifier}; | ||||
|             let verifying = VerifyingKey::from_bytes(&key.public_key.clone().try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 public key length".to_string()))?) | ||||
|                 .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|             let sig = Signature::from_bytes(&signature.try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 signature length".to_string()))?); | ||||
|             Ok(verifying.verify(message, &sig).is_ok()) | ||||
|         } | ||||
|         KeyType::Secp256k1 => { | ||||
|             use k256::ecdsa::{VerifyingKey, Signature, signature::Verifier}; | ||||
|             let pk = VerifyingKey::from_sec1_bytes(&key.public_key).map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|             let sig = Signature::from_der(signature).map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|             Ok(pk.verify(message, &sig).is_ok()) | ||||
|     /// Verify a signature with a stored keypair in a keyspace | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `keyspace` - Keyspace name | ||||
|     /// * `password` - Keyspace password | ||||
|     /// * `key_id` - Keypair ID | ||||
|     /// * `message` - Message that was signed | ||||
|     /// * `signature` - Signature to verify | ||||
|     pub async fn verify( | ||||
|         &self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         key_id: &str, | ||||
|         message: &[u8], | ||||
|         signature: &[u8], | ||||
|     ) -> Result<bool, VaultError> { | ||||
|         let data = self.unlock_keyspace(keyspace, password).await?; | ||||
|         let key = data | ||||
|             .keypairs | ||||
|             .iter() | ||||
|             .find(|k| k.id == key_id) | ||||
|             .ok_or(VaultError::KeyNotFound(key_id.to_string()))?; | ||||
|         match key.key_type { | ||||
|             KeyType::Ed25519 => { | ||||
|                 use ed25519_dalek::{Signature, Verifier, VerifyingKey}; | ||||
|                 let verifying = | ||||
|                     VerifyingKey::from_bytes(&key.public_key.clone().try_into().map_err(|_| { | ||||
|                         VaultError::Crypto("Invalid Ed25519 public key length".to_string()) | ||||
|                     })?) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 let sig = Signature::from_bytes(&signature.try_into().map_err(|_| { | ||||
|                     VaultError::Crypto("Invalid Ed25519 signature length".to_string()) | ||||
|                 })?); | ||||
|                 Ok(verifying.verify(message, &sig).is_ok()) | ||||
|             } | ||||
|             KeyType::Secp256k1 => { | ||||
|                 use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; | ||||
|                 let pk = VerifyingKey::from_sec1_bytes(&key.public_key) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 let sig = Signature::from_der(signature) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 Ok(pk.verify(message, &sig).is_ok()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Encrypt a message using the keyspace symmetric cipher | ||||
|     /// (for simplicity, uses keyspace password-derived key) | ||||
|     pub async fn encrypt( | ||||
|         &self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         plaintext: &[u8], | ||||
|     ) -> Result<Vec<u8>, VaultError> { | ||||
|         debug!("encrypt"); | ||||
|  | ||||
|         // 1. Load keyspace metadata | ||||
|         let meta_bytes = self | ||||
|             .storage | ||||
|             .get(keyspace) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         let meta_bytes = match meta_bytes { | ||||
|             Some(val) => val, | ||||
|             None => { | ||||
|                 debug!("keyspace not found"); | ||||
|                 return Err(VaultError::Other("Keyspace not found".to_string())); | ||||
|             } | ||||
|         }; | ||||
|         let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) { | ||||
|             Ok(val) => val, | ||||
|             Err(e) => { | ||||
|                 debug!("serialization error: {}", e); | ||||
|                 return Err(VaultError::Serialization(e.to_string())); | ||||
|             } | ||||
|         }; | ||||
|         debug!( | ||||
|             "salt={:?} (hex salt: {})", | ||||
|             meta.salt, | ||||
|             hex::encode(&meta.salt) | ||||
|         ); | ||||
|         // 2. Derive key | ||||
|         let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); | ||||
|         // 3. Generate nonce | ||||
|         let nonce = random_salt(12); | ||||
|         debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(&nonce)); | ||||
|         // 4. Encrypt | ||||
|         let ciphertext = encrypt_chacha20(&key, plaintext, &nonce).map_err(VaultError::Crypto)?; | ||||
|         let mut out = nonce; | ||||
|         out.extend_from_slice(&ciphertext); | ||||
|         Ok(out) | ||||
|     } | ||||
|  | ||||
|     /// Decrypt a message using the keyspace symmetric cipher | ||||
|     /// (for simplicity, uses keyspace password-derived key) | ||||
|     pub async fn decrypt( | ||||
|         &self, | ||||
|         keyspace: &str, | ||||
|         password: &[u8], | ||||
|         ciphertext: &[u8], | ||||
|     ) -> Result<Vec<u8>, VaultError> { | ||||
|         debug!("decrypt"); | ||||
|  | ||||
|         // 1. Load keyspace metadata | ||||
|         let meta_bytes = self | ||||
|             .storage | ||||
|             .get(keyspace) | ||||
|             .await | ||||
|             .map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|         let meta_bytes = match meta_bytes { | ||||
|             Some(val) => val, | ||||
|             None => { | ||||
|                 debug!("keyspace not found"); | ||||
|                 return Err(VaultError::Other("Keyspace not found".to_string())); | ||||
|             } | ||||
|         }; | ||||
|         let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) { | ||||
|             Ok(val) => val, | ||||
|             Err(e) => { | ||||
|                 debug!("serialization error: {}", e); | ||||
|                 return Err(VaultError::Serialization(e.to_string())); | ||||
|             } | ||||
|         }; | ||||
|         debug!( | ||||
|             "salt={:?} (hex salt: {})", | ||||
|             meta.salt, | ||||
|             hex::encode(&meta.salt) | ||||
|         ); | ||||
|         // 2. Derive key | ||||
|         let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); | ||||
|         // 3. Extract nonce | ||||
|         let nonce = &ciphertext[..12]; | ||||
|         debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce)); | ||||
|         // 4. Decrypt | ||||
|         let plaintext = | ||||
|             decrypt_chacha20(&key, &ciphertext[12..], nonce).map_err(VaultError::Crypto)?; | ||||
|         Ok(plaintext) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Encrypt a message using the keyspace symmetric cipher | ||||
| /// (for simplicity, uses keyspace password-derived key) | ||||
| pub async fn encrypt(&self, keyspace: &str, password: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|     debug!("encrypt"); | ||||
|  | ||||
|     // 1. Load keyspace metadata | ||||
|     let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|     let meta_bytes = match meta_bytes { | ||||
|         Some(val) => val, | ||||
|         None => { | ||||
|             debug!("keyspace not found"); | ||||
|             return Err(VaultError::Other("Keyspace not found".to_string())); | ||||
|         } | ||||
|     }; | ||||
|     let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) { | ||||
|         Ok(val) => val, | ||||
|         Err(e) => { | ||||
|             debug!("serialization error: {}", e); | ||||
|             return Err(VaultError::Serialization(e.to_string())); | ||||
|         } | ||||
|     }; | ||||
|     debug!("salt={:?} (hex salt: {})", meta.salt, hex::encode(&meta.salt)); | ||||
|     // 2. Derive key | ||||
|     let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); | ||||
|     // 3. Generate nonce | ||||
|     let nonce = random_salt(12); | ||||
|     debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(&nonce)); | ||||
|     // 4. Encrypt | ||||
|     let ciphertext = encrypt_chacha20(&key, plaintext, &nonce).map_err(VaultError::Crypto)?; | ||||
|     let mut out = nonce; | ||||
|     out.extend_from_slice(&ciphertext); | ||||
|     Ok(out) | ||||
| } | ||||
|  | ||||
| /// Decrypt a message using the keyspace symmetric cipher | ||||
| /// (for simplicity, uses keyspace password-derived key) | ||||
| pub async fn decrypt(&self, keyspace: &str, password: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|     debug!("decrypt"); | ||||
|  | ||||
|     // 1. Load keyspace metadata | ||||
|     let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; | ||||
|     let meta_bytes = match meta_bytes { | ||||
|         Some(val) => val, | ||||
|         None => { | ||||
|             debug!("keyspace not found"); | ||||
|             return Err(VaultError::Other("Keyspace not found".to_string())); | ||||
|         } | ||||
|     }; | ||||
|     let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) { | ||||
|         Ok(val) => val, | ||||
|         Err(e) => { | ||||
|             debug!("serialization error: {}", e); | ||||
|             return Err(VaultError::Serialization(e.to_string())); | ||||
|         } | ||||
|     }; | ||||
|     debug!("salt={:?} (hex salt: {})", meta.salt, hex::encode(&meta.salt)); | ||||
|     // 2. Derive key | ||||
|     let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); | ||||
|     // 3. Extract nonce | ||||
|     let nonce = &ciphertext[..12]; | ||||
|     debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce)); | ||||
|     // 4. Decrypt | ||||
|     let plaintext = decrypt_chacha20(&key, &ciphertext[12..], nonce).map_err(VaultError::Crypto)?; | ||||
|     Ok(plaintext) | ||||
| } | ||||
| } | ||||
| @@ -136,6 +136,38 @@ impl<S: KVStore + Send + Sync> SessionManager<S> { | ||||
|         self.vault.sign(name, password, &keypair.id, message).await | ||||
|     } | ||||
|  | ||||
|     /// Verify a signature using the currently selected keypair | ||||
|     pub async fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         let keypair = self | ||||
|             .current_keypair() | ||||
|             .ok_or(VaultError::Crypto("No keypair selected".to_string()))?; | ||||
|         self.vault.verify(name, password, &keypair.id, message, signature).await | ||||
|     } | ||||
|  | ||||
|     /// Encrypt data using the keyspace symmetric cipher | ||||
|     /// Returns the encrypted data with the nonce prepended | ||||
|     pub async fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         self.vault.encrypt(name, password, plaintext).await | ||||
|     } | ||||
|  | ||||
|     /// Decrypt data using the keyspace symmetric cipher | ||||
|     /// Expects the nonce to be prepended to the ciphertext (as returned by encrypt) | ||||
|     pub async fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         self.vault.decrypt(name, password, ciphertext).await | ||||
|     } | ||||
|  | ||||
|     pub fn get_vault(&self) -> &Vault<S> { | ||||
|         &self.vault | ||||
|     } | ||||
| @@ -262,6 +294,38 @@ impl<S: KVStore> SessionManager<S> { | ||||
|         self.vault.sign(name, password, &keypair.id, message).await | ||||
|     } | ||||
|  | ||||
|     /// Verify a signature using the currently selected keypair | ||||
|     pub async fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         let keypair = self | ||||
|             .current_keypair() | ||||
|             .ok_or(VaultError::Crypto("No keypair selected".to_string()))?; | ||||
|         self.vault.verify(name, password, &keypair.id, message, signature).await | ||||
|     } | ||||
|  | ||||
|     /// Encrypt data using the keyspace symmetric cipher | ||||
|     /// Returns the encrypted data with the nonce prepended | ||||
|     pub async fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         self.vault.encrypt(name, password, plaintext).await | ||||
|     } | ||||
|  | ||||
|     /// Decrypt data using the keyspace symmetric cipher | ||||
|     /// Expects the nonce to be prepended to the ciphertext (as returned by encrypt) | ||||
|     pub async fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         self.vault.decrypt(name, password, ciphertext).await | ||||
|     } | ||||
|  | ||||
|     pub fn get_vault(&self) -> &Vault<S> { | ||||
|         &self.vault | ||||
|     } | ||||
|   | ||||
| @@ -221,15 +221,10 @@ pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|         // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope. | ||||
|         let session_ptr = | ||||
|             SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let password_opt = SESSION_PASSWORD.with(|pw| pw.borrow().clone()); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|         let password = match password_opt { | ||||
|             Some(p) => p, | ||||
|             None => return Err(JsValue::from_str("Session password not set")), | ||||
|         }; | ||||
|         match session.sign(message).await { | ||||
|             Ok(sig_bytes) => { | ||||
|                 let hex_sig = hex::encode(&sig_bytes); | ||||
| @@ -239,3 +234,72 @@ pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Verify a signature with the current session's selected keypair | ||||
| #[wasm_bindgen] | ||||
| pub async fn verify(message: &[u8], signature: &str) -> Result<JsValue, JsValue> { | ||||
|     { | ||||
|         // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope. | ||||
|         let session_ptr = | ||||
|             SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|          | ||||
|         // Convert hex signature to bytes | ||||
|         let sig_bytes = match hex::decode(signature) { | ||||
|             Ok(bytes) => bytes, | ||||
|             Err(e) => return Err(JsValue::from_str(&format!("Invalid signature format: {e}"))), | ||||
|         }; | ||||
|          | ||||
|         match session.verify(message, &sig_bytes).await { | ||||
|             Ok(is_valid) => Ok(JsValue::from_bool(is_valid)), | ||||
|             Err(e) => Err(JsValue::from_str(&format!("Verify error: {e}"))), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Encrypt data using the current session's keyspace symmetric cipher | ||||
| #[wasm_bindgen] | ||||
| pub async fn encrypt_data(data: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     { | ||||
|         // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope. | ||||
|         let session_ptr = | ||||
|             SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|          | ||||
|         match session.encrypt(data).await { | ||||
|             Ok(encrypted) => { | ||||
|                 // Return as Uint8Array for JavaScript | ||||
|                 Ok(Uint8Array::from(&encrypted[..]).into()) | ||||
|             } | ||||
|             Err(e) => Err(JsValue::from_str(&format!("Encryption error: {e}"))), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Decrypt data using the current session's keyspace symmetric cipher | ||||
| #[wasm_bindgen] | ||||
| pub async fn decrypt_data(encrypted: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     { | ||||
|         // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope. | ||||
|         let session_ptr = | ||||
|             SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|          | ||||
|         match session.decrypt(encrypted).await { | ||||
|             Ok(decrypted) => { | ||||
|                 // Return as Uint8Array for JavaScript | ||||
|                 Ok(Uint8Array::from(&decrypted[..]).into()) | ||||
|             } | ||||
|             Err(e) => Err(JsValue::from_str(&format!("Decryption error: {e}"))), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||