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',
|
|
||||||
});
|
|