remove ts extension

This commit is contained in:
Sameh Abouel-saad 2025-06-11 14:43:42 +03:00
parent 4f3f98a954
commit 1d3d0a4fa4
47 changed files with 0 additions and 8955 deletions

View File

@ -1 +0,0 @@
dist

View File

@ -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)

View File

@ -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)}

View File

@ -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()});

View File

@ -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();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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>

View File

@ -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'"
}
}

View File

@ -1 +0,0 @@
import './assets/simple-background.ts-e63275e1.js';

View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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'"
}
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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;

View File

@ -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');

View File

@ -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();
});
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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>
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 })
}));

View File

@ -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;
}
}
}));

View File

@ -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;
}

View File

@ -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

View File

@ -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/*';

View File

@ -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>;
}

View File

@ -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;
};

View File

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

View File

@ -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" }]
}

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

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