remove ts extension
1
hero_vault_extension/.gitignore
vendored
@ -1 +0,0 @@
|
||||
dist
|
@ -1,88 +0,0 @@
|
||||
# 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 +0,0 @@
|
||||
: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)}
|
@ -1 +0,0 @@
|
||||
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()});
|
61
hero_vault_extension/dist/background.js
vendored
@ -1,61 +0,0 @@
|
||||
|
||||
// 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
Before Width: | Height: | Size: 1.9 KiB |
BIN
hero_vault_extension/dist/icons/icon-16.png
vendored
Before Width: | Height: | Size: 454 B |
BIN
hero_vault_extension/dist/icons/icon-32.png
vendored
Before Width: | Height: | Size: 712 B |
BIN
hero_vault_extension/dist/icons/icon-48.png
vendored
Before Width: | Height: | Size: 1.1 KiB |
14
hero_vault_extension/dist/index.html
vendored
@ -1,14 +0,0 @@
|
||||
<!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
@ -1,26 +0,0 @@
|
||||
{
|
||||
"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 +0,0 @@
|
||||
import './assets/simple-background.ts-e63275e1.js';
|
@ -1,12 +0,0 @@
|
||||
<!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
@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "hero-vault-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Hero Vault - A secure browser extension for cryptographic operations",
|
||||
"scripts": {
|
||||
"dev": "node scripts/copy-wasm.js && vite",
|
||||
"build": "node scripts/copy-wasm.js && ([ \"$NO_TYPECHECK\" = \"true\" ] || tsc) && vite build",
|
||||
"watch": "node scripts/copy-wasm.js && tsc && vite build --watch",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,css,scss}\"",
|
||||
"copy-wasm": "node scripts/copy-wasm.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.3",
|
||||
"@mui/material": "^5.14.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.18",
|
||||
"@types/chrome": "^0.0.243",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"esbuild": "^0.25.4",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.64.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 1.1 KiB |
@ -1,26 +0,0 @@
|
||||
{
|
||||
"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'"
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
/**
|
||||
* 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();
|
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
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;
|
@ -1,145 +0,0 @@
|
||||
/**
|
||||
* 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');
|
@ -1,115 +0,0 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
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;
|
@ -1,130 +0,0 @@
|
||||
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;
|
@ -1,38 +0,0 @@
|
||||
: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);
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
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>
|
||||
);
|
@ -1,392 +0,0 @@
|
||||
/**
|
||||
* 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;
|
@ -1,155 +0,0 @@
|
||||
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;
|
@ -1,242 +0,0 @@
|
||||
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;
|
@ -1,557 +0,0 @@
|
||||
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;
|
@ -1,191 +0,0 @@
|
||||
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;
|
@ -1,246 +0,0 @@
|
||||
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;
|
@ -1,248 +0,0 @@
|
||||
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;
|
@ -1,144 +0,0 @@
|
||||
/**
|
||||
* 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 })
|
||||
}));
|
@ -1,416 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}));
|
@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 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
@ -1,5 +0,0 @@
|
||||
/// <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
@ -1,14 +0,0 @@
|
||||
// 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
@ -1,16 +0,0 @@
|
||||
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>;
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
@ -1,139 +0,0 @@
|
||||
/**
|
||||
* 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('');
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"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" }]
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
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',
|
||||
});
|