refactor: migrate extension to TypeScript and add Material-UI components
							
								
								
									
										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(); | ||||
|       }); | ||||
|      | ||||
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/dist/icons/icon-128.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/dist/icons/icon-16.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 454 B | 
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/dist/icons/icon-32.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 712 B | 
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/dist/icons/icon-48.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										14
									
								
								hero_vault_extension/dist/index.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <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> | ||||
|      | ||||
|   </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'; | ||||
							
								
								
									
										12
									
								
								hero_vault_extension/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <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" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/public/icons/icon-128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/public/icons/icon-16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 454 B | 
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/public/icons/icon-32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 712 B | 
							
								
								
									
										
											BIN
										
									
								
								hero_vault_extension/public/icons/icon-48.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 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', | ||||
| }); | ||||