Compare commits
17 Commits
087720f61f
...
main_brows
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d3d0a4fa4 | ||
|
|
4f3f98a954 | ||
|
|
c641d0ae2e | ||
|
|
6f42e5ab8d | ||
|
|
203cde1cba | ||
|
|
6b037537bf | ||
|
|
580fd72dce | ||
|
|
a0622629ae | ||
|
|
4e1e707f85 | ||
|
|
9f143ded9d | ||
|
|
b0d0aaa53d | ||
|
|
e00c140396 | ||
|
|
4ba1e43f4e | ||
|
|
b82d457873 | ||
|
|
b0b6359be1 | ||
|
|
536c077fbf | ||
|
|
31975aa9d3 |
@@ -5,5 +5,5 @@ members = [
|
|||||||
"vault",
|
"vault",
|
||||||
"evm_client",
|
"evm_client",
|
||||||
"wasm_app",
|
"wasm_app",
|
||||||
|
"sigsocket_client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
7
Makefile
@@ -26,6 +26,7 @@ build-wasm-app:
|
|||||||
cd wasm_app && wasm-pack build --target web
|
cd wasm_app && wasm-pack build --target web
|
||||||
|
|
||||||
# Build Hero Vault extension: wasm, copy, then extension
|
# Build Hero Vault extension: wasm, copy, then extension
|
||||||
build-hero-vault-extension:
|
build-crypto-vault-extension: build-wasm-app
|
||||||
cd wasm_app && wasm-pack build --target web
|
cp wasm_app/pkg/wasm_app* crypto_vault_extension/wasm/
|
||||||
cd hero_vault_extension && npm run build
|
cp wasm_app/pkg/*.d.ts crypto_vault_extension/wasm/
|
||||||
|
cp wasm_app/pkg/*.js crypto_vault_extension/wasm/
|
||||||
|
|||||||
@@ -134,13 +134,13 @@ For questions, contributions, or more details, see the architecture docs or open
|
|||||||
|
|
||||||
### Native
|
### Native
|
||||||
```sh
|
```sh
|
||||||
cargo check --workspace --features kvstore/native
|
cargo check --workspace
|
||||||
|
cargo test --workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
### WASM (kvstore only)
|
### WASM
|
||||||
```sh
|
```sh
|
||||||
cd kvstore
|
make test-browser-all
|
||||||
wasm-pack test --headless --firefox --features web
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Rhai Scripting System
|
# Rhai Scripting System
|
||||||
|
|||||||
26
build.sh
@@ -17,32 +17,22 @@ cd "$(dirname "$0")/wasm_app" || exit 1
|
|||||||
wasm-pack build --target web
|
wasm-pack build --target web
|
||||||
echo -e "${GREEN}✓ WASM build successful!${RESET}"
|
echo -e "${GREEN}✓ WASM build successful!${RESET}"
|
||||||
|
|
||||||
# Step 2: Build the frontend extension
|
# Step 2: Prepare the frontend extension
|
||||||
echo -e "${BLUE}Building frontend extension...${RESET}"
|
echo -e "${BLUE}Preparing frontend extension...${RESET}"
|
||||||
cd ../hero_vault_extension || exit 1
|
cd ../crypto_vault_extension || exit 1
|
||||||
|
|
||||||
# Copy WASM files to the extension's public directory
|
# Copy WASM files to the extension's public directory
|
||||||
echo "Copying WASM files..."
|
echo "Copying WASM files..."
|
||||||
mkdir -p public/wasm
|
cp ../wasm_app/pkg/wasm_app* wasm/
|
||||||
cp ../wasm_app/pkg/wasm_app* public/wasm/
|
cp ../wasm_app/pkg/*.d.ts wasm/
|
||||||
cp ../wasm_app/pkg/*.d.ts public/wasm/
|
cp ../wasm_app/pkg/*.js wasm/
|
||||||
cp ../wasm_app/pkg/package.json public/wasm/
|
|
||||||
|
|
||||||
# Build the extension without TypeScript checking
|
|
||||||
echo "Building extension..."
|
|
||||||
export NO_TYPECHECK=true
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Ensure the background script is properly built
|
|
||||||
echo "Building background script..."
|
|
||||||
node scripts/build-background.js
|
|
||||||
echo -e "${GREEN}✓ Frontend build successful!${RESET}"
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== Build Complete ===${RESET}"
|
echo -e "${GREEN}=== Build Complete ===${RESET}"
|
||||||
echo "Extension is ready in: $(pwd)/dist"
|
echo "Extension is ready in: $(pwd)"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}To load the extension in Chrome:${RESET}"
|
echo -e "${BLUE}To load the extension in Chrome:${RESET}"
|
||||||
echo "1. Go to chrome://extensions/"
|
echo "1. Go to chrome://extensions/"
|
||||||
echo "2. Enable Developer mode (toggle in top-right)"
|
echo "2. Enable Developer mode (toggle in top-right)"
|
||||||
echo "3. Click 'Load unpacked'"
|
echo "3. Click 'Load unpacked'"
|
||||||
echo "4. Select the 'dist' directory: $(pwd)/dist"
|
echo "4. Select the $(pwd) directory"
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ let vault = null;
|
|||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let currentSession = null;
|
let currentSession = null;
|
||||||
let keepAliveInterval = null;
|
let keepAliveInterval = null;
|
||||||
|
let sessionTimeoutDuration = 15; // Default 15 seconds
|
||||||
|
let sessionTimeoutId = null; // Background timer
|
||||||
|
let popupPort = null; // Track popup connection
|
||||||
|
|
||||||
|
// SigSocket service instance
|
||||||
|
let sigSocketService = null;
|
||||||
|
|
||||||
// Utility function to convert Uint8Array to hex
|
// Utility function to convert Uint8Array to hex
|
||||||
function toHex(uint8Array) {
|
function toHex(uint8Array) {
|
||||||
@@ -9,24 +15,70 @@ function toHex(uint8Array) {
|
|||||||
.map(b => b.toString(16).padStart(2, '0'))
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
// Background session timeout management
|
||||||
|
async function loadTimeoutSetting() {
|
||||||
|
const result = await chrome.storage.local.get(['sessionTimeout']);
|
||||||
|
sessionTimeoutDuration = result.sessionTimeout || 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSessionTimeout() {
|
||||||
|
clearSessionTimeout();
|
||||||
|
|
||||||
|
if (currentSession && sessionTimeoutDuration > 0) {
|
||||||
|
sessionTimeoutId = setTimeout(async () => {
|
||||||
|
if (vault && currentSession) {
|
||||||
|
// Lock the session
|
||||||
|
vault.lock_session();
|
||||||
|
|
||||||
|
// Keep the session info for SigSocket connection but mark it as timed out
|
||||||
|
const keyspace = currentSession.keyspace;
|
||||||
|
await sessionManager.clear();
|
||||||
|
|
||||||
|
// Maintain SigSocket connection for the locked keyspace to receive pending requests
|
||||||
|
if (sigSocketService && keyspace) {
|
||||||
|
try {
|
||||||
|
// Keep SigSocket connected to receive requests even when locked
|
||||||
|
console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to maintain SigSocket connection after timeout:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify popup if it's open
|
||||||
|
if (popupPort) {
|
||||||
|
popupPort.postMessage({
|
||||||
|
type: 'sessionTimeout',
|
||||||
|
message: 'Session timed out due to inactivity'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, sessionTimeoutDuration * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSessionTimeout() {
|
||||||
|
if (sessionTimeoutId) {
|
||||||
|
clearTimeout(sessionTimeoutId);
|
||||||
|
sessionTimeoutId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSessionTimeout() {
|
||||||
|
if (currentSession) {
|
||||||
|
startSessionTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Session persistence functions
|
// Session persistence functions
|
||||||
async function saveSession(keyspace) {
|
async function saveSession(keyspace) {
|
||||||
currentSession = { keyspace, timestamp: Date.now() };
|
currentSession = { keyspace, timestamp: Date.now() };
|
||||||
|
|
||||||
// Save to both session and local storage for better persistence
|
// Save to both session and local storage for better persistence
|
||||||
try {
|
|
||||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||||
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
|
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save session:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSession() {
|
async function loadSession() {
|
||||||
try {
|
|
||||||
// Try session storage first
|
// Try session storage first
|
||||||
let result = await chrome.storage.session.get(['cryptoVaultSession']);
|
let result = await chrome.storage.session.get(['cryptoVaultSession']);
|
||||||
if (result.cryptoVaultSession) {
|
if (result.cryptoVaultSession) {
|
||||||
@@ -42,20 +94,13 @@ async function loadSession() {
|
|||||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||||
return currentSession;
|
return currentSession;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load session:', error);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearSession() {
|
async function clearSession() {
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
try {
|
|
||||||
await chrome.storage.session.remove(['cryptoVaultSession']);
|
await chrome.storage.session.remove(['cryptoVaultSession']);
|
||||||
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
|
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear session:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep service worker alive
|
// Keep service worker alive
|
||||||
@@ -64,12 +109,8 @@ function startKeepAlive() {
|
|||||||
clearInterval(keepAliveInterval);
|
clearInterval(keepAliveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping every 20 seconds to keep service worker alive
|
|
||||||
keepAliveInterval = setInterval(() => {
|
keepAliveInterval = setInterval(() => {
|
||||||
// Simple operation to keep service worker active
|
chrome.storage.session.get(['keepAlive']).catch(() => {});
|
||||||
chrome.storage.session.get(['keepAlive']).catch(() => {
|
|
||||||
// Ignore errors
|
|
||||||
});
|
|
||||||
}, 20000);
|
}, 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,54 +121,76 @@ function stopKeepAlive() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced session management with keep-alive
|
// Consolidated session management
|
||||||
async function saveSessionWithKeepAlive(keyspace) {
|
const sessionManager = {
|
||||||
|
async save(keyspace) {
|
||||||
await saveSession(keyspace);
|
await saveSession(keyspace);
|
||||||
startKeepAlive();
|
startKeepAlive();
|
||||||
}
|
await loadTimeoutSetting();
|
||||||
|
startSessionTimeout();
|
||||||
async function clearSessionWithKeepAlive() {
|
},
|
||||||
|
async clear() {
|
||||||
await clearSession();
|
await clearSession();
|
||||||
stopKeepAlive();
|
stopKeepAlive();
|
||||||
}
|
clearSessionTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function restoreSession() {
|
async function restoreSession() {
|
||||||
const session = await loadSession();
|
const session = await loadSession();
|
||||||
if (session && vault) {
|
if (session && vault) {
|
||||||
try {
|
|
||||||
// Check if the session is still valid by testing if vault is unlocked
|
// Check if the session is still valid by testing if vault is unlocked
|
||||||
const isUnlocked = vault.is_unlocked();
|
const isUnlocked = vault.is_unlocked();
|
||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
// Restart keep-alive for restored session
|
// Restart keep-alive for restored session
|
||||||
startKeepAlive();
|
startKeepAlive();
|
||||||
return session;
|
|
||||||
} else {
|
// Connect to SigSocket for the restored session
|
||||||
await clearSessionWithKeepAlive();
|
if (sigSocketService) {
|
||||||
|
try {
|
||||||
|
const connected = await sigSocketService.connectToServer(session.keyspace);
|
||||||
|
if (connected) {
|
||||||
|
console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking session validity:', error);
|
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
|
||||||
await clearSessionWithKeepAlive();
|
if (error.message && error.message.includes('Workspace not found')) {
|
||||||
|
console.log(`ℹ️ SigSocket connection skipped for restored session: No workspace available yet`);
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to reconnect SigSocket for restored session:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
} else {
|
||||||
|
// Session exists but is locked - still try to connect SigSocket to receive pending requests
|
||||||
|
if (sigSocketService && session.keyspace) {
|
||||||
|
try {
|
||||||
|
const connected = await sigSocketService.connectToServer(session.keyspace);
|
||||||
|
if (connected) {
|
||||||
|
console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
|
||||||
|
if (error.message && error.message.includes('Workspace not found')) {
|
||||||
|
console.log(`ℹ️ SigSocket connection skipped for locked session: No workspace available yet`);
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to connect SigSocket for locked session:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clear the session - keep it for SigSocket connection
|
||||||
|
// await sessionManager.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session; // Return session even if locked, so we know which keyspace to use
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import WASM module functions
|
// Import WASM module functions and SigSocket service
|
||||||
import init, {
|
import init, * as wasmFunctions from './wasm/wasm_app.js';
|
||||||
create_keyspace,
|
import SigSocketService from './background/sigsocket.js';
|
||||||
init_session,
|
|
||||||
is_unlocked,
|
|
||||||
add_keypair,
|
|
||||||
list_keypairs,
|
|
||||||
select_keypair,
|
|
||||||
current_keypair_metadata,
|
|
||||||
current_keypair_public_key,
|
|
||||||
sign,
|
|
||||||
verify,
|
|
||||||
encrypt_data,
|
|
||||||
decrypt_data,
|
|
||||||
lock_session
|
|
||||||
} from './wasm/wasm_app.js';
|
|
||||||
|
|
||||||
// Initialize WASM module
|
// Initialize WASM module
|
||||||
async function initVault() {
|
async function initVault() {
|
||||||
@@ -138,25 +201,17 @@ async function initVault() {
|
|||||||
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
||||||
await init(wasmUrl);
|
await init(wasmUrl);
|
||||||
|
|
||||||
// Create a vault object with all the imported functions
|
// Use imported functions directly
|
||||||
vault = {
|
vault = wasmFunctions;
|
||||||
create_keyspace,
|
|
||||||
init_session,
|
|
||||||
is_unlocked,
|
|
||||||
add_keypair,
|
|
||||||
list_keypairs,
|
|
||||||
select_keypair,
|
|
||||||
current_keypair_metadata,
|
|
||||||
current_keypair_public_key,
|
|
||||||
sign,
|
|
||||||
verify,
|
|
||||||
encrypt_data,
|
|
||||||
decrypt_data,
|
|
||||||
lock_session
|
|
||||||
};
|
|
||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
|
||||||
|
// Initialize SigSocket service
|
||||||
|
if (!sigSocketService) {
|
||||||
|
sigSocketService = new SigSocketService();
|
||||||
|
await sigSocketService.initialize(vault);
|
||||||
|
console.log('🔌 SigSocket service initialized');
|
||||||
|
}
|
||||||
|
|
||||||
// Try to restore previous session
|
// Try to restore previous session
|
||||||
await restoreSession();
|
await restoreSession();
|
||||||
|
|
||||||
@@ -167,20 +222,217 @@ async function initVault() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle popup connection/disconnection
|
|
||||||
chrome.runtime.onConnect.addListener((port) => {
|
// Consolidated message handlers
|
||||||
if (port.name === 'popup') {
|
const messageHandlers = {
|
||||||
// If we have an active session, ensure keep-alive is running
|
createKeyspace: async (request) => {
|
||||||
if (currentSession) {
|
await vault.create_keyspace(request.keyspace, request.password);
|
||||||
startKeepAlive();
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
initSession: async (request) => {
|
||||||
|
await vault.init_session(request.keyspace, request.password);
|
||||||
|
await sessionManager.save(request.keyspace);
|
||||||
|
|
||||||
|
// Smart auto-connect to SigSocket when session is initialized
|
||||||
|
if (sigSocketService) {
|
||||||
|
try {
|
||||||
|
console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`);
|
||||||
|
|
||||||
|
// This will reuse existing connection if same workspace, or switch if different
|
||||||
|
const connected = await sigSocketService.connectToServer(request.keyspace);
|
||||||
|
if (connected) {
|
||||||
|
console.log(`✅ SigSocket ready for workspace: ${request.keyspace}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ SigSocket connection failed for workspace: ${request.keyspace}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to auto-connect to SigSocket:', error);
|
||||||
|
|
||||||
|
// If connection fails, try once more after a short delay
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`);
|
||||||
|
await sigSocketService.connectToServer(request.keyspace);
|
||||||
|
} catch (retryError) {
|
||||||
|
console.warn('SigSocket retry connection also failed:', retryError);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
port.onDisconnect.addListener(() => {
|
// Notify SigSocket service that keyspace is now unlocked
|
||||||
// Keep the keep-alive running even after popup disconnects
|
await sigSocketService.onKeypaceUnlocked();
|
||||||
// This ensures session persistence across popup closes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
isUnlocked: () => ({ success: true, unlocked: vault.is_unlocked() }),
|
||||||
|
|
||||||
|
addKeypair: async (request) => {
|
||||||
|
const result = await vault.add_keypair(request.keyType, request.metadata);
|
||||||
|
return { success: true, result };
|
||||||
|
},
|
||||||
|
|
||||||
|
listKeypairs: async () => {
|
||||||
|
if (!vault.is_unlocked()) {
|
||||||
|
return { success: false, error: 'Session is not unlocked' };
|
||||||
|
}
|
||||||
|
const keypairsRaw = await vault.list_keypairs();
|
||||||
|
const keypairs = typeof keypairsRaw === 'string' ? JSON.parse(keypairsRaw) : keypairsRaw;
|
||||||
|
return { success: true, keypairs };
|
||||||
|
},
|
||||||
|
|
||||||
|
selectKeypair: (request) => {
|
||||||
|
vault.select_keypair(request.keyId);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentKeypairMetadata: () => ({ success: true, metadata: vault.current_keypair_metadata() }),
|
||||||
|
|
||||||
|
getCurrentKeypairPublicKey: () => ({ success: true, publicKey: toHex(vault.current_keypair_public_key()) }),
|
||||||
|
|
||||||
|
sign: async (request) => {
|
||||||
|
const signature = await vault.sign(new Uint8Array(request.message));
|
||||||
|
return { success: true, signature };
|
||||||
|
},
|
||||||
|
|
||||||
|
encrypt: async (request) => {
|
||||||
|
if (!vault.is_unlocked()) {
|
||||||
|
return { success: false, error: 'Session is not unlocked' };
|
||||||
|
}
|
||||||
|
const messageBytes = new TextEncoder().encode(request.message);
|
||||||
|
const encryptedData = await vault.encrypt_data(messageBytes);
|
||||||
|
const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
|
||||||
|
return { success: true, encryptedMessage };
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: async (request) => {
|
||||||
|
if (!vault.is_unlocked()) {
|
||||||
|
return { success: false, error: 'Session is not unlocked' };
|
||||||
|
}
|
||||||
|
const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0)));
|
||||||
|
const decryptedData = await vault.decrypt_data(encryptedBytes);
|
||||||
|
const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData));
|
||||||
|
return { success: true, decryptedMessage };
|
||||||
|
},
|
||||||
|
|
||||||
|
verify: async (request) => {
|
||||||
|
const metadata = vault.current_keypair_metadata();
|
||||||
|
if (!metadata) {
|
||||||
|
return { success: false, error: 'No keypair selected' };
|
||||||
|
}
|
||||||
|
const isValid = await vault.verify(new Uint8Array(request.message), request.signature);
|
||||||
|
return { success: true, isValid };
|
||||||
|
},
|
||||||
|
|
||||||
|
lockSession: async () => {
|
||||||
|
vault.lock_session();
|
||||||
|
await sessionManager.clear();
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatus: async () => {
|
||||||
|
const status = vault ? vault.is_unlocked() : false;
|
||||||
|
const session = await loadSession();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
status,
|
||||||
|
session: session ? { keyspace: session.keyspace } : null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Timeout management handlers
|
||||||
|
resetTimeout: async () => {
|
||||||
|
resetSessionTimeout();
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTimeout: async (request) => {
|
||||||
|
sessionTimeoutDuration = request.timeout;
|
||||||
|
await chrome.storage.local.set({ sessionTimeout: request.timeout });
|
||||||
|
resetSessionTimeout(); // Restart with new duration
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSigSocketUrl: async (request) => {
|
||||||
|
if (sigSocketService) {
|
||||||
|
// Update the server URL in the SigSocket service
|
||||||
|
sigSocketService.defaultServerUrl = request.serverUrl;
|
||||||
|
|
||||||
|
// Save to storage (already done in popup, but ensure consistency)
|
||||||
|
await chrome.storage.local.set({ sigSocketUrl: request.serverUrl });
|
||||||
|
|
||||||
|
console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`);
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
// SigSocket handlers
|
||||||
|
connectSigSocket: async (request) => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
const connected = await sigSocketService.connectToServer(request.workspace);
|
||||||
|
return { success: connected };
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnectSigSocket: async () => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
await sigSocketService.disconnect();
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
getSigSocketStatus: async () => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
const status = await sigSocketService.getStatus();
|
||||||
|
return { success: true, status };
|
||||||
|
},
|
||||||
|
|
||||||
|
getSigSocketStatusWithTest: async () => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
// Use the enhanced connection testing method
|
||||||
|
const status = await sigSocketService.getStatusWithConnectionTest();
|
||||||
|
return { success: true, status };
|
||||||
|
},
|
||||||
|
|
||||||
|
getPendingSignRequests: async () => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use WASM filtered requests which handles workspace filtering
|
||||||
|
const requests = await sigSocketService.getFilteredRequests();
|
||||||
|
return { success: true, requests };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get pending requests:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
approveSignRequest: async (request) => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
const approved = await sigSocketService.approveSignRequest(request.requestId);
|
||||||
|
return { success: approved };
|
||||||
|
},
|
||||||
|
|
||||||
|
rejectSignRequest: async (request) => {
|
||||||
|
if (!sigSocketService) {
|
||||||
|
return { success: false, error: 'SigSocket service not initialized' };
|
||||||
|
}
|
||||||
|
const rejected = await sigSocketService.rejectSignRequest(request.requestId, request.reason);
|
||||||
|
return { success: rejected };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle messages from popup and content scripts
|
// Handle messages from popup and content scripts
|
||||||
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||||
@@ -190,143 +442,13 @@ chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|||||||
await initVault();
|
await initVault();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.action) {
|
const handler = messageHandlers[request.action];
|
||||||
case 'createKeyspace':
|
if (handler) {
|
||||||
await vault.create_keyspace(request.keyspace, request.password);
|
return await handler(request);
|
||||||
return { success: true };
|
|
||||||
|
|
||||||
case 'initSession':
|
|
||||||
await vault.init_session(request.keyspace, request.password);
|
|
||||||
await saveSessionWithKeepAlive(request.keyspace);
|
|
||||||
return { success: true };
|
|
||||||
|
|
||||||
case 'isUnlocked':
|
|
||||||
const unlocked = vault.is_unlocked();
|
|
||||||
return { success: true, unlocked };
|
|
||||||
|
|
||||||
case 'addKeypair':
|
|
||||||
const result = await vault.add_keypair(request.keyType, request.metadata);
|
|
||||||
return { success: true, result };
|
|
||||||
|
|
||||||
case 'listKeypairs':
|
|
||||||
// Check if session is unlocked first
|
|
||||||
const isUnlocked = vault.is_unlocked();
|
|
||||||
|
|
||||||
if (!isUnlocked) {
|
|
||||||
return { success: false, error: 'Session is not unlocked' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const keypairsRaw = await vault.list_keypairs();
|
|
||||||
|
|
||||||
// Parse JSON string if needed
|
|
||||||
let keypairs;
|
|
||||||
if (typeof keypairsRaw === 'string') {
|
|
||||||
keypairs = JSON.parse(keypairsRaw);
|
|
||||||
} else {
|
} else {
|
||||||
keypairs = keypairsRaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, keypairs };
|
|
||||||
} catch (listError) {
|
|
||||||
console.error('Background: Error calling list_keypairs:', listError);
|
|
||||||
throw listError;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'selectKeypair':
|
|
||||||
vault.select_keypair(request.keyId);
|
|
||||||
return { success: true };
|
|
||||||
|
|
||||||
case 'getCurrentKeypairMetadata':
|
|
||||||
const metadata = vault.current_keypair_metadata();
|
|
||||||
return { success: true, metadata };
|
|
||||||
|
|
||||||
case 'getCurrentKeypairPublicKey':
|
|
||||||
const publicKey = vault.current_keypair_public_key();
|
|
||||||
const hexKey = toHex(publicKey);
|
|
||||||
return { success: true, publicKey: hexKey };
|
|
||||||
|
|
||||||
case 'sign':
|
|
||||||
const signature = await vault.sign(new Uint8Array(request.message));
|
|
||||||
return { success: true, signature };
|
|
||||||
|
|
||||||
case 'encrypt':
|
|
||||||
// Check if session is unlocked
|
|
||||||
if (!vault.is_unlocked()) {
|
|
||||||
return { success: false, error: 'Session is not unlocked' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Convert message to Uint8Array for WASM
|
|
||||||
const messageBytes = new TextEncoder().encode(request.message);
|
|
||||||
|
|
||||||
// Use WASM encrypt_data function with ChaCha20-Poly1305
|
|
||||||
const encryptedData = await vault.encrypt_data(messageBytes);
|
|
||||||
|
|
||||||
// Convert result to base64 for easy handling
|
|
||||||
const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
|
|
||||||
return { success: true, encryptedMessage };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Encryption error:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'decrypt':
|
|
||||||
// Check if session is unlocked
|
|
||||||
if (!vault.is_unlocked()) {
|
|
||||||
return { success: false, error: 'Session is not unlocked' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Convert base64 back to Uint8Array
|
|
||||||
const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0)));
|
|
||||||
|
|
||||||
// Use WASM decrypt_data function with ChaCha20-Poly1305
|
|
||||||
const decryptedData = await vault.decrypt_data(encryptedBytes);
|
|
||||||
|
|
||||||
// Convert result back to string
|
|
||||||
const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData));
|
|
||||||
return { success: true, decryptedMessage };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Decryption error:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'verify':
|
|
||||||
// Check if a keypair is selected
|
|
||||||
try {
|
|
||||||
const metadata = vault.current_keypair_metadata();
|
|
||||||
if (!metadata) {
|
|
||||||
return { success: false, error: 'No keypair selected' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use WASM verify function
|
|
||||||
const isValid = await vault.verify(new Uint8Array(request.message), request.signature);
|
|
||||||
return { success: true, isValid };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Verification error:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'lockSession':
|
|
||||||
vault.lock_session();
|
|
||||||
await clearSessionWithKeepAlive();
|
|
||||||
return { success: true };
|
|
||||||
|
|
||||||
case 'getStatus':
|
|
||||||
const status = vault ? vault.is_unlocked() : false;
|
|
||||||
const session = await loadSession();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
status,
|
|
||||||
session: session ? { keyspace: session.keyspace } : null
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('Unknown action: ' + request.action);
|
throw new Error('Unknown action: ' + request.action);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Background script error:', error);
|
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -343,3 +465,206 @@ chrome.runtime.onStartup.addListener(() => {
|
|||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
initVault();
|
initVault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle popup connection for keep-alive and timeout notifications
|
||||||
|
chrome.runtime.onConnect.addListener((port) => {
|
||||||
|
if (port.name === 'popup') {
|
||||||
|
// Track popup connection
|
||||||
|
popupPort = port;
|
||||||
|
|
||||||
|
// Connect SigSocket service to popup
|
||||||
|
if (sigSocketService) {
|
||||||
|
sigSocketService.setPopupPort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an active session, ensure keep-alive is running
|
||||||
|
if (currentSession) {
|
||||||
|
startKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages from popup
|
||||||
|
port.onMessage.addListener(async (message) => {
|
||||||
|
if (message.type === 'REQUEST_IMMEDIATE_STATUS') {
|
||||||
|
// Immediately send current SigSocket status to popup
|
||||||
|
if (sigSocketService) {
|
||||||
|
try {
|
||||||
|
const status = await sigSocketService.getStatus();
|
||||||
|
port.postMessage({
|
||||||
|
type: 'CONNECTION_STATUS_CHANGED',
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to send immediate status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
port.onDisconnect.addListener(() => {
|
||||||
|
// Popup closed, clear reference and stop keep-alive
|
||||||
|
popupPort = null;
|
||||||
|
stopKeepAlive();
|
||||||
|
|
||||||
|
// Disconnect SigSocket service from popup
|
||||||
|
if (sigSocketService) {
|
||||||
|
sigSocketService.setPopupPort(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle notification clicks to open extension (notifications are now clickable without buttons)
|
||||||
|
chrome.notifications.onClicked.addListener(async (notificationId) => {
|
||||||
|
console.log(`🔔 Notification clicked: ${notificationId}`);
|
||||||
|
|
||||||
|
// Check if this is a SigSocket notification
|
||||||
|
if (notificationId.startsWith('sigsocket-request-')) {
|
||||||
|
console.log('🔔 SigSocket notification clicked, opening extension...');
|
||||||
|
try {
|
||||||
|
await openExtensionPopup();
|
||||||
|
// Clear the notification after successfully opening
|
||||||
|
chrome.notifications.clear(notificationId);
|
||||||
|
console.log('✅ Notification cleared after opening extension');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to handle notification click:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🔔 Non-SigSocket notification clicked, ignoring');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Notification button handler removed - notifications are now clickable without buttons
|
||||||
|
|
||||||
|
// Function to open extension popup with best UX
|
||||||
|
async function openExtensionPopup() {
|
||||||
|
try {
|
||||||
|
console.log('🔔 Opening extension popup from notification...');
|
||||||
|
|
||||||
|
// First, check if there's already a popup window open
|
||||||
|
const windows = await chrome.windows.getAll({ populate: true });
|
||||||
|
const existingPopup = windows.find(window =>
|
||||||
|
window.type === 'popup' &&
|
||||||
|
window.tabs?.some(tab => tab.url?.includes('popup.html'))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingPopup) {
|
||||||
|
// Focus existing popup and send focus message
|
||||||
|
await chrome.windows.update(existingPopup.id, { focused: true });
|
||||||
|
console.log('✅ Focused existing popup window');
|
||||||
|
|
||||||
|
// Send message to focus on SigSocket section
|
||||||
|
if (popupPort) {
|
||||||
|
popupPort.postMessage({
|
||||||
|
type: 'FOCUS_SIGSOCKET',
|
||||||
|
fromNotification: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best UX: Try to use the normal popup experience
|
||||||
|
// The action API gives the same popup as clicking the extension icon
|
||||||
|
try {
|
||||||
|
if (chrome.action && chrome.action.openPopup) {
|
||||||
|
await chrome.action.openPopup();
|
||||||
|
console.log('✅ Extension popup opened via action API (best UX - normal popup)');
|
||||||
|
|
||||||
|
// Send focus message after popup opens
|
||||||
|
setTimeout(() => {
|
||||||
|
if (popupPort) {
|
||||||
|
popupPort.postMessage({
|
||||||
|
type: 'FOCUS_SIGSOCKET',
|
||||||
|
fromNotification: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (actionError) {
|
||||||
|
// The action API fails when there's no active browser window
|
||||||
|
// This is common when all browser windows are closed but extension is still running
|
||||||
|
console.log('⚠️ Action API failed (likely no active window):', actionError.message);
|
||||||
|
|
||||||
|
// Check if we have any normal browser windows
|
||||||
|
const allWindows = await chrome.windows.getAll();
|
||||||
|
const normalWindows = allWindows.filter(w => w.type === 'normal');
|
||||||
|
|
||||||
|
if (normalWindows.length > 0) {
|
||||||
|
// We have browser windows, try to focus one and retry action API
|
||||||
|
try {
|
||||||
|
const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0];
|
||||||
|
await chrome.windows.update(targetWindow.id, { focused: true });
|
||||||
|
|
||||||
|
// Small delay and retry
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
await chrome.action.openPopup();
|
||||||
|
console.log('✅ Extension popup opened via action API after focusing window');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (popupPort) {
|
||||||
|
popupPort.postMessage({
|
||||||
|
type: 'FOCUS_SIGSOCKET',
|
||||||
|
fromNotification: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (retryError) {
|
||||||
|
console.log('⚠️ Action API retry also failed:', retryError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If action API fails completely, we need to create a window
|
||||||
|
// But let's make it as close to the normal popup experience as possible
|
||||||
|
console.log('⚠️ Creating popup window as fallback (action API unavailable)');
|
||||||
|
|
||||||
|
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
|
||||||
|
|
||||||
|
// Position the popup where the extension icon would normally show its popup
|
||||||
|
// Try to position it in the top-right area like a normal extension popup
|
||||||
|
let left = screen.width - 420; // 400px width + 20px margin
|
||||||
|
let top = 80; // Below browser toolbar area
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If we have a browser window, position relative to it
|
||||||
|
const allWindows = await chrome.windows.getAll();
|
||||||
|
const normalWindows = allWindows.filter(w => w.type === 'normal');
|
||||||
|
|
||||||
|
if (normalWindows.length > 0) {
|
||||||
|
const referenceWindow = normalWindows[0];
|
||||||
|
left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420;
|
||||||
|
top = (referenceWindow.top || 0) + 80;
|
||||||
|
}
|
||||||
|
} catch (positionError) {
|
||||||
|
console.log('⚠️ Could not get window position, using screen-based positioning');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWindow = await chrome.windows.create({
|
||||||
|
url: popupUrl,
|
||||||
|
type: 'popup',
|
||||||
|
width: 400,
|
||||||
|
height: 600,
|
||||||
|
left: Math.max(0, left),
|
||||||
|
top: Math.max(0, top),
|
||||||
|
focused: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Extension popup window created: ${newWindow.id}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to open extension popup:', error);
|
||||||
|
|
||||||
|
// Final fallback: open in new tab (least ideal but still functional)
|
||||||
|
try {
|
||||||
|
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
|
||||||
|
await chrome.tabs.create({ url: popupUrl, active: true });
|
||||||
|
console.log('✅ Opened extension in new tab as final fallback');
|
||||||
|
} catch (tabError) {
|
||||||
|
console.error('❌ All popup opening methods failed:', tabError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
876
crypto_vault_extension/background/sigsocket.js
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
/**
|
||||||
|
* SigSocket Service - Clean Implementation with New WASM APIs
|
||||||
|
*
|
||||||
|
* This service provides a clean interface for SigSocket functionality using
|
||||||
|
* the new WASM-based APIs that handle all WebSocket management, request storage,
|
||||||
|
* and security validation internally.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - WASM handles: WebSocket connection, message parsing, request storage, security
|
||||||
|
* - Extension handles: UI notifications, badge updates, user interactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SigSocketService {
|
||||||
|
constructor() {
|
||||||
|
// Connection state
|
||||||
|
this.isConnected = false;
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
this.connectedPublicKey = null;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
this.defaultServerUrl = "ws://localhost:8080/ws";
|
||||||
|
|
||||||
|
// WASM module reference
|
||||||
|
this.wasmModule = null;
|
||||||
|
|
||||||
|
// UI communication
|
||||||
|
this.popupPort = null;
|
||||||
|
|
||||||
|
// Status monitoring
|
||||||
|
this.statusMonitorInterval = null;
|
||||||
|
this.lastKnownConnectionState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service with WASM module
|
||||||
|
* @param {Object} wasmModule - The loaded WASM module with SigSocketManager
|
||||||
|
*/
|
||||||
|
async initialize(wasmModule) {
|
||||||
|
this.wasmModule = wasmModule;
|
||||||
|
|
||||||
|
// Load server URL from storage
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get(['sigSocketUrl']);
|
||||||
|
if (result.sigSocketUrl) {
|
||||||
|
this.defaultServerUrl = result.sigSocketUrl;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load SigSocket URL from storage:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore any persisted pending requests
|
||||||
|
await this.restorePendingRequests();
|
||||||
|
|
||||||
|
console.log('🔌 SigSocket service initialized with WASM APIs');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore pending requests from persistent storage
|
||||||
|
* Only restore requests that match the current workspace
|
||||||
|
*/
|
||||||
|
async restorePendingRequests() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
|
||||||
|
if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) {
|
||||||
|
console.log(`🔄 Found ${result.sigSocketPendingRequests.length} stored requests`);
|
||||||
|
|
||||||
|
// Filter requests for current workspace only
|
||||||
|
const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request =>
|
||||||
|
request.target_public_key === this.connectedPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🔄 Restoring ${currentWorkspaceRequests.length} requests for current workspace`);
|
||||||
|
|
||||||
|
// Add each workspace-specific request back to WASM storage
|
||||||
|
for (const request of currentWorkspaceRequests) {
|
||||||
|
try {
|
||||||
|
await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request));
|
||||||
|
console.log(`✅ Restored request: ${request.id || request.request?.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update badge after restoration
|
||||||
|
this.updateBadge();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore pending requests:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist pending requests to storage with workspace isolation
|
||||||
|
*/
|
||||||
|
async persistPendingRequests() {
|
||||||
|
try {
|
||||||
|
const requests = await this.getFilteredRequests();
|
||||||
|
|
||||||
|
// Get existing storage to merge with other workspaces
|
||||||
|
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
|
||||||
|
const existingRequests = result.sigSocketPendingRequests || [];
|
||||||
|
|
||||||
|
// Remove old requests for current workspace
|
||||||
|
const otherWorkspaceRequests = existingRequests.filter(request =>
|
||||||
|
request.target_public_key !== this.connectedPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine with current workspace requests
|
||||||
|
const allRequests = [...otherWorkspaceRequests, ...requests];
|
||||||
|
|
||||||
|
await chrome.storage.local.set({ sigSocketPendingRequests: allRequests });
|
||||||
|
console.log(`💾 Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to persist pending requests:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to SigSocket server using WASM APIs
|
||||||
|
* WASM handles all connection logic (reuse, switching, etc.)
|
||||||
|
* @param {string} workspaceId - The workspace/keyspace identifier
|
||||||
|
* @param {number} retryCount - Number of retry attempts (default: 3)
|
||||||
|
* @returns {Promise<boolean>} - True if connected successfully
|
||||||
|
*/
|
||||||
|
async connectToServer(workspaceId, retryCount = 3) {
|
||||||
|
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
||||||
|
try {
|
||||||
|
if (!this.wasmModule?.SigSocketManager) {
|
||||||
|
throw new Error('WASM SigSocketManager not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`);
|
||||||
|
|
||||||
|
// Clean workspace switching
|
||||||
|
if (this.currentWorkspace && this.currentWorkspace !== workspaceId) {
|
||||||
|
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`);
|
||||||
|
await this.cleanWorkspaceSwitch(workspaceId);
|
||||||
|
// Small delay to ensure clean state transition
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let WASM handle all connection logic (reuse, switching, etc.)
|
||||||
|
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
|
||||||
|
workspaceId,
|
||||||
|
this.defaultServerUrl,
|
||||||
|
(event) => this.handleSigSocketEvent(event)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse connection info
|
||||||
|
const info = JSON.parse(connectionInfo);
|
||||||
|
this.currentWorkspace = workspaceId; // Use the parameter we passed, not WASM response
|
||||||
|
this.connectedPublicKey = info.public_key;
|
||||||
|
this.isConnected = info.is_connected;
|
||||||
|
|
||||||
|
console.log(`✅ SigSocket connection result:`, {
|
||||||
|
workspace: this.currentWorkspace,
|
||||||
|
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
|
||||||
|
connected: this.isConnected,
|
||||||
|
serverUrl: this.defaultServerUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate that we have a public key if connected
|
||||||
|
if (this.isConnected && !this.connectedPublicKey) {
|
||||||
|
console.warn('⚠️ Connected but no public key received - this may cause request issues');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update badge to show current state
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
if (this.isConnected) {
|
||||||
|
// Clean flow: Connect -> Restore workspace requests -> Update UI
|
||||||
|
console.log(`🔗 Connected to workspace: ${workspaceId}, restoring pending requests...`);
|
||||||
|
|
||||||
|
// 1. Restore requests for this specific workspace
|
||||||
|
await this.restorePendingRequests();
|
||||||
|
|
||||||
|
// 2. Update badge with current count
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
console.log(`✅ Workspace ${workspaceId} ready with restored requests`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not connected but no error, try again
|
||||||
|
if (attempt < retryCount) {
|
||||||
|
console.log(`⏳ Connection not established, retrying in 1 second...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Check if this is an expected "no workspace" error during startup
|
||||||
|
const isExpectedStartupError = error.message &&
|
||||||
|
(error.message.includes('Workspace not found') ||
|
||||||
|
error.message.includes('no keypairs available'));
|
||||||
|
|
||||||
|
if (isExpectedStartupError && attempt === 1) {
|
||||||
|
console.log(`⏳ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a public key related error
|
||||||
|
if (error.message && error.message.includes('public key')) {
|
||||||
|
console.error(`🔑 Public key error detected: ${error.message}`);
|
||||||
|
// For public key errors, don't retry immediately - might need workspace change
|
||||||
|
if (attempt === 1) {
|
||||||
|
console.log(`🔄 Public key error on first attempt, trying to disconnect and reconnect...`);
|
||||||
|
await this.disconnect();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < retryCount) {
|
||||||
|
if (!isExpectedStartupError) {
|
||||||
|
console.log(`⏳ Retrying connection in 2 seconds...`);
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
} else {
|
||||||
|
// Final attempt failed
|
||||||
|
this.isConnected = false;
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
this.connectedPublicKey = null;
|
||||||
|
|
||||||
|
if (isExpectedStartupError) {
|
||||||
|
console.log(`ℹ️ SigSocket connection failed: No active workspace. Will connect when user logs in.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle events from the WASM SigSocket client
|
||||||
|
* This is called automatically when requests arrive
|
||||||
|
* @param {Object} event - Event from WASM layer
|
||||||
|
*/
|
||||||
|
async handleSigSocketEvent(event) {
|
||||||
|
console.log('📨 Received SigSocket event:', event);
|
||||||
|
|
||||||
|
if (event.type === 'sign_request') {
|
||||||
|
console.log(`🔐 New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`);
|
||||||
|
|
||||||
|
// Clean flow: Request arrives -> Store -> Persist -> Update UI
|
||||||
|
try {
|
||||||
|
// 1. Request is automatically stored in WASM (already done by WASM layer)
|
||||||
|
|
||||||
|
// 2. Persist to storage with workspace isolation
|
||||||
|
await this.persistPendingRequests();
|
||||||
|
|
||||||
|
// 3. Update badge count
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
// 4. Show notification
|
||||||
|
this.showSignRequestNotification();
|
||||||
|
|
||||||
|
// 5. Notify popup if connected
|
||||||
|
this.notifyPopupOfNewRequest();
|
||||||
|
|
||||||
|
console.log(`✅ Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to process request ${event.request_id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve a sign request using WASM APIs
|
||||||
|
* @param {string} requestId - Request to approve
|
||||||
|
* @returns {Promise<boolean>} - True if approved successfully
|
||||||
|
*/
|
||||||
|
async approveSignRequest(requestId) {
|
||||||
|
try {
|
||||||
|
if (!this.wasmModule?.SigSocketManager) {
|
||||||
|
throw new Error('WASM SigSocketManager not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're connected before attempting approval
|
||||||
|
if (!this.isConnected) {
|
||||||
|
console.warn(`⚠️ Not connected to SigSocket server, cannot approve request: ${requestId}`);
|
||||||
|
throw new Error('Not connected to SigSocket server');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we can approve this request
|
||||||
|
const canApprove = await this.canApproveRequest(requestId);
|
||||||
|
if (!canApprove) {
|
||||||
|
console.warn(`⚠️ Cannot approve request ${requestId} - keyspace may be locked or request not found`);
|
||||||
|
throw new Error('Cannot approve request - keyspace may be locked or request not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Approving request: ${requestId}`);
|
||||||
|
|
||||||
|
// WASM handles all validation, signing, and server communication
|
||||||
|
await this.wasmModule.SigSocketManager.approve_request(requestId);
|
||||||
|
|
||||||
|
console.log(`🎉 Request approved successfully: ${requestId}`);
|
||||||
|
|
||||||
|
// Clean flow: Approve -> Remove from storage -> Update UI
|
||||||
|
// 1. Remove from persistent storage (WASM already removed it)
|
||||||
|
await this.persistPendingRequests();
|
||||||
|
|
||||||
|
// 2. Update badge count
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
// 3. Notify popup of updated state
|
||||||
|
this.notifyPopupOfRequestUpdate();
|
||||||
|
|
||||||
|
console.log(`✅ Request ${requestId} approved and cleaned up`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to approve request ${requestId}:`, error);
|
||||||
|
|
||||||
|
// Check if this is a connection-related error
|
||||||
|
if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) {
|
||||||
|
console.error(`🔑 Connection/public key error during approval. Current state:`, {
|
||||||
|
connected: this.isConnected,
|
||||||
|
workspace: this.currentWorkspace,
|
||||||
|
publicKey: this.connectedPublicKey?.substring(0, 16) + '...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to reconnect for next time
|
||||||
|
if (this.currentWorkspace) {
|
||||||
|
console.log(`🔄 Attempting to reconnect to workspace: ${this.currentWorkspace}`);
|
||||||
|
setTimeout(() => this.connectToServer(this.currentWorkspace), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a sign request using WASM APIs
|
||||||
|
* @param {string} requestId - Request to reject
|
||||||
|
* @param {string} reason - Reason for rejection
|
||||||
|
* @returns {Promise<boolean>} - True if rejected successfully
|
||||||
|
*/
|
||||||
|
async rejectSignRequest(requestId, reason = 'User rejected') {
|
||||||
|
try {
|
||||||
|
if (!this.wasmModule?.SigSocketManager) {
|
||||||
|
throw new Error('WASM SigSocketManager not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`❌ Rejecting request: ${requestId}, reason: ${reason}`);
|
||||||
|
|
||||||
|
// WASM handles rejection and server communication
|
||||||
|
await this.wasmModule.SigSocketManager.reject_request(requestId, reason);
|
||||||
|
|
||||||
|
console.log(`✅ Request rejected successfully: ${requestId}`);
|
||||||
|
|
||||||
|
// Clean flow: Reject -> Remove from storage -> Update UI
|
||||||
|
// 1. Remove from persistent storage (WASM already removed it)
|
||||||
|
await this.persistPendingRequests();
|
||||||
|
|
||||||
|
// 2. Update badge count
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
// 3. Notify popup of updated state
|
||||||
|
this.notifyPopupOfRequestUpdate();
|
||||||
|
|
||||||
|
console.log(`✅ Request ${requestId} rejected and cleaned up`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to reject request ${requestId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending requests from WASM (filtered by current workspace)
|
||||||
|
* @returns {Promise<Array>} - Array of pending requests for current workspace
|
||||||
|
*/
|
||||||
|
async getPendingRequests() {
|
||||||
|
return this.getFilteredRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filtered requests from WASM (workspace-aware)
|
||||||
|
* @returns {Promise<Array>} - Array of filtered requests
|
||||||
|
*/
|
||||||
|
async getFilteredRequests() {
|
||||||
|
try {
|
||||||
|
if (!this.wasmModule?.SigSocketManager) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestsJson = await this.wasmModule.SigSocketManager.get_filtered_requests();
|
||||||
|
const requests = JSON.parse(requestsJson);
|
||||||
|
|
||||||
|
console.log(`📋 Retrieved ${requests.length} filtered requests for current workspace`);
|
||||||
|
return requests;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get filtered requests:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a request can be approved (keyspace validation)
|
||||||
|
* @param {string} requestId - Request ID to check
|
||||||
|
* @returns {Promise<boolean>} - True if can be approved
|
||||||
|
*/
|
||||||
|
async canApproveRequest(requestId) {
|
||||||
|
try {
|
||||||
|
if (!this.wasmModule?.SigSocketManager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.wasmModule.SigSocketManager.can_approve_request(requestId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check request approval status:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show clickable notification for new sign request
|
||||||
|
* Call this AFTER the request has been stored and persisted
|
||||||
|
*/
|
||||||
|
async showSignRequestNotification() {
|
||||||
|
try {
|
||||||
|
if (chrome.notifications && chrome.notifications.create) {
|
||||||
|
// Small delay to ensure request is fully stored
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
console.log(`📢 Preparing notification for new signature request`);
|
||||||
|
|
||||||
|
// Check if keyspace is currently unlocked to customize message
|
||||||
|
let message = 'New signature request received. Click to review and approve.';
|
||||||
|
let title = 'SigSocket Sign Request';
|
||||||
|
|
||||||
|
// Try to determine if keyspace is locked
|
||||||
|
try {
|
||||||
|
const requests = await this.getPendingRequests();
|
||||||
|
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
|
||||||
|
if (!canApprove) {
|
||||||
|
message = 'New signature request received. Click to unlock keyspace and approve.';
|
||||||
|
title = 'SigSocket Request';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't check, use generic message
|
||||||
|
message = 'New signature request received. Click to open extension.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create clickable notification with unique ID
|
||||||
|
const notificationId = `sigsocket-request-${Date.now()}`;
|
||||||
|
|
||||||
|
const notificationOptions = {
|
||||||
|
type: 'basic',
|
||||||
|
iconUrl: 'icons/icon48.png',
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
requireInteraction: true // Keep notification visible until user interacts
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`📢 Creating notification: ${notificationId}`, notificationOptions);
|
||||||
|
|
||||||
|
chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error('❌ Failed to create notification:', chrome.runtime.lastError);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Notification created successfully: ${createdId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('📢 Notifications not available, skipping notification');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to show notification:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update extension badge with pending request count
|
||||||
|
*/
|
||||||
|
async updateBadge() {
|
||||||
|
try {
|
||||||
|
const requests = await this.getPendingRequests();
|
||||||
|
const count = requests.length;
|
||||||
|
const badgeText = count > 0 ? count.toString() : '';
|
||||||
|
|
||||||
|
console.log(`🔢 Updating badge: ${count} pending requests`);
|
||||||
|
|
||||||
|
chrome.action.setBadgeText({ text: badgeText });
|
||||||
|
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update badge:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify popup about new request
|
||||||
|
*/
|
||||||
|
async notifyPopupOfNewRequest() {
|
||||||
|
if (!this.popupPort) {
|
||||||
|
console.log('No popup connected, skipping notification');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requests = await this.getPendingRequests();
|
||||||
|
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
|
||||||
|
|
||||||
|
this.popupPort.postMessage({
|
||||||
|
type: 'NEW_SIGN_REQUEST',
|
||||||
|
canApprove,
|
||||||
|
pendingRequests: requests
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📤 Notified popup: ${requests.length} requests, canApprove: ${canApprove}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to notify popup:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify popup about request updates
|
||||||
|
*/
|
||||||
|
async notifyPopupOfRequestUpdate() {
|
||||||
|
if (!this.popupPort) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requests = await this.getPendingRequests();
|
||||||
|
|
||||||
|
this.popupPort.postMessage({
|
||||||
|
type: 'REQUESTS_UPDATED',
|
||||||
|
pendingRequests: requests
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to notify popup of update:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from SigSocket server
|
||||||
|
* WASM handles all disconnection logic
|
||||||
|
*/
|
||||||
|
async disconnect() {
|
||||||
|
try {
|
||||||
|
if (this.wasmModule?.SigSocketManager) {
|
||||||
|
await this.wasmModule.SigSocketManager.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear local state
|
||||||
|
this.isConnected = false;
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
this.connectedPublicKey = null;
|
||||||
|
this.lastKnownConnectionState = false;
|
||||||
|
|
||||||
|
// Stop status monitoring
|
||||||
|
this.stopStatusMonitoring();
|
||||||
|
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
console.log('🔌 SigSocket disconnection requested');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to disconnect:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear persisted pending requests from storage
|
||||||
|
*/
|
||||||
|
async clearPersistedRequests() {
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.remove(['sigSocketPendingRequests']);
|
||||||
|
console.log('🗑️ Cleared persisted pending requests from storage');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to clear persisted requests:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean workspace switch - clear current workspace requests only
|
||||||
|
*/
|
||||||
|
async cleanWorkspaceSwitch(newWorkspace) {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`);
|
||||||
|
|
||||||
|
// 1. Persist current workspace requests before switching
|
||||||
|
if (this.currentWorkspace && this.isConnected) {
|
||||||
|
await this.persistPendingRequests();
|
||||||
|
console.log(`💾 Saved requests for workspace: ${this.currentWorkspace}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Clear WASM state (will be restored for new workspace)
|
||||||
|
if (this.wasmModule?.SigSocketManager) {
|
||||||
|
await this.wasmModule.SigSocketManager.clear_pending_requests();
|
||||||
|
console.log('🧹 Cleared WASM request state');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Reset local state
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
this.connectedPublicKey = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
|
||||||
|
console.log('✅ Workspace switch cleanup completed');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to clean workspace switch:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection status with real connection verification
|
||||||
|
* @returns {Promise<Object>} - Connection status information
|
||||||
|
*/
|
||||||
|
async getStatus() {
|
||||||
|
try {
|
||||||
|
if (!this.wasmModule?.SigSocketManager) {
|
||||||
|
return {
|
||||||
|
isConnected: false,
|
||||||
|
workspace: null,
|
||||||
|
publicKey: null,
|
||||||
|
pendingRequestCount: 0,
|
||||||
|
serverUrl: this.defaultServerUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get WASM status first
|
||||||
|
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
|
||||||
|
const status = JSON.parse(statusJson);
|
||||||
|
|
||||||
|
// Verify connection by trying to get requests (this will fail if not connected)
|
||||||
|
let actuallyConnected = false;
|
||||||
|
let requests = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
requests = await this.getPendingRequests();
|
||||||
|
// If we can get requests and WASM says connected, we're probably connected
|
||||||
|
actuallyConnected = status.is_connected && Array.isArray(requests);
|
||||||
|
} catch (error) {
|
||||||
|
// If getting requests fails, we're definitely not connected
|
||||||
|
console.warn('Connection verification failed:', error);
|
||||||
|
actuallyConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update our internal state
|
||||||
|
this.isConnected = actuallyConnected;
|
||||||
|
|
||||||
|
if (status.connected_public_key && actuallyConnected) {
|
||||||
|
this.connectedPublicKey = status.connected_public_key;
|
||||||
|
} else {
|
||||||
|
this.connectedPublicKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're disconnected, clear our workspace
|
||||||
|
if (!actuallyConnected) {
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusResult = {
|
||||||
|
isConnected: actuallyConnected,
|
||||||
|
workspace: this.currentWorkspace,
|
||||||
|
publicKey: status.connected_public_key,
|
||||||
|
pendingRequestCount: requests.length,
|
||||||
|
serverUrl: this.defaultServerUrl,
|
||||||
|
// Clean flow status indicators
|
||||||
|
cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📊 Clean flow status:', {
|
||||||
|
connected: statusResult.isConnected,
|
||||||
|
workspace: statusResult.workspace,
|
||||||
|
requestCount: statusResult.pendingRequestCount,
|
||||||
|
flowReady: statusResult.cleanFlowReady
|
||||||
|
});
|
||||||
|
|
||||||
|
return statusResult;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get status:', error);
|
||||||
|
// Clear state on error
|
||||||
|
this.isConnected = false;
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
this.connectedPublicKey = null;
|
||||||
|
return {
|
||||||
|
isConnected: false,
|
||||||
|
workspace: null,
|
||||||
|
publicKey: null,
|
||||||
|
pendingRequestCount: 0,
|
||||||
|
serverUrl: this.defaultServerUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the popup port for communication
|
||||||
|
* @param {chrome.runtime.Port|null} port - The popup port or null to disconnect
|
||||||
|
*/
|
||||||
|
setPopupPort(port) {
|
||||||
|
this.popupPort = port;
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
console.log('📱 Popup connected to SigSocket service');
|
||||||
|
|
||||||
|
// Immediately check connection status when popup opens
|
||||||
|
this.checkConnectionStatusNow();
|
||||||
|
|
||||||
|
// Start monitoring connection status when popup connects
|
||||||
|
this.startStatusMonitoring();
|
||||||
|
} else {
|
||||||
|
console.log('📱 Popup disconnected from SigSocket service');
|
||||||
|
// Stop monitoring when popup disconnects
|
||||||
|
this.stopStatusMonitoring();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately check and update connection status
|
||||||
|
*/
|
||||||
|
async checkConnectionStatusNow() {
|
||||||
|
try {
|
||||||
|
// Force a fresh connection check
|
||||||
|
const currentStatus = await this.getStatusWithConnectionTest();
|
||||||
|
this.lastKnownConnectionState = currentStatus.isConnected;
|
||||||
|
|
||||||
|
// Notify popup of current status
|
||||||
|
this.notifyPopupOfStatusChange(currentStatus);
|
||||||
|
|
||||||
|
console.log(`🔍 Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to check connection status immediately:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status with additional connection testing
|
||||||
|
*/
|
||||||
|
async getStatusWithConnectionTest() {
|
||||||
|
const status = await this.getStatus();
|
||||||
|
|
||||||
|
// If WASM claims we're connected, do an additional verification
|
||||||
|
if (status.isConnected) {
|
||||||
|
try {
|
||||||
|
// Try to get connection status again - if this fails, we're not really connected
|
||||||
|
const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status();
|
||||||
|
const verifyStatus = JSON.parse(verifyJson);
|
||||||
|
|
||||||
|
if (!verifyStatus.is_connected) {
|
||||||
|
console.log('🔍 Connection verification failed - marking as disconnected');
|
||||||
|
status.isConnected = false;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('🔍 Connection test failed - marking as disconnected:', error.message);
|
||||||
|
status.isConnected = false;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.currentWorkspace = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start periodic status monitoring to detect connection changes
|
||||||
|
*/
|
||||||
|
startStatusMonitoring() {
|
||||||
|
// Clear any existing monitoring
|
||||||
|
if (this.statusMonitorInterval) {
|
||||||
|
clearInterval(this.statusMonitorInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status every 2 seconds when popup is open (more responsive)
|
||||||
|
this.statusMonitorInterval = setInterval(async () => {
|
||||||
|
if (this.popupPort) {
|
||||||
|
try {
|
||||||
|
const currentStatus = await this.getStatusWithConnectionTest();
|
||||||
|
|
||||||
|
// Check if connection status changed
|
||||||
|
if (currentStatus.isConnected !== this.lastKnownConnectionState) {
|
||||||
|
console.log(`🔄 Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`);
|
||||||
|
this.lastKnownConnectionState = currentStatus.isConnected;
|
||||||
|
|
||||||
|
// Notify popup of status change
|
||||||
|
this.notifyPopupOfStatusChange(currentStatus);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Status monitoring error:', error);
|
||||||
|
// On error, assume disconnected
|
||||||
|
if (this.lastKnownConnectionState !== false) {
|
||||||
|
console.log('🔄 Status monitoring error - marking as disconnected');
|
||||||
|
this.lastKnownConnectionState = false;
|
||||||
|
this.notifyPopupOfStatusChange({
|
||||||
|
isConnected: false,
|
||||||
|
workspace: null,
|
||||||
|
publicKey: null,
|
||||||
|
pendingRequestCount: 0,
|
||||||
|
serverUrl: this.defaultServerUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stop monitoring when popup is closed
|
||||||
|
this.stopStatusMonitoring();
|
||||||
|
}
|
||||||
|
}, 2000); // 2 seconds for better responsiveness
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop status monitoring
|
||||||
|
*/
|
||||||
|
stopStatusMonitoring() {
|
||||||
|
if (this.statusMonitorInterval) {
|
||||||
|
clearInterval(this.statusMonitorInterval);
|
||||||
|
this.statusMonitorInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify popup of connection status change
|
||||||
|
* @param {Object} status - Current connection status
|
||||||
|
*/
|
||||||
|
notifyPopupOfStatusChange(status) {
|
||||||
|
if (this.popupPort) {
|
||||||
|
this.popupPort.postMessage({
|
||||||
|
type: 'CONNECTION_STATUS_CHANGED',
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
console.log(`📡 Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when keyspace is unlocked - clean approach to show pending requests
|
||||||
|
*/
|
||||||
|
async onKeypaceUnlocked() {
|
||||||
|
try {
|
||||||
|
console.log('🔓 Keyspace unlocked - preparing to show pending requests');
|
||||||
|
|
||||||
|
// 1. Restore any persisted requests for this workspace
|
||||||
|
await this.restorePendingRequests();
|
||||||
|
|
||||||
|
// 2. Get current requests (includes restored + any new ones)
|
||||||
|
const requests = await this.getPendingRequests();
|
||||||
|
|
||||||
|
// 3. Check if we can approve requests (keyspace should be unlocked now)
|
||||||
|
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true;
|
||||||
|
|
||||||
|
// 4. Update badge with current count
|
||||||
|
this.updateBadge();
|
||||||
|
|
||||||
|
// 5. Notify popup if connected
|
||||||
|
if (this.popupPort) {
|
||||||
|
this.popupPort.postMessage({
|
||||||
|
type: 'KEYSPACE_UNLOCKED',
|
||||||
|
canApprove,
|
||||||
|
pendingRequests: requests
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔓 Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`);
|
||||||
|
|
||||||
|
return requests;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to handle keyspace unlock:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in background script
|
||||||
|
export default SigSocketService;
|
||||||
75
crypto_vault_extension/demo/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Mock SigSocket Server Demo
|
||||||
|
|
||||||
|
This directory contains a mock SigSocket server for testing the browser extension functionality.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the mock server:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will listen on `ws://localhost:8080/ws`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Interactive Commands
|
||||||
|
|
||||||
|
Once the server is running, you can use these commands:
|
||||||
|
|
||||||
|
- `test` - Send a test sign request to all connected clients
|
||||||
|
- `status` - Show server status and connected clients
|
||||||
|
- `quit` - Shutdown the server
|
||||||
|
|
||||||
|
### Testing Flow
|
||||||
|
|
||||||
|
1. Start the mock server
|
||||||
|
2. Load the browser extension in Chrome
|
||||||
|
3. Create a keyspace and keypair in the extension
|
||||||
|
4. The extension should automatically connect to the server
|
||||||
|
5. The server will send a test sign request after 3 seconds
|
||||||
|
6. Use the extension popup to approve or reject the request
|
||||||
|
7. The server will log the response and send another request after 10 seconds
|
||||||
|
|
||||||
|
### Expected Output
|
||||||
|
|
||||||
|
When a client connects:
|
||||||
|
```
|
||||||
|
New WebSocket connection from: ::1
|
||||||
|
Received message: 04a8b2c3d4e5f6...
|
||||||
|
Client registered: client_1234567890_abc123 with public key: 04a8b2c3d4e5f6...
|
||||||
|
📝 Sending sign request to client_1234567890_abc123: req_1_1234567890
|
||||||
|
Message: "Test message 1 - 2024-01-01T12:00:00.000Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
When a sign response is received:
|
||||||
|
```
|
||||||
|
Received sign response from client_1234567890_abc123: {
|
||||||
|
id: 'req_1_1234567890',
|
||||||
|
message: 'VGVzdCBtZXNzYWdlIDEgLSAyMDI0LTAxLTAxVDEyOjAwOjAwLjAwMFo=',
|
||||||
|
signature: '3045022100...'
|
||||||
|
}
|
||||||
|
✅ Sign request req_1_1234567890 completed successfully
|
||||||
|
Signature: 3045022100...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
|
||||||
|
The mock server implements a simplified version of the SigSocket protocol:
|
||||||
|
|
||||||
|
1. **Client Introduction**: Client sends hex-encoded public key
|
||||||
|
2. **Welcome Message**: Server responds with welcome JSON
|
||||||
|
3. **Sign Requests**: Server sends JSON with `id` and `message` (base64)
|
||||||
|
4. **Sign Responses**: Client sends JSON with `id`, `message`, and `signature`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Connection refused**: Make sure the server is running on port 8080
|
||||||
|
- **No sign requests**: Check that the extension is properly connected
|
||||||
|
- **Extension errors**: Check the browser console for JavaScript errors
|
||||||
|
- **WASM errors**: Ensure the WASM files are properly built and loaded
|
||||||
232
crypto_vault_extension/demo/mock_sigsocket_server.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock SigSocket Server for Testing Browser Extension
|
||||||
|
*
|
||||||
|
* This is a simple WebSocket server that simulates the SigSocket protocol
|
||||||
|
* for testing the browser extension functionality.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node mock_sigsocket_server.js
|
||||||
|
*
|
||||||
|
* The server will listen on ws://localhost:8080/ws
|
||||||
|
*/
|
||||||
|
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
class MockSigSocketServer {
|
||||||
|
constructor(port = 8080) {
|
||||||
|
this.port = port;
|
||||||
|
this.clients = new Map(); // clientId -> { ws, publicKey }
|
||||||
|
this.requestCounter = 0;
|
||||||
|
|
||||||
|
this.setupServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupServer() {
|
||||||
|
// Create HTTP server
|
||||||
|
this.httpServer = http.createServer();
|
||||||
|
|
||||||
|
// Create WebSocket server
|
||||||
|
this.wss = new WebSocket.Server({
|
||||||
|
server: this.httpServer,
|
||||||
|
path: '/ws'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wss.on('connection', (ws, req) => {
|
||||||
|
console.log('New WebSocket connection from:', req.socket.remoteAddress);
|
||||||
|
this.handleConnection(ws);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.httpServer.listen(this.port, () => {
|
||||||
|
console.log(`Mock SigSocket Server listening on ws://localhost:${this.port}/ws`);
|
||||||
|
console.log('Waiting for browser extension connections...');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnection(ws) {
|
||||||
|
let clientId = null;
|
||||||
|
let publicKey = null;
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
try {
|
||||||
|
const message = data.toString();
|
||||||
|
console.log('Received message:', message);
|
||||||
|
|
||||||
|
// Check if this is a client introduction (hex-encoded public key)
|
||||||
|
if (!clientId && this.isHexString(message)) {
|
||||||
|
publicKey = message;
|
||||||
|
clientId = this.generateClientId();
|
||||||
|
|
||||||
|
this.clients.set(clientId, { ws, publicKey });
|
||||||
|
|
||||||
|
console.log(`Client registered: ${clientId} with public key: ${publicKey.substring(0, 16)}...`);
|
||||||
|
|
||||||
|
// Send welcome message
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'welcome',
|
||||||
|
clientId: clientId,
|
||||||
|
message: 'Connected to Mock SigSocket Server'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Schedule a test sign request after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendTestSignRequest(clientId);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as JSON (sign response)
|
||||||
|
try {
|
||||||
|
const jsonMessage = JSON.parse(message);
|
||||||
|
this.handleSignResponse(clientId, jsonMessage);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Received non-JSON message:', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling message:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
if (clientId) {
|
||||||
|
this.clients.delete(clientId);
|
||||||
|
console.log(`Client disconnected: ${clientId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSignResponse(clientId, response) {
|
||||||
|
console.log(`Received sign response from ${clientId}:`, response);
|
||||||
|
|
||||||
|
if (response.id && response.signature) {
|
||||||
|
console.log(`✅ Sign request ${response.id} completed successfully`);
|
||||||
|
console.log(` Signature: ${response.signature.substring(0, 32)}...`);
|
||||||
|
|
||||||
|
// Send another test request after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendTestSignRequest(clientId);
|
||||||
|
}, 10000);
|
||||||
|
} else {
|
||||||
|
console.log('❌ Invalid sign response format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTestSignRequest(clientId) {
|
||||||
|
const client = this.clients.get(clientId);
|
||||||
|
if (!client) {
|
||||||
|
console.log(`Client ${clientId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestCounter++;
|
||||||
|
const requestId = `req_${this.requestCounter}_${Date.now()}`;
|
||||||
|
const testMessage = `Test message ${this.requestCounter} - ${new Date().toISOString()}`;
|
||||||
|
const messageBase64 = Buffer.from(testMessage).toString('base64');
|
||||||
|
|
||||||
|
const signRequest = {
|
||||||
|
id: requestId,
|
||||||
|
message: messageBase64
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`📝 Sending sign request to ${clientId}:`, requestId);
|
||||||
|
console.log(` Message: "${testMessage}"`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.ws.send(JSON.stringify(signRequest));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to send sign request to ${clientId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHexString(str) {
|
||||||
|
return /^[0-9a-fA-F]+$/.test(str) && str.length >= 32; // At least 16 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
generateClientId() {
|
||||||
|
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a test request to all connected clients
|
||||||
|
broadcastTestRequest() {
|
||||||
|
console.log('\n📢 Broadcasting test sign request to all clients...');
|
||||||
|
for (const [clientId] of this.clients) {
|
||||||
|
this.sendTestSignRequest(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get server status
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
port: this.port,
|
||||||
|
connectedClients: this.clients.size,
|
||||||
|
clients: Array.from(this.clients.keys())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and start the server
|
||||||
|
const server = new MockSigSocketServer();
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n🛑 Shutting down Mock SigSocket Server...');
|
||||||
|
server.httpServer.close(() => {
|
||||||
|
console.log('Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add some interactive commands
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
console.log('\n📋 Available commands:');
|
||||||
|
console.log(' "test" - Send test sign request to all clients');
|
||||||
|
console.log(' "status" - Show server status');
|
||||||
|
console.log(' "quit" - Shutdown server');
|
||||||
|
console.log(' Type a command and press Enter\n');
|
||||||
|
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
const chunk = process.stdin.read();
|
||||||
|
if (chunk !== null) {
|
||||||
|
const command = chunk.trim().toLowerCase();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'test':
|
||||||
|
server.broadcastTestRequest();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
const status = server.getStatus();
|
||||||
|
console.log('\n📊 Server Status:');
|
||||||
|
console.log(` Port: ${status.port}`);
|
||||||
|
console.log(` Connected clients: ${status.connectedClients}`);
|
||||||
|
if (status.clients.length > 0) {
|
||||||
|
console.log(` Client IDs: ${status.clients.join(', ')}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'quit':
|
||||||
|
case 'exit':
|
||||||
|
process.emit('SIGINT');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '':
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`Unknown command: ${command}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
module.exports = MockSigSocketServer;
|
||||||
21
crypto_vault_extension/demo/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "mock-sigsocket-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Mock SigSocket server for testing browser extension",
|
||||||
|
"main": "mock_sigsocket_server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node mock_sigsocket_server.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.14.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"websocket",
|
||||||
|
"sigsocket",
|
||||||
|
"testing",
|
||||||
|
"mock"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 676 B |
BIN
crypto_vault_extension/icons/icon32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
crypto_vault_extension/icons/icon36.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -1,5 +1,3 @@
|
|||||||
// Enhanced Error Handling System for CryptoVault Extension
|
|
||||||
|
|
||||||
class CryptoVaultError extends Error {
|
class CryptoVaultError extends Error {
|
||||||
constructor(message, code, retryable = false, userMessage = null) {
|
constructor(message, code, retryable = false, userMessage = null) {
|
||||||
super(message);
|
super(message);
|
||||||
@@ -11,35 +9,24 @@ class CryptoVaultError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error codes for different types of errors
|
|
||||||
const ERROR_CODES = {
|
const ERROR_CODES = {
|
||||||
// Network/Connection errors (retryable)
|
|
||||||
NETWORK_ERROR: 'NETWORK_ERROR',
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||||
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
||||||
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
||||||
|
|
||||||
// Authentication errors (not retryable)
|
|
||||||
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
||||||
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
||||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||||
|
|
||||||
// Crypto errors (not retryable)
|
|
||||||
CRYPTO_ERROR: 'CRYPTO_ERROR',
|
CRYPTO_ERROR: 'CRYPTO_ERROR',
|
||||||
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
||||||
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
|
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
|
||||||
|
|
||||||
// Validation errors (not retryable)
|
|
||||||
INVALID_INPUT: 'INVALID_INPUT',
|
INVALID_INPUT: 'INVALID_INPUT',
|
||||||
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
|
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
|
||||||
INVALID_FORMAT: 'INVALID_FORMAT',
|
INVALID_FORMAT: 'INVALID_FORMAT',
|
||||||
|
|
||||||
// System errors (sometimes retryable)
|
|
||||||
WASM_ERROR: 'WASM_ERROR',
|
WASM_ERROR: 'WASM_ERROR',
|
||||||
STORAGE_ERROR: 'STORAGE_ERROR',
|
STORAGE_ERROR: 'STORAGE_ERROR',
|
||||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
// User-friendly error messages
|
|
||||||
const ERROR_MESSAGES = {
|
const ERROR_MESSAGES = {
|
||||||
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
|
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
|
||||||
[ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.',
|
[ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.',
|
||||||
@@ -62,7 +49,6 @@ const ERROR_MESSAGES = {
|
|||||||
[ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.'
|
[ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if an error is retryable
|
|
||||||
const RETRYABLE_ERRORS = new Set([
|
const RETRYABLE_ERRORS = new Set([
|
||||||
ERROR_CODES.NETWORK_ERROR,
|
ERROR_CODES.NETWORK_ERROR,
|
||||||
ERROR_CODES.TIMEOUT_ERROR,
|
ERROR_CODES.TIMEOUT_ERROR,
|
||||||
@@ -71,11 +57,9 @@ const RETRYABLE_ERRORS = new Set([
|
|||||||
ERROR_CODES.STORAGE_ERROR
|
ERROR_CODES.STORAGE_ERROR
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Enhanced error classification
|
|
||||||
function classifyError(error) {
|
function classifyError(error) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
|
||||||
// Network/Connection errors
|
|
||||||
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
|
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -85,7 +69,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication errors
|
|
||||||
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
|
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -104,7 +87,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crypto errors
|
|
||||||
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
|
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -123,7 +105,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation errors
|
|
||||||
if (errorMessage.includes('No keypair selected')) {
|
if (errorMessage.includes('No keypair selected')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -133,7 +114,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM errors
|
|
||||||
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
|
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -143,7 +123,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to unknown error
|
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
ERROR_CODES.UNKNOWN_ERROR,
|
ERROR_CODES.UNKNOWN_ERROR,
|
||||||
@@ -152,7 +131,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get error message from various error types
|
|
||||||
function getErrorMessage(error) {
|
function getErrorMessage(error) {
|
||||||
if (!error) return 'Unknown error';
|
if (!error) return 'Unknown error';
|
||||||
|
|
||||||
@@ -179,14 +157,13 @@ function getErrorMessage(error) {
|
|||||||
return stringified;
|
return stringified;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore JSON stringify errors
|
// Silently handle JSON stringify errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Unknown error';
|
return 'Unknown error';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry logic with exponential backoff
|
|
||||||
async function withRetry(operation, options = {}) {
|
async function withRetry(operation, options = {}) {
|
||||||
const {
|
const {
|
||||||
maxRetries = 3,
|
maxRetries = 3,
|
||||||
@@ -205,20 +182,16 @@ async function withRetry(operation, options = {}) {
|
|||||||
const classifiedError = classifyError(error);
|
const classifiedError = classifyError(error);
|
||||||
lastError = classifiedError;
|
lastError = classifiedError;
|
||||||
|
|
||||||
// Don't retry if it's the last attempt or error is not retryable
|
|
||||||
if (attempt === maxRetries || !classifiedError.retryable) {
|
if (attempt === maxRetries || !classifiedError.retryable) {
|
||||||
throw classifiedError;
|
throw classifiedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate delay with exponential backoff
|
|
||||||
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
||||||
|
|
||||||
// Call retry callback if provided
|
|
||||||
if (onRetry) {
|
if (onRetry) {
|
||||||
onRetry(attempt + 1, delay, classifiedError);
|
onRetry(attempt + 1, delay, classifiedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait before retrying
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +199,6 @@ async function withRetry(operation, options = {}) {
|
|||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced operation wrapper with loading states
|
|
||||||
async function executeOperation(operation, options = {}) {
|
async function executeOperation(operation, options = {}) {
|
||||||
const {
|
const {
|
||||||
loadingElement = null,
|
loadingElement = null,
|
||||||
@@ -235,7 +207,6 @@ async function executeOperation(operation, options = {}) {
|
|||||||
onProgress = null
|
onProgress = null
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
setButtonLoading(loadingElement, true);
|
setButtonLoading(loadingElement, true);
|
||||||
}
|
}
|
||||||
@@ -253,25 +224,21 @@ async function executeOperation(operation, options = {}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show success message if provided
|
|
||||||
if (successMessage) {
|
if (successMessage) {
|
||||||
showToast(successMessage, 'success');
|
showToast(successMessage, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Show user-friendly error message
|
|
||||||
showToast(error.userMessage || error.message, 'error');
|
showToast(error.userMessage || error.message, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Hide loading state
|
|
||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
setButtonLoading(loadingElement, false);
|
setButtonLoading(loadingElement, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export for use in other modules
|
|
||||||
window.CryptoVaultError = CryptoVaultError;
|
window.CryptoVaultError = CryptoVaultError;
|
||||||
window.ERROR_CODES = ERROR_CODES;
|
window.ERROR_CODES = ERROR_CODES;
|
||||||
window.classifyError = classifyError;
|
window.classifyError = classifyError;
|
||||||
|
|||||||
@@ -6,20 +6,27 @@
|
|||||||
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"activeTab"
|
"activeTab",
|
||||||
|
"notifications"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
},
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js",
|
"service_worker": "background.js",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
"48": "icons/icon48.png",
|
"48": "icons/icon48.png",
|
||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,17 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="logo">
|
<div class="logo clickable-header" id="headerTitle">
|
||||||
<div class="logo-icon">🔐</div>
|
<div class="logo-icon">🔐</div>
|
||||||
<h1>CryptoVault</h1>
|
<h1>CryptoVault</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<button id="settingsBtn" class="btn-icon-only" title="Settings">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
|
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
@@ -44,10 +50,7 @@
|
|||||||
<!-- Status Section -->
|
<!-- Status Section -->
|
||||||
<div class="vault-status" id="vaultStatus">
|
<div class="vault-status" id="vaultStatus">
|
||||||
<div class="status-indicator" id="statusIndicator">
|
<div class="status-indicator" id="statusIndicator">
|
||||||
<div class="status-content">
|
<span id="statusText"></span>
|
||||||
<div class="status-dot"></div>
|
|
||||||
<span id="statusText">Initializing...</span>
|
|
||||||
</div>
|
|
||||||
<button id="lockBtn" class="btn btn-ghost btn-small hidden">
|
<button id="lockBtn" class="btn btn-ghost btn-small hidden">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
@@ -59,6 +62,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SigSocket Requests Section -->
|
||||||
|
<div class="card sigsocket-section" id="sigSocketSection">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>🔌 SigSocket Requests</h3>
|
||||||
|
<div class="connection-status" id="connectionStatus">
|
||||||
|
<span class="status-dot" id="connectionDot"></span>
|
||||||
|
<span id="connectionText">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="requests-container" id="requestsContainer">
|
||||||
|
<div class="loading-requests hidden" id="loadingRequestsMessage">
|
||||||
|
<div class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>Loading requests...</p>
|
||||||
|
<small>Fetching pending signature requests</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="no-requests" id="noRequestsMessage">
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">📝</div>
|
||||||
|
<p>No pending sign requests</p>
|
||||||
|
<small>Requests will appear here when received from SigSocket server</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="requests-list hidden" id="requestsList">
|
||||||
|
<!-- Requests will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sigsocket-actions">
|
||||||
|
<button id="refreshRequestsBtn" class="btn btn-ghost btn-small">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
|
<polyline points="1 20 1 14 7 14"></polyline>
|
||||||
|
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||||
|
</svg>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vault-header">
|
<div class="vault-header">
|
||||||
<h2>Your Keypairs</h2>
|
<h2>Your Keypairs</h2>
|
||||||
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
||||||
@@ -177,8 +224,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<section class="section hidden" id="settingsSection">
|
||||||
|
<div class="settings-header">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Settings -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>Session Settings</h3>
|
||||||
|
<div class="settings-item">
|
||||||
|
<label for="timeoutInput">Session Timeout</label>
|
||||||
|
<div class="timeout-input-group">
|
||||||
|
<input type="number" id="timeoutInput" min="3" max="300" value="15">
|
||||||
|
<span>seconds</span>
|
||||||
|
</div>
|
||||||
|
<small class="settings-help">Automatically lock session after inactivity</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SigSocket Settings -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>SigSocket Settings</h3>
|
||||||
|
<div class="settings-item">
|
||||||
|
<label for="serverUrlInput">Server URL</label>
|
||||||
|
<div class="server-input-group">
|
||||||
|
<input type="text" id="serverUrlInput" placeholder="ws://localhost:8080/ws" value="ws://localhost:8080/ws">
|
||||||
|
<button id="saveServerUrlBtn" class="btn btn-small btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
<small class="settings-help">WebSocket URL for SigSocket server (ws:// or wss://)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
443
crypto_vault_extension/popup/components/SignRequestManager.js
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
/**
|
||||||
|
* Sign Request Manager Component
|
||||||
|
*
|
||||||
|
* Handles the display and management of SigSocket sign requests in the popup.
|
||||||
|
* Manages different UI states:
|
||||||
|
* 1. Keyspace locked: Show unlock form
|
||||||
|
* 2. Wrong keyspace: Show mismatch message
|
||||||
|
* 3. Correct keyspace: Show approval UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SignRequestManager {
|
||||||
|
constructor() {
|
||||||
|
this.pendingRequests = [];
|
||||||
|
this.isKeypaceUnlocked = false;
|
||||||
|
this.keypaceMatch = false;
|
||||||
|
this.connectionStatus = { isConnected: false };
|
||||||
|
|
||||||
|
this.container = null;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component
|
||||||
|
* @param {HTMLElement} container - Container element to render into
|
||||||
|
*/
|
||||||
|
async initialize(container) {
|
||||||
|
this.container = container;
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Load initial state
|
||||||
|
await this.loadState();
|
||||||
|
|
||||||
|
// Render initial UI
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Listen for background messages
|
||||||
|
this.setupBackgroundListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load current state from background script
|
||||||
|
*/
|
||||||
|
async loadState() {
|
||||||
|
try {
|
||||||
|
// Check if keyspace is unlocked
|
||||||
|
const unlockedResponse = await this.sendMessage('isUnlocked');
|
||||||
|
this.isKeypaceUnlocked = unlockedResponse?.unlocked || false;
|
||||||
|
|
||||||
|
// Get pending requests
|
||||||
|
const requestsResponse = await this.sendMessage('getPendingRequests');
|
||||||
|
this.pendingRequests = requestsResponse?.requests || [];
|
||||||
|
|
||||||
|
// Get SigSocket status
|
||||||
|
const statusResponse = await this.sendMessage('getSigSocketStatus');
|
||||||
|
this.connectionStatus = statusResponse?.status || { isConnected: false };
|
||||||
|
|
||||||
|
// If keyspace is unlocked, notify background to check keyspace match
|
||||||
|
if (this.isKeypaceUnlocked) {
|
||||||
|
await this.sendMessage('keypaceUnlocked');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load sign request state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the component UI
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const hasRequests = this.pendingRequests.length > 0;
|
||||||
|
|
||||||
|
if (!hasRequests) {
|
||||||
|
this.renderNoRequests();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isKeypaceUnlocked) {
|
||||||
|
this.renderUnlockPrompt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.keypaceMatch) {
|
||||||
|
this.renderKeypaceMismatch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderApprovalUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render no requests state
|
||||||
|
*/
|
||||||
|
renderNoRequests() {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="sign-request-manager">
|
||||||
|
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
|
||||||
|
</div>
|
||||||
|
<div class="no-requests">
|
||||||
|
<p>No pending sign requests</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render unlock prompt
|
||||||
|
*/
|
||||||
|
renderUnlockPrompt() {
|
||||||
|
const requestCount = this.pendingRequests.length;
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="sign-request-manager">
|
||||||
|
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
|
||||||
|
</div>
|
||||||
|
<div class="unlock-prompt">
|
||||||
|
<h3>🔒 Unlock Keyspace</h3>
|
||||||
|
<p>Unlock your keyspace to see ${requestCount} pending sign request${requestCount !== 1 ? 's' : ''}.</p>
|
||||||
|
<p class="hint">Use the login form above to unlock your keyspace.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render keyspace mismatch message
|
||||||
|
*/
|
||||||
|
renderKeypaceMismatch() {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="sign-request-manager">
|
||||||
|
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
|
||||||
|
</div>
|
||||||
|
<div class="keyspace-mismatch">
|
||||||
|
<h3>⚠️ Wrong Keyspace</h3>
|
||||||
|
<p>The unlocked keyspace doesn't match the connected SigSocket session.</p>
|
||||||
|
<p class="hint">Please unlock the correct keyspace to approve sign requests.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render approval UI with pending requests
|
||||||
|
*/
|
||||||
|
renderApprovalUI() {
|
||||||
|
const requestsHtml = this.pendingRequests.map(request => this.renderSignRequestCard(request)).join('');
|
||||||
|
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="sign-request-manager">
|
||||||
|
<div class="connection-status connected">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
SigSocket: Connected
|
||||||
|
</div>
|
||||||
|
<div class="requests-header">
|
||||||
|
<h3>📝 Sign Requests (${this.pendingRequests.length})</h3>
|
||||||
|
</div>
|
||||||
|
<div class="requests-list">
|
||||||
|
${requestsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render individual sign request card
|
||||||
|
* @param {Object} request - Sign request data
|
||||||
|
* @returns {string} - HTML string for the request card
|
||||||
|
*/
|
||||||
|
renderSignRequestCard(request) {
|
||||||
|
const timestamp = new Date(request.timestamp).toLocaleTimeString();
|
||||||
|
const messagePreview = this.getMessagePreview(request.message);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="sign-request-card" data-request-id="${request.id}">
|
||||||
|
<div class="request-header">
|
||||||
|
<div class="request-id">Request: ${request.id.substring(0, 8)}...</div>
|
||||||
|
<div class="request-time">${timestamp}</div>
|
||||||
|
</div>
|
||||||
|
<div class="request-message">
|
||||||
|
<label>Message:</label>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-preview">${messagePreview}</div>
|
||||||
|
<button class="expand-message" data-request-id="${request.id}">
|
||||||
|
<span class="expand-text">Show Full</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="request-actions">
|
||||||
|
<button class="btn-reject" data-request-id="${request.id}">
|
||||||
|
❌ Reject
|
||||||
|
</button>
|
||||||
|
<button class="btn-approve" data-request-id="${request.id}">
|
||||||
|
✅ Approve & Sign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a preview of the message content
|
||||||
|
* @param {string} messageBase64 - Base64 encoded message
|
||||||
|
* @returns {string} - Preview text
|
||||||
|
*/
|
||||||
|
getMessagePreview(messageBase64) {
|
||||||
|
try {
|
||||||
|
const decoded = atob(messageBase64);
|
||||||
|
const preview = decoded.length > 50 ? decoded.substring(0, 50) + '...' : decoded;
|
||||||
|
return preview;
|
||||||
|
} catch (error) {
|
||||||
|
return `Base64: ${messageBase64.substring(0, 20)}...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up event listeners
|
||||||
|
*/
|
||||||
|
setupEventListeners() {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
// Use event delegation for dynamic content
|
||||||
|
this.container.addEventListener('click', (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
|
||||||
|
if (target.classList.contains('btn-approve')) {
|
||||||
|
const requestId = target.getAttribute('data-request-id');
|
||||||
|
this.approveRequest(requestId);
|
||||||
|
} else if (target.classList.contains('btn-reject')) {
|
||||||
|
const requestId = target.getAttribute('data-request-id');
|
||||||
|
this.rejectRequest(requestId);
|
||||||
|
} else if (target.classList.contains('expand-message')) {
|
||||||
|
const requestId = target.getAttribute('data-request-id');
|
||||||
|
this.toggleMessageExpansion(requestId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up listener for background script messages
|
||||||
|
*/
|
||||||
|
setupBackgroundListener() {
|
||||||
|
// Listen for keyspace unlock events
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.type === 'KEYSPACE_UNLOCKED') {
|
||||||
|
this.isKeypaceUnlocked = true;
|
||||||
|
this.keypaceMatch = message.keypaceMatches;
|
||||||
|
this.pendingRequests = message.pendingRequests || [];
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve a sign request
|
||||||
|
* @param {string} requestId - Request ID to approve
|
||||||
|
*/
|
||||||
|
async approveRequest(requestId) {
|
||||||
|
try {
|
||||||
|
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`);
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Signing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.sendMessage('approveSignRequest', { requestId });
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
// Remove the request from UI
|
||||||
|
this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId);
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
this.showToast('Sign request approved successfully!', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.error || 'Failed to approve request');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to approve request:', error);
|
||||||
|
this.showToast('Failed to approve request: ' + error.message, 'error');
|
||||||
|
|
||||||
|
// Re-enable button
|
||||||
|
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`);
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = '✅ Approve & Sign';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a sign request
|
||||||
|
* @param {string} requestId - Request ID to reject
|
||||||
|
*/
|
||||||
|
async rejectRequest(requestId) {
|
||||||
|
try {
|
||||||
|
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`);
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Rejecting...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.sendMessage('rejectSignRequest', {
|
||||||
|
requestId,
|
||||||
|
reason: 'User rejected'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
// Remove the request from UI
|
||||||
|
this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId);
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
this.showToast('Sign request rejected', 'info');
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.error || 'Failed to reject request');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reject request:', error);
|
||||||
|
this.showToast('Failed to reject request: ' + error.message, 'error');
|
||||||
|
|
||||||
|
// Re-enable button
|
||||||
|
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`);
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = '❌ Reject';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle message expansion
|
||||||
|
* @param {string} requestId - Request ID
|
||||||
|
*/
|
||||||
|
toggleMessageExpansion(requestId) {
|
||||||
|
const request = this.pendingRequests.find(r => r.id === requestId);
|
||||||
|
if (!request) return;
|
||||||
|
|
||||||
|
const card = this.container.querySelector(`[data-request-id="${requestId}"]`);
|
||||||
|
const messageContent = card.querySelector('.message-content');
|
||||||
|
const expandButton = card.querySelector('.expand-message');
|
||||||
|
|
||||||
|
const isExpanded = messageContent.classList.contains('expanded');
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
messageContent.classList.remove('expanded');
|
||||||
|
messageContent.querySelector('.message-preview').textContent = this.getMessagePreview(request.message);
|
||||||
|
expandButton.querySelector('.expand-text').textContent = 'Show Full';
|
||||||
|
} else {
|
||||||
|
messageContent.classList.add('expanded');
|
||||||
|
try {
|
||||||
|
const fullMessage = atob(request.message);
|
||||||
|
messageContent.querySelector('.message-preview').textContent = fullMessage;
|
||||||
|
} catch (error) {
|
||||||
|
messageContent.querySelector('.message-preview').textContent = `Base64: ${request.message}`;
|
||||||
|
}
|
||||||
|
expandButton.querySelector('.expand-text').textContent = 'Show Less';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to background script
|
||||||
|
* @param {string} action - Action to perform
|
||||||
|
* @param {Object} data - Additional data
|
||||||
|
* @returns {Promise<Object>} - Response from background script
|
||||||
|
*/
|
||||||
|
async sendMessage(action, data = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ action, ...data }, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show toast notification
|
||||||
|
* @param {string} message - Message to show
|
||||||
|
* @param {string} type - Toast type (success, error, info)
|
||||||
|
*/
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
// Use the existing toast system from popup.js
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(message, type);
|
||||||
|
} else {
|
||||||
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update component state
|
||||||
|
* @param {Object} newState - New state data
|
||||||
|
*/
|
||||||
|
updateState(newState) {
|
||||||
|
console.log('SignRequestManager.updateState called with:', newState);
|
||||||
|
console.log('Current state before update:', {
|
||||||
|
isKeypaceUnlocked: this.isKeypaceUnlocked,
|
||||||
|
keypaceMatch: this.keypaceMatch,
|
||||||
|
pendingRequests: this.pendingRequests.length
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(this, newState);
|
||||||
|
|
||||||
|
// Fix the property name mismatch
|
||||||
|
if (newState.keypaceMatches !== undefined) {
|
||||||
|
this.keypaceMatch = newState.keypaceMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('State after update:', {
|
||||||
|
isKeypaceUnlocked: this.isKeypaceUnlocked,
|
||||||
|
keypaceMatch: this.keypaceMatch,
|
||||||
|
pendingRequests: this.pendingRequests.length
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.initialized) {
|
||||||
|
console.log('Rendering SignRequestManager with new state');
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh component data
|
||||||
|
*/
|
||||||
|
async refresh() {
|
||||||
|
await this.loadState();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in popup
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = SignRequestManager;
|
||||||
|
} else {
|
||||||
|
window.SignRequestManager = SignRequestManager;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Enhanced CSS Variables for harmonious theming */
|
/* Balanced CSS Variables for harmonious theming */
|
||||||
:root {
|
:root {
|
||||||
/* Core color foundation - mathematically harmonious */
|
/* Core color foundation - mathematically harmonious */
|
||||||
--primary-hue: 235;
|
--primary-hue: 235;
|
||||||
@@ -6,27 +6,27 @@
|
|||||||
--secondary-hue: 200;
|
--secondary-hue: 200;
|
||||||
--accent-hue: 160;
|
--accent-hue: 160;
|
||||||
|
|
||||||
/* Light theme - clean and professional */
|
/* Light theme - softer, less stark */
|
||||||
--bg-primary: linear-gradient(135deg, hsl(var(--primary-hue), 15%, 96%) 0%, hsl(var(--secondary-hue), 20%, 94%) 100%);
|
--bg-primary: linear-gradient(135deg, hsl(var(--primary-hue), 12%, 92%) 0%, hsl(var(--secondary-hue), 15%, 90%) 100%);
|
||||||
--bg-secondary: rgba(255, 255, 255, 0.98);
|
--bg-secondary: rgba(255, 255, 255, 0.92);
|
||||||
--bg-card: rgba(255, 255, 255, 0.95);
|
--bg-card: rgba(255, 255, 255, 0.88);
|
||||||
--bg-input: hsl(var(--primary-hue), 20%, 98%);
|
--bg-input: hsl(var(--primary-hue), 15%, 94%);
|
||||||
--bg-button-primary: hsl(var(--primary-hue), var(--primary-saturation), 55%);
|
--bg-button-primary: hsl(var(--primary-hue), var(--primary-saturation), 55%);
|
||||||
--bg-button-secondary: hsl(var(--primary-hue), 15%, 92%);
|
--bg-button-secondary: hsl(var(--primary-hue), 12%, 88%);
|
||||||
--bg-button-ghost: transparent;
|
--bg-button-ghost: transparent;
|
||||||
|
|
||||||
--text-primary: hsl(var(--primary-hue), 25%, 15%);
|
--text-primary: hsl(var(--primary-hue), 20%, 20%);
|
||||||
--text-secondary: hsl(var(--primary-hue), 15%, 35%);
|
--text-secondary: hsl(var(--primary-hue), 12%, 40%);
|
||||||
--text-muted: hsl(var(--primary-hue), 10%, 55%);
|
--text-muted: hsl(var(--primary-hue), 8%, 58%);
|
||||||
--text-inverse: white;
|
--text-inverse: white;
|
||||||
--text-button-primary: white;
|
--text-button-primary: white;
|
||||||
--text-button-secondary: hsl(var(--primary-hue), 25%, 25%);
|
--text-button-secondary: hsl(var(--primary-hue), 20%, 30%);
|
||||||
|
|
||||||
--border-color: hsl(var(--primary-hue), 20%, 88%);
|
--border-color: hsl(var(--primary-hue), 15%, 82%);
|
||||||
--border-input: hsl(var(--primary-hue), 15%, 82%);
|
--border-input: hsl(var(--primary-hue), 12%, 78%);
|
||||||
--border-focus: hsl(var(--primary-hue), var(--primary-saturation), 55%);
|
--border-focus: hsl(var(--primary-hue), var(--primary-saturation), 55%);
|
||||||
|
|
||||||
--shadow-card: 0 4px 20px hsla(var(--primary-hue), 30%, 20%, 0.08);
|
--shadow-card: 0 4px 20px hsla(var(--primary-hue), 25%, 25%, 0.12);
|
||||||
--shadow-button: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.25);
|
--shadow-button: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.25);
|
||||||
--shadow-button-hover: 0 4px 16px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.35);
|
--shadow-button-hover: 0 4px 16px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.35);
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
--accent-success: hsl(var(--accent-hue), 65%, 45%);
|
--accent-success: hsl(var(--accent-hue), 65%, 45%);
|
||||||
--accent-error: hsl(0, 70%, 55%);
|
--accent-error: hsl(0, 70%, 55%);
|
||||||
--accent-warning: hsl(35, 85%, 55%);
|
--accent-warning: hsl(35, 85%, 55%);
|
||||||
--accent-info: hsl(var(--secondary-hue), 70%, 55%);
|
--accent-info: hsl(var(--primary-hue), 35%, 60%);
|
||||||
|
|
||||||
/* Spacing system */
|
/* Spacing system */
|
||||||
--spacing-xs: 4px;
|
--spacing-xs: 4px;
|
||||||
@@ -46,95 +46,92 @@
|
|||||||
--spacing-3xl: 32px;
|
--spacing-3xl: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme - harmonious complement to light theme */
|
/* Dark theme - balanced complement to light theme */
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--bg-primary: linear-gradient(135deg, hsl(var(--primary-hue), 20%, 8%) 0%, hsl(var(--secondary-hue), 15%, 12%) 100%);
|
--bg-primary: linear-gradient(135deg, hsl(var(--primary-hue), 15%, 18%) 0%, hsl(var(--secondary-hue), 12%, 22%) 100%);
|
||||||
--bg-secondary: hsla(var(--primary-hue), 25%, 10%, 0.98);
|
--bg-secondary: hsla(var(--primary-hue), 18%, 20%, 0.92);
|
||||||
--bg-card: hsla(var(--primary-hue), 20%, 14%, 0.95);
|
--bg-card: hsla(var(--primary-hue), 15%, 24%, 0.88);
|
||||||
--bg-input: hsl(var(--primary-hue), 15%, 18%);
|
--bg-input: hsl(var(--primary-hue), 12%, 28%);
|
||||||
--bg-button-primary: hsl(var(--primary-hue), var(--primary-saturation), 60%);
|
--bg-button-primary: hsl(var(--primary-hue), var(--primary-saturation), 58%);
|
||||||
--bg-button-secondary: hsl(var(--primary-hue), 15%, 22%);
|
--bg-button-secondary: hsl(var(--primary-hue), 12%, 32%);
|
||||||
--bg-button-ghost: transparent;
|
--bg-button-ghost: transparent;
|
||||||
|
|
||||||
--text-primary: hsl(var(--primary-hue), 15%, 92%);
|
--text-primary: hsl(var(--primary-hue), 12%, 85%);
|
||||||
--text-secondary: hsl(var(--primary-hue), 10%, 75%);
|
--text-secondary: hsl(var(--primary-hue), 8%, 68%);
|
||||||
--text-muted: hsl(var(--primary-hue), 8%, 55%);
|
--text-muted: hsl(var(--primary-hue), 6%, 52%);
|
||||||
--text-inverse: hsl(var(--primary-hue), 20%, 8%);
|
--text-inverse: hsl(var(--primary-hue), 15%, 18%);
|
||||||
--text-button-primary: white;
|
--text-button-primary: white;
|
||||||
--text-button-secondary: hsl(var(--primary-hue), 15%, 85%);
|
--text-button-secondary: hsl(var(--primary-hue), 12%, 78%);
|
||||||
|
|
||||||
--border-color: hsl(var(--primary-hue), 15%, 25%);
|
--border-color: hsl(var(--primary-hue), 12%, 38%);
|
||||||
--border-input: hsl(var(--primary-hue), 12%, 30%);
|
--border-input: hsl(var(--primary-hue), 10%, 42%);
|
||||||
--border-focus: hsl(var(--primary-hue), var(--primary-saturation), 60%);
|
--border-focus: hsl(var(--primary-hue), var(--primary-saturation), 58%);
|
||||||
|
|
||||||
--shadow-card: 0 4px 20px hsla(var(--primary-hue), 30%, 5%, 0.4);
|
--shadow-card: 0 4px 20px hsla(var(--primary-hue), 20%, 10%, 0.25);
|
||||||
--shadow-button: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 60%, 0.3);
|
--shadow-button: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 58%, 0.3);
|
||||||
--shadow-button-hover: 0 4px 16px hsla(var(--primary-hue), var(--primary-saturation), 60%, 0.4);
|
--shadow-button-hover: 0 4px 16px hsla(var(--primary-hue), var(--primary-saturation), 58%, 0.4);
|
||||||
|
|
||||||
/* Enhanced accent colors for dark theme */
|
/* Balanced accent colors for dark theme */
|
||||||
--accent-success: hsl(var(--accent-hue), 60%, 55%);
|
--accent-success: hsl(var(--accent-hue), 55%, 52%);
|
||||||
--accent-error: hsl(0, 65%, 60%);
|
--accent-error: hsl(0, 60%, 58%);
|
||||||
--accent-warning: hsl(35, 80%, 60%);
|
--accent-warning: hsl(35, 75%, 58%);
|
||||||
--accent-info: hsl(var(--secondary-hue), 65%, 60%);
|
--accent-info: hsl(var(--primary-hue), 28%, 68%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Harmonious button styling system */
|
/* Consolidated button styling system */
|
||||||
.btn-primary {
|
.btn-primary, .btn-secondary, .btn-ghost {
|
||||||
background: var(--bg-button-primary);
|
|
||||||
color: var(--text-button-primary);
|
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
box-shadow: var(--shadow-button);
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary {
|
||||||
background: hsl(var(--primary-hue), var(--primary-saturation), 50%);
|
background: var(--bg-button-primary);
|
||||||
box-shadow: var(--shadow-button-hover);
|
color: var(--text-button-primary);
|
||||||
transform: translateY(-1px);
|
font-weight: 600;
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: var(--shadow-button);
|
box-shadow: var(--shadow-button);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme primary button adjustments */
|
|
||||||
[data-theme="dark"] .btn-primary:hover {
|
|
||||||
background: hsl(var(--primary-hue), var(--primary-saturation), 65%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--bg-button-secondary);
|
background: var(--bg-button-secondary);
|
||||||
color: var(--text-button-secondary);
|
color: var(--text-button-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border-color: var(--border-focus);
|
|
||||||
color: var(--text-primary);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
background: var(--bg-button-ghost);
|
background: var(--bg-button-ghost);
|
||||||
color: var(--border-focus);
|
color: var(--border-focus);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
font-weight: 500;
|
}
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
.btn-primary:hover, .btn-secondary:hover, .btn-ghost:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active, .btn-secondary:active, .btn-ghost:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: hsl(var(--primary-hue), var(--primary-saturation), 50%);
|
||||||
|
box-shadow: var(--shadow-button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover, .btn-ghost:hover {
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
border-color: var(--border-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
background: var(--bg-input);
|
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
color: var(--text-primary);
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn-primary:hover {
|
||||||
|
background: hsl(var(--primary-hue), var(--primary-saturation), 65%);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -191,12 +188,157 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable-header {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-header:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
min-width: 200px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dropdown.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-input-group input {
|
||||||
|
width: 60px;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-input-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeout-input-group span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input-group input {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-help {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings page styles */
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.about-info {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info p {
|
||||||
|
margin: 0 0 var(--spacing-xs) 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-icon-only {
|
.btn-icon-only {
|
||||||
background: var(--bg-button-ghost);
|
background: var(--bg-button-ghost);
|
||||||
border: none;
|
border: none;
|
||||||
@@ -224,39 +366,15 @@ body {
|
|||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator .status-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent-warning);
|
|
||||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.connected .status-dot {
|
|
||||||
background: var(--accent-success);
|
|
||||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#statusText {
|
#statusText {
|
||||||
font-size: 14px;
|
font-size: 22px;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vault status specific styling */
|
|
||||||
.vault-status .status-indicator {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vault-status #statusText {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced lock button styling */
|
/* Enhanced lock button styling */
|
||||||
#lockBtn {
|
#lockBtn {
|
||||||
@@ -381,14 +499,14 @@ input:focus, select:focus, textarea:focus {
|
|||||||
/* Placeholder styling */
|
/* Placeholder styling */
|
||||||
input::placeholder, textarea::placeholder {
|
input::placeholder, textarea::placeholder {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
opacity: 1;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme placeholder styling */
|
/* Dark theme placeholder styling */
|
||||||
[data-theme="dark"] input::placeholder,
|
[data-theme="dark"] input::placeholder,
|
||||||
[data-theme="dark"] textarea::placeholder {
|
[data-theme="dark"] textarea::placeholder {
|
||||||
color: #e2e8f0;
|
color: var(--text-muted);
|
||||||
opacity: 0.8;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
@@ -416,6 +534,17 @@ input::placeholder, textarea::placeholder {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button icon spacing */
|
||||||
|
.btn svg {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn svg:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -515,7 +644,7 @@ input::placeholder, textarea::placeholder {
|
|||||||
|
|
||||||
.vault-header h2 {
|
.vault-header h2 {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,31 +878,6 @@ input::placeholder, textarea::placeholder {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Signature Result */
|
|
||||||
.signature-result {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
background: hsla(var(--accent-hue), 60%, 95%, 0.8);
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid hsla(var(--accent-hue), 50%, 70%, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .signature-result {
|
|
||||||
background: hsla(var(--accent-hue), 40%, 15%, 0.6);
|
|
||||||
border-color: hsla(var(--accent-hue), 50%, 40%, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.signature-result label {
|
|
||||||
color: var(--accent-success);
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Enhanced Toast Notifications */
|
/* Enhanced Toast Notifications */
|
||||||
.toast-notification {
|
.toast-notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -828,31 +932,7 @@ input::placeholder, textarea::placeholder {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1;
|
|
||||||
opacity: 0.7;
|
|
||||||
padding: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-close:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Success Toast */
|
/* Success Toast */
|
||||||
.toast-success {
|
.toast-success {
|
||||||
@@ -880,9 +960,9 @@ input::placeholder, textarea::placeholder {
|
|||||||
|
|
||||||
/* Info Toast */
|
/* Info Toast */
|
||||||
.toast-info {
|
.toast-info {
|
||||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%);
|
background: var(--accent-info);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: rgba(59, 130, 246, 0.3);
|
border-color: hsla(var(--primary-hue), 35%, 60%, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-info .toast-icon {
|
.toast-info .toast-icon {
|
||||||
@@ -981,10 +1061,7 @@ input::placeholder, textarea::placeholder {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .tab-btn:hover {
|
[data-theme="dark"] .tab-btn:hover,
|
||||||
color: var(--border-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .tab-btn.active {
|
[data-theme="dark"] .tab-btn.active {
|
||||||
color: var(--border-focus);
|
color: var(--border-focus);
|
||||||
}
|
}
|
||||||
@@ -997,8 +1074,8 @@ input::placeholder, textarea::placeholder {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Result Styling */
|
/* Consolidated result styling */
|
||||||
.encrypt-result, .decrypt-result, .verify-result {
|
.encrypt-result, .decrypt-result, .verify-result, .signature-result {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -1006,55 +1083,55 @@ input::placeholder, textarea::placeholder {
|
|||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.encrypt-result {
|
.encrypt-result, .signature-result {
|
||||||
background: hsla(var(--accent-hue), 60%, 95%, 0.8);
|
background: hsla(var(--accent-hue), 60%, 95%, 0.8);
|
||||||
border-color: hsla(var(--accent-hue), 50%, 70%, 0.3);
|
border-color: hsla(var(--accent-hue), 50%, 70%, 0.3);
|
||||||
box-shadow: 0 2px 12px hsla(var(--accent-hue), 50%, 50%, 0.1);
|
box-shadow: 0 2px 12px hsla(var(--accent-hue), 50%, 50%, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .encrypt-result {
|
|
||||||
background: hsla(var(--accent-hue), 40%, 15%, 0.6);
|
|
||||||
border-color: hsla(var(--accent-hue), 50%, 40%, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.encrypt-result label {
|
|
||||||
color: var(--accent-success);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.decrypt-result {
|
.decrypt-result {
|
||||||
background: hsla(var(--secondary-hue), 60%, 95%, 0.8);
|
background: hsla(var(--secondary-hue), 60%, 95%, 0.8);
|
||||||
border-color: hsla(var(--secondary-hue), 50%, 70%, 0.3);
|
border-color: hsla(var(--secondary-hue), 50%, 70%, 0.3);
|
||||||
box-shadow: 0 2px 12px hsla(var(--secondary-hue), 50%, 50%, 0.1);
|
box-shadow: 0 2px 12px hsla(var(--secondary-hue), 50%, 50%, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .decrypt-result {
|
|
||||||
background: hsla(var(--secondary-hue), 40%, 15%, 0.6);
|
|
||||||
border-color: hsla(var(--secondary-hue), 50%, 40%, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.decrypt-result label {
|
|
||||||
color: var(--accent-info);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verify-result {
|
.verify-result {
|
||||||
background: hsla(var(--primary-hue), 30%, 95%, 0.8);
|
background: hsla(var(--primary-hue), 30%, 95%, 0.8);
|
||||||
border-color: hsla(var(--primary-hue), 40%, 70%, 0.3);
|
border-color: hsla(var(--primary-hue), 40%, 70%, 0.3);
|
||||||
box-shadow: 0 2px 12px hsla(var(--primary-hue), 40%, 50%, 0.1);
|
box-shadow: 0 2px 12px hsla(var(--primary-hue), 40%, 50%, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .encrypt-result,
|
||||||
|
[data-theme="dark"] .signature-result {
|
||||||
|
background: hsla(var(--accent-hue), 40%, 15%, 0.6);
|
||||||
|
border-color: hsla(var(--accent-hue), 50%, 40%, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .decrypt-result {
|
||||||
|
background: hsla(var(--secondary-hue), 40%, 15%, 0.6);
|
||||||
|
border-color: hsla(var(--secondary-hue), 50%, 40%, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .verify-result {
|
[data-theme="dark"] .verify-result {
|
||||||
background: hsla(var(--primary-hue), 20%, 15%, 0.6);
|
background: hsla(var(--primary-hue), 20%, 15%, 0.6);
|
||||||
border-color: hsla(var(--primary-hue), 30%, 40%, 0.4);
|
border-color: hsla(var(--primary-hue), 30%, 40%, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.encrypt-result label, .decrypt-result label, .signature-result label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypt-result label, .signature-result label {
|
||||||
|
color: var(--accent-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.decrypt-result label {
|
||||||
|
color: var(--accent-info);
|
||||||
|
}
|
||||||
|
|
||||||
.verification-status {
|
.verification-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1071,19 +1148,272 @@ input::placeholder, textarea::placeholder {
|
|||||||
color: var(--accent-error);
|
color: var(--accent-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verification-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-icon svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SigSocket Requests Styles */
|
||||||
|
.sigsocket-section {
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-error);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.connected {
|
||||||
|
background: var(--accent-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.loading {
|
||||||
|
background: var(--accent-warning);
|
||||||
|
animation: pulse-dot 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.requests-container {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-top: 2px solid var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto var(--spacing-sm) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state p {
|
||||||
|
margin: var(--spacing-sm) 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state small {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state small {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-item {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-item:hover {
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
box-shadow: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-id {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-input);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-message {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 60px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve {
|
||||||
|
background: var(--accent-success);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve:hover {
|
||||||
|
background: hsl(var(--accent-hue), 65%, 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject {
|
||||||
|
background: var(--accent-error);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject:hover {
|
||||||
|
background: hsl(0, 70%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve:disabled,
|
||||||
|
.btn-reject:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sigsocket-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure refresh button follows design system */
|
||||||
|
#refreshRequestsBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Request item locked state styles */
|
||||||
|
.request-item.locked {
|
||||||
|
opacity: 0.8;
|
||||||
|
border-left: 3px solid var(--warning-color, #ffa500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-status.pending {
|
||||||
|
background: var(--warning-bg, #fff3cd);
|
||||||
|
color: var(--warning-text, #856404);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: var(--spacing-xs) 0;
|
||||||
|
border: 1px solid var(--warning-border, #ffeaa7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-actions.locked button {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-actions.locked button:hover {
|
||||||
|
background: var(--button-bg) !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-mismatch {
|
||||||
|
background: hsla(35, 85%, 85%, 0.8);
|
||||||
|
border: 1px solid hsla(35, 85%, 70%, 0.5);
|
||||||
|
color: hsl(35, 70%, 30%);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .workspace-mismatch {
|
||||||
|
background: hsla(35, 60%, 15%, 0.8);
|
||||||
|
border-color: hsla(35, 60%, 30%, 0.5);
|
||||||
|
color: hsl(35, 70%, 70%);
|
||||||
|
}
|
||||||
114
crypto_vault_extension/test_extension.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Testing the SigSocket Browser Extension
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **SigSocket Server**: You need a running SigSocket server at `ws://localhost:8080/ws`
|
||||||
|
2. **Browser**: Chrome or Chromium-based browser with developer mode enabled
|
||||||
|
|
||||||
|
## Test Steps
|
||||||
|
|
||||||
|
### 1. Load the Extension
|
||||||
|
|
||||||
|
1. Open Chrome and go to `chrome://extensions/`
|
||||||
|
2. Enable "Developer mode" in the top right
|
||||||
|
3. Click "Load unpacked" and select the `crypto_vault_extension` directory
|
||||||
|
4. The CryptoVault extension should appear in your extensions list
|
||||||
|
|
||||||
|
### 2. Basic Functionality Test
|
||||||
|
|
||||||
|
1. Click the CryptoVault extension icon in the toolbar
|
||||||
|
2. Create a new keyspace:
|
||||||
|
- Enter a keyspace name (e.g., "test-workspace")
|
||||||
|
- Enter a password
|
||||||
|
- Click "Create New"
|
||||||
|
3. The extension should automatically connect to the SigSocket server
|
||||||
|
4. Add a keypair:
|
||||||
|
- Click "Add Keypair"
|
||||||
|
- Enter a name for the keypair
|
||||||
|
- Click "Create Keypair"
|
||||||
|
|
||||||
|
### 3. SigSocket Integration Test
|
||||||
|
|
||||||
|
1. **Check Connection Status**:
|
||||||
|
- Look for the SigSocket connection status at the bottom of the popup
|
||||||
|
- It should show "SigSocket: Connected" with a green indicator
|
||||||
|
|
||||||
|
2. **Test Sign Request Flow**:
|
||||||
|
- Send a sign request to the SigSocket server (you'll need to implement this on the server side)
|
||||||
|
- The extension should show a notification
|
||||||
|
- The extension badge should show the number of pending requests
|
||||||
|
- Open the extension popup to see the sign request
|
||||||
|
|
||||||
|
3. **Test Approval Flow**:
|
||||||
|
- If keyspace is locked, you should see "Unlock keyspace to see X pending requests"
|
||||||
|
- Unlock the keyspace using the login form
|
||||||
|
- You should see the sign request details
|
||||||
|
- Click "Approve & Sign" to approve the request
|
||||||
|
- The request should be signed and sent back to the server
|
||||||
|
|
||||||
|
### 4. Settings Test
|
||||||
|
|
||||||
|
1. Click the settings gear icon in the extension popup
|
||||||
|
2. Change the SigSocket server URL if needed
|
||||||
|
3. Adjust the session timeout if desired
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
- ✅ Extension loads without errors
|
||||||
|
- ✅ Can create keyspaces and keypairs
|
||||||
|
- ✅ SigSocket connection is established automatically
|
||||||
|
- ✅ Sign requests are received and displayed
|
||||||
|
- ✅ Approval flow works correctly
|
||||||
|
- ✅ Settings can be configured
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Extension won't load**: Check the console for JavaScript errors
|
||||||
|
2. **SigSocket won't connect**: Verify the server is running and the URL is correct
|
||||||
|
3. **WASM errors**: Check that the WASM files are properly built and copied
|
||||||
|
4. **Sign requests not appearing**: Check the browser console for callback errors
|
||||||
|
|
||||||
|
### Debug Steps
|
||||||
|
|
||||||
|
1. Open Chrome DevTools
|
||||||
|
2. Go to the Extensions tab
|
||||||
|
3. Find CryptoVault and click "Inspect views: background page"
|
||||||
|
4. Check the console for any errors
|
||||||
|
5. Also inspect the popup by right-clicking the extension icon and selecting "Inspect popup"
|
||||||
|
|
||||||
|
## Server-Side Testing
|
||||||
|
|
||||||
|
To fully test the extension, you'll need a SigSocket server that can:
|
||||||
|
|
||||||
|
1. Accept WebSocket connections at `/ws`
|
||||||
|
2. Handle client introduction messages (hex-encoded public keys)
|
||||||
|
3. Send sign requests in the format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "unique-request-id",
|
||||||
|
"message": "base64-encoded-message"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Receive sign responses in the format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "request-id",
|
||||||
|
"message": "base64-encoded-message",
|
||||||
|
"signature": "base64-encoded-signature"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
If basic functionality works:
|
||||||
|
|
||||||
|
1. Test with multiple concurrent sign requests
|
||||||
|
2. Test connection recovery after network issues
|
||||||
|
3. Test with different keyspace configurations
|
||||||
|
4. Test the rejection flow
|
||||||
|
5. Test session timeout behavior
|
||||||
@@ -277,6 +277,42 @@ export function is_unlocked() {
|
|||||||
return ret !== 0;
|
return ret !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default public key for a workspace (keyspace)
|
||||||
|
* This returns the public key of the first keypair in the keyspace
|
||||||
|
* @param {string} workspace_id
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function get_workspace_default_public_key(workspace_id) {
|
||||||
|
const ptr0 = passStringToWasm0(workspace_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.get_workspace_default_public_key(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current unlocked public key as hex string
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function get_current_unlocked_public_key() {
|
||||||
|
let deferred2_0;
|
||||||
|
let deferred2_1;
|
||||||
|
try {
|
||||||
|
const ret = wasm.get_current_unlocked_public_key();
|
||||||
|
var ptr1 = ret[0];
|
||||||
|
var len1 = ret[1];
|
||||||
|
if (ret[3]) {
|
||||||
|
ptr1 = 0; len1 = 0;
|
||||||
|
throw takeFromExternrefTable0(ret[2]);
|
||||||
|
}
|
||||||
|
deferred2_0 = ptr1;
|
||||||
|
deferred2_1 = len1;
|
||||||
|
return getStringFromWasm0(ptr1, len1);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all keypairs from the current session
|
* Get all keypairs from the current session
|
||||||
* Returns an array of keypair objects with id, type, and metadata
|
* Returns an array of keypair objects with id, type, and metadata
|
||||||
@@ -323,7 +359,7 @@ function passArray8ToWasm0(arg, malloc) {
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sign message with current session
|
* Sign message with current session (requires selected keypair)
|
||||||
* @param {Uint8Array} message
|
* @param {Uint8Array} message
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
@@ -334,6 +370,41 @@ export function sign(message) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current keyspace name
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function get_current_keyspace_name() {
|
||||||
|
let deferred2_0;
|
||||||
|
let deferred2_1;
|
||||||
|
try {
|
||||||
|
const ret = wasm.get_current_keyspace_name();
|
||||||
|
var ptr1 = ret[0];
|
||||||
|
var len1 = ret[1];
|
||||||
|
if (ret[3]) {
|
||||||
|
ptr1 = 0; len1 = 0;
|
||||||
|
throw takeFromExternrefTable0(ret[2]);
|
||||||
|
}
|
||||||
|
deferred2_0 = ptr1;
|
||||||
|
deferred2_1 = len1;
|
||||||
|
return getStringFromWasm0(ptr1, len1);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign message with default keypair (first keypair in keyspace) without changing session state
|
||||||
|
* @param {Uint8Array} message
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function sign_with_default_keypair(message) {
|
||||||
|
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sign_with_default_keypair(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a signature with the current session's selected keypair
|
* Verify a signature with the current session's selected keypair
|
||||||
* @param {Uint8Array} message
|
* @param {Uint8Array} message
|
||||||
@@ -395,24 +466,391 @@ export function run_rhai(script) {
|
|||||||
return takeFromExternrefTable0(ret[0]);
|
return takeFromExternrefTable0(ret[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_32(arg0, arg1, arg2) {
|
function __wbg_adapter_34(arg0, arg1, arg2) {
|
||||||
wasm.closure121_externref_shim(arg0, arg1, arg2);
|
wasm.closure203_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_35(arg0, arg1, arg2) {
|
function __wbg_adapter_39(arg0, arg1) {
|
||||||
wasm.closure150_externref_shim(arg0, arg1, arg2);
|
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79bf9f6d48e92f7(arg0, arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_38(arg0, arg1, arg2) {
|
function __wbg_adapter_44(arg0, arg1, arg2) {
|
||||||
wasm.closure227_externref_shim(arg0, arg1, arg2);
|
wasm.closure239_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_138(arg0, arg1, arg2, arg3) {
|
function __wbg_adapter_49(arg0, arg1) {
|
||||||
wasm.closure1879_externref_shim(arg0, arg1, arg2, arg3);
|
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf103de07b8856532(arg0, arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_52(arg0, arg1, arg2) {
|
||||||
|
wasm.closure319_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_55(arg0, arg1, arg2) {
|
||||||
|
wasm.closure395_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_207(arg0, arg1, arg2, arg3) {
|
||||||
|
wasm.closure2042_externref_shim(arg0, arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"];
|
||||||
|
|
||||||
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||||
|
|
||||||
|
const SigSocketConnectionFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(ptr => wasm.__wbg_sigsocketconnection_free(ptr >>> 0, 1));
|
||||||
|
/**
|
||||||
|
* WASM-bindgen wrapper for SigSocket client
|
||||||
|
*
|
||||||
|
* This provides a clean JavaScript API for the browser extension to:
|
||||||
|
* - Connect to SigSocket servers
|
||||||
|
* - Send responses to sign requests
|
||||||
|
* - Manage connection state
|
||||||
|
*/
|
||||||
|
export class SigSocketConnection {
|
||||||
|
|
||||||
|
__destroy_into_raw() {
|
||||||
|
const ptr = this.__wbg_ptr;
|
||||||
|
this.__wbg_ptr = 0;
|
||||||
|
SigSocketConnectionFinalization.unregister(this);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_sigsocketconnection_free(ptr, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create a new SigSocket connection
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
const ret = wasm.sigsocketconnection_new();
|
||||||
|
this.__wbg_ptr = ret >>> 0;
|
||||||
|
SigSocketConnectionFinalization.register(this, this.__wbg_ptr, this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Connect to a SigSocket server
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
* * `public_key_hex` - Client's public key as hex string
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Successfully connected
|
||||||
|
* * `Err(error)` - Connection failed
|
||||||
|
* @param {string} server_url
|
||||||
|
* @param {string} public_key_hex
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
connect(server_url, public_key_hex) {
|
||||||
|
const ptr0 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(public_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketconnection_connect(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Send a response to a sign request
|
||||||
|
*
|
||||||
|
* This should be called by the extension after the user has approved
|
||||||
|
* a sign request and the message has been signed.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `request_id` - ID of the original request
|
||||||
|
* * `message_base64` - Original message (base64-encoded)
|
||||||
|
* * `signature_hex` - Signature as hex string
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Response sent successfully
|
||||||
|
* * `Err(error)` - Failed to send response
|
||||||
|
* @param {string} request_id
|
||||||
|
* @param {string} message_base64
|
||||||
|
* @param {string} signature_hex
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
send_response(request_id, message_base64, signature_hex) {
|
||||||
|
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(message_base64, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ptr2 = passStringToWasm0(signature_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len2 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketconnection_send_response(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Send a rejection for a sign request
|
||||||
|
*
|
||||||
|
* This should be called when the user rejects a sign request.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `request_id` - ID of the request to reject
|
||||||
|
* * `reason` - Reason for rejection (optional)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Rejection sent successfully
|
||||||
|
* * `Err(error)` - Failed to send rejection
|
||||||
|
* @param {string} request_id
|
||||||
|
* @param {string} reason
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
send_rejection(request_id, reason) {
|
||||||
|
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketconnection_send_rejection(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Disconnect from the SigSocket server
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
wasm.sigsocketconnection_disconnect(this.__wbg_ptr);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if connected to the server
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
is_connected() {
|
||||||
|
const ret = wasm.sigsocketconnection_is_connected(this.__wbg_ptr);
|
||||||
|
return ret !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SigSocketManagerFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(ptr => wasm.__wbg_sigsocketmanager_free(ptr >>> 0, 1));
|
||||||
|
/**
|
||||||
|
* SigSocket manager for high-level operations
|
||||||
|
*/
|
||||||
|
export class SigSocketManager {
|
||||||
|
|
||||||
|
__destroy_into_raw() {
|
||||||
|
const ptr = this.__wbg_ptr;
|
||||||
|
this.__wbg_ptr = 0;
|
||||||
|
SigSocketManagerFinalization.unregister(this);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_sigsocketmanager_free(ptr, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Connect to SigSocket server with smart connection management
|
||||||
|
*
|
||||||
|
* This handles all connection logic:
|
||||||
|
* - Reuses existing connection if same workspace
|
||||||
|
* - Switches connection if different workspace
|
||||||
|
* - Creates new connection if none exists
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `workspace` - The workspace name to connect with
|
||||||
|
* * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
* * `event_callback` - JavaScript function to call when events occur
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(connection_info)` - JSON string with connection details
|
||||||
|
* * `Err(error)` - If connection failed or workspace is invalid
|
||||||
|
* @param {string} workspace
|
||||||
|
* @param {string} server_url
|
||||||
|
* @param {Function} event_callback
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static connect_workspace_with_events(workspace, server_url, event_callback) {
|
||||||
|
const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketmanager_connect_workspace_with_events(ptr0, len0, ptr1, len1, event_callback);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Connect to SigSocket server with a specific workspace (backward compatibility)
|
||||||
|
*
|
||||||
|
* This is a simpler version that doesn't set up event callbacks.
|
||||||
|
* Use connect_workspace_with_events for full functionality.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `workspace` - The workspace name to connect with
|
||||||
|
* * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(connection_info)` - JSON string with connection details
|
||||||
|
* * `Err(error)` - If connection failed or workspace is invalid
|
||||||
|
* @param {string} workspace
|
||||||
|
* @param {string} server_url
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static connect_workspace(workspace, server_url) {
|
||||||
|
const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketmanager_connect_workspace(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Disconnect from SigSocket server
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Successfully disconnected
|
||||||
|
* * `Err(error)` - If disconnect failed
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static disconnect() {
|
||||||
|
const ret = wasm.sigsocketmanager_disconnect();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if we can approve a specific sign request
|
||||||
|
*
|
||||||
|
* This validates that:
|
||||||
|
* 1. The request exists
|
||||||
|
* 2. The vault session is unlocked
|
||||||
|
* 3. The current workspace matches the request's target
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `request_id` - The ID of the request to validate
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(true)` - Request can be approved
|
||||||
|
* * `Ok(false)` - Request cannot be approved
|
||||||
|
* * `Err(error)` - Validation error
|
||||||
|
* @param {string} request_id
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
static can_approve_request(request_id) {
|
||||||
|
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketmanager_can_approve_request(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Approve a sign request and send the signature to the server
|
||||||
|
*
|
||||||
|
* This performs the complete approval flow:
|
||||||
|
* 1. Validates the request can be approved
|
||||||
|
* 2. Signs the message using the vault
|
||||||
|
* 3. Sends the signature to the SigSocket server
|
||||||
|
* 4. Removes the request from pending list
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `request_id` - The ID of the request to approve
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(signature)` - Base64-encoded signature that was sent
|
||||||
|
* * `Err(error)` - If approval failed
|
||||||
|
* @param {string} request_id
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static approve_request(request_id) {
|
||||||
|
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketmanager_approve_request(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reject a sign request
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `request_id` - The ID of the request to reject
|
||||||
|
* * `reason` - The reason for rejection
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Request rejected successfully
|
||||||
|
* * `Err(error)` - If rejection failed
|
||||||
|
* @param {string} request_id
|
||||||
|
* @param {string} reason
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static reject_request(request_id, reason) {
|
||||||
|
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketmanager_reject_request(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get pending requests filtered by current workspace
|
||||||
|
*
|
||||||
|
* This returns only the requests that the current vault session can handle,
|
||||||
|
* based on the unlocked workspace and its public key.
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(requests_json)` - JSON array of filtered requests
|
||||||
|
* * `Err(error)` - If filtering failed
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static get_filtered_requests() {
|
||||||
|
const ret = wasm.sigsocketmanager_get_filtered_requests();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a pending sign request (called when request arrives from server)
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `request_json` - JSON string containing the sign request
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Request added successfully
|
||||||
|
* * `Err(error)` - If adding failed
|
||||||
|
* @param {string} request_json
|
||||||
|
*/
|
||||||
|
static add_pending_request(request_json) {
|
||||||
|
const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sigsocketmanager_add_pending_request(ptr0, len0);
|
||||||
|
if (ret[1]) {
|
||||||
|
throw takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get connection status
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(status_json)` - JSON object with connection status
|
||||||
|
* * `Err(error)` - If getting status failed
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static get_connection_status() {
|
||||||
|
let deferred2_0;
|
||||||
|
let deferred2_1;
|
||||||
|
try {
|
||||||
|
const ret = wasm.sigsocketmanager_get_connection_status();
|
||||||
|
var ptr1 = ret[0];
|
||||||
|
var len1 = ret[1];
|
||||||
|
if (ret[3]) {
|
||||||
|
ptr1 = 0; len1 = 0;
|
||||||
|
throw takeFromExternrefTable0(ret[2]);
|
||||||
|
}
|
||||||
|
deferred2_0 = ptr1;
|
||||||
|
deferred2_1 = len1;
|
||||||
|
return getStringFromWasm0(ptr1, len1);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Clear all pending requests
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* * `Ok(())` - Requests cleared successfully
|
||||||
|
*/
|
||||||
|
static clear_pending_requests() {
|
||||||
|
const ret = wasm.sigsocketmanager_clear_pending_requests();
|
||||||
|
if (ret[1]) {
|
||||||
|
throw takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function __wbg_load(module, imports) {
|
async function __wbg_load(module, imports) {
|
||||||
if (typeof Response === 'function' && module instanceof Response) {
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
@@ -459,6 +897,9 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.call(arg1, arg2);
|
const ret = arg0.call(arg1, arg2);
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_close_2893b7d056a0627d = function() { return handleError(function (arg0) {
|
||||||
|
arg0.close();
|
||||||
|
}, arguments) };
|
||||||
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -467,6 +908,10 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.crypto;
|
const ret = arg0.crypto;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_data_432d9c3df2630942 = function(arg0) {
|
||||||
|
const ret = arg0.data;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||||
console.error(arg0);
|
console.error(arg0);
|
||||||
};
|
};
|
||||||
@@ -539,10 +984,23 @@ function __wbg_get_imports() {
|
|||||||
const ret = result;
|
const ret = result;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof Window;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||||
const ret = arg0.length;
|
const ret = arg0.length;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_log_c222819a41e063d3 = function(arg0) {
|
||||||
|
console.log(arg0);
|
||||||
|
};
|
||||||
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||||
const ret = arg0.msCrypto;
|
const ret = arg0.msCrypto;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -558,7 +1016,7 @@ function __wbg_get_imports() {
|
|||||||
const a = state0.a;
|
const a = state0.a;
|
||||||
state0.a = 0;
|
state0.a = 0;
|
||||||
try {
|
try {
|
||||||
return __wbg_adapter_138(a, state0.b, arg0, arg1);
|
return __wbg_adapter_207(a, state0.b, arg0, arg1);
|
||||||
} finally {
|
} finally {
|
||||||
state0.a = a;
|
state0.a = a;
|
||||||
}
|
}
|
||||||
@@ -577,6 +1035,10 @@ function __wbg_get_imports() {
|
|||||||
const ret = new Array();
|
const ret = new Array();
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_new_92c54fc74574ef55 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = new WebSocket(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||||
const ret = new Uint8Array(arg0);
|
const ret = new Uint8Array(arg0);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -609,6 +1071,12 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_onConnectionStateChanged_b0dc098522afadba = function(arg0) {
|
||||||
|
onConnectionStateChanged(arg0 !== 0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_onSignRequestReceived_93232ba7a0919705 = function(arg0, arg1, arg2, arg3) {
|
||||||
|
onSignRequestReceived(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
|
||||||
|
};
|
||||||
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||||
return ret;
|
return ret;
|
||||||
@@ -643,6 +1111,10 @@ function __wbg_get_imports() {
|
|||||||
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||||
arg0.randomFillSync(arg1);
|
arg0.randomFillSync(arg1);
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_readyState_7ef6e63c349899ed = function(arg0) {
|
||||||
|
const ret = arg0.readyState;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||||
const ret = module.require;
|
const ret = module.require;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -655,12 +1127,38 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.result;
|
const ret = arg0.result;
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_send_0293179ba074ffb4 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
arg0.send(getStringFromWasm0(arg1, arg2));
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_setTimeout_f2fe5af8e3debeb3 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.setTimeout(arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||||
arg0.set(arg1, arg2 >>> 0);
|
arg0.set(arg1, arg2 >>> 0);
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = Reflect.set(arg0, arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_setbinaryType_92fa1ffd873b327c = function(arg0, arg1) {
|
||||||
|
arg0.binaryType = __wbindgen_enum_BinaryType[arg1];
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonclose_14fc475a49d488fc = function(arg0, arg1) {
|
||||||
|
arg0.onclose = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonerror_8639efe354b947cd = function(arg0, arg1) {
|
||||||
|
arg0.onerror = arg1;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||||
arg0.onerror = arg1;
|
arg0.onerror = arg1;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_setonmessage_6eccab530a8fb4c7 = function(arg0, arg1) {
|
||||||
|
arg0.onmessage = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonopen_2da654e1f39745d5 = function(arg0, arg1) {
|
||||||
|
arg0.onopen = arg1;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||||
arg0.onsuccess = arg1;
|
arg0.onsuccess = arg1;
|
||||||
};
|
};
|
||||||
@@ -695,6 +1193,10 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.then(arg1);
|
const ret = arg0.then(arg1);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.then(arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -703,6 +1205,9 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.versions;
|
const ret = arg0.versions;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbg_warn_4ca3906c248c47c4 = function(arg0) {
|
||||||
|
console.warn(arg0);
|
||||||
|
};
|
||||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||||
const obj = arg0.original;
|
const obj = arg0.original;
|
||||||
if (obj.cnt-- == 1) {
|
if (obj.cnt-- == 1) {
|
||||||
@@ -712,16 +1217,40 @@ function __wbg_get_imports() {
|
|||||||
const ret = false;
|
const ret = false;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper1036 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 122, __wbg_adapter_32);
|
const ret = makeMutClosure(arg0, arg1, 320, __wbg_adapter_52);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper549 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper1329 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_35);
|
const ret = makeMutClosure(arg0, arg1, 396, __wbg_adapter_55);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper857 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper624 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_38);
|
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper625 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper626 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_39);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper630 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper765 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper768 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_49);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||||
@@ -778,6 +1307,14 @@ function __wbg_get_imports() {
|
|||||||
const ret = wasm.memory;
|
const ret = wasm.memory;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
|
||||||
|
const obj = arg1;
|
||||||
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||||
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||||
const ret = getStringFromWasm0(arg0, arg1);
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl EvmClient {
|
|||||||
mut tx: provider::Transaction,
|
mut tx: provider::Transaction,
|
||||||
signer: &dyn crate::signer::Signer,
|
signer: &dyn crate::signer::Signer,
|
||||||
) -> Result<ethers_core::types::H256, EvmError> {
|
) -> Result<ethers_core::types::H256, EvmError> {
|
||||||
use ethers_core::types::{U256, H256, Bytes, Address};
|
use ethers_core::types::{U256, H256};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use crate::provider::{send_rpc, parse_signature_rs_v};
|
use crate::provider::{send_rpc, parse_signature_rs_v};
|
||||||
@@ -131,7 +131,7 @@ impl EvmClient {
|
|||||||
|
|
||||||
// 3. Sign the RLP-encoded unsigned transaction
|
// 3. Sign the RLP-encoded unsigned transaction
|
||||||
let sig = signer.sign(&rlp_unsigned).await?;
|
let sig = signer.sign(&rlp_unsigned).await?;
|
||||||
let (r, s, v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?;
|
let (r, s, _v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?;
|
||||||
|
|
||||||
// 4. RLP encode signed transaction (EIP-155)
|
// 4. RLP encode signed transaction (EIP-155)
|
||||||
use rlp::RlpStream;
|
use rlp::RlpStream;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Rhai bindings for EVM Client module
|
//! Rhai bindings for EVM Client module
|
||||||
//! Provides a single source of truth for scripting integration for EVM actions.
|
//! Provides a single source of truth for scripting integration for EVM actions.
|
||||||
|
|
||||||
use rhai::{Engine, Map};
|
use rhai::Engine;
|
||||||
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
|
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
|
||||||
|
|
||||||
/// Register EVM Client APIs with the Rhai scripting engine.
|
/// Register EVM Client APIs with the Rhai scripting engine.
|
||||||
@@ -25,7 +25,7 @@ pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClie
|
|||||||
engine.register_type::<RhaiEvmClient>();
|
engine.register_type::<RhaiEvmClient>();
|
||||||
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
||||||
// Register instance for scripts
|
// Register instance for scripts
|
||||||
let rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
|
let _rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
|
||||||
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
|
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
//! These use block_on for native, and should be adapted for WASM as needed.
|
//! These use block_on for native, and should be adapted for WASM as needed.
|
||||||
|
|
||||||
use crate::EvmClient;
|
use crate::EvmClient;
|
||||||
use rhai::Map;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_balance_real_address() {
|
async fn test_get_balance_real_address() {
|
||||||
use ethers_core::types::{Address, U256};
|
|
||||||
use evm_client::provider::get_balance;
|
use evm_client::provider::get_balance;
|
||||||
|
|
||||||
// Vitalik's address
|
// Vitalik's address
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
# SAL Modular Cryptographic Browser Extension
|
|
||||||
|
|
||||||
A modern, secure browser extension for interacting with the SAL modular Rust cryptographic stack, enabling key management, cryptographic operations, and secure Rhai script execution.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Session & Key Management
|
|
||||||
- Create and unlock encrypted keyspaces with password protection
|
|
||||||
- Create, select, and manage multiple keypairs (Ed25519, Secp256k1)
|
|
||||||
- Clear session state visualization and management
|
|
||||||
|
|
||||||
### Cryptographic Operations
|
|
||||||
- Sign and verify messages using selected keypair
|
|
||||||
- Encrypt and decrypt messages using asymmetric cryptography
|
|
||||||
- Support for symmetric encryption using password-derived keys
|
|
||||||
|
|
||||||
### Scripting (Rhai)
|
|
||||||
- Execute Rhai scripts securely within the extension
|
|
||||||
- Explicit user approval for all script executions
|
|
||||||
- Script history and audit trail
|
|
||||||
|
|
||||||
### WebSocket Integration
|
|
||||||
- Connect to WebSocket servers using keypair's public key
|
|
||||||
- Receive, review, and approve/reject incoming scripts
|
|
||||||
- Support for both local and remote script execution
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- Dark mode UI with modern, responsive design
|
|
||||||
- Session auto-lock after configurable inactivity period
|
|
||||||
- Explicit user approval for all sensitive operations
|
|
||||||
- No persistent storage of passwords or private keys in plaintext
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
The extension is built with a modern tech stack:
|
|
||||||
|
|
||||||
- **Frontend**: React with TypeScript, Material-UI
|
|
||||||
- **State Management**: Zustand
|
|
||||||
- **Backend**: WebAssembly (WASM) modules compiled from Rust
|
|
||||||
- **Storage**: Chrome extension storage API with encryption
|
|
||||||
- **Networking**: WebSocket for server communication
|
|
||||||
|
|
||||||
## Development Setup
|
|
||||||
|
|
||||||
1. Install dependencies:
|
|
||||||
```
|
|
||||||
cd sal_extension
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Build the extension:
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Load the extension in Chrome/Edge:
|
|
||||||
- Navigate to `chrome://extensions/`
|
|
||||||
- Enable "Developer mode"
|
|
||||||
- Click "Load unpacked" and select the `dist` directory
|
|
||||||
|
|
||||||
4. For development with hot-reload:
|
|
||||||
```
|
|
||||||
npm run watch
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with WASM
|
|
||||||
|
|
||||||
The extension uses WebAssembly modules compiled from Rust to perform cryptographic operations securely. The WASM modules are loaded in the extension's background script and provide a secure API for the frontend.
|
|
||||||
|
|
||||||
Key WASM functions exposed:
|
|
||||||
- `init_session` - Unlock a keyspace with password
|
|
||||||
- `create_keyspace` - Create a new keyspace
|
|
||||||
- `add_keypair` - Create a new keypair
|
|
||||||
- `select_keypair` - Select a keypair for use
|
|
||||||
- `sign` - Sign a message with the selected keypair
|
|
||||||
- `run_rhai` - Execute a Rhai script securely
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- The extension follows the principle of least privilege
|
|
||||||
- All sensitive operations require explicit user approval
|
|
||||||
- Passwords are never stored persistently, only kept in memory during an active session
|
|
||||||
- Session state is automatically cleared when the extension is locked
|
|
||||||
- WebSocket connections are authenticated using the user's public key
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT License](LICENSE)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
:root{font-family:Roboto,system-ui,sans-serif;line-height:1.5;font-weight:400;color-scheme:dark}body{margin:0;min-width:360px;min-height:520px;overflow-x:hidden}#root{width:100%;height:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.3)}
|
|
||||||
205
hero_vault_extension/dist/assets/index-b58c7e43.js
vendored
@@ -1 +0,0 @@
|
|||||||
console.log("Background script initialized");let i=!1,e=null;chrome.runtime.onMessage.addListener((o,l,r)=>{if(console.log("Background received message:",o.type),o.type==="SESSION_STATUS")return r({active:i}),!0;if(o.type==="SESSION_UNLOCK")return i=!0,r({success:!0}),!0;if(o.type==="SESSION_LOCK")return i=!1,e&&(e.close(),e=null),r({success:!0}),!0;if(o.type==="CONNECT_WEBSOCKET"&&o.serverUrl&&o.publicKey){try{e&&e.close(),e=new WebSocket(o.serverUrl),e.onopen=()=>{console.log("WebSocket connection established"),e&&e.send(JSON.stringify({type:"IDENTIFY",publicKey:o.publicKey}))},e.onmessage=c=>{try{const t=JSON.parse(c.data);console.log("WebSocket message received:",t),chrome.runtime.sendMessage({type:"WEBSOCKET_MESSAGE",data:t}).catch(n=>{console.error("Failed to forward WebSocket message:",n)})}catch(t){console.error("Failed to parse WebSocket message:",t)}},e.onerror=c=>{console.error("WebSocket error:",c)},e.onclose=()=>{console.log("WebSocket connection closed"),e=null},r({success:!0})}catch(c){console.error("Failed to connect to WebSocket:",c),r({success:!1,error:c.message})}return!0}return o.type==="DISCONNECT_WEBSOCKET"?(e?(e.close(),e=null,r({success:!0})):r({success:!1,error:"No active WebSocket connection"}),!0):!1});chrome.notifications&&chrome.notifications.onClicked&&chrome.notifications.onClicked.addListener(o=>{chrome.action.openPopup()});
|
|
||||||
61
hero_vault_extension/dist/background.js
vendored
@@ -1,61 +0,0 @@
|
|||||||
|
|
||||||
// Background Service Worker for SAL Modular Cryptographic Extension
|
|
||||||
// This is a simplified version that only handles messaging
|
|
||||||
|
|
||||||
console.log('Background script initialized');
|
|
||||||
|
|
||||||
// Store active WebSocket connection
|
|
||||||
let activeWebSocket = null;
|
|
||||||
let sessionActive = false;
|
|
||||||
|
|
||||||
// Listen for messages from popup or content scripts
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
||||||
console.log('Background received message:', message.type);
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_STATUS') {
|
|
||||||
sendResponse({ active: sessionActive });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_UNLOCK') {
|
|
||||||
sessionActive = true;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_LOCK') {
|
|
||||||
sessionActive = false;
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
}
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'CONNECT_WEBSOCKET') {
|
|
||||||
// Simplified WebSocket handling
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
} else {
|
|
||||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize notification setup
|
|
||||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
|
||||||
// Open the extension popup when a notification is clicked
|
|
||||||
chrome.action.openPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
BIN
hero_vault_extension/dist/icons/icon-128.png
vendored
|
Before Width: | Height: | Size: 1.9 KiB |
BIN
hero_vault_extension/dist/icons/icon-16.png
vendored
|
Before Width: | Height: | Size: 454 B |
BIN
hero_vault_extension/dist/icons/icon-32.png
vendored
|
Before Width: | Height: | Size: 712 B |
BIN
hero_vault_extension/dist/icons/icon-48.png
vendored
|
Before Width: | Height: | Size: 1.1 KiB |
14
hero_vault_extension/dist/index.html
vendored
@@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Hero Vault</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-b58c7e43.js"></script>
|
|
||||||
<link rel="stylesheet" href="/assets/index-11057528.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
26
hero_vault_extension/dist/manifest.json
vendored
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Hero Vault",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "A secure browser extension for cryptographic operations and Rhai script execution",
|
|
||||||
"action": {
|
|
||||||
"default_popup": "index.html",
|
|
||||||
"default_title": "Hero Vault"
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"16": "icons/icon-16.png",
|
|
||||||
"48": "icons/icon-48.png",
|
|
||||||
"128": "icons/icon-128.png"
|
|
||||||
},
|
|
||||||
"permissions": [
|
|
||||||
"storage",
|
|
||||||
"unlimitedStorage"
|
|
||||||
],
|
|
||||||
"background": {
|
|
||||||
"service_worker": "service-worker-loader.js",
|
|
||||||
"type": "module"
|
|
||||||
},
|
|
||||||
"content_security_policy": {
|
|
||||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import './assets/simple-background.ts-e63275e1.js';
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Hero Vault</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
4862
hero_vault_extension/package-lock.json
generated
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hero-vault-extension",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Hero Vault - A secure browser extension for cryptographic operations",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "node scripts/copy-wasm.js && vite",
|
|
||||||
"build": "node scripts/copy-wasm.js && ([ \"$NO_TYPECHECK\" = \"true\" ] || tsc) && vite build",
|
|
||||||
"watch": "node scripts/copy-wasm.js && tsc && vite build --watch",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,css,scss}\"",
|
|
||||||
"copy-wasm": "node scripts/copy-wasm.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.11.1",
|
|
||||||
"@emotion/styled": "^11.11.0",
|
|
||||||
"@mui/icons-material": "^5.14.3",
|
|
||||||
"@mui/material": "^5.14.3",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-router-dom": "^6.14.2",
|
|
||||||
"zustand": "^4.4.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@crxjs/vite-plugin": "^2.0.0-beta.18",
|
|
||||||
"@types/chrome": "^0.0.243",
|
|
||||||
"@types/node": "^20.4.5",
|
|
||||||
"@types/react": "^18.2.15",
|
|
||||||
"@types/react-dom": "^18.2.7",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
|
||||||
"esbuild": "^0.25.4",
|
|
||||||
"eslint": "^8.45.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
|
||||||
"prettier": "^3.0.0",
|
|
||||||
"sass": "^1.64.1",
|
|
||||||
"typescript": "^5.0.2",
|
|
||||||
"vite": "^4.4.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 454 B |
|
Before Width: | Height: | Size: 712 B |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Hero Vault",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "A secure browser extension for cryptographic operations and Rhai script execution",
|
|
||||||
"action": {
|
|
||||||
"default_popup": "index.html",
|
|
||||||
"default_title": "Hero Vault"
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"16": "icons/icon-16.png",
|
|
||||||
"48": "icons/icon-48.png",
|
|
||||||
"128": "icons/icon-128.png"
|
|
||||||
},
|
|
||||||
"permissions": [
|
|
||||||
"storage",
|
|
||||||
"unlimitedStorage"
|
|
||||||
],
|
|
||||||
"background": {
|
|
||||||
"service_worker": "src/background/simple-background.ts",
|
|
||||||
"type": "module"
|
|
||||||
},
|
|
||||||
"content_security_policy": {
|
|
||||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script to build the background script for the extension
|
|
||||||
*/
|
|
||||||
const { build } = require('esbuild');
|
|
||||||
const { resolve } = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
async function buildBackground() {
|
|
||||||
try {
|
|
||||||
console.log('Building background script...');
|
|
||||||
|
|
||||||
// First, create a simplified background script that doesn't import WASM
|
|
||||||
const backgroundContent = `
|
|
||||||
// Background Service Worker for SAL Modular Cryptographic Extension
|
|
||||||
// This is a simplified version that only handles messaging
|
|
||||||
|
|
||||||
console.log('Background script initialized');
|
|
||||||
|
|
||||||
// Store active WebSocket connection
|
|
||||||
let activeWebSocket = null;
|
|
||||||
let sessionActive = false;
|
|
||||||
|
|
||||||
// Listen for messages from popup or content scripts
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
||||||
console.log('Background received message:', message.type);
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_STATUS') {
|
|
||||||
sendResponse({ active: sessionActive });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_UNLOCK') {
|
|
||||||
sessionActive = true;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_LOCK') {
|
|
||||||
sessionActive = false;
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
}
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'CONNECT_WEBSOCKET') {
|
|
||||||
// Simplified WebSocket handling
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
} else {
|
|
||||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize notification setup
|
|
||||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
|
||||||
// Open the extension popup when a notification is clicked
|
|
||||||
chrome.action.openPopup();
|
|
||||||
});
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Write the simplified background script to a temporary file
|
|
||||||
fs.writeFileSync(resolve(__dirname, '../dist/background.js'), backgroundContent);
|
|
||||||
|
|
||||||
console.log('Background script built successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error building background script:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildBackground();
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script to copy WASM files from wasm_app/pkg to the extension build directory
|
|
||||||
*/
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Source and destination paths
|
|
||||||
const sourceDir = path.resolve(__dirname, '../../wasm_app/pkg');
|
|
||||||
const destDir = path.resolve(__dirname, '../public/wasm');
|
|
||||||
|
|
||||||
// Create destination directory if it doesn't exist
|
|
||||||
if (!fs.existsSync(destDir)) {
|
|
||||||
fs.mkdirSync(destDir, { recursive: true });
|
|
||||||
console.log(`Created directory: ${destDir}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy all files from source to destination
|
|
||||||
try {
|
|
||||||
const files = fs.readdirSync(sourceDir);
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
const sourcePath = path.join(sourceDir, file);
|
|
||||||
const destPath = path.join(destDir, file);
|
|
||||||
|
|
||||||
fs.copyFileSync(sourcePath, destPath);
|
|
||||||
console.log(`Copied: ${file}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('WASM files copied successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error copying WASM files:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Box, Container, Paper } from '@mui/material';
|
|
||||||
import { Routes, Route, HashRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
// Import pages
|
|
||||||
import HomePage from './pages/HomePage';
|
|
||||||
import SessionPage from './pages/SessionPage';
|
|
||||||
import KeypairPage from './pages/KeypairPage';
|
|
||||||
import ScriptPage from './pages/ScriptPage';
|
|
||||||
import SettingsPage from './pages/SettingsPage';
|
|
||||||
import WebSocketPage from './pages/WebSocketPage';
|
|
||||||
import CryptoPage from './pages/CryptoPage';
|
|
||||||
|
|
||||||
// Import components
|
|
||||||
import Header from './components/Header';
|
|
||||||
import Navigation from './components/Navigation';
|
|
||||||
|
|
||||||
// Import session state management
|
|
||||||
import { useSessionStore } from './store/sessionStore';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const { checkSessionStatus, initWasm } = useSessionStore();
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [wasmError, setWasmError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Initialize WASM and check session status on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeApp = async () => {
|
|
||||||
try {
|
|
||||||
// First initialize WASM module
|
|
||||||
const wasmInitialized = await initWasm();
|
|
||||||
|
|
||||||
if (!wasmInitialized) {
|
|
||||||
throw new Error('Failed to initialize WASM module');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check session status
|
|
||||||
await checkSessionStatus();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Initialization error:', error);
|
|
||||||
setWasmError((error as Error).message || 'Failed to initialize the extension');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeApp();
|
|
||||||
}, [checkSessionStatus, initWasm]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100vh',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wasmError) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100vh',
|
|
||||||
p: 3,
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper sx={{ p: 3, maxWidth: 400 }}>
|
|
||||||
<h6 style={{ color: 'red', marginBottom: '8px' }}>
|
|
||||||
WASM Module Failed to Initialize
|
|
||||||
</h6>
|
|
||||||
<p style={{ marginBottom: '16px' }}>
|
|
||||||
The WASM module could not be loaded. Please try reloading the extension.
|
|
||||||
</p>
|
|
||||||
<p style={{ fontSize: '0.875rem', color: 'gray' }}>
|
|
||||||
Error: {wasmError} Please contact support if the problem persists.
|
|
||||||
</p>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HashRouter>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<Container component="main" sx={{ flexGrow: 1, overflow: 'auto', py: 2 }}>
|
|
||||||
<Paper
|
|
||||||
elevation={3}
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<HomePage />} />
|
|
||||||
<Route path="/session" element={<SessionPage />} />
|
|
||||||
<Route path="/keypair" element={<KeypairPage />} />
|
|
||||||
<Route path="/crypto" element={<CryptoPage />} />
|
|
||||||
<Route path="/script" element={<ScriptPage />} />
|
|
||||||
<Route path="/websocket" element={<WebSocketPage />} />
|
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
|
||||||
</Routes>
|
|
||||||
</Paper>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
<Navigation />
|
|
||||||
</Box>
|
|
||||||
</HashRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Background Service Worker for Hero Vault Extension
|
|
||||||
*
|
|
||||||
* Responsibilities:
|
|
||||||
* - Maintain WebSocket connections
|
|
||||||
* - Handle incoming script requests
|
|
||||||
* - Manage session state when popup is closed
|
|
||||||
* - Provide messaging interface for popup/content scripts
|
|
||||||
* - Initialize WASM module when extension starts
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Import WASM helper functions
|
|
||||||
import { initWasm } from '../wasm/wasmHelper';
|
|
||||||
|
|
||||||
// Initialize WASM module when service worker starts
|
|
||||||
initWasm().catch(error => {
|
|
||||||
console.error('Failed to initialize WASM module:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store active WebSocket connection
|
|
||||||
let activeWebSocket: WebSocket | null = null;
|
|
||||||
let sessionActive = false;
|
|
||||||
|
|
||||||
// Listen for messages from popup or content scripts
|
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
||||||
if (message.type === 'SESSION_STATUS') {
|
|
||||||
sendResponse({ active: sessionActive });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_UNLOCK') {
|
|
||||||
sessionActive = true;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_LOCK') {
|
|
||||||
sessionActive = false;
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
}
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) {
|
|
||||||
connectToWebSocket(message.serverUrl, message.publicKey)
|
|
||||||
.then(success => sendResponse({ success }))
|
|
||||||
.catch(error => sendResponse({ success: false, error: error.message }));
|
|
||||||
return true; // Indicates we'll respond asynchronously
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
} else {
|
|
||||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to a WebSocket server with the user's public key
|
|
||||||
*/
|
|
||||||
async function connectToWebSocket(serverUrl: string, publicKey: string): Promise<boolean> {
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const ws = new WebSocket(serverUrl);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
// Send authentication message with public key
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'AUTH',
|
|
||||||
publicKey
|
|
||||||
}));
|
|
||||||
|
|
||||||
activeWebSocket = ws;
|
|
||||||
resolve(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
reject(new Error('Failed to connect to WebSocket server'));
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
activeWebSocket = null;
|
|
||||||
console.log('WebSocket connection closed');
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = async (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
|
|
||||||
// Handle incoming script requests
|
|
||||||
if (data.type === 'SCRIPT_REQUEST') {
|
|
||||||
// Notify the user of the script request
|
|
||||||
chrome.notifications.create({
|
|
||||||
type: 'basic',
|
|
||||||
iconUrl: 'icons/icon128.png',
|
|
||||||
title: 'Script Request',
|
|
||||||
message: `Received script request: ${data.title || 'Untitled Script'}`,
|
|
||||||
priority: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store the script request for the popup to handle
|
|
||||||
await chrome.storage.local.set({
|
|
||||||
pendingScripts: [
|
|
||||||
...(await chrome.storage.local.get('pendingScripts')).pendingScripts || [],
|
|
||||||
{
|
|
||||||
id: data.id,
|
|
||||||
title: data.title || 'Untitled Script',
|
|
||||||
description: data.description || '',
|
|
||||||
script: data.script,
|
|
||||||
tags: data.tags || [],
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing WebSocket message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize notification setup
|
|
||||||
chrome.notifications.onClicked.addListener((_notificationId) => {
|
|
||||||
// Open the extension popup when a notification is clicked
|
|
||||||
chrome.action.openPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Hero Vault Extension background service worker initialized');
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simplified Background Service Worker for Hero Vault Extension
|
|
||||||
*
|
|
||||||
* This is a version that doesn't use WASM to avoid service worker limitations
|
|
||||||
* with dynamic imports. It only handles basic messaging between components.
|
|
||||||
*/
|
|
||||||
|
|
||||||
console.log('Background script initialized');
|
|
||||||
|
|
||||||
// Store session state
|
|
||||||
let sessionActive = false;
|
|
||||||
let activeWebSocket: WebSocket | null = null;
|
|
||||||
|
|
||||||
// Listen for messages from popup or content scripts
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
||||||
console.log('Background received message:', message.type);
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_STATUS') {
|
|
||||||
sendResponse({ active: sessionActive });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_UNLOCK') {
|
|
||||||
sessionActive = true;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'SESSION_LOCK') {
|
|
||||||
sessionActive = false;
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
}
|
|
||||||
sendResponse({ success: true });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) {
|
|
||||||
// Simplified WebSocket handling
|
|
||||||
try {
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
activeWebSocket = new WebSocket(message.serverUrl);
|
|
||||||
|
|
||||||
activeWebSocket.onopen = () => {
|
|
||||||
console.log('WebSocket connection established');
|
|
||||||
// Send public key to identify this client
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.send(JSON.stringify({
|
|
||||||
type: 'IDENTIFY',
|
|
||||||
publicKey: message.publicKey
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
activeWebSocket.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
console.log('WebSocket message received:', data);
|
|
||||||
|
|
||||||
// Forward message to popup
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
type: 'WEBSOCKET_MESSAGE',
|
|
||||||
data
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Failed to forward WebSocket message:', error);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse WebSocket message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
activeWebSocket.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
};
|
|
||||||
|
|
||||||
activeWebSocket.onclose = () => {
|
|
||||||
console.log('WebSocket connection closed');
|
|
||||||
activeWebSocket = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
sendResponse({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to WebSocket:', error);
|
|
||||||
sendResponse({ success: false, error: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
|
||||||
if (activeWebSocket) {
|
|
||||||
activeWebSocket.close();
|
|
||||||
activeWebSocket = null;
|
|
||||||
sendResponse({ success: true });
|
|
||||||
} else {
|
|
||||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't handle the message, return false
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle notifications if available
|
|
||||||
if (chrome.notifications && chrome.notifications.onClicked) {
|
|
||||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
|
||||||
// Open the extension popup when a notification is clicked
|
|
||||||
chrome.action.openPopup();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { AppBar, Toolbar, Typography, IconButton, Box, Chip } from '@mui/material';
|
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
|
||||||
import SignalWifiStatusbar4BarIcon from '@mui/icons-material/SignalWifiStatusbar4Bar';
|
|
||||||
import SignalWifiOffIcon from '@mui/icons-material/SignalWifiOff';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
const {
|
|
||||||
isSessionUnlocked,
|
|
||||||
currentKeyspace,
|
|
||||||
currentKeypair,
|
|
||||||
isWebSocketConnected,
|
|
||||||
lockSession
|
|
||||||
} = useSessionStore();
|
|
||||||
|
|
||||||
const handleLockClick = async () => {
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
await lockSession();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppBar position="static" color="primary" elevation={0}>
|
|
||||||
<Toolbar>
|
|
||||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
|
||||||
Hero Vault
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|
||||||
{/* WebSocket connection status */}
|
|
||||||
{isWebSocketConnected ? (
|
|
||||||
<Chip
|
|
||||||
icon={<SignalWifiStatusbar4BarIcon fontSize="small" />}
|
|
||||||
label="Connected"
|
|
||||||
size="small"
|
|
||||||
color="success"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Chip
|
|
||||||
icon={<SignalWifiOffIcon fontSize="small" />}
|
|
||||||
label="Offline"
|
|
||||||
size="small"
|
|
||||||
color="default"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Session status */}
|
|
||||||
{isSessionUnlocked ? (
|
|
||||||
<Chip
|
|
||||||
icon={<LockOpenIcon fontSize="small" />}
|
|
||||||
label={currentKeyspace || 'Unlocked'}
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Chip
|
|
||||||
icon={<LockIcon fontSize="small" />}
|
|
||||||
label="Locked"
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current keypair */}
|
|
||||||
{isSessionUnlocked && currentKeypair && (
|
|
||||||
<Chip
|
|
||||||
label={currentKeypair.name || currentKeypair.id}
|
|
||||||
size="small"
|
|
||||||
color="secondary"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Lock button */}
|
|
||||||
{isSessionUnlocked && (
|
|
||||||
<IconButton
|
|
||||||
edge="end"
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleLockClick}
|
|
||||||
size="small"
|
|
||||||
aria-label="lock session"
|
|
||||||
>
|
|
||||||
<LockIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { BottomNavigation, BottomNavigationAction, Paper, Box, IconButton, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
|
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import HomeIcon from '@mui/icons-material/Home';
|
|
||||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
|
||||||
import CodeIcon from '@mui/icons-material/Code';
|
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
|
|
||||||
const Navigation = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const { isSessionUnlocked } = useSessionStore();
|
|
||||||
|
|
||||||
// Get current path without leading slash
|
|
||||||
const currentPath = location.pathname.substring(1) || 'home';
|
|
||||||
|
|
||||||
// State for the more menu
|
|
||||||
const [moreAnchorEl, setMoreAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
const isMoreMenuOpen = Boolean(moreAnchorEl);
|
|
||||||
|
|
||||||
const handleMoreClick = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
||||||
setMoreAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMoreClose = () => {
|
|
||||||
setMoreAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNavigation = (path: string) => {
|
|
||||||
navigate(`/${path === 'home' ? '' : path}`);
|
|
||||||
handleMoreClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{ position: 'static', bottom: 0, left: 0, right: 0 }}
|
|
||||||
elevation={3}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', width: '100%' }}>
|
|
||||||
<BottomNavigation
|
|
||||||
showLabels
|
|
||||||
value={currentPath}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
navigate(`/${newValue === 'home' ? '' : newValue}`);
|
|
||||||
}}
|
|
||||||
sx={{ flexGrow: 1 }}
|
|
||||||
>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="Home"
|
|
||||||
value="home"
|
|
||||||
icon={<HomeIcon />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="Keys"
|
|
||||||
value="keypair"
|
|
||||||
icon={<VpnKeyIcon />}
|
|
||||||
disabled={!isSessionUnlocked}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="Crypto"
|
|
||||||
value="crypto"
|
|
||||||
icon={<LockIcon />}
|
|
||||||
disabled={!isSessionUnlocked}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BottomNavigationAction
|
|
||||||
label="More"
|
|
||||||
value="more"
|
|
||||||
icon={<MoreVertIcon />}
|
|
||||||
onClick={handleMoreClick}
|
|
||||||
/>
|
|
||||||
</BottomNavigation>
|
|
||||||
|
|
||||||
<Menu
|
|
||||||
anchorEl={moreAnchorEl}
|
|
||||||
open={isMoreMenuOpen}
|
|
||||||
onClose={handleMoreClose}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => handleNavigation('script')}
|
|
||||||
disabled={!isSessionUnlocked}
|
|
||||||
selected={currentPath === 'script'}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CodeIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Scripts</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => handleNavigation('websocket')}
|
|
||||||
disabled={!isSessionUnlocked}
|
|
||||||
selected={currentPath === 'websocket'}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<WifiIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>WebSocket</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => handleNavigation('settings')}
|
|
||||||
selected={currentPath === 'settings'}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<SettingsIcon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>Settings</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Navigation;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: 'Roboto', system-ui, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-width: 360px;
|
|
||||||
min-height: 520px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
|
||||||
import App from './App';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
// Create a dark theme for the extension
|
|
||||||
const darkTheme = createTheme({
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
primary: {
|
|
||||||
main: '#6200ee',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
main: '#03dac6',
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
default: '#121212',
|
|
||||||
paper: '#1e1e1e',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typography: {
|
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
|
||||||
h1: {
|
|
||||||
fontSize: '1.5rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
h2: {
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
h3: {
|
|
||||||
fontSize: '1.125rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
MuiButton: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: 8,
|
|
||||||
textTransform: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiPaper: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<ThemeProvider theme={darkTheme}>
|
|
||||||
<CssBaseline />
|
|
||||||
<App />
|
|
||||||
</ThemeProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cryptographic Operations Page
|
|
||||||
*
|
|
||||||
* This page provides a UI for:
|
|
||||||
* - Encrypting/decrypting data using the keyspace's symmetric cipher
|
|
||||||
* - Signing/verifying messages using the selected keypair
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import type { SyntheticEvent } from '../types';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
CircularProgress,
|
|
||||||
Alert,
|
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mui/material';
|
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
import { useCryptoStore } from '../store/cryptoStore';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
const CryptoPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { isSessionUnlocked, currentKeypair } = useSessionStore();
|
|
||||||
const {
|
|
||||||
encryptData,
|
|
||||||
decryptData,
|
|
||||||
signMessage,
|
|
||||||
verifySignature,
|
|
||||||
isEncrypting,
|
|
||||||
isDecrypting,
|
|
||||||
isSigning,
|
|
||||||
isVerifying,
|
|
||||||
error,
|
|
||||||
clearError
|
|
||||||
} = useCryptoStore();
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
const [copySuccess, setCopySuccess] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Encryption state
|
|
||||||
const [plaintext, setPlaintext] = useState('');
|
|
||||||
const [encryptedData, setEncryptedData] = useState('');
|
|
||||||
|
|
||||||
// Decryption state
|
|
||||||
const [ciphertext, setCiphertext] = useState('');
|
|
||||||
const [decryptedData, setDecryptedData] = useState('');
|
|
||||||
|
|
||||||
// Signing state
|
|
||||||
const [messageToSign, setMessageToSign] = useState('');
|
|
||||||
const [signature, setSignature] = useState('');
|
|
||||||
|
|
||||||
// Verification state
|
|
||||||
const [messageToVerify, setMessageToVerify] = useState('');
|
|
||||||
const [signatureToVerify, setSignatureToVerify] = useState('');
|
|
||||||
const [isVerified, setIsVerified] = useState<boolean | null>(null);
|
|
||||||
|
|
||||||
// Redirect if not unlocked
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked, navigate]);
|
|
||||||
|
|
||||||
const handleTabChange = (_event: React.SyntheticEvent<Element, Event>, newValue: number) => {
|
|
||||||
setActiveTab(newValue);
|
|
||||||
clearError();
|
|
||||||
setCopySuccess(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEncrypt = async () => {
|
|
||||||
try {
|
|
||||||
const result = await encryptData(plaintext);
|
|
||||||
setEncryptedData(result);
|
|
||||||
} catch (err) {
|
|
||||||
// Error is already handled in the store
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDecrypt = async () => {
|
|
||||||
try {
|
|
||||||
const result = await decryptData(ciphertext);
|
|
||||||
setDecryptedData(result);
|
|
||||||
} catch (err) {
|
|
||||||
// Error is already handled in the store
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSign = async () => {
|
|
||||||
try {
|
|
||||||
const result = await signMessage(messageToSign);
|
|
||||||
setSignature(result);
|
|
||||||
} catch (err) {
|
|
||||||
// Error is already handled in the store
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerify = async () => {
|
|
||||||
try {
|
|
||||||
const result = await verifySignature(messageToVerify, signatureToVerify);
|
|
||||||
setIsVerified(result);
|
|
||||||
} catch (err) {
|
|
||||||
setIsVerified(false);
|
|
||||||
// Error is already handled in the store
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = (text: string, label: string) => {
|
|
||||||
navigator.clipboard.writeText(text).then(
|
|
||||||
() => {
|
|
||||||
setCopySuccess(`${label} copied to clipboard!`);
|
|
||||||
setTimeout(() => setCopySuccess(null), 2000);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
setCopySuccess('Failed to copy!');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
return null; // Will redirect via useEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>Cryptographic Operations</Typography>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{copySuccess && (
|
|
||||||
<Alert severity="success" sx={{ mb: 2 }}>
|
|
||||||
{copySuccess}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paper sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
||||||
{/* Tabs with smaller width and scrollable */}
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
variant="scrollable"
|
|
||||||
scrollButtons="auto"
|
|
||||||
allowScrollButtonsMobile
|
|
||||||
sx={{ minHeight: '48px' }}
|
|
||||||
>
|
|
||||||
<Tab label="Encrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
|
||||||
<Tab label="Decrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
|
||||||
<Tab label="Sign" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
|
||||||
<Tab label="Verify" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
|
||||||
</Tabs>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Content area with proper scrolling */}
|
|
||||||
<Box sx={{ p: 2, flexGrow: 1, overflow: 'auto', height: 'calc(100% - 48px)' }}>
|
|
||||||
{/* Encryption Tab */}
|
|
||||||
{activeTab === 0 && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle1" gutterBottom>Encrypt Data</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" paragraph>
|
|
||||||
Data will be encrypted using ChaCha20-Poly1305 with a key derived from your keyspace password.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Data to Encrypt"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={plaintext}
|
|
||||||
onChange={(e) => setPlaintext(e.target.value)}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleEncrypt}
|
|
||||||
disabled={!plaintext || isEncrypting}
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
|
||||||
{isEncrypting ? <CircularProgress size={24} /> : 'Encrypt'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{encryptedData && (
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Divider sx={{ my: 2 }} />
|
|
||||||
<Typography variant="subtitle1">Encrypted Result</Typography>
|
|
||||||
<Box sx={{ position: 'relative' }}>
|
|
||||||
<TextField
|
|
||||||
label="Encrypted Data (Base64)"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={encryptedData}
|
|
||||||
InputProps={{ readOnly: true }}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Tooltip title="Copy to clipboard">
|
|
||||||
<IconButton
|
|
||||||
sx={{ position: 'absolute', top: 8, right: 8 }}
|
|
||||||
onClick={() => copyToClipboard(encryptedData, 'Encrypted data')}
|
|
||||||
>
|
|
||||||
<ContentCopyIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Decryption Tab */}
|
|
||||||
{activeTab === 1 && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle1" gutterBottom>Decrypt Data</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" paragraph>
|
|
||||||
Paste encrypted data (in Base64 format) to decrypt it using your keyspace password.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Encrypted Data (Base64)"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={ciphertext}
|
|
||||||
onChange={(e) => setCiphertext(e.target.value)}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleDecrypt}
|
|
||||||
disabled={!ciphertext || isDecrypting}
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
|
||||||
{isDecrypting ? <CircularProgress size={24} /> : 'Decrypt'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{decryptedData && (
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Divider sx={{ my: 2 }} />
|
|
||||||
<Typography variant="subtitle1">Decrypted Result</Typography>
|
|
||||||
<Box sx={{ position: 'relative' }}>
|
|
||||||
<TextField
|
|
||||||
label="Decrypted Data"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={decryptedData}
|
|
||||||
InputProps={{ readOnly: true }}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Tooltip title="Copy to clipboard">
|
|
||||||
<IconButton
|
|
||||||
sx={{ position: 'absolute', top: 8, right: 8 }}
|
|
||||||
onClick={() => copyToClipboard(decryptedData, 'Decrypted data')}
|
|
||||||
>
|
|
||||||
<ContentCopyIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Signing Tab */}
|
|
||||||
{activeTab === 2 && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle1" gutterBottom>Sign Message</Typography>
|
|
||||||
|
|
||||||
{!currentKeypair ? (
|
|
||||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
||||||
Please select a keypair from the Keypair page before signing messages.
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
|
||||||
Signing with keypair: {currentKeypair.name || currentKeypair.id.substring(0, 8)}...
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Message to Sign"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={messageToSign}
|
|
||||||
onChange={(e) => setMessageToSign(e.target.value)}
|
|
||||||
margin="normal"
|
|
||||||
disabled={!currentKeypair}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleSign}
|
|
||||||
disabled={!messageToSign || !currentKeypair || isSigning}
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
|
||||||
{isSigning ? <CircularProgress size={24} /> : 'Sign Message'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{signature && (
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Divider sx={{ my: 2 }} />
|
|
||||||
<Typography variant="subtitle1">Signature</Typography>
|
|
||||||
<Box sx={{ position: 'relative' }}>
|
|
||||||
<TextField
|
|
||||||
label="Signature (Hex)"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={signature}
|
|
||||||
InputProps={{ readOnly: true }}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Tooltip title="Copy to clipboard">
|
|
||||||
<IconButton
|
|
||||||
sx={{ position: 'absolute', top: 8, right: 8 }}
|
|
||||||
onClick={() => copyToClipboard(signature, 'Signature')}
|
|
||||||
>
|
|
||||||
<ContentCopyIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Verification Tab */}
|
|
||||||
{activeTab === 3 && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle1" gutterBottom>Verify Signature</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" paragraph>
|
|
||||||
Verify that a message was signed by the currently selected keypair.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Message"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
fullWidth
|
|
||||||
value={messageToVerify}
|
|
||||||
onChange={(e) => setMessageToVerify(e.target.value)}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
label="Signature (Hex)"
|
|
||||||
multiline
|
|
||||||
rows={2}
|
|
||||||
fullWidth
|
|
||||||
value={signatureToVerify}
|
|
||||||
onChange={(e) => setSignatureToVerify(e.target.value)}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleVerify}
|
|
||||||
disabled={!messageToVerify || !signatureToVerify || isVerifying}
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
>
|
|
||||||
{isVerifying ? <CircularProgress size={24} /> : 'Verify Signature'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isVerified !== null && (
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Alert severity={isVerified ? "success" : "error"}>
|
|
||||||
{isVerified
|
|
||||||
? "Signature is valid! The message was signed by the expected keypair."
|
|
||||||
: "Invalid signature. The message may have been tampered with or signed by a different keypair."}
|
|
||||||
</Alert>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CryptoPage;
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
TextField,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Stack,
|
|
||||||
Alert,
|
|
||||||
CircularProgress
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
|
|
||||||
const HomePage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { isSessionUnlocked, unlockSession, createKeyspace } = useSessionStore();
|
|
||||||
|
|
||||||
const [keyspace, setKeyspace] = useState<string>('');
|
|
||||||
const [password, setPassword] = useState<string>('');
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [mode, setMode] = useState<'unlock' | 'create'>('unlock');
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError(null);
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let success = false;
|
|
||||||
|
|
||||||
if (mode === 'unlock') {
|
|
||||||
success = await unlockSession(keyspace, password);
|
|
||||||
} else {
|
|
||||||
success = await createKeyspace(keyspace, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Navigate to keypair page on success
|
|
||||||
navigate('/keypair');
|
|
||||||
} else {
|
|
||||||
setError(mode === 'unlock'
|
|
||||||
? 'Failed to unlock keyspace. Check your password and try again.'
|
|
||||||
: 'Failed to create keyspace. Please try again.');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'An unexpected error occurred');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
return (
|
|
||||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
||||||
<Typography variant="h5" gutterBottom>
|
|
||||||
Welcome to Hero Vault
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary" paragraph>
|
|
||||||
Your session is unlocked. You can now use the extension features.
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" spacing={2} justifyContent="center" mt={3}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => navigate('/keypair')}
|
|
||||||
>
|
|
||||||
Manage Keys
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
onClick={() => navigate('/script')}
|
|
||||||
>
|
|
||||||
Run Scripts
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ maxWidth: 400, mx: 'auto', py: 2 }}>
|
|
||||||
<Typography variant="h5" align="center" gutterBottom>
|
|
||||||
Hero Vault
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Card variant="outlined" sx={{ mt: 3 }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
{mode === 'unlock' ? 'Unlock Keyspace' : 'Create New Keyspace'}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<TextField
|
|
||||||
label="Keyspace Name"
|
|
||||||
value={keyspace}
|
|
||||||
onChange={(e) => setKeyspace(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
onClick={() => setMode(mode === 'unlock' ? 'create' : 'unlock')}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{mode === 'unlock' ? 'Create New Keyspace' : 'Unlock Existing'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
disabled={isLoading || !keyspace || !password}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<CircularProgress size={24} color="inherit" />
|
|
||||||
) : mode === 'unlock' ? (
|
|
||||||
'Unlock'
|
|
||||||
) : (
|
|
||||||
'Create'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HomePage;
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
ListItemSecondaryAction,
|
|
||||||
IconButton,
|
|
||||||
Divider,
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
TextField,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
CircularProgress,
|
|
||||||
Paper,
|
|
||||||
Alert,
|
|
||||||
Chip
|
|
||||||
} from '@mui/material';
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
const KeypairPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
isSessionUnlocked,
|
|
||||||
availableKeypairs,
|
|
||||||
currentKeypair,
|
|
||||||
listKeypairs,
|
|
||||||
selectKeypair,
|
|
||||||
createKeypair
|
|
||||||
} = useSessionStore();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
||||||
const [newKeypairName, setNewKeypairName] = useState('');
|
|
||||||
const [newKeypairType, setNewKeypairType] = useState('Secp256k1');
|
|
||||||
const [newKeypairDescription, setNewKeypairDescription] = useState('');
|
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
|
||||||
|
|
||||||
// Redirect if not unlocked
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked, navigate]);
|
|
||||||
|
|
||||||
// Load keypairs on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const loadKeypairs = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
await listKeypairs();
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to load keypairs');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
loadKeypairs();
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked, listKeypairs]);
|
|
||||||
|
|
||||||
const handleSelectKeypair = async (keypairId: string) => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
await selectKeypair(keypairId);
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to select keypair');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateKeypair = async () => {
|
|
||||||
try {
|
|
||||||
setIsCreating(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
await createKeypair(newKeypairType, {
|
|
||||||
name: newKeypairName,
|
|
||||||
description: newKeypairDescription
|
|
||||||
});
|
|
||||||
|
|
||||||
setCreateDialogOpen(false);
|
|
||||||
setNewKeypairName('');
|
|
||||||
setNewKeypairDescription('');
|
|
||||||
|
|
||||||
// Refresh the list
|
|
||||||
await listKeypairs();
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to create keypair');
|
|
||||||
} finally {
|
|
||||||
setIsCreating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
return null; // Will redirect via useEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
||||||
<Typography variant="h6">Keypair Management</Typography>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={<AddIcon />}
|
|
||||||
onClick={() => setCreateDialogOpen(true)}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Create New
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : availableKeypairs.length === 0 ? (
|
|
||||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
No keypairs found. Create your first keypair to get started.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
) : (
|
|
||||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
|
||||||
<List disablePadding>
|
|
||||||
{availableKeypairs.map((keypair: any, index: number) => (
|
|
||||||
<Box key={keypair.id}>
|
|
||||||
{index > 0 && <Divider />}
|
|
||||||
<ListItem
|
|
||||||
button
|
|
||||||
selected={currentKeypair?.id === keypair.id}
|
|
||||||
onClick={() => handleSelectKeypair(keypair.id)}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
{keypair.name || keypair.id}
|
|
||||||
<Chip
|
|
||||||
label={keypair.type}
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{keypair.description || 'No description'}
|
|
||||||
<br />
|
|
||||||
Created: {new Date(keypair.createdAt).toLocaleString()}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
{currentKeypair?.id === keypair.id && (
|
|
||||||
<IconButton edge="end" disabled>
|
|
||||||
<CheckIcon color="success" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Create Keypair Dialog */}
|
|
||||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth>
|
|
||||||
<DialogTitle>Create New Keypair</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<TextField
|
|
||||||
label="Name"
|
|
||||||
value={newKeypairName}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairName(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
disabled={isCreating}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControl fullWidth margin="normal">
|
|
||||||
<InputLabel>Type</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={newKeypairType}
|
|
||||||
onChange={(e) => setNewKeypairType(e.target.value)}
|
|
||||||
disabled={isCreating}
|
|
||||||
>
|
|
||||||
<MenuItem value="Ed25519">Ed25519</MenuItem>
|
|
||||||
<MenuItem value="Secp256k1">Secp256k1 (Ethereum)</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Description"
|
|
||||||
value={newKeypairDescription}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairDescription(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
multiline
|
|
||||||
rows={2}
|
|
||||||
disabled={isCreating}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => setCreateDialogOpen(false)} disabled={isCreating}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateKeypair}
|
|
||||||
color="primary"
|
|
||||||
variant="contained"
|
|
||||||
disabled={isCreating || !newKeypairName}
|
|
||||||
>
|
|
||||||
{isCreating ? <CircularProgress size={24} /> : 'Create'}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeypairPage;
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { getChromeApi } from '../utils/chromeApi';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
TextField,
|
|
||||||
Paper,
|
|
||||||
Alert,
|
|
||||||
CircularProgress,
|
|
||||||
Divider,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
ListItemSecondaryAction,
|
|
||||||
IconButton,
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Chip
|
|
||||||
} from '@mui/material';
|
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
|
||||||
// DeleteIcon removed as it's not used
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
|
|
||||||
interface ScriptResult {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
script: string;
|
|
||||||
result: string;
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PendingScript {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
script: string;
|
|
||||||
tags: string[];
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScriptPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { isSessionUnlocked, currentKeypair } = useSessionStore();
|
|
||||||
|
|
||||||
const [tabValue, setTabValue] = useState<number>(0);
|
|
||||||
const [scriptInput, setScriptInput] = useState<string>('');
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
|
||||||
const [executionResult, setExecutionResult] = useState<string | null>(null);
|
|
||||||
const [executionSuccess, setExecutionSuccess] = useState<boolean | null>(null);
|
|
||||||
const [scriptResults, setScriptResults] = useState<ScriptResult[]>([]);
|
|
||||||
const [pendingScripts, setPendingScripts] = useState<PendingScript[]>([]);
|
|
||||||
const [selectedPendingScript, setSelectedPendingScript] = useState<PendingScript | null>(null);
|
|
||||||
const [scriptDialogOpen, setScriptDialogOpen] = useState<boolean>(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Redirect if not unlocked
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked, navigate]);
|
|
||||||
|
|
||||||
// Load pending scripts from storage
|
|
||||||
useEffect(() => {
|
|
||||||
const loadPendingScripts = async () => {
|
|
||||||
try {
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
const data = await chromeApi.storage.local.get('pendingScripts');
|
|
||||||
if (data.pendingScripts) {
|
|
||||||
setPendingScripts(data.pendingScripts);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load pending scripts:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
loadPendingScripts();
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked]);
|
|
||||||
|
|
||||||
// Load script history from storage
|
|
||||||
useEffect(() => {
|
|
||||||
const loadScriptResults = async () => {
|
|
||||||
try {
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
const data = await chromeApi.storage.local.get('scriptResults');
|
|
||||||
if (data.scriptResults) {
|
|
||||||
setScriptResults(data.scriptResults);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load script results:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
loadScriptResults();
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked]);
|
|
||||||
|
|
||||||
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
|
|
||||||
setTabValue(newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExecuteScript = async () => {
|
|
||||||
if (!scriptInput.trim()) return;
|
|
||||||
|
|
||||||
setIsExecuting(true);
|
|
||||||
setError(null);
|
|
||||||
setExecutionResult(null);
|
|
||||||
setExecutionSuccess(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call the WASM run_rhai function via our store
|
|
||||||
const result = await useSessionStore.getState().executeScript(scriptInput);
|
|
||||||
|
|
||||||
setExecutionResult(result);
|
|
||||||
setExecutionSuccess(true);
|
|
||||||
|
|
||||||
// Save to history
|
|
||||||
const newResult: ScriptResult = {
|
|
||||||
id: `script-${Date.now()}`,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
script: scriptInput,
|
|
||||||
result,
|
|
||||||
success: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedResults = [newResult, ...scriptResults].slice(0, 20); // Keep last 20
|
|
||||||
setScriptResults(updatedResults);
|
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.storage.local.set({ scriptResults: updatedResults });
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to execute script');
|
|
||||||
setExecutionSuccess(false);
|
|
||||||
setExecutionResult('Execution failed');
|
|
||||||
} finally {
|
|
||||||
setIsExecuting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewPendingScript = (script: PendingScript) => {
|
|
||||||
setSelectedPendingScript(script);
|
|
||||||
setScriptDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApprovePendingScript = async () => {
|
|
||||||
if (!selectedPendingScript) return;
|
|
||||||
|
|
||||||
setScriptDialogOpen(false);
|
|
||||||
setScriptInput(selectedPendingScript.script);
|
|
||||||
setTabValue(0); // Switch to execute tab
|
|
||||||
|
|
||||||
// Remove from pending list
|
|
||||||
const updatedPendingScripts = pendingScripts.filter(
|
|
||||||
script => script.id !== selectedPendingScript.id
|
|
||||||
);
|
|
||||||
|
|
||||||
setPendingScripts(updatedPendingScripts);
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts });
|
|
||||||
setSelectedPendingScript(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRejectPendingScript = async () => {
|
|
||||||
if (!selectedPendingScript) return;
|
|
||||||
|
|
||||||
// Remove from pending list
|
|
||||||
const updatedPendingScripts = pendingScripts.filter(
|
|
||||||
script => script.id !== selectedPendingScript.id
|
|
||||||
);
|
|
||||||
|
|
||||||
setPendingScripts(updatedPendingScripts);
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts });
|
|
||||||
|
|
||||||
setScriptDialogOpen(false);
|
|
||||||
setSelectedPendingScript(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearHistory = async () => {
|
|
||||||
setScriptResults([]);
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.storage.local.set({ scriptResults: [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
return null; // Will redirect via useEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
|
||||||
<Tabs
|
|
||||||
value={tabValue}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
aria-label="script tabs"
|
|
||||||
variant="scrollable"
|
|
||||||
scrollButtons="auto"
|
|
||||||
allowScrollButtonsMobile
|
|
||||||
sx={{ minHeight: '48px' }}
|
|
||||||
>
|
|
||||||
<Tab label="Execute" sx={{ minHeight: '48px', py: 0 }} />
|
|
||||||
<Tab
|
|
||||||
label={
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
Pending
|
|
||||||
{pendingScripts.length > 0 && (
|
|
||||||
<Chip
|
|
||||||
label={pendingScripts.length}
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
sx={{ minHeight: '48px', py: 0 }}
|
|
||||||
/>
|
|
||||||
<Tab label="History" sx={{ minHeight: '48px', py: 0 }} />
|
|
||||||
</Tabs>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Execute Tab */}
|
|
||||||
{tabValue === 0 && (
|
|
||||||
<Box sx={{
|
|
||||||
p: 2,
|
|
||||||
flexGrow: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: 'calc(100% - 48px)' // Subtract tab height
|
|
||||||
}}>
|
|
||||||
<Box sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflow: 'auto',
|
|
||||||
height: '100%',
|
|
||||||
pb: 2 // Add padding at bottom for scrolling
|
|
||||||
}}>
|
|
||||||
{!currentKeypair && (
|
|
||||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
||||||
No keypair selected. Select a keypair to enable script execution with signing capabilities.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Rhai Script"
|
|
||||||
multiline
|
|
||||||
rows={6} // Reduced from 8 to leave more space for results
|
|
||||||
value={scriptInput}
|
|
||||||
onChange={(e) => setScriptInput(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
placeholder="Enter your Rhai script here..."
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
disabled={isExecuting}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<PlayArrowIcon />}
|
|
||||||
onClick={handleExecuteScript}
|
|
||||||
disabled={isExecuting || !scriptInput.trim()}
|
|
||||||
>
|
|
||||||
{isExecuting ? <CircularProgress size={24} /> : 'Execute'}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{executionResult && (
|
|
||||||
<Paper
|
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
bgcolor: executionSuccess ? 'success.dark' : 'error.dark',
|
|
||||||
color: 'white',
|
|
||||||
overflowY: 'auto',
|
|
||||||
mb: 2, // Add margin at bottom
|
|
||||||
minHeight: '100px', // Ensure minimum height for visibility
|
|
||||||
maxHeight: '200px' // Limit maximum height
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
Execution Result:
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
component="pre"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'monospace'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{executionResult}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pending Scripts Tab */}
|
|
||||||
{tabValue === 1 && (
|
|
||||||
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
|
||||||
{pendingScripts.length === 0 ? (
|
|
||||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
No pending scripts. Incoming scripts from connected WebSocket servers will appear here.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
) : (
|
|
||||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
|
||||||
<List disablePadding>
|
|
||||||
{pendingScripts.map((script, index) => (
|
|
||||||
<Box key={script.id}>
|
|
||||||
{index > 0 && <Divider />}
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary={script.title}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{script.description || 'No description'}
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ mt: 0.5 }}>
|
|
||||||
{script.tags.map(tag => (
|
|
||||||
<Chip
|
|
||||||
key={tag}
|
|
||||||
label={tag}
|
|
||||||
size="small"
|
|
||||||
color={tag === 'remote' ? 'secondary' : 'primary'}
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ mr: 0.5 }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<IconButton
|
|
||||||
edge="end"
|
|
||||||
onClick={() => handleViewPendingScript(script)}
|
|
||||||
aria-label="view script"
|
|
||||||
>
|
|
||||||
<VisibilityIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* History Tab */}
|
|
||||||
{tabValue === 2 && (
|
|
||||||
<Box sx={{
|
|
||||||
p: 2,
|
|
||||||
flexGrow: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: 'calc(100% - 48px)' // Subtract tab height
|
|
||||||
}}>
|
|
||||||
<Box sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflow: 'auto',
|
|
||||||
height: '100%',
|
|
||||||
pb: 2 // Add padding at bottom for scrolling
|
|
||||||
}}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
size="small"
|
|
||||||
onClick={handleClearHistory}
|
|
||||||
disabled={scriptResults.length === 0}
|
|
||||||
>
|
|
||||||
Clear History
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{scriptResults.length === 0 ? (
|
|
||||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
No script execution history yet.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
) : (
|
|
||||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
|
||||||
<List disablePadding>
|
|
||||||
{scriptResults.map((result, index) => (
|
|
||||||
<Box key={result.id}>
|
|
||||||
{index > 0 && <Divider />}
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
{new Date(result.timestamp).toLocaleString()}
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label={result.success ? 'Success' : 'Failed'}
|
|
||||||
size="small"
|
|
||||||
color={result.success ? 'success' : 'error'}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
maxWidth: '280px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result.script}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<IconButton
|
|
||||||
edge="end"
|
|
||||||
onClick={() => {
|
|
||||||
setScriptInput(result.script);
|
|
||||||
setTabValue(0);
|
|
||||||
}}
|
|
||||||
aria-label="reuse script"
|
|
||||||
>
|
|
||||||
<PlayArrowIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pending Script Dialog */}
|
|
||||||
<Dialog
|
|
||||||
open={scriptDialogOpen}
|
|
||||||
onClose={() => setScriptDialogOpen(false)}
|
|
||||||
maxWidth="md"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<DialogTitle>
|
|
||||||
{selectedPendingScript?.title || 'Script Details'}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
{selectedPendingScript && (
|
|
||||||
<>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
Description:
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" paragraph>
|
|
||||||
{selectedPendingScript.description || 'No description provided'}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
{selectedPendingScript.tags.map(tag => (
|
|
||||||
<Chip
|
|
||||||
key={tag}
|
|
||||||
label={tag}
|
|
||||||
size="small"
|
|
||||||
color={tag === 'remote' ? 'secondary' : 'primary'}
|
|
||||||
sx={{ mr: 0.5 }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
Script Content:
|
|
||||||
</Typography>
|
|
||||||
<Paper
|
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
maxHeight: '300px',
|
|
||||||
overflow: 'auto'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
component="pre"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: 'monospace'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedPendingScript.script}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{selectedPendingScript.tags.includes('remote')
|
|
||||||
? 'This is a remote script. If approved, your signature will be sent to the server and the script may execute remotely.'
|
|
||||||
: 'This script will execute locally in your browser extension if approved.'}
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
onClick={handleRejectPendingScript}
|
|
||||||
color="error"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleApprovePendingScript}
|
|
||||||
color="primary"
|
|
||||||
variant="contained"
|
|
||||||
>
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ScriptPage;
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
Alert,
|
|
||||||
CircularProgress,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Divider,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Grid
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import SecurityIcon from '@mui/icons-material/Security';
|
|
||||||
// HistoryIcon removed as it's not used
|
|
||||||
|
|
||||||
interface SessionActivity {
|
|
||||||
id: string;
|
|
||||||
action: string;
|
|
||||||
timestamp: number;
|
|
||||||
details?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SessionPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
isSessionUnlocked,
|
|
||||||
currentKeyspace,
|
|
||||||
currentKeypair,
|
|
||||||
lockSession
|
|
||||||
} = useSessionStore();
|
|
||||||
|
|
||||||
const [sessionActivities, setSessionActivities] = useState<SessionActivity[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
// Redirect if not unlocked
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked, navigate]);
|
|
||||||
|
|
||||||
// Load session activities from storage
|
|
||||||
useEffect(() => {
|
|
||||||
const loadSessionActivities = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const data = await chrome.storage.local.get('sessionActivities');
|
|
||||||
if (data.sessionActivities) {
|
|
||||||
setSessionActivities(data.sessionActivities);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load session activities:', err);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
loadSessionActivities();
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked]);
|
|
||||||
|
|
||||||
const handleLockSession = async () => {
|
|
||||||
try {
|
|
||||||
await lockSession();
|
|
||||||
navigate('/');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to lock session:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
return null; // Will redirect via useEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Session Management
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Card variant="outlined">
|
|
||||||
<CardContent>
|
|
||||||
<Typography color="text.secondary" gutterBottom>
|
|
||||||
Current Keyspace
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5" component="div">
|
|
||||||
{currentKeyspace || 'None'}
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Card variant="outlined">
|
|
||||||
<CardContent>
|
|
||||||
<Typography color="text.secondary" gutterBottom>
|
|
||||||
Selected Keypair
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5" component="div">
|
|
||||||
{currentKeypair?.name || currentKeypair?.id || 'None'}
|
|
||||||
</Typography>
|
|
||||||
{currentKeypair && (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Type: {currentKeypair.type}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
|
||||||
<Typography variant="subtitle1">
|
|
||||||
Session Activity
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
startIcon={<LockIcon />}
|
|
||||||
onClick={handleLockSession}
|
|
||||||
>
|
|
||||||
Lock Session
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
) : sessionActivities.length === 0 ? (
|
|
||||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
No session activity recorded yet.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
) : (
|
|
||||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
|
||||||
<List disablePadding>
|
|
||||||
{sessionActivities.map((activity, index) => (
|
|
||||||
<Box key={activity.id}>
|
|
||||||
{index > 0 && <Divider />}
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
{activity.action}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{new Date(activity.timestamp).toLocaleString()}
|
|
||||||
</Typography>
|
|
||||||
{activity.details && (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{activity.details}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Alert severity="info" icon={<SecurityIcon />}>
|
|
||||||
Your session is active. All cryptographic operations and script executions require explicit approval.
|
|
||||||
</Alert>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SessionPage;
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Switch,
|
|
||||||
// FormControlLabel removed as it's not used
|
|
||||||
Divider,
|
|
||||||
Paper,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
TextField,
|
|
||||||
Alert,
|
|
||||||
Snackbar
|
|
||||||
} from '@mui/material';
|
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
|
||||||
|
|
||||||
interface Settings {
|
|
||||||
darkMode: boolean;
|
|
||||||
autoLockTimeout: number; // minutes
|
|
||||||
confirmCryptoOperations: boolean;
|
|
||||||
showScriptNotifications: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsPage = () => {
|
|
||||||
const [settings, setSettings] = useState<Settings>({
|
|
||||||
darkMode: true,
|
|
||||||
autoLockTimeout: 15,
|
|
||||||
confirmCryptoOperations: true,
|
|
||||||
showScriptNotifications: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const [clearDataDialogOpen, setClearDataDialogOpen] = useState(false);
|
|
||||||
const [confirmText, setConfirmText] = useState('');
|
|
||||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
|
||||||
|
|
||||||
// Load settings from storage
|
|
||||||
useEffect(() => {
|
|
||||||
const loadSettings = async () => {
|
|
||||||
try {
|
|
||||||
const data = await chrome.storage.local.get('settings');
|
|
||||||
if (data.settings) {
|
|
||||||
setSettings(data.settings);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load settings:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Save settings when changed
|
|
||||||
const handleSettingChange = (key: keyof Settings, value: boolean | number) => {
|
|
||||||
const updatedSettings = { ...settings, [key]: value };
|
|
||||||
setSettings(updatedSettings);
|
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
chrome.storage.local.set({ settings: updatedSettings })
|
|
||||||
.then(() => {
|
|
||||||
setSnackbarMessage('Settings saved');
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Failed to save settings:', err);
|
|
||||||
setSnackbarMessage('Failed to save settings');
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAllData = () => {
|
|
||||||
if (confirmText !== 'CLEAR ALL DATA') {
|
|
||||||
setSnackbarMessage('Please type the confirmation text exactly');
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all extension data
|
|
||||||
chrome.storage.local.clear()
|
|
||||||
.then(() => {
|
|
||||||
setSnackbarMessage('All data cleared successfully');
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
setClearDataDialogOpen(false);
|
|
||||||
setConfirmText('');
|
|
||||||
|
|
||||||
// Reset settings to defaults
|
|
||||||
setSettings({
|
|
||||||
darkMode: true,
|
|
||||||
autoLockTimeout: 15,
|
|
||||||
confirmCryptoOperations: true,
|
|
||||||
showScriptNotifications: true
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Failed to clear data:', err);
|
|
||||||
setSnackbarMessage('Failed to clear data');
|
|
||||||
setSnackbarOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Settings
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
|
||||||
<List disablePadding>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary="Dark Mode"
|
|
||||||
secondary="Use dark theme for the extension"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={settings.darkMode}
|
|
||||||
onChange={(e) => handleSettingChange('darkMode', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary="Auto-Lock Timeout"
|
|
||||||
secondary={`Automatically lock session after ${settings.autoLockTimeout} minutes of inactivity`}
|
|
||||||
/>
|
|
||||||
<Box sx={{ width: 120 }}>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
size="small"
|
|
||||||
value={settings.autoLockTimeout}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseInt(e.target.value);
|
|
||||||
if (!isNaN(value) && value >= 1) {
|
|
||||||
handleSettingChange('autoLockTimeout', value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
InputProps={{ inputProps: { min: 1, max: 60 } }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary="Confirm Cryptographic Operations"
|
|
||||||
secondary="Always ask for confirmation before signing or encrypting"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={settings.confirmCryptoOperations}
|
|
||||||
onChange={(e) => handleSettingChange('confirmCryptoOperations', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary="Script Notifications"
|
|
||||||
secondary="Show notifications when new scripts are received"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={settings.showScriptNotifications}
|
|
||||||
onChange={(e) => handleSettingChange('showScriptNotifications', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Alert
|
|
||||||
severity="info"
|
|
||||||
icon={<InfoIcon />}
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
>
|
|
||||||
<Typography variant="body2">
|
|
||||||
The extension stores all cryptographic keys in encrypted form. Your password is never stored and is only kept in memory while the session is unlocked.
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
onClick={() => setClearDataDialogOpen(true)}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
Clear All Data
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Clear Data Confirmation Dialog */}
|
|
||||||
<Dialog open={clearDataDialogOpen} onClose={() => setClearDataDialogOpen(false)}>
|
|
||||||
<DialogTitle>Clear All Extension Data</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Typography variant="body1" paragraph>
|
|
||||||
This will permanently delete all your keyspaces, keypairs, and settings. This action cannot be undone.
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="error" paragraph>
|
|
||||||
Type "CLEAR ALL DATA" to confirm:
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
value={confirmText}
|
|
||||||
onChange={(e) => setConfirmText(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
placeholder="CLEAR ALL DATA"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => setClearDataDialogOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleClearAllData}
|
|
||||||
color="error"
|
|
||||||
disabled={confirmText !== 'CLEAR ALL DATA'}
|
|
||||||
>
|
|
||||||
Clear All Data
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Snackbar for notifications */}
|
|
||||||
<Snackbar
|
|
||||||
open={snackbarOpen}
|
|
||||||
autoHideDuration={3000}
|
|
||||||
onClose={() => setSnackbarOpen(false)}
|
|
||||||
message={snackbarMessage}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsPage;
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
TextField,
|
|
||||||
Paper,
|
|
||||||
Alert,
|
|
||||||
CircularProgress,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Divider,
|
|
||||||
Chip
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useSessionStore } from '../store/sessionStore';
|
|
||||||
|
|
||||||
interface ConnectionHistory {
|
|
||||||
id: string;
|
|
||||||
url: string;
|
|
||||||
timestamp: number;
|
|
||||||
status: 'connected' | 'disconnected';
|
|
||||||
}
|
|
||||||
|
|
||||||
const WebSocketPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
isSessionUnlocked,
|
|
||||||
currentKeypair,
|
|
||||||
isWebSocketConnected,
|
|
||||||
webSocketUrl,
|
|
||||||
connectWebSocket,
|
|
||||||
disconnectWebSocket
|
|
||||||
} = useSessionStore();
|
|
||||||
|
|
||||||
const [serverUrl, setServerUrl] = useState('');
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [connectionHistory, setConnectionHistory] = useState<ConnectionHistory[]>([]);
|
|
||||||
|
|
||||||
// Redirect if not unlocked
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked, navigate]);
|
|
||||||
|
|
||||||
// Load connection history from storage
|
|
||||||
useEffect(() => {
|
|
||||||
const loadConnectionHistory = async () => {
|
|
||||||
try {
|
|
||||||
const data = await chrome.storage.local.get('connectionHistory');
|
|
||||||
if (data.connectionHistory) {
|
|
||||||
setConnectionHistory(data.connectionHistory);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load connection history:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSessionUnlocked) {
|
|
||||||
loadConnectionHistory();
|
|
||||||
}
|
|
||||||
}, [isSessionUnlocked]);
|
|
||||||
|
|
||||||
const handleConnect = async () => {
|
|
||||||
if (!serverUrl.trim() || !currentKeypair) return;
|
|
||||||
|
|
||||||
setIsConnecting(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const success = await connectWebSocket(serverUrl);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Add to connection history
|
|
||||||
const newConnection: ConnectionHistory = {
|
|
||||||
id: `conn-${Date.now()}`,
|
|
||||||
url: serverUrl,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
status: 'connected'
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedHistory = [newConnection, ...connectionHistory].slice(0, 10); // Keep last 10
|
|
||||||
setConnectionHistory(updatedHistory);
|
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
await chrome.storage.local.set({ connectionHistory: updatedHistory });
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to connect to WebSocket server');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to connect to WebSocket server');
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = async () => {
|
|
||||||
try {
|
|
||||||
const success = await disconnectWebSocket();
|
|
||||||
|
|
||||||
if (success && webSocketUrl) {
|
|
||||||
// Update connection history
|
|
||||||
const updatedHistory = connectionHistory.map(conn =>
|
|
||||||
conn.url === webSocketUrl && conn.status === 'connected'
|
|
||||||
? { ...conn, status: 'disconnected' }
|
|
||||||
: conn
|
|
||||||
);
|
|
||||||
|
|
||||||
setConnectionHistory(updatedHistory);
|
|
||||||
|
|
||||||
// Save to storage
|
|
||||||
await chrome.storage.local.set({ connectionHistory: updatedHistory });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to disconnect from WebSocket server');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuickConnect = (url: string) => {
|
|
||||||
setServerUrl(url);
|
|
||||||
// Don't auto-connect to avoid unexpected connections
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isSessionUnlocked) {
|
|
||||||
return null; // Will redirect via useEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
WebSocket Connection
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{!currentKeypair && (
|
|
||||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
||||||
No keypair selected. Select a keypair before connecting to a WebSocket server.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
Connection Status:
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label={isWebSocketConnected ? 'Connected' : 'Disconnected'}
|
|
||||||
color={isWebSocketConnected ? 'success' : 'default'}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
{isWebSocketConnected && webSocketUrl && (
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
|
||||||
Connected to: {webSocketUrl}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
||||||
<TextField
|
|
||||||
label="WebSocket Server URL"
|
|
||||||
placeholder="wss://example.com/ws"
|
|
||||||
value={serverUrl}
|
|
||||||
onChange={(e) => setServerUrl(e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
disabled={isConnecting || isWebSocketConnected || !currentKeypair}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isWebSocketConnected ? (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={handleDisconnect}
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={handleConnect}
|
|
||||||
disabled={isConnecting || !serverUrl.trim() || !currentKeypair}
|
|
||||||
>
|
|
||||||
{isConnecting ? <CircularProgress size={24} /> : 'Connect'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
|
||||||
Connection History
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{connectionHistory.length === 0 ? (
|
|
||||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
No connection history yet.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
) : (
|
|
||||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
|
||||||
<List disablePadding>
|
|
||||||
{connectionHistory.map((conn, index) => (
|
|
||||||
<Box key={conn.id}>
|
|
||||||
{index > 0 && <Divider />}
|
|
||||||
<ListItem
|
|
||||||
button
|
|
||||||
onClick={() => handleQuickConnect(conn.url)}
|
|
||||||
disabled={isWebSocketConnected}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography variant="subtitle2">
|
|
||||||
{conn.url}
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label={conn.status}
|
|
||||||
size="small"
|
|
||||||
color={conn.status === 'connected' ? 'success' : 'default'}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{new Date(conn.timestamp).toLocaleString()}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WebSocketPage;
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
/**
|
|
||||||
* Crypto Store for Hero Vault Extension
|
|
||||||
*
|
|
||||||
* This store manages cryptographic operations such as:
|
|
||||||
* - Encryption/decryption using the keyspace's symmetric cipher
|
|
||||||
* - Signing/verification using the selected keypair
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { create } from 'zustand';
|
|
||||||
import { getWasmModule, stringToUint8Array, uint8ArrayToString } from '../wasm/wasmHelper';
|
|
||||||
|
|
||||||
// Helper functions for Unicode-safe base64 encoding/decoding
|
|
||||||
function base64Encode(data: Uint8Array): string {
|
|
||||||
// Convert binary data to a string that only uses the low 8 bits of each character
|
|
||||||
const binaryString = Array.from(data)
|
|
||||||
.map(byte => String.fromCharCode(byte))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
// Use btoa on the binary string
|
|
||||||
return btoa(binaryString);
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64Decode(base64: string): Uint8Array {
|
|
||||||
// Decode base64 to binary string
|
|
||||||
const binaryString = atob(base64);
|
|
||||||
|
|
||||||
// Convert binary string to Uint8Array
|
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CryptoState {
|
|
||||||
// State
|
|
||||||
isEncrypting: boolean;
|
|
||||||
isDecrypting: boolean;
|
|
||||||
isSigning: boolean;
|
|
||||||
isVerifying: boolean;
|
|
||||||
error: string | null;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
encryptData: (data: string) => Promise<string>;
|
|
||||||
decryptData: (encrypted: string) => Promise<string>;
|
|
||||||
signMessage: (message: string) => Promise<string>;
|
|
||||||
verifySignature: (message: string, signature: string) => Promise<boolean>;
|
|
||||||
clearError: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCryptoStore = create<CryptoState>()((set, get) => ({
|
|
||||||
isEncrypting: false,
|
|
||||||
isDecrypting: false,
|
|
||||||
isSigning: false,
|
|
||||||
isVerifying: false,
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
encryptData: async (data: string) => {
|
|
||||||
try {
|
|
||||||
set({ isEncrypting: true, error: null });
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Convert input to Uint8Array
|
|
||||||
const dataBytes = stringToUint8Array(data);
|
|
||||||
|
|
||||||
// Encrypt the data
|
|
||||||
const encrypted = await wasmModule.encrypt_data(dataBytes);
|
|
||||||
|
|
||||||
// Convert result to base64 for storage/display using our Unicode-safe function
|
|
||||||
const encryptedBase64 = base64Encode(encrypted);
|
|
||||||
|
|
||||||
return encryptedBase64;
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: (error as Error).message || 'Failed to encrypt data' });
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
set({ isEncrypting: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
decryptData: async (encrypted: string) => {
|
|
||||||
try {
|
|
||||||
set({ isDecrypting: true, error: null });
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Convert input from base64 using our Unicode-safe function
|
|
||||||
const encryptedBytes = base64Decode(encrypted);
|
|
||||||
|
|
||||||
// Decrypt the data
|
|
||||||
const decrypted = await wasmModule.decrypt_data(encryptedBytes);
|
|
||||||
|
|
||||||
// Convert result to string
|
|
||||||
return uint8ArrayToString(decrypted);
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: (error as Error).message || 'Failed to decrypt data' });
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
set({ isDecrypting: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
signMessage: async (message: string) => {
|
|
||||||
try {
|
|
||||||
set({ isSigning: true, error: null });
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Convert message to Uint8Array
|
|
||||||
const messageBytes = stringToUint8Array(message);
|
|
||||||
|
|
||||||
// Sign the message
|
|
||||||
const signature = await wasmModule.sign(messageBytes);
|
|
||||||
|
|
||||||
return signature;
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: (error as Error).message || 'Failed to sign message' });
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
set({ isSigning: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
verifySignature: async (message: string, signature: string) => {
|
|
||||||
try {
|
|
||||||
set({ isVerifying: true, error: null });
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Convert inputs
|
|
||||||
const messageBytes = stringToUint8Array(message);
|
|
||||||
|
|
||||||
// Verify the signature
|
|
||||||
const isValid = await wasmModule.verify(messageBytes, signature);
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: (error as Error).message || 'Failed to verify signature' });
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
set({ isVerifying: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearError: () => set({ error: null })
|
|
||||||
}));
|
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
import { getWasmModule, stringToUint8Array } from '../wasm/wasmHelper';
|
|
||||||
import { getChromeApi } from '../utils/chromeApi';
|
|
||||||
|
|
||||||
// Import Chrome types
|
|
||||||
/// <reference types="chrome" />
|
|
||||||
|
|
||||||
interface KeypairMetadata {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionState {
|
|
||||||
isSessionUnlocked: boolean;
|
|
||||||
currentKeyspace: string | null;
|
|
||||||
currentKeypair: KeypairMetadata | null;
|
|
||||||
availableKeypairs: KeypairMetadata[];
|
|
||||||
isWebSocketConnected: boolean;
|
|
||||||
webSocketUrl: string | null;
|
|
||||||
isWasmLoaded: boolean;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
initWasm: () => Promise<boolean>;
|
|
||||||
checkSessionStatus: () => Promise<boolean>;
|
|
||||||
unlockSession: (keyspace: string, password: string) => Promise<boolean>;
|
|
||||||
lockSession: () => Promise<boolean>;
|
|
||||||
createKeyspace: (keyspace: string, password: string) => Promise<boolean>;
|
|
||||||
listKeypairs: () => Promise<KeypairMetadata[]>;
|
|
||||||
selectKeypair: (keypairId: string) => Promise<boolean>;
|
|
||||||
createKeypair: (type: string, metadata?: Record<string, any>) => Promise<string>;
|
|
||||||
connectWebSocket: (url: string) => Promise<boolean>;
|
|
||||||
disconnectWebSocket: () => Promise<boolean>;
|
|
||||||
executeScript: (script: string) => Promise<string>;
|
|
||||||
signMessage: (message: string) => Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the store
|
|
||||||
export const useSessionStore = create<SessionState>((set: any, get: any) => ({
|
|
||||||
isSessionUnlocked: false,
|
|
||||||
currentKeyspace: null,
|
|
||||||
currentKeypair: null,
|
|
||||||
availableKeypairs: [],
|
|
||||||
isWebSocketConnected: false,
|
|
||||||
webSocketUrl: null,
|
|
||||||
isWasmLoaded: false,
|
|
||||||
|
|
||||||
// Initialize WASM module
|
|
||||||
initWasm: async () => {
|
|
||||||
try {
|
|
||||||
set({ isWasmLoaded: true });
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize WASM module:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Check if a session is currently active
|
|
||||||
checkSessionStatus: async () => {
|
|
||||||
try {
|
|
||||||
// First check with the background service worker
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
const response = await chromeApi.runtime.sendMessage({ type: 'SESSION_STATUS' });
|
|
||||||
|
|
||||||
if (response && response.active) {
|
|
||||||
// If session is active in the background, check with WASM
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
const isUnlocked = wasmModule.is_unlocked();
|
|
||||||
|
|
||||||
if (isUnlocked) {
|
|
||||||
// Get current keypair metadata if available
|
|
||||||
try {
|
|
||||||
const keypairMetadata = await wasmModule.current_keypair_metadata();
|
|
||||||
const parsedMetadata = JSON.parse(keypairMetadata);
|
|
||||||
|
|
||||||
set({
|
|
||||||
isSessionUnlocked: true,
|
|
||||||
currentKeypair: parsedMetadata
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load keypairs
|
|
||||||
await get().listKeypairs();
|
|
||||||
} catch (e) {
|
|
||||||
// No keypair selected, but session is unlocked
|
|
||||||
set({ isSessionUnlocked: true });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (wasmError) {
|
|
||||||
console.error('WASM error checking session status:', wasmError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set({ isSessionUnlocked: false });
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to check session status:', error);
|
|
||||||
set({ isSessionUnlocked: false });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Unlock a session with keyspace and password
|
|
||||||
unlockSession: async (keyspace: string, password: string) => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Call the WASM init_session function
|
|
||||||
await wasmModule.init_session(keyspace, password);
|
|
||||||
|
|
||||||
// Initialize Rhai environment
|
|
||||||
wasmModule.init_rhai_env();
|
|
||||||
|
|
||||||
// Notify background service worker
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' });
|
|
||||||
|
|
||||||
set({
|
|
||||||
isSessionUnlocked: true,
|
|
||||||
currentKeyspace: keyspace,
|
|
||||||
currentKeypair: null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load keypairs after unlocking
|
|
||||||
const keypairs = await get().listKeypairs();
|
|
||||||
set({ availableKeypairs: keypairs });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to unlock session:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lock the current session
|
|
||||||
lockSession: async () => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Call the WASM lock_session function
|
|
||||||
wasmModule.lock_session();
|
|
||||||
|
|
||||||
// Notify background service worker
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.runtime.sendMessage({ type: 'SESSION_LOCK' });
|
|
||||||
|
|
||||||
set({
|
|
||||||
isSessionUnlocked: false,
|
|
||||||
currentKeyspace: null,
|
|
||||||
currentKeypair: null,
|
|
||||||
availableKeypairs: [],
|
|
||||||
isWebSocketConnected: false,
|
|
||||||
webSocketUrl: null
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to lock session:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Create a new keyspace
|
|
||||||
createKeyspace: async (keyspace: string, password: string) => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Call the WASM create_keyspace function
|
|
||||||
await wasmModule.create_keyspace(keyspace, password);
|
|
||||||
|
|
||||||
// Initialize Rhai environment
|
|
||||||
wasmModule.init_rhai_env();
|
|
||||||
|
|
||||||
// Notify background service worker
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' });
|
|
||||||
|
|
||||||
set({
|
|
||||||
isSessionUnlocked: true,
|
|
||||||
currentKeyspace: keyspace,
|
|
||||||
currentKeypair: null,
|
|
||||||
availableKeypairs: []
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to create keyspace:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// List all keypairs in the current keyspace
|
|
||||||
listKeypairs: async () => {
|
|
||||||
try {
|
|
||||||
console.log('Listing keypairs from WASM module');
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
console.log('WASM module loaded, calling list_keypairs');
|
|
||||||
|
|
||||||
// Call the WASM list_keypairs function
|
|
||||||
let keypairsJson;
|
|
||||||
try {
|
|
||||||
keypairsJson = await wasmModule.list_keypairs();
|
|
||||||
console.log('Raw keypairs JSON from WASM:', keypairsJson);
|
|
||||||
} catch (listError) {
|
|
||||||
console.error('Error calling list_keypairs:', listError);
|
|
||||||
throw new Error(`Failed to list keypairs: ${listError.message || listError}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let keypairs;
|
|
||||||
try {
|
|
||||||
keypairs = JSON.parse(keypairsJson);
|
|
||||||
console.log('Parsed keypairs object:', keypairs);
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('Error parsing keypairs JSON:', parseError);
|
|
||||||
throw new Error(`Failed to parse keypairs JSON: ${parseError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform the keypairs to our expected format
|
|
||||||
const formattedKeypairs: KeypairMetadata[] = keypairs.map((keypair: any, index: number) => {
|
|
||||||
console.log(`Processing keypair at index ${index}:`, keypair);
|
|
||||||
return {
|
|
||||||
id: keypair.id, // Use the actual keypair ID from the WASM module
|
|
||||||
type: keypair.key_type || 'Unknown',
|
|
||||||
name: keypair.metadata?.name,
|
|
||||||
description: keypair.metadata?.description,
|
|
||||||
createdAt: keypair.metadata?.created_at || Date.now()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Formatted keypairs for UI:', formattedKeypairs);
|
|
||||||
set({ availableKeypairs: formattedKeypairs });
|
|
||||||
return formattedKeypairs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to list keypairs:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Select a keypair for use
|
|
||||||
selectKeypair: async (keypairId: string) => {
|
|
||||||
try {
|
|
||||||
console.log('Selecting keypair with ID:', keypairId);
|
|
||||||
|
|
||||||
// First, let's log the available keypairs to see what we have
|
|
||||||
const { availableKeypairs } = get();
|
|
||||||
console.log('Available keypairs:', JSON.stringify(availableKeypairs));
|
|
||||||
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
console.log('WASM module loaded, attempting to select keypair');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call the WASM select_keypair function
|
|
||||||
await wasmModule.select_keypair(keypairId);
|
|
||||||
console.log('Successfully selected keypair in WASM');
|
|
||||||
} catch (selectError) {
|
|
||||||
console.error('Error in WASM select_keypair:', selectError);
|
|
||||||
throw new Error(`select_keypair error: ${selectError.message || selectError}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the keypair in our availableKeypairs list
|
|
||||||
const selectedKeypair = availableKeypairs.find((kp: KeypairMetadata) => kp.id === keypairId);
|
|
||||||
|
|
||||||
if (selectedKeypair) {
|
|
||||||
console.log('Found keypair in available list, setting as current');
|
|
||||||
set({ currentKeypair: selectedKeypair });
|
|
||||||
} else {
|
|
||||||
console.log('Keypair not found in available list, creating new entry from available data');
|
|
||||||
// If not found in our list (rare case), create a new entry with what we know
|
|
||||||
// Since we can't get metadata from WASM, use what we have from the keypair list
|
|
||||||
const matchingKeypair = availableKeypairs.find(k => k.id === keypairId);
|
|
||||||
|
|
||||||
if (matchingKeypair) {
|
|
||||||
set({ currentKeypair: matchingKeypair });
|
|
||||||
} else {
|
|
||||||
// Last resort: create a minimal keypair entry
|
|
||||||
const newKeypair: KeypairMetadata = {
|
|
||||||
id: keypairId,
|
|
||||||
type: 'Unknown',
|
|
||||||
name: `Keypair ${keypairId.substring(0, 8)}...`,
|
|
||||||
createdAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
set({ currentKeypair: newKeypair });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to select keypair:', error);
|
|
||||||
throw error; // Re-throw to show error in UI
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Create a new keypair
|
|
||||||
createKeypair: async (type: string, metadata?: Record<string, any>) => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Format metadata for WASM
|
|
||||||
const metadataJson = metadata ? JSON.stringify({
|
|
||||||
name: metadata.name,
|
|
||||||
description: metadata.description,
|
|
||||||
created_at: Date.now()
|
|
||||||
}) : undefined;
|
|
||||||
|
|
||||||
// Call the WASM add_keypair function
|
|
||||||
const keypairId = await wasmModule.add_keypair(type, metadataJson);
|
|
||||||
|
|
||||||
// Refresh the keypair list
|
|
||||||
await get().listKeypairs();
|
|
||||||
|
|
||||||
return keypairId;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to create keypair:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Connect to a WebSocket server
|
|
||||||
connectWebSocket: async (url: string) => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
const { currentKeypair } = get();
|
|
||||||
|
|
||||||
if (!currentKeypair) {
|
|
||||||
throw new Error('No keypair selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the public key from WASM
|
|
||||||
const publicKeyArray = await wasmModule.current_keypair_public_key();
|
|
||||||
const publicKeyHex = Array.from(publicKeyArray)
|
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
// Connect to WebSocket via background service worker
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
const response = await chromeApi.runtime.sendMessage({
|
|
||||||
type: 'CONNECT_WEBSOCKET',
|
|
||||||
serverUrl: url,
|
|
||||||
publicKey: publicKeyHex
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response && response.success) {
|
|
||||||
set({
|
|
||||||
isWebSocketConnected: true,
|
|
||||||
webSocketUrl: url
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(response?.error || 'Failed to connect to WebSocket server');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to WebSocket:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Disconnect from WebSocket server
|
|
||||||
disconnectWebSocket: async () => {
|
|
||||||
try {
|
|
||||||
// Disconnect via background service worker
|
|
||||||
const chromeApi = getChromeApi();
|
|
||||||
const response = await chromeApi.runtime.sendMessage({
|
|
||||||
type: 'DISCONNECT_WEBSOCKET'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response && response.success) {
|
|
||||||
set({
|
|
||||||
isWebSocketConnected: false,
|
|
||||||
webSocketUrl: null
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(response?.error || 'Failed to disconnect from WebSocket server');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to disconnect from WebSocket:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Execute a Rhai script
|
|
||||||
executeScript: async (script: string) => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Call the WASM run_rhai function
|
|
||||||
const result = await wasmModule.run_rhai(script);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to execute script:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sign a message with the current keypair
|
|
||||||
signMessage: async (message: string) => {
|
|
||||||
try {
|
|
||||||
const wasmModule = await getWasmModule();
|
|
||||||
|
|
||||||
// Convert message to Uint8Array
|
|
||||||
const messageBytes = stringToUint8Array(message);
|
|
||||||
|
|
||||||
// Call the WASM sign function
|
|
||||||
const signature = await wasmModule.sign(messageBytes);
|
|
||||||
return signature;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to sign message:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* Common TypeScript types for the Hero Vault Extension
|
|
||||||
*/
|
|
||||||
|
|
||||||
// React types
|
|
||||||
export type SyntheticEvent<T = Element, E = Event> = React.BaseSyntheticEvent<E, EventTarget & T, EventTarget>;
|
|
||||||
|
|
||||||
// Session types
|
|
||||||
export interface SessionActivity {
|
|
||||||
timestamp: number;
|
|
||||||
action: string;
|
|
||||||
details?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Script types
|
|
||||||
export interface ScriptResult {
|
|
||||||
id: string;
|
|
||||||
script: string;
|
|
||||||
result: string;
|
|
||||||
timestamp: number;
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PendingScript {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
script: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket types
|
|
||||||
export interface ConnectionHistory {
|
|
||||||
id: string;
|
|
||||||
url: string;
|
|
||||||
timestamp: number;
|
|
||||||
status: 'connected' | 'disconnected' | 'error';
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings types
|
|
||||||
export interface Settings {
|
|
||||||
darkMode: boolean;
|
|
||||||
autoLockTimeout: number;
|
|
||||||
defaultKeyType: string;
|
|
||||||
showScriptNotifications: boolean;
|
|
||||||
}
|
|
||||||
5
hero_vault_extension/src/types/chrome.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="chrome" />
|
|
||||||
|
|
||||||
// This file provides type declarations for Chrome extension APIs
|
|
||||||
// It's needed because we're using the Chrome extension API in a TypeScript project
|
|
||||||
// The actual implementation is provided by the browser at runtime
|
|
||||||
14
hero_vault_extension/src/types/declarations.d.ts
vendored
@@ -1,14 +0,0 @@
|
|||||||
// Type declarations for modules without type definitions
|
|
||||||
|
|
||||||
// React and Material UI
|
|
||||||
declare module 'react';
|
|
||||||
declare module 'react-dom';
|
|
||||||
declare module 'react-router-dom';
|
|
||||||
declare module '@mui/material';
|
|
||||||
declare module '@mui/material/*';
|
|
||||||
declare module '@mui/icons-material/*';
|
|
||||||
|
|
||||||
// Project modules
|
|
||||||
declare module './pages/*';
|
|
||||||
declare module './components/*';
|
|
||||||
declare module './store/*';
|
|
||||||
16
hero_vault_extension/src/types/wasm.d.ts
vendored
@@ -1,16 +0,0 @@
|
|||||||
declare module '*/wasm_app.js' {
|
|
||||||
export default function init(): Promise<void>;
|
|
||||||
export function init_session(keyspace: string, password: string): Promise<void>;
|
|
||||||
export function create_keyspace(keyspace: string, password: string): Promise<void>;
|
|
||||||
export function lock_session(): void;
|
|
||||||
export function is_unlocked(): boolean;
|
|
||||||
export function add_keypair(key_type: string | undefined, metadata: string | undefined): Promise<string>;
|
|
||||||
export function list_keypairs(): Promise<string>;
|
|
||||||
export function select_keypair(key_id: string): Promise<void>;
|
|
||||||
export function current_keypair_metadata(): Promise<any>;
|
|
||||||
export function current_keypair_public_key(): Promise<Uint8Array>;
|
|
||||||
export function sign(message: Uint8Array): Promise<string>;
|
|
||||||
export function verify(signature: string, message: Uint8Array): Promise<boolean>;
|
|
||||||
export function init_rhai_env(): void;
|
|
||||||
export function run_rhai(script: string): Promise<string>;
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* Chrome API utilities for Hero Vault Extension
|
|
||||||
*
|
|
||||||
* This module provides Chrome API detection and mocks for development mode
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Check if we're running in a Chrome extension environment
|
|
||||||
export const isExtensionEnvironment = (): boolean => {
|
|
||||||
return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock storage for development mode
|
|
||||||
const mockStorage: Record<string, any> = {
|
|
||||||
// Initialize with some default values for script storage
|
|
||||||
pendingScripts: [],
|
|
||||||
scriptResults: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock Chrome API for development mode
|
|
||||||
export const getChromeApi = () => {
|
|
||||||
// If we're in a Chrome extension environment, return the real Chrome API
|
|
||||||
if (isExtensionEnvironment()) {
|
|
||||||
return chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, return a mock implementation
|
|
||||||
return {
|
|
||||||
runtime: {
|
|
||||||
sendMessage: (message: any): Promise<any> => {
|
|
||||||
console.log('Mock sendMessage called with:', message);
|
|
||||||
|
|
||||||
// Mock responses based on message type
|
|
||||||
if (message.type === 'SESSION_STATUS') {
|
|
||||||
return Promise.resolve({ active: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'CREATE_KEYSPACE') {
|
|
||||||
mockStorage['currentKeyspace'] = message.keyspace;
|
|
||||||
return Promise.resolve({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'UNLOCK_SESSION') {
|
|
||||||
mockStorage['currentKeyspace'] = message.keyspace;
|
|
||||||
return Promise.resolve({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'LOCK_SESSION') {
|
|
||||||
delete mockStorage['currentKeyspace'];
|
|
||||||
return Promise.resolve({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({ success: false });
|
|
||||||
},
|
|
||||||
getURL: (path: string): string => {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
local: {
|
|
||||||
get: (keys: string | string[] | object): Promise<Record<string, any>> => {
|
|
||||||
console.log('Mock storage.local.get called with:', keys);
|
|
||||||
|
|
||||||
if (typeof keys === 'string') {
|
|
||||||
// Handle specific script storage keys
|
|
||||||
if (keys === 'pendingScripts' && !mockStorage[keys]) {
|
|
||||||
mockStorage[keys] = [];
|
|
||||||
}
|
|
||||||
if (keys === 'scriptResults' && !mockStorage[keys]) {
|
|
||||||
mockStorage[keys] = [];
|
|
||||||
}
|
|
||||||
return Promise.resolve({ [keys]: mockStorage[keys] });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(keys)) {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
keys.forEach(key => {
|
|
||||||
// Handle specific script storage keys
|
|
||||||
if (key === 'pendingScripts' && !mockStorage[key]) {
|
|
||||||
mockStorage[key] = [];
|
|
||||||
}
|
|
||||||
if (key === 'scriptResults' && !mockStorage[key]) {
|
|
||||||
mockStorage[key] = [];
|
|
||||||
}
|
|
||||||
result[key] = mockStorage[key];
|
|
||||||
});
|
|
||||||
return Promise.resolve(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(mockStorage);
|
|
||||||
},
|
|
||||||
set: (items: Record<string, any>): Promise<void> => {
|
|
||||||
console.log('Mock storage.local.set called with:', items);
|
|
||||||
|
|
||||||
Object.keys(items).forEach(key => {
|
|
||||||
mockStorage[key] = items[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as typeof chrome;
|
|
||||||
};
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
/**
|
|
||||||
* WASM Helper for Hero Vault Extension
|
|
||||||
*
|
|
||||||
* This module handles loading and initializing the WASM module,
|
|
||||||
* and provides a typed interface to the WASM functions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Import types for TypeScript
|
|
||||||
interface WasmModule {
|
|
||||||
// Session management
|
|
||||||
init_session: (keyspace: string, password: string) => Promise<void>;
|
|
||||||
create_keyspace: (keyspace: string, password: string) => Promise<void>;
|
|
||||||
lock_session: () => void;
|
|
||||||
is_unlocked: () => boolean;
|
|
||||||
|
|
||||||
// Keypair management
|
|
||||||
add_keypair: (key_type: string | undefined, metadata: string | undefined) => Promise<string>;
|
|
||||||
list_keypairs: () => Promise<string>;
|
|
||||||
select_keypair: (key_id: string) => Promise<void>;
|
|
||||||
current_keypair_metadata: () => Promise<any>;
|
|
||||||
current_keypair_public_key: () => Promise<Uint8Array>;
|
|
||||||
|
|
||||||
// Cryptographic operations
|
|
||||||
sign: (message: Uint8Array) => Promise<string>;
|
|
||||||
verify: (message: Uint8Array, signature: string) => Promise<boolean>;
|
|
||||||
encrypt_data: (data: Uint8Array) => Promise<Uint8Array>;
|
|
||||||
decrypt_data: (encrypted: Uint8Array) => Promise<Uint8Array>;
|
|
||||||
|
|
||||||
// Rhai scripting
|
|
||||||
init_rhai_env: () => void;
|
|
||||||
run_rhai: (script: string) => Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global reference to the WASM module
|
|
||||||
let wasmModule: WasmModule | null = null;
|
|
||||||
let isInitializing = false;
|
|
||||||
let initPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the WASM module
|
|
||||||
* This should be called before any other WASM functions
|
|
||||||
*/
|
|
||||||
export const initWasm = async (): Promise<void> => {
|
|
||||||
if (wasmModule) {
|
|
||||||
return Promise.resolve(); // Already initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInitializing && initPromise) {
|
|
||||||
return initPromise; // Already initializing
|
|
||||||
}
|
|
||||||
|
|
||||||
isInitializing = true;
|
|
||||||
|
|
||||||
initPromise = new Promise<void>(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
// Import the WASM module
|
|
||||||
// Use a relative path that will be resolved by Vite during build
|
|
||||||
const wasmImport = await import('../../public/wasm/wasm_app.js');
|
|
||||||
|
|
||||||
// Initialize the WASM module
|
|
||||||
await wasmImport.default();
|
|
||||||
|
|
||||||
// Store the WASM module globally
|
|
||||||
wasmModule = wasmImport as unknown as WasmModule;
|
|
||||||
|
|
||||||
console.log('WASM module initialized successfully');
|
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize WASM module:', error);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
isInitializing = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return initPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the WASM module
|
|
||||||
* This will initialize the module if it hasn't been initialized yet
|
|
||||||
*/
|
|
||||||
export const getWasmModule = async (): Promise<WasmModule> => {
|
|
||||||
if (!wasmModule) {
|
|
||||||
await initWasm();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wasmModule) {
|
|
||||||
throw new Error('WASM module failed to initialize');
|
|
||||||
}
|
|
||||||
|
|
||||||
return wasmModule;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the WASM module is initialized
|
|
||||||
*/
|
|
||||||
export const isWasmInitialized = (): boolean => {
|
|
||||||
return wasmModule !== null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to convert string to Uint8Array
|
|
||||||
*/
|
|
||||||
export const stringToUint8Array = (str: string): Uint8Array => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
return encoder.encode(str);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to convert Uint8Array to string
|
|
||||||
*/
|
|
||||||
export const uint8ArrayToString = (array: Uint8Array): string => {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
return decoder.decode(array);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to convert hex string to Uint8Array
|
|
||||||
*/
|
|
||||||
export const hexToUint8Array = (hex: string): Uint8Array => {
|
|
||||||
const bytes = new Uint8Array(hex.length / 2);
|
|
||||||
for (let i = 0; i < hex.length; i += 2) {
|
|
||||||
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to convert Uint8Array to hex string
|
|
||||||
*/
|
|
||||||
export const uint8ArrayToHex = (array: Uint8Array): string => {
|
|
||||||
return Array.from(array)
|
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": false,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
},
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"typeRoots": ["./node_modules/@types", "./src/types"],
|
|
||||||
"jsxImportSource": "react"
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import { crx } from '@crxjs/vite-plugin';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
const manifest = JSON.parse(
|
|
||||||
readFileSync('public/manifest.json', 'utf-8')
|
|
||||||
);
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
react(),
|
|
||||||
crx({ manifest }),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
outDir: 'dist',
|
|
||||||
emptyOutDir: true,
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
index: resolve(__dirname, 'index.html'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Copy WASM files to the dist directory
|
|
||||||
publicDir: 'public',
|
|
||||||
});
|
|
||||||
@@ -130,6 +130,7 @@ store.put(&js_value, Some(&JsValue::from_str(key)))?.await
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct WasmStore;
|
pub struct WasmStore;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl KVStore for WasmStore {
|
impl KVStore for WasmStore {
|
||||||
@@ -139,10 +140,16 @@ impl KVStore for WasmStore {
|
|||||||
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
|
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
|
||||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
}
|
}
|
||||||
async fn delete(&self, _key: &str) -> Result<()> {
|
async fn remove(&self, _key: &str) -> Result<()> {
|
||||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
}
|
}
|
||||||
async fn exists(&self, _key: &str) -> Result<bool> {
|
async fn contains_key(&self, _key: &str) -> Result<bool> {
|
||||||
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
|
}
|
||||||
|
async fn keys(&self) -> Result<Vec<String>> {
|
||||||
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
|
}
|
||||||
|
async fn clear(&self) -> Result<()> {
|
||||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
sigsocket_client/Cargo.toml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
[package]
|
||||||
|
name = "sigsocket_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "WebSocket client for sigsocket server with WASM-first support"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://git.ourworld.tf/samehabouelsaad/sal-modular"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Core dependencies (both native and WASM)
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
hex = "0.4"
|
||||||
|
base64 = "0.21"
|
||||||
|
url = "2.5"
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
# Native-only dependencies
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
tokio-tungstenite = "0.21"
|
||||||
|
futures-util = "0.3"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
# WASM-only dependencies
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
web-sys = { version = "0.3", features = [
|
||||||
|
"console",
|
||||||
|
"WebSocket",
|
||||||
|
"MessageEvent",
|
||||||
|
"Event",
|
||||||
|
"BinaryType",
|
||||||
|
"CloseEvent",
|
||||||
|
"ErrorEvent",
|
||||||
|
"Window",
|
||||||
|
] }
|
||||||
|
js-sys = "0.3"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
env_logger = "0.10"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||||
|
wasm-bindgen-test = "0.3"
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
214
sigsocket_client/IMPLEMENTATION.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# SigSocket Client Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the implementation of the `sigsocket_client` crate, a WebSocket client library designed for connecting to sigsocket servers with **WASM-first support**.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Design Principles
|
||||||
|
|
||||||
|
1. **WASM-First**: Designed primarily for browser environments with native support as a secondary target
|
||||||
|
2. **No Signing Logic**: The client delegates all signing operations to the application
|
||||||
|
3. **User Approval Flow**: Applications are notified about incoming requests and handle user approval
|
||||||
|
4. **Protocol Compatibility**: Fully compatible with the sigsocket server protocol
|
||||||
|
5. **Async/Await**: Modern async Rust API throughout
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
sigsocket_client/
|
||||||
|
├── src/
|
||||||
|
│ ├── lib.rs # Main library entry point
|
||||||
|
│ ├── error.rs # Error types (native + WASM versions)
|
||||||
|
│ ├── protocol.rs # Protocol message definitions
|
||||||
|
│ ├── client.rs # Main client interface
|
||||||
|
│ ├── native.rs # Native (tokio) implementation
|
||||||
|
│ └── wasm.rs # WASM (web-sys) implementation
|
||||||
|
├── examples/
|
||||||
|
│ ├── basic_usage.rs # Native usage example
|
||||||
|
│ └── wasm_usage.rs # WASM usage example
|
||||||
|
├── tests/
|
||||||
|
│ └── integration_test.rs
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol Implementation
|
||||||
|
|
||||||
|
The sigsocket protocol is simple and consists of three message types:
|
||||||
|
|
||||||
|
### 1. Introduction Message
|
||||||
|
When connecting, the client sends its public key as a hex-encoded string:
|
||||||
|
```
|
||||||
|
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Sign Request (Server → Client)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req_123",
|
||||||
|
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Sign Response (Client → Server)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req_123",
|
||||||
|
"message": "dGVzdCBtZXNzYWdl", // original message
|
||||||
|
"signature": "c2lnbmF0dXJl" // base64-encoded signature
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features Implemented
|
||||||
|
|
||||||
|
### ✅ Dual Platform Support
|
||||||
|
- **Native**: Uses `tokio` and `tokio-tungstenite` for async WebSocket communication
|
||||||
|
- **WASM**: Uses `web-sys` and `wasm-bindgen` for browser WebSocket API
|
||||||
|
|
||||||
|
### ✅ Type-Safe Protocol
|
||||||
|
- `SignRequest` and `SignResponse` structs with serde serialization
|
||||||
|
- Helper methods for base64 encoding/decoding
|
||||||
|
- Comprehensive error handling
|
||||||
|
|
||||||
|
### ✅ Flexible Sign Handler Interface
|
||||||
|
```rust
|
||||||
|
trait SignRequestHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Connection Management
|
||||||
|
- Automatic connection state tracking
|
||||||
|
- Clean disconnect handling
|
||||||
|
- Connection status queries
|
||||||
|
|
||||||
|
### ✅ Error Handling
|
||||||
|
- Comprehensive error types for different failure modes
|
||||||
|
- Platform-specific error conversions
|
||||||
|
- WASM-compatible error handling (no `std::error::Error` dependency)
|
||||||
|
|
||||||
|
## Platform-Specific Implementations
|
||||||
|
|
||||||
|
### Native Implementation (`native.rs`)
|
||||||
|
- Uses `tokio-tungstenite` for WebSocket communication
|
||||||
|
- Spawns separate tasks for reading and writing
|
||||||
|
- Thread-safe with `Arc<RwLock<T>>` for shared state
|
||||||
|
- Supports `Send + Sync` trait bounds
|
||||||
|
|
||||||
|
### WASM Implementation (`wasm.rs`)
|
||||||
|
- Uses `web-sys::WebSocket` for browser WebSocket API
|
||||||
|
- Event-driven with JavaScript closures
|
||||||
|
- Single-threaded (no `Send + Sync` requirements)
|
||||||
|
- Browser console logging for debugging
|
||||||
|
|
||||||
|
## Usage Patterns
|
||||||
|
|
||||||
|
### Native Usage
|
||||||
|
```rust
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let public_key = hex::decode("02f9308a...")?;
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||||
|
|
||||||
|
client.set_sign_handler(MySignHandler);
|
||||||
|
client.connect().await?;
|
||||||
|
|
||||||
|
// Client handles requests automatically
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM Usage
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
|
||||||
|
let public_key = get_user_public_key()?;
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||||
|
|
||||||
|
client.set_sign_handler(WasmSignHandler);
|
||||||
|
client.connect().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Protocol message serialization/deserialization
|
||||||
|
- Error handling and conversion
|
||||||
|
- Client creation and configuration
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- End-to-end usage patterns
|
||||||
|
- Sign request/response cycles
|
||||||
|
- Error scenarios
|
||||||
|
|
||||||
|
### Documentation Tests
|
||||||
|
- Example code in documentation is verified to compile
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Core Dependencies (Both Platforms)
|
||||||
|
- `serde` + `serde_json` - JSON serialization
|
||||||
|
- `hex` - Hex encoding/decoding
|
||||||
|
- `base64` - Base64 encoding/decoding
|
||||||
|
- `url` - URL parsing and validation
|
||||||
|
|
||||||
|
### Native-Only Dependencies
|
||||||
|
- `tokio` - Async runtime
|
||||||
|
- `tokio-tungstenite` - WebSocket client
|
||||||
|
- `futures-util` - Stream utilities
|
||||||
|
- `thiserror` - Error derive macros
|
||||||
|
|
||||||
|
### WASM-Only Dependencies
|
||||||
|
- `wasm-bindgen` - Rust/JavaScript interop
|
||||||
|
- `web-sys` - Browser API bindings
|
||||||
|
- `js-sys` - JavaScript type bindings
|
||||||
|
- `wasm-bindgen-futures` - Async support
|
||||||
|
|
||||||
|
## Build Targets
|
||||||
|
|
||||||
|
### Native Build
|
||||||
|
```bash
|
||||||
|
cargo build --features native
|
||||||
|
cargo test --features native
|
||||||
|
cargo run --example basic_usage --features native
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM Build
|
||||||
|
```bash
|
||||||
|
cargo check --target wasm32-unknown-unknown --features wasm
|
||||||
|
wasm-pack build --target web --features wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Improvements
|
||||||
|
1. **Reconnection Logic**: Automatic reconnection with exponential backoff
|
||||||
|
2. **Request Queuing**: Queue multiple concurrent sign requests
|
||||||
|
3. **Timeout Handling**: Configurable timeouts for requests
|
||||||
|
4. **Metrics**: Connection and request metrics
|
||||||
|
5. **Logging**: Structured logging with configurable levels
|
||||||
|
|
||||||
|
### WASM Enhancements
|
||||||
|
1. **Better Callback System**: More ergonomic callback handling in WASM
|
||||||
|
2. **Browser Wallet Integration**: Direct integration with MetaMask, etc.
|
||||||
|
3. **Service Worker Support**: Background request handling
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **No Private Key Storage**: The client never handles private keys
|
||||||
|
2. **User Approval Required**: All signing requires explicit user approval
|
||||||
|
3. **Message Validation**: All incoming messages are validated
|
||||||
|
4. **Secure Transport**: Requires WebSocket Secure (WSS) in production
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- **Rust Version**: 1.70+
|
||||||
|
- **WASM Target**: `wasm32-unknown-unknown`
|
||||||
|
- **Browser Support**: Modern browsers with WebSocket support
|
||||||
|
- **Server Compatibility**: Compatible with sigsocket server protocol
|
||||||
|
|
||||||
|
This implementation provides a solid foundation for applications that need to connect to sigsocket servers while maintaining security and user control over signing operations.
|
||||||
218
sigsocket_client/README.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# SigSocket Client
|
||||||
|
|
||||||
|
A WebSocket client library for connecting to sigsocket servers with **WASM-first support**.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🌐 **WASM-first design**: Optimized for browser environments
|
||||||
|
- 🖥️ **Native support**: Works in native Rust applications
|
||||||
|
- 🔐 **No signing logic**: Delegates signing to the application
|
||||||
|
- 👤 **User approval flow**: Notifies applications about incoming requests
|
||||||
|
- 🔌 **sigsocket compatible**: Fully compatible with sigsocket server protocol
|
||||||
|
- 🚀 **Async/await**: Modern async Rust API
|
||||||
|
- 🔄 **Automatic reconnection**: Both platforms support reconnection with exponential backoff
|
||||||
|
- ⏱️ **Connection timeouts**: Proper timeout handling and connection management
|
||||||
|
- 🛡️ **Production ready**: Comprehensive error handling and reliability features
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Native Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
|
||||||
|
|
||||||
|
struct MySignHandler;
|
||||||
|
|
||||||
|
impl SignRequestHandler for MySignHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
// 1. Present request to user
|
||||||
|
println!("Sign request: {}", request.message);
|
||||||
|
|
||||||
|
// 2. Get user approval
|
||||||
|
// ... your UI logic here ...
|
||||||
|
|
||||||
|
// 3. Sign the message (using your signing logic)
|
||||||
|
let signature = your_signing_function(&request.message_bytes()?)?;
|
||||||
|
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Your public key bytes
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")?;
|
||||||
|
|
||||||
|
// Create and configure client
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||||
|
client.set_sign_handler(MySignHandler);
|
||||||
|
|
||||||
|
// Connect and handle requests
|
||||||
|
client.connect().await?;
|
||||||
|
|
||||||
|
// Client will automatically handle incoming signature requests
|
||||||
|
// Keep the connection alive...
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
struct WasmSignHandler;
|
||||||
|
|
||||||
|
impl SignRequestHandler for WasmSignHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
// Show request to user in browser
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.alert_with_message(&format!("Sign request: {}", request.id))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Your signing logic here...
|
||||||
|
let signature = sign_with_browser_wallet(&request.message_bytes()?)?;
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
|
||||||
|
let public_key = get_user_public_key()?;
|
||||||
|
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
|
client.set_sign_handler(WasmSignHandler);
|
||||||
|
|
||||||
|
client.connect().await
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
|
||||||
|
The sigsocket client implements a simple WebSocket protocol:
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
Upon connection, the client sends its public key as a hex-encoded string:
|
||||||
|
```
|
||||||
|
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Sign Requests
|
||||||
|
The server sends signature requests as JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req_123",
|
||||||
|
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Sign Responses
|
||||||
|
The client responds with signatures as JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req_123",
|
||||||
|
"message": "dGVzdCBtZXNzYWdl", // original message
|
||||||
|
"signature": "c2lnbmF0dXJl" // base64-encoded signature
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `SigSocketClient`
|
||||||
|
|
||||||
|
Main client for connecting to sigsocket servers.
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
- `new(url, public_key)` - Create a new client
|
||||||
|
- `set_sign_handler(handler)` - Set the signature request handler
|
||||||
|
- `connect()` - Connect to the server with automatic reconnection
|
||||||
|
- `disconnect()` - Disconnect from the server
|
||||||
|
- `send_sign_response(response)` - Manually send a signature response
|
||||||
|
- `state()` - Get current connection state
|
||||||
|
- `is_connected()` - Check if connected
|
||||||
|
|
||||||
|
#### Reconnection Configuration (WASM only)
|
||||||
|
|
||||||
|
- `set_auto_reconnect(enabled)` - Enable/disable automatic reconnection
|
||||||
|
- `set_reconnect_config(max_attempts, initial_delay_ms)` - Configure reconnection parameters
|
||||||
|
|
||||||
|
**Default settings:**
|
||||||
|
- Max attempts: 5
|
||||||
|
- Initial delay: 1000ms (with exponential backoff: 1s, 2s, 4s, 8s, 16s)
|
||||||
|
- Auto-reconnect: enabled
|
||||||
|
|
||||||
|
### `SignRequestHandler` Trait
|
||||||
|
|
||||||
|
Implement this trait to handle incoming signature requests.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
trait SignRequestHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `SignRequest`
|
||||||
|
|
||||||
|
Represents a signature request from the server.
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
- `id: String` - Unique request identifier
|
||||||
|
- `message: String` - Base64-encoded message to sign
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
- `message_bytes()` - Decode message to bytes
|
||||||
|
- `message_hex()` - Get message as hex string
|
||||||
|
|
||||||
|
### `SignResponse`
|
||||||
|
|
||||||
|
Represents a signature response to send to the server.
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
- `new(id, message, signature)` - Create a new response
|
||||||
|
- `from_request_and_signature(request, signature)` - Create from request and signature bytes
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Run the basic example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --example basic_usage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### Native Build
|
||||||
|
```bash
|
||||||
|
cargo build
|
||||||
|
cargo test
|
||||||
|
cargo run --example basic_usage
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM Build
|
||||||
|
```bash
|
||||||
|
wasm-pack build --target web
|
||||||
|
wasm-pack test --headless --firefox # Run WASM tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Native
|
||||||
|
- Rust 1.70+
|
||||||
|
- tokio runtime
|
||||||
|
|
||||||
|
### WASM
|
||||||
|
- wasm-pack
|
||||||
|
- Modern browser with WebSocket support
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT OR Apache-2.0
|
||||||
133
sigsocket_client/examples/basic_usage.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
//! Basic usage example for sigsocket_client
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to:
|
||||||
|
//! 1. Create a sigsocket client
|
||||||
|
//! 2. Set up a sign request handler
|
||||||
|
//! 3. Connect to a sigsocket server
|
||||||
|
//! 4. Handle incoming signature requests
|
||||||
|
//!
|
||||||
|
//! This example only runs on native (non-WASM) targets.
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
/// Example sign request handler
|
||||||
|
///
|
||||||
|
/// In a real application, this would:
|
||||||
|
/// - Present the request to the user
|
||||||
|
/// - Get user approval
|
||||||
|
/// - Use a secure signing method (hardware wallet, etc.)
|
||||||
|
/// - Return the signature
|
||||||
|
struct ExampleSignHandler;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl SignRequestHandler for ExampleSignHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
println!("📝 Received sign request:");
|
||||||
|
println!(" ID: {}", request.id);
|
||||||
|
println!(" Message (base64): {}", request.message);
|
||||||
|
|
||||||
|
// Decode the message to show what we're signing
|
||||||
|
match request.message_bytes() {
|
||||||
|
Ok(message_bytes) => {
|
||||||
|
println!(" Message (hex): {}", hex::encode(&message_bytes));
|
||||||
|
println!(" Message (text): {}", String::from_utf8_lossy(&message_bytes));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!(" ⚠️ Failed to decode message: {}", e);
|
||||||
|
return Err(SigSocketError::Base64(e.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Show this to the user
|
||||||
|
// 2. Get user approval
|
||||||
|
// 3. Sign the message using a secure method
|
||||||
|
|
||||||
|
println!("🤔 Would you like to sign this message? (This is a simulation)");
|
||||||
|
println!("✅ Auto-approving for demo purposes...");
|
||||||
|
|
||||||
|
// Simulate signing - in reality, this would be a real signature
|
||||||
|
let fake_signature = format!("fake_signature_for_{}", request.id);
|
||||||
|
Ok(fake_signature.into_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Initialize logging
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
println!("🚀 SigSocket Client Example");
|
||||||
|
println!("============================");
|
||||||
|
|
||||||
|
// Example public key (in a real app, this would be your actual public key)
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")
|
||||||
|
.expect("Invalid public key hex");
|
||||||
|
|
||||||
|
println!("🔑 Public key: {}", hex::encode(&public_key));
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||||
|
println!("📡 Created client for: {}", client.url());
|
||||||
|
|
||||||
|
// Set up the sign request handler
|
||||||
|
client.set_sign_handler(ExampleSignHandler);
|
||||||
|
println!("✅ Sign request handler configured");
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
println!("🔌 Connecting to sigsocket server...");
|
||||||
|
match client.connect().await {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("✅ Connected successfully!");
|
||||||
|
println!("📊 Connection state: {:?}", client.state());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ Failed to connect: {}", e);
|
||||||
|
println!("💡 Make sure the sigsocket server is running on localhost:8080");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the connection alive and handle requests
|
||||||
|
println!("👂 Listening for signature requests...");
|
||||||
|
println!(" (Press Ctrl+C to exit)");
|
||||||
|
|
||||||
|
// In a real application, you might want to:
|
||||||
|
// - Handle reconnection
|
||||||
|
// - Provide a UI for user interaction
|
||||||
|
// - Manage multiple concurrent requests
|
||||||
|
// - Store and manage signatures
|
||||||
|
|
||||||
|
// For this example, we'll just wait
|
||||||
|
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl-c");
|
||||||
|
|
||||||
|
println!("\n🛑 Shutting down...");
|
||||||
|
client.disconnect().await?;
|
||||||
|
println!("✅ Disconnected cleanly");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example of how you might manually send a response (if needed)
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn send_manual_response(client: &SigSocketClient) -> Result<()> {
|
||||||
|
let response = SignResponse::new(
|
||||||
|
"example-request-id",
|
||||||
|
"dGVzdCBtZXNzYWdl", // "test message" in base64
|
||||||
|
"ZmFrZV9zaWduYXR1cmU=", // "fake_signature" in base64
|
||||||
|
);
|
||||||
|
|
||||||
|
client.send_sign_response(&response).await?;
|
||||||
|
println!("📤 Sent manual response: {}", response.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM main function (does nothing since this example is native-only)
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn main() {
|
||||||
|
// This example is designed for native use only
|
||||||
|
}
|
||||||
384
sigsocket_client/src/client.rs
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
//! Main client interface for sigsocket communication
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use alloc::{string::String, vec::Vec, boxed::Box, string::ToString};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use alloc::collections::BTreeMap as HashMap;
|
||||||
|
|
||||||
|
use crate::{SignRequest, SignResponse, Result, SigSocketError};
|
||||||
|
use crate::protocol::ManagedSignRequest;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Connection state of the sigsocket client
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ConnectionState {
|
||||||
|
/// Client is disconnected
|
||||||
|
Disconnected,
|
||||||
|
/// Client is connecting
|
||||||
|
Connecting,
|
||||||
|
/// Client is connected and ready
|
||||||
|
Connected,
|
||||||
|
/// Client connection failed
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for handling sign requests from the sigsocket server
|
||||||
|
///
|
||||||
|
/// Applications should implement this trait to handle incoming signature requests.
|
||||||
|
/// The implementation should:
|
||||||
|
/// 1. Present the request to the user
|
||||||
|
/// 2. Get user approval
|
||||||
|
/// 3. Sign the message (using external signing logic)
|
||||||
|
/// 4. Return the signature
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub trait SignRequestHandler: Send + Sync {
|
||||||
|
/// Handle a sign request from the server
|
||||||
|
///
|
||||||
|
/// This method is called when the server sends a signature request.
|
||||||
|
/// The implementation should:
|
||||||
|
/// - Decode and validate the message
|
||||||
|
/// - Present it to the user for approval
|
||||||
|
/// - If approved, sign the message and return the signature
|
||||||
|
/// - If rejected, return an error
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The sign request from the server
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(signature_bytes)` - The signature as raw bytes
|
||||||
|
/// * `Err(error)` - If the request was rejected or signing failed
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WASM version of SignRequestHandler (no Send + Sync requirements)
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub trait SignRequestHandler {
|
||||||
|
/// Handle a sign request from the server
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main sigsocket client
|
||||||
|
///
|
||||||
|
/// This is the primary interface for connecting to sigsocket servers.
|
||||||
|
/// It handles the WebSocket connection, protocol communication, and
|
||||||
|
/// delegates signing requests to the application.
|
||||||
|
pub struct SigSocketClient {
|
||||||
|
/// WebSocket server URL
|
||||||
|
url: String,
|
||||||
|
/// Client's public key (hex-encoded)
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
/// Current connection state
|
||||||
|
state: ConnectionState,
|
||||||
|
/// Sign request handler
|
||||||
|
sign_handler: Option<Box<dyn SignRequestHandler>>,
|
||||||
|
/// Pending sign requests managed by the client
|
||||||
|
pending_requests: HashMap<String, ManagedSignRequest>,
|
||||||
|
/// Connected public key (hex-encoded) - set when connection is established
|
||||||
|
connected_public_key: Option<String>,
|
||||||
|
/// Platform-specific implementation
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
inner: Option<crate::native::NativeClient>,
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
inner: Option<crate::wasm::WasmClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SigSocketClient {
|
||||||
|
/// Create a new sigsocket client
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
/// * `public_key` - Client's public key as bytes
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(client)` - New client instance
|
||||||
|
/// * `Err(error)` - If the URL is invalid or public key is invalid
|
||||||
|
pub fn new(url: impl Into<String>, public_key: Vec<u8>) -> Result<Self> {
|
||||||
|
let url = url.into();
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
let _ = url::Url::parse(&url)?;
|
||||||
|
|
||||||
|
// Validate public key (should be 33 bytes for compressed secp256k1)
|
||||||
|
if public_key.is_empty() {
|
||||||
|
return Err(SigSocketError::InvalidPublicKey("Public key cannot be empty".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
url,
|
||||||
|
public_key,
|
||||||
|
state: ConnectionState::Disconnected,
|
||||||
|
sign_handler: None,
|
||||||
|
pending_requests: HashMap::new(),
|
||||||
|
connected_public_key: None,
|
||||||
|
inner: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the sign request handler
|
||||||
|
///
|
||||||
|
/// This handler will be called whenever the server sends a signature request.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `handler` - Implementation of SignRequestHandler trait
|
||||||
|
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||||
|
where
|
||||||
|
H: SignRequestHandler + 'static,
|
||||||
|
{
|
||||||
|
self.sign_handler = Some(Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Get the current connection state
|
||||||
|
pub fn state(&self) -> ConnectionState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the client is connected
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
self.state == ConnectionState::Connected
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the client's public key as hex string
|
||||||
|
pub fn public_key_hex(&self) -> String {
|
||||||
|
hex::encode(&self.public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the WebSocket server URL
|
||||||
|
pub fn url(&self) -> &str {
|
||||||
|
&self.url
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the connected public key (if connected)
|
||||||
|
pub fn connected_public_key(&self) -> Option<&str> {
|
||||||
|
self.connected_public_key.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Request Management Methods ===
|
||||||
|
|
||||||
|
/// Add a pending sign request
|
||||||
|
///
|
||||||
|
/// This is typically called when a sign request is received from the server.
|
||||||
|
/// The request will be stored and can be retrieved later for processing.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The sign request to add
|
||||||
|
/// * `target_public_key` - The public key this request is intended for
|
||||||
|
pub fn add_pending_request(&mut self, request: SignRequest, target_public_key: String) {
|
||||||
|
let managed_request = ManagedSignRequest::new(request, target_public_key);
|
||||||
|
self.pending_requests.insert(managed_request.id().to_string(), managed_request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a pending request by ID
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to remove
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Some(request)` - The removed request if it existed
|
||||||
|
/// * `None` - If no request with that ID was found
|
||||||
|
pub fn remove_pending_request(&mut self, request_id: &str) -> Option<ManagedSignRequest> {
|
||||||
|
self.pending_requests.remove(request_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a pending request by ID
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to retrieve
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Some(request)` - The request if it exists
|
||||||
|
/// * `None` - If no request with that ID was found
|
||||||
|
pub fn get_pending_request(&self, request_id: &str) -> Option<&ManagedSignRequest> {
|
||||||
|
self.pending_requests.get(request_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all pending requests
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * A reference to the HashMap containing all pending requests
|
||||||
|
pub fn get_pending_requests(&self) -> &HashMap<String, ManagedSignRequest> {
|
||||||
|
&self.pending_requests
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pending requests filtered by public key
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `public_key` - The public key to filter by (hex-encoded)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * A vector of references to requests for the specified public key
|
||||||
|
pub fn get_requests_for_public_key(&self, public_key: &str) -> Vec<&ManagedSignRequest> {
|
||||||
|
self.pending_requests
|
||||||
|
.values()
|
||||||
|
.filter(|req| req.is_for_public_key(public_key))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a request can be handled for the given public key
|
||||||
|
///
|
||||||
|
/// This performs protocol-level validation without cryptographic operations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The sign request to validate
|
||||||
|
/// * `public_key` - The public key to check against (hex-encoded)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `true` - If the request can be handled for this public key
|
||||||
|
/// * `false` - If the request cannot be handled
|
||||||
|
pub fn can_handle_request_for_key(&self, request: &SignRequest, public_key: &str) -> bool {
|
||||||
|
// Basic protocol validation
|
||||||
|
if request.id.is_empty() || request.message.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can decode the message
|
||||||
|
if request.message_bytes().is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, we assume any valid request can be handled for any public key
|
||||||
|
// More sophisticated validation can be added here
|
||||||
|
!public_key.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all pending requests
|
||||||
|
pub fn clear_pending_requests(&mut self) {
|
||||||
|
self.pending_requests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the count of pending requests
|
||||||
|
pub fn pending_request_count(&self) -> usize {
|
||||||
|
self.pending_requests.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific implementations will be added in separate modules
|
||||||
|
impl SigSocketClient {
|
||||||
|
/// Connect to the sigsocket server
|
||||||
|
///
|
||||||
|
/// This establishes a WebSocket connection and sends the introduction message
|
||||||
|
/// with the client's public key.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Successfully connected
|
||||||
|
/// * `Err(error)` - Connection failed
|
||||||
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
if self.state == ConnectionState::Connected {
|
||||||
|
return Err(SigSocketError::AlreadyConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = ConnectionState::Connecting;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let mut client = crate::native::NativeClient::new(&self.url, &self.public_key)?;
|
||||||
|
if let Some(handler) = self.sign_handler.take() {
|
||||||
|
client.set_sign_handler_boxed(handler);
|
||||||
|
}
|
||||||
|
client.connect().await?;
|
||||||
|
self.inner = Some(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let mut client = crate::wasm::WasmClient::new(&self.url, &self.public_key)?;
|
||||||
|
if let Some(handler) = self.sign_handler.take() {
|
||||||
|
client.set_sign_handler_boxed(handler);
|
||||||
|
}
|
||||||
|
client.connect().await?;
|
||||||
|
self.inner = Some(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = ConnectionState::Connected;
|
||||||
|
self.connected_public_key = Some(self.public_key_hex());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the sigsocket server
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Successfully disconnected
|
||||||
|
/// * `Err(error)` - Disconnect failed
|
||||||
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
if let Some(inner) = &mut self.inner {
|
||||||
|
inner.disconnect().await?;
|
||||||
|
}
|
||||||
|
self.inner = None;
|
||||||
|
self.state = ConnectionState::Disconnected;
|
||||||
|
self.connected_public_key = None;
|
||||||
|
self.clear_pending_requests();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a sign response to the server
|
||||||
|
///
|
||||||
|
/// This is typically called after the user has approved a signature request
|
||||||
|
/// and the application has generated the signature.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `response` - The sign response containing the signature
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Response sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send response
|
||||||
|
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||||
|
if !self.is_connected() {
|
||||||
|
return Err(SigSocketError::NotConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inner) = &self.inner {
|
||||||
|
inner.send_sign_response(response).await
|
||||||
|
} else {
|
||||||
|
Err(SigSocketError::NotConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a response for a specific request ID with signature
|
||||||
|
///
|
||||||
|
/// This is a convenience method that creates a SignResponse and sends it.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request being responded to
|
||||||
|
/// * `message` - The original message (base64-encoded)
|
||||||
|
/// * `signature` - The signature (base64-encoded)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Response sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send response
|
||||||
|
pub async fn send_response(&self, request_id: &str, message: &str, signature: &str) -> Result<()> {
|
||||||
|
let response = SignResponse::new(request_id, message, signature);
|
||||||
|
self.send_sign_response(&response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a rejection for a specific request ID
|
||||||
|
///
|
||||||
|
/// This sends an error response to indicate the request was rejected.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request being rejected
|
||||||
|
/// * `reason` - The reason for rejection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Rejection sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send rejection
|
||||||
|
pub async fn send_rejection(&self, request_id: &str, _reason: &str) -> Result<()> {
|
||||||
|
// For now, we'll send an empty signature to indicate rejection
|
||||||
|
// This can be improved with a proper rejection protocol
|
||||||
|
let response = SignResponse::new(request_id, "", "");
|
||||||
|
self.send_sign_response(&response).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SigSocketClient {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Cleanup will be handled by the platform-specific implementations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
168
sigsocket_client/src/error.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
//! Error types for the sigsocket client
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use alloc::{string::{String, ToString}, format};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
/// Result type alias for sigsocket client operations
|
||||||
|
pub type Result<T> = core::result::Result<T, SigSocketError>;
|
||||||
|
|
||||||
|
/// Error types that can occur when using the sigsocket client
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SigSocketError {
|
||||||
|
/// WebSocket connection error
|
||||||
|
#[error("Connection error: {0}")]
|
||||||
|
Connection(String),
|
||||||
|
|
||||||
|
/// WebSocket protocol error
|
||||||
|
#[error("Protocol error: {0}")]
|
||||||
|
Protocol(String),
|
||||||
|
|
||||||
|
/// Message serialization/deserialization error
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serialization(String),
|
||||||
|
|
||||||
|
/// Invalid public key format
|
||||||
|
#[error("Invalid public key: {0}")]
|
||||||
|
InvalidPublicKey(String),
|
||||||
|
|
||||||
|
/// Invalid URL format
|
||||||
|
#[error("Invalid URL: {0}")]
|
||||||
|
InvalidUrl(String),
|
||||||
|
|
||||||
|
/// Client is not connected
|
||||||
|
#[error("Client is not connected")]
|
||||||
|
NotConnected,
|
||||||
|
|
||||||
|
/// Client is already connected
|
||||||
|
#[error("Client is already connected")]
|
||||||
|
AlreadyConnected,
|
||||||
|
|
||||||
|
/// Timeout error
|
||||||
|
#[error("Operation timed out")]
|
||||||
|
Timeout,
|
||||||
|
|
||||||
|
/// Send error
|
||||||
|
#[error("Failed to send message: {0}")]
|
||||||
|
Send(String),
|
||||||
|
|
||||||
|
/// Receive error
|
||||||
|
#[error("Failed to receive message: {0}")]
|
||||||
|
Receive(String),
|
||||||
|
|
||||||
|
/// Base64 encoding/decoding error
|
||||||
|
#[error("Base64 error: {0}")]
|
||||||
|
Base64(String),
|
||||||
|
|
||||||
|
/// Hex encoding/decoding error
|
||||||
|
#[error("Hex error: {0}")]
|
||||||
|
Hex(String),
|
||||||
|
|
||||||
|
/// Generic error
|
||||||
|
#[error("Error: {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WASM version of error types (no thiserror dependency)
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SigSocketError {
|
||||||
|
/// WebSocket connection error
|
||||||
|
Connection(String),
|
||||||
|
/// WebSocket protocol error
|
||||||
|
Protocol(String),
|
||||||
|
/// Message serialization/deserialization error
|
||||||
|
Serialization(String),
|
||||||
|
/// Invalid public key format
|
||||||
|
InvalidPublicKey(String),
|
||||||
|
/// Invalid URL format
|
||||||
|
InvalidUrl(String),
|
||||||
|
/// Client is not connected
|
||||||
|
NotConnected,
|
||||||
|
/// Client is already connected
|
||||||
|
AlreadyConnected,
|
||||||
|
/// Timeout error
|
||||||
|
Timeout,
|
||||||
|
/// Send error
|
||||||
|
Send(String),
|
||||||
|
/// Receive error
|
||||||
|
Receive(String),
|
||||||
|
/// Base64 encoding/decoding error
|
||||||
|
Base64(String),
|
||||||
|
/// Hex encoding/decoding error
|
||||||
|
Hex(String),
|
||||||
|
/// Generic error
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl fmt::Display for SigSocketError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
SigSocketError::Connection(msg) => write!(f, "Connection error: {}", msg),
|
||||||
|
SigSocketError::Protocol(msg) => write!(f, "Protocol error: {}", msg),
|
||||||
|
SigSocketError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
|
||||||
|
SigSocketError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {}", msg),
|
||||||
|
SigSocketError::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
|
||||||
|
SigSocketError::NotConnected => write!(f, "Client is not connected"),
|
||||||
|
SigSocketError::AlreadyConnected => write!(f, "Client is already connected"),
|
||||||
|
SigSocketError::Timeout => write!(f, "Operation timed out"),
|
||||||
|
SigSocketError::Send(msg) => write!(f, "Failed to send message: {}", msg),
|
||||||
|
SigSocketError::Receive(msg) => write!(f, "Failed to receive message: {}", msg),
|
||||||
|
SigSocketError::Base64(msg) => write!(f, "Base64 error: {}", msg),
|
||||||
|
SigSocketError::Hex(msg) => write!(f, "Hex error: {}", msg),
|
||||||
|
SigSocketError::Other(msg) => write!(f, "Error: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement From traits for common error types
|
||||||
|
impl From<serde_json::Error> for SigSocketError {
|
||||||
|
fn from(err: serde_json::Error) -> Self {
|
||||||
|
SigSocketError::Serialization(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<base64::DecodeError> for SigSocketError {
|
||||||
|
fn from(err: base64::DecodeError) -> Self {
|
||||||
|
SigSocketError::Base64(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<hex::FromHexError> for SigSocketError {
|
||||||
|
fn from(err: hex::FromHexError) -> Self {
|
||||||
|
SigSocketError::Hex(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<url::ParseError> for SigSocketError {
|
||||||
|
fn from(err: url::ParseError) -> Self {
|
||||||
|
SigSocketError::InvalidUrl(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native-specific error conversions
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
mod native_errors {
|
||||||
|
use super::SigSocketError;
|
||||||
|
|
||||||
|
impl From<tokio_tungstenite::tungstenite::Error> for SigSocketError {
|
||||||
|
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
|
||||||
|
SigSocketError::Connection(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM-specific error conversions
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl From<wasm_bindgen::JsValue> for SigSocketError {
|
||||||
|
fn from(err: wasm_bindgen::JsValue) -> Self {
|
||||||
|
SigSocketError::Other(format!("{:?}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
72
sigsocket_client/src/lib.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//! # SigSocket Client
|
||||||
|
//!
|
||||||
|
//! A WebSocket client library for connecting to sigsocket servers with WASM-first support.
|
||||||
|
//!
|
||||||
|
//! This library provides a unified interface for both native and WASM environments,
|
||||||
|
//! allowing applications to connect to sigsocket servers using a public key and handle
|
||||||
|
//! incoming signature requests.
|
||||||
|
//!
|
||||||
|
//! ## Features
|
||||||
|
//!
|
||||||
|
//! - **WASM-first design**: Optimized for browser environments
|
||||||
|
//! - **Native support**: Works in native Rust applications
|
||||||
|
//! - **No signing logic**: Delegates signing to the application
|
||||||
|
//! - **User approval flow**: Notifies applications about incoming requests
|
||||||
|
//! - **sigsocket compatible**: Fully compatible with sigsocket server protocol
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result};
|
||||||
|
//!
|
||||||
|
//! struct MyHandler;
|
||||||
|
//! impl SignRequestHandler for MyHandler {
|
||||||
|
//! fn handle_sign_request(&self, _request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
//! Ok(b"fake_signature".to_vec())
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> Result<()> {
|
||||||
|
//! // Create client with public key
|
||||||
|
//! let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
//! let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||||
|
//!
|
||||||
|
//! // Set up request handler
|
||||||
|
//! client.set_sign_handler(MyHandler);
|
||||||
|
//!
|
||||||
|
//! // Connect to server
|
||||||
|
//! client.connect().await?;
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![cfg_attr(target_arch = "wasm32", no_std)]
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod protocol;
|
||||||
|
mod client;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
mod native;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod wasm;
|
||||||
|
|
||||||
|
pub use error::{SigSocketError, Result};
|
||||||
|
pub use protocol::{SignRequest, SignResponse, ManagedSignRequest, RequestStatus};
|
||||||
|
pub use client::{SigSocketClient, SignRequestHandler, ConnectionState};
|
||||||
|
|
||||||
|
// Re-export for convenience
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::{
|
||||||
|
SigSocketClient, SignRequest, SignResponse, ManagedSignRequest, RequestStatus,
|
||||||
|
SignRequestHandler, ConnectionState, SigSocketError, Result
|
||||||
|
};
|
||||||
|
}
|
||||||
232
sigsocket_client/src/native.rs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
//! Native (non-WASM) implementation of the sigsocket client
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{mpsc, RwLock};
|
||||||
|
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||||
|
|
||||||
|
/// Native WebSocket client implementation
|
||||||
|
pub struct NativeClient {
|
||||||
|
url: String,
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
sign_handler: Option<Arc<dyn SignRequestHandler>>,
|
||||||
|
sender: Option<mpsc::UnboundedSender<Message>>,
|
||||||
|
connected: Arc<RwLock<bool>>,
|
||||||
|
reconnect_attempts: u32,
|
||||||
|
max_reconnect_attempts: u32,
|
||||||
|
reconnect_delay_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeClient {
|
||||||
|
/// Create a new native client
|
||||||
|
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
url: url.to_string(),
|
||||||
|
public_key: public_key.to_vec(),
|
||||||
|
sign_handler: None,
|
||||||
|
sender: None,
|
||||||
|
connected: Arc::new(RwLock::new(false)),
|
||||||
|
reconnect_attempts: 0,
|
||||||
|
max_reconnect_attempts: 5,
|
||||||
|
reconnect_delay_ms: 1000, // Start with 1 second
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the sign request handler
|
||||||
|
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||||
|
where
|
||||||
|
H: SignRequestHandler + 'static,
|
||||||
|
{
|
||||||
|
self.sign_handler = Some(Arc::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the sign request handler from a boxed trait object
|
||||||
|
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
|
||||||
|
self.sign_handler = Some(Arc::from(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to the WebSocket server with automatic reconnection
|
||||||
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
self.reconnect_attempts = 0;
|
||||||
|
self.connect_with_retry().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect with retry logic
|
||||||
|
async fn connect_with_retry(&mut self) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match self.try_connect().await {
|
||||||
|
Ok(()) => {
|
||||||
|
self.reconnect_attempts = 0; // Reset on successful connection
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.reconnect_attempts += 1;
|
||||||
|
|
||||||
|
if self.reconnect_attempts > self.max_reconnect_attempts {
|
||||||
|
log::error!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = self.reconnect_delay_ms * (2_u64.pow(self.reconnect_attempts - 1)); // Exponential backoff
|
||||||
|
log::warn!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||||
|
self.reconnect_attempts, self.max_reconnect_attempts, delay, e);
|
||||||
|
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single connection attempt
|
||||||
|
async fn try_connect(&mut self) -> Result<()> {
|
||||||
|
let url = Url::parse(&self.url)?;
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
|
let (ws_stream, _) = connect_async(url).await
|
||||||
|
.map_err(|e| SigSocketError::Connection(e.to_string()))?;
|
||||||
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
|
// Send introduction message (hex-encoded public key)
|
||||||
|
let intro_message = hex::encode(&self.public_key);
|
||||||
|
write.send(Message::Text(intro_message)).await
|
||||||
|
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||||
|
|
||||||
|
// Set up message sender channel
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
self.sender = Some(tx);
|
||||||
|
|
||||||
|
// Set connected state
|
||||||
|
*self.connected.write().await = true;
|
||||||
|
|
||||||
|
// Spawn write task
|
||||||
|
let write_task = tokio::spawn(async move {
|
||||||
|
while let Some(message) = rx.recv().await {
|
||||||
|
if let Err(e) = write.send(message).await {
|
||||||
|
log::error!("Failed to send message: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn read task
|
||||||
|
let connected = self.connected.clone();
|
||||||
|
let sign_handler = self.sign_handler.clone();
|
||||||
|
let sender = self.sender.as_ref().unwrap().clone();
|
||||||
|
|
||||||
|
let read_task = tokio::spawn(async move {
|
||||||
|
while let Some(message) = read.next().await {
|
||||||
|
match message {
|
||||||
|
Ok(Message::Text(text)) => {
|
||||||
|
if let Err(e) = Self::handle_text_message(&text, &sign_handler, &sender).await {
|
||||||
|
log::error!("Failed to handle message: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Message::Close(_)) => {
|
||||||
|
log::info!("WebSocket connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("WebSocket error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Ignore other message types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as disconnected
|
||||||
|
*connected.write().await = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store tasks (in a real implementation, you'd want to manage these properly)
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = tokio::try_join!(write_task, read_task);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle incoming text messages
|
||||||
|
async fn handle_text_message(
|
||||||
|
text: &str,
|
||||||
|
sign_handler: &Option<Arc<dyn SignRequestHandler>>,
|
||||||
|
sender: &mpsc::UnboundedSender<Message>,
|
||||||
|
) -> Result<()> {
|
||||||
|
log::debug!("Received message: {}", text);
|
||||||
|
|
||||||
|
// Handle simple acknowledgment messages
|
||||||
|
if text == "Connected" {
|
||||||
|
log::info!("Server acknowledged connection");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as sign request
|
||||||
|
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
|
||||||
|
if let Some(handler) = sign_handler {
|
||||||
|
// Handle the sign request
|
||||||
|
match handler.handle_sign_request(&sign_request) {
|
||||||
|
Ok(signature) => {
|
||||||
|
// Create and send response
|
||||||
|
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
|
||||||
|
let response_json = serde_json::to_string(&response)?;
|
||||||
|
|
||||||
|
sender.send(Message::Text(response_json))
|
||||||
|
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||||
|
|
||||||
|
log::info!("Sent signature response for request {}", response.id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Sign request rejected: {}", e);
|
||||||
|
// Optionally send an error response to the server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("No sign request handler registered, ignoring request");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
log::warn!("Failed to parse message: {}", text);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the WebSocket server
|
||||||
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
*self.connected.write().await = false;
|
||||||
|
|
||||||
|
if let Some(sender) = &self.sender {
|
||||||
|
// Send close message
|
||||||
|
let _ = sender.send(Message::Close(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sender = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a sign response to the server
|
||||||
|
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||||
|
if let Some(sender) = &self.sender {
|
||||||
|
let response_json = serde_json::to_string(response)?;
|
||||||
|
sender.send(Message::Text(response_json))
|
||||||
|
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SigSocketError::NotConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if connected
|
||||||
|
pub async fn is_connected(&self) -> bool {
|
||||||
|
*self.connected.read().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for NativeClient {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Cleanup will be handled by the async tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
256
sigsocket_client/src/protocol.rs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
//! Protocol definitions for sigsocket communication
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Sign request from the sigsocket server
|
||||||
|
///
|
||||||
|
/// This represents a request from the server for the client to sign a message.
|
||||||
|
/// The client should present this to the user for approval before signing.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct SignRequest {
|
||||||
|
/// Unique identifier for this request
|
||||||
|
pub id: String,
|
||||||
|
/// Message to be signed (base64-encoded)
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign response to send back to the sigsocket server
|
||||||
|
///
|
||||||
|
/// This represents the client's response after the user has approved and signed the message.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct SignResponse {
|
||||||
|
/// Request identifier (must match the original request)
|
||||||
|
pub id: String,
|
||||||
|
/// Original message that was signed (base64-encoded)
|
||||||
|
pub message: String,
|
||||||
|
/// Signature of the message (base64-encoded)
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignRequest {
|
||||||
|
/// Create a new sign request
|
||||||
|
pub fn new(id: impl Into<String>, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the message as bytes (decoded from base64)
|
||||||
|
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the message as a hex string (for display purposes)
|
||||||
|
pub fn message_hex(&self) -> Result<String, base64::DecodeError> {
|
||||||
|
self.message_bytes().map(|bytes| hex::encode(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignResponse {
|
||||||
|
/// Create a new sign response
|
||||||
|
pub fn new(
|
||||||
|
id: impl Into<String>,
|
||||||
|
message: impl Into<String>,
|
||||||
|
signature: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
message: message.into(),
|
||||||
|
signature: signature.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a sign response from a request and signature bytes
|
||||||
|
pub fn from_request_and_signature(
|
||||||
|
request: &SignRequest,
|
||||||
|
signature: &[u8],
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: request.id.clone(),
|
||||||
|
message: request.message.clone(),
|
||||||
|
signature: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the signature as bytes (decoded from base64)
|
||||||
|
pub fn signature_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enhanced sign request with additional metadata for request management
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ManagedSignRequest {
|
||||||
|
/// The original sign request
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub request: SignRequest,
|
||||||
|
/// Timestamp when the request was received (Unix timestamp in milliseconds)
|
||||||
|
pub timestamp: u64,
|
||||||
|
/// Target public key for this request (hex-encoded)
|
||||||
|
pub target_public_key: String,
|
||||||
|
/// Current status of the request
|
||||||
|
pub status: RequestStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status of a sign request
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum RequestStatus {
|
||||||
|
/// Request is pending user approval
|
||||||
|
Pending,
|
||||||
|
/// Request has been approved and signed
|
||||||
|
Approved,
|
||||||
|
/// Request has been rejected by user
|
||||||
|
Rejected,
|
||||||
|
/// Request has expired or been cancelled
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagedSignRequest {
|
||||||
|
/// Create a new managed sign request
|
||||||
|
pub fn new(request: SignRequest, target_public_key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
request,
|
||||||
|
timestamp: current_timestamp_ms(),
|
||||||
|
target_public_key,
|
||||||
|
status: RequestStatus::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the request ID
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.request.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the message as bytes (decoded from base64)
|
||||||
|
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
self.request.message_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this request is for the given public key
|
||||||
|
pub fn is_for_public_key(&self, public_key: &str) -> bool {
|
||||||
|
self.target_public_key == public_key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the request as approved
|
||||||
|
pub fn mark_approved(&mut self) {
|
||||||
|
self.status = RequestStatus::Approved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the request as rejected
|
||||||
|
pub fn mark_rejected(&mut self) {
|
||||||
|
self.status = RequestStatus::Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the request is still pending
|
||||||
|
pub fn is_pending(&self) -> bool {
|
||||||
|
matches!(self.status, RequestStatus::Pending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current timestamp in milliseconds
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn current_timestamp_ms() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current timestamp in milliseconds (WASM version)
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn current_timestamp_ms() -> u64 {
|
||||||
|
// In WASM, we'll use a simple counter or Date.now() via JS
|
||||||
|
// For now, return 0 - this can be improved later
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_request_creation() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||||
|
assert_eq!(request.id, "test-id");
|
||||||
|
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_request_message_bytes() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||||
|
let bytes = request.message_bytes().unwrap();
|
||||||
|
assert_eq!(bytes, b"test message");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_request_message_hex() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||||
|
let hex = request.message_hex().unwrap();
|
||||||
|
assert_eq!(hex, hex::encode(b"test message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_response_creation() {
|
||||||
|
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl"); // "signature" in base64
|
||||||
|
assert_eq!(response.id, "test-id");
|
||||||
|
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||||
|
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_response_from_request() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||||
|
let signature = b"signature";
|
||||||
|
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||||
|
|
||||||
|
assert_eq!(response.id, request.id);
|
||||||
|
assert_eq!(response.message, request.message);
|
||||||
|
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||||
|
let json = serde_json::to_string(&request).unwrap();
|
||||||
|
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(request, deserialized);
|
||||||
|
|
||||||
|
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(response, deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_managed_sign_request() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||||
|
let managed = ManagedSignRequest::new(request.clone(), "test-public-key".to_string());
|
||||||
|
|
||||||
|
assert_eq!(managed.id(), "test-id");
|
||||||
|
assert_eq!(managed.request, request);
|
||||||
|
assert_eq!(managed.target_public_key, "test-public-key");
|
||||||
|
assert!(managed.is_pending());
|
||||||
|
assert!(managed.is_for_public_key("test-public-key"));
|
||||||
|
assert!(!managed.is_for_public_key("other-key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_managed_request_status_changes() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||||
|
let mut managed = ManagedSignRequest::new(request, "test-public-key".to_string());
|
||||||
|
|
||||||
|
assert!(managed.is_pending());
|
||||||
|
|
||||||
|
managed.mark_approved();
|
||||||
|
assert_eq!(managed.status, RequestStatus::Approved);
|
||||||
|
assert!(!managed.is_pending());
|
||||||
|
|
||||||
|
managed.mark_rejected();
|
||||||
|
assert_eq!(managed.status, RequestStatus::Rejected);
|
||||||
|
assert!(!managed.is_pending());
|
||||||
|
}
|
||||||
|
}
|
||||||
549
sigsocket_client/src/wasm.rs
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
//! WASM implementation of the sigsocket client
|
||||||
|
|
||||||
|
use alloc::{string::{String, ToString}, vec::Vec, boxed::Box, rc::Rc, format};
|
||||||
|
use core::cell::RefCell;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::{WebSocket, MessageEvent, Event, BinaryType};
|
||||||
|
|
||||||
|
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||||
|
|
||||||
|
/// WASM WebSocket client implementation
|
||||||
|
pub struct WasmClient {
|
||||||
|
url: String,
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||||
|
websocket: Option<WebSocket>,
|
||||||
|
connected: Rc<RefCell<bool>>,
|
||||||
|
reconnect_attempts: Rc<RefCell<u32>>,
|
||||||
|
max_reconnect_attempts: u32,
|
||||||
|
reconnect_delay_ms: u64,
|
||||||
|
auto_reconnect: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasmClient {
|
||||||
|
/// Create a new WASM client
|
||||||
|
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
url: url.to_string(),
|
||||||
|
public_key: public_key.to_vec(),
|
||||||
|
sign_handler: None,
|
||||||
|
websocket: None,
|
||||||
|
connected: Rc::new(RefCell::new(false)),
|
||||||
|
reconnect_attempts: Rc::new(RefCell::new(0)),
|
||||||
|
max_reconnect_attempts: 5,
|
||||||
|
reconnect_delay_ms: 1000, // Start with 1 second
|
||||||
|
auto_reconnect: false, // Disable auto-reconnect to avoid multiple connections
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the sign request handler from a boxed trait object
|
||||||
|
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
|
||||||
|
self.sign_handler = Some(Rc::new(RefCell::new(handler)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable automatic reconnection
|
||||||
|
pub fn set_auto_reconnect(&mut self, enabled: bool) {
|
||||||
|
self.auto_reconnect = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set reconnection parameters
|
||||||
|
pub fn set_reconnect_config(&mut self, max_attempts: u32, initial_delay_ms: u64) {
|
||||||
|
self.max_reconnect_attempts = max_attempts;
|
||||||
|
self.reconnect_delay_ms = initial_delay_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to the WebSocket server with automatic reconnection
|
||||||
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
|
*self.reconnect_attempts.borrow_mut() = 0;
|
||||||
|
self.connect_with_retry().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect with retry logic
|
||||||
|
async fn connect_with_retry(&mut self) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match self.try_connect().await {
|
||||||
|
Ok(()) => {
|
||||||
|
*self.reconnect_attempts.borrow_mut() = 0; // Reset on successful connection
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut attempts = self.reconnect_attempts.borrow_mut();
|
||||||
|
*attempts += 1;
|
||||||
|
|
||||||
|
if *attempts > self.max_reconnect_attempts {
|
||||||
|
web_sys::console::error_1(&format!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts).into());
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = self.reconnect_delay_ms * (2_u64.pow(*attempts - 1)); // Exponential backoff
|
||||||
|
web_sys::console::warn_1(&format!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||||
|
*attempts, self.max_reconnect_attempts, delay, e).into());
|
||||||
|
|
||||||
|
// Drop the borrow before the async sleep
|
||||||
|
drop(attempts);
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
self.sleep_ms(delay).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sleep for the specified number of milliseconds (WASM-compatible)
|
||||||
|
async fn sleep_ms(&self, ms: u64) -> () {
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use js_sys::Promise;
|
||||||
|
|
||||||
|
let promise = Promise::new(&mut |resolve, _reject| {
|
||||||
|
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||||
|
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
timeout_callback.as_ref().unchecked_ref(),
|
||||||
|
ms as i32,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
timeout_callback.forget();
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = JsFuture::from(promise).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single connection attempt
|
||||||
|
async fn try_connect(&mut self) -> Result<()> {
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use js_sys::Promise;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&format!("try_connect: Creating WebSocket to {}", self.url).into());
|
||||||
|
|
||||||
|
// Create WebSocket
|
||||||
|
let ws = WebSocket::new(&self.url)
|
||||||
|
.map_err(|e| {
|
||||||
|
web_sys::console::error_1(&format!("Failed to create WebSocket: {:?}", e).into());
|
||||||
|
SigSocketError::Connection(format!("{:?}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"try_connect: WebSocket created successfully".into());
|
||||||
|
|
||||||
|
// Set binary type
|
||||||
|
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"try_connect: Binary type set, setting up event handlers".into());
|
||||||
|
|
||||||
|
let connected = self.connected.clone();
|
||||||
|
let public_key = self.public_key.clone();
|
||||||
|
|
||||||
|
// Set up onopen handler
|
||||||
|
{
|
||||||
|
let ws_clone = ws.clone();
|
||||||
|
let public_key_clone = public_key.clone();
|
||||||
|
|
||||||
|
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||||
|
web_sys::console::log_1(&"MAIN CONNECTION: WebSocket opened, sending public key introduction".into());
|
||||||
|
|
||||||
|
// Send introduction message (hex-encoded public key)
|
||||||
|
let intro_message = hex::encode(&public_key_clone);
|
||||||
|
web_sys::console::log_1(&format!("MAIN CONNECTION: Sending public key: {}", &intro_message[..16]).into());
|
||||||
|
|
||||||
|
if let Err(e) = ws_clone.send_with_str(&intro_message) {
|
||||||
|
web_sys::console::error_1(&format!("MAIN CONNECTION: Failed to send introduction: {:?}", e).into());
|
||||||
|
} else {
|
||||||
|
web_sys::console::log_1(&"MAIN CONNECTION: Public key sent successfully".into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||||
|
onopen_callback.forget(); // Prevent cleanup
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"try_connect: onopen handler set up".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up onmessage handler
|
||||||
|
{
|
||||||
|
let ws_clone = ws.clone();
|
||||||
|
let handler_clone = self.sign_handler.clone();
|
||||||
|
let connected_clone = connected.clone();
|
||||||
|
|
||||||
|
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||||
|
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
|
||||||
|
let message = text.as_string().unwrap_or_default();
|
||||||
|
web_sys::console::log_1(&format!("MAIN CONNECTION: Received message: {}", message).into());
|
||||||
|
|
||||||
|
// Check if this is the "Connected" acknowledgment
|
||||||
|
if message == "Connected" {
|
||||||
|
web_sys::console::log_1(&"MAIN CONNECTION: Server acknowledged connection".into());
|
||||||
|
*connected_clone.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the message with proper sign request support
|
||||||
|
Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||||
|
onmessage_callback.forget(); // Prevent cleanup
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"try_connect: onmessage handler set up".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up onerror handler
|
||||||
|
{
|
||||||
|
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
|
||||||
|
web_sys::console::error_1(&format!("MAIN CONNECTION: WebSocket error: {:?}", event).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||||
|
onerror_callback.forget(); // Prevent cleanup
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"try_connect: onerror handler set up".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up onclose handler with auto-reconnection support
|
||||||
|
{
|
||||||
|
let connected = connected.clone();
|
||||||
|
let auto_reconnect = self.auto_reconnect;
|
||||||
|
let reconnect_attempts = self.reconnect_attempts.clone();
|
||||||
|
let max_attempts = self.max_reconnect_attempts;
|
||||||
|
let url = self.url.clone();
|
||||||
|
let public_key = self.public_key.clone();
|
||||||
|
let sign_handler = self.sign_handler.clone();
|
||||||
|
let delay_ms = self.reconnect_delay_ms;
|
||||||
|
|
||||||
|
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||||
|
*connected.borrow_mut() = false;
|
||||||
|
web_sys::console::log_1(&"WebSocket connection closed".into());
|
||||||
|
|
||||||
|
// Trigger auto-reconnection if enabled
|
||||||
|
if auto_reconnect {
|
||||||
|
let attempts = reconnect_attempts.clone();
|
||||||
|
let current_attempts = *attempts.borrow();
|
||||||
|
|
||||||
|
if current_attempts < max_attempts {
|
||||||
|
web_sys::console::log_1(&"Attempting automatic reconnection...".into());
|
||||||
|
|
||||||
|
// Schedule reconnection attempt
|
||||||
|
Self::schedule_reconnection(
|
||||||
|
url.clone(),
|
||||||
|
public_key.clone(),
|
||||||
|
sign_handler.clone(),
|
||||||
|
attempts.clone(),
|
||||||
|
max_attempts,
|
||||||
|
delay_ms,
|
||||||
|
connected.clone(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
web_sys::console::error_1(&format!("Max reconnection attempts ({}) reached, giving up", max_attempts).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
|
||||||
|
onclose_callback.forget(); // Prevent cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check WebSocket state before storing
|
||||||
|
let ready_state = ws.ready_state();
|
||||||
|
web_sys::console::log_1(&format!("try_connect: WebSocket ready state: {}", ready_state).into());
|
||||||
|
|
||||||
|
self.websocket = Some(ws);
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"try_connect: WebSocket stored, waiting for connection to be established".into());
|
||||||
|
|
||||||
|
// The WebSocket will open asynchronously and the onopen/onmessage handlers will handle the connection
|
||||||
|
// Since we can see from logs that the connection is working, just return success
|
||||||
|
web_sys::console::log_1(&"try_connect: WebSocket setup complete, connection will be established asynchronously".into());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for WebSocket connection to be established
|
||||||
|
async fn wait_for_connection(&self) -> Result<()> {
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use js_sys::Promise;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"wait_for_connection: Starting to wait for connection".into());
|
||||||
|
|
||||||
|
// Simple approach: just wait a bit and check if we're connected
|
||||||
|
// The onopen handler should have fired by now if the connection is working
|
||||||
|
|
||||||
|
let connected = self.connected.clone();
|
||||||
|
|
||||||
|
// Wait up to 30 seconds, checking every 500ms
|
||||||
|
for attempt in 1..=60 {
|
||||||
|
// Check if we're connected
|
||||||
|
if *connected.borrow() {
|
||||||
|
web_sys::console::log_1(&format!("wait_for_connection: Connected after {} attempts ({}ms)", attempt, attempt * 500).into());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 500ms before next check
|
||||||
|
let promise = Promise::new(&mut |resolve, _reject| {
|
||||||
|
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||||
|
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
timeout_callback.as_ref().unchecked_ref(),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
timeout_callback.forget();
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = JsFuture::from(promise).await;
|
||||||
|
|
||||||
|
if attempt % 10 == 0 {
|
||||||
|
web_sys::console::log_1(&format!("wait_for_connection: Still waiting... attempt {}/60", attempt).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
web_sys::console::error_1(&"wait_for_connection: Timeout after 30 seconds".into());
|
||||||
|
Err(SigSocketError::Connection("Connection timeout".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule a reconnection attempt (called from onclose handler)
|
||||||
|
fn schedule_reconnection(
|
||||||
|
url: String,
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||||
|
reconnect_attempts: Rc<RefCell<u32>>,
|
||||||
|
_max_attempts: u32,
|
||||||
|
delay_ms: u64,
|
||||||
|
connected: Rc<RefCell<bool>>,
|
||||||
|
) {
|
||||||
|
let mut attempts = reconnect_attempts.borrow_mut();
|
||||||
|
*attempts += 1;
|
||||||
|
let current_attempt = *attempts;
|
||||||
|
drop(attempts); // Release the borrow
|
||||||
|
|
||||||
|
let delay = delay_ms * (2_u64.pow(current_attempt - 1)); // Exponential backoff
|
||||||
|
|
||||||
|
web_sys::console::log_1(&format!("Scheduling reconnection attempt {} in {}ms", current_attempt, delay).into());
|
||||||
|
|
||||||
|
// Schedule the reconnection attempt
|
||||||
|
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||||
|
// Create a new client instance for reconnection
|
||||||
|
match Self::attempt_reconnection(url.clone(), public_key.clone(), sign_handler.clone(), connected.clone()) {
|
||||||
|
Ok(_) => {
|
||||||
|
web_sys::console::log_1(&"Reconnection attempt initiated".into());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::error_1(&format!("Failed to initiate reconnection: {:?}", e).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
timeout_callback.as_ref().unchecked_ref(),
|
||||||
|
delay as i32,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
timeout_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to reconnect (helper method)
|
||||||
|
fn attempt_reconnection(
|
||||||
|
url: String,
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||||
|
connected: Rc<RefCell<bool>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Create WebSocket
|
||||||
|
let ws = WebSocket::new(&url)
|
||||||
|
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||||
|
|
||||||
|
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||||
|
|
||||||
|
// Send public key on open
|
||||||
|
{
|
||||||
|
let public_key_clone = public_key.clone();
|
||||||
|
let connected_clone = connected.clone();
|
||||||
|
let ws_clone = ws.clone();
|
||||||
|
|
||||||
|
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||||
|
web_sys::console::log_1(&"Reconnection WebSocket opened, sending public key introduction".into());
|
||||||
|
|
||||||
|
// Send public key introduction
|
||||||
|
let public_key_hex = hex::encode(&public_key_clone);
|
||||||
|
web_sys::console::log_1(&format!("Reconnection sending public key: {}", &public_key_hex[..16]).into());
|
||||||
|
|
||||||
|
if let Err(e) = ws_clone.send_with_str(&public_key_hex) {
|
||||||
|
web_sys::console::error_1(&format!("Failed to send public key on reconnection: {:?}", e).into());
|
||||||
|
} else {
|
||||||
|
web_sys::console::log_1(&"Reconnection public key sent successfully, waiting for server acknowledgment".into());
|
||||||
|
// Don't set connected=true here, wait for "Connected" message
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||||
|
onopen_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up message handler for reconnected socket
|
||||||
|
{
|
||||||
|
let ws_clone = ws.clone();
|
||||||
|
let handler_clone = sign_handler.clone();
|
||||||
|
let connected_clone = connected.clone();
|
||||||
|
|
||||||
|
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||||
|
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
|
||||||
|
let message = text.as_string().unwrap_or_default();
|
||||||
|
Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||||
|
onmessage_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up error handler
|
||||||
|
{
|
||||||
|
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
|
||||||
|
web_sys::console::error_1(&format!("Reconnection WebSocket error: {:?}", event).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||||
|
onerror_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up close handler (for potential future reconnections)
|
||||||
|
{
|
||||||
|
let connected_clone = connected.clone();
|
||||||
|
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||||
|
*connected_clone.borrow_mut() = false;
|
||||||
|
web_sys::console::log_1(&"Reconnected WebSocket closed".into());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
|
||||||
|
onclose_callback.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle incoming messages with full sign request support
|
||||||
|
fn handle_message(
|
||||||
|
text: &str,
|
||||||
|
ws: &WebSocket,
|
||||||
|
sign_handler: &Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||||
|
connected: &Rc<RefCell<bool>>
|
||||||
|
) {
|
||||||
|
web_sys::console::log_1(&format!("Received message: {}", text).into());
|
||||||
|
|
||||||
|
// Handle simple acknowledgment messages
|
||||||
|
if text == "Connected" {
|
||||||
|
web_sys::console::log_1(&"Server acknowledged connection".into());
|
||||||
|
*connected.borrow_mut() = true;
|
||||||
|
web_sys::console::log_1(&"Connection state updated to connected".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as sign request
|
||||||
|
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
|
||||||
|
web_sys::console::log_1(&format!("Received sign request: {}", sign_request.id).into());
|
||||||
|
|
||||||
|
// Handle the sign request if we have a handler
|
||||||
|
if let Some(handler_rc) = sign_handler {
|
||||||
|
match handler_rc.try_borrow() {
|
||||||
|
Ok(handler) => {
|
||||||
|
match handler.handle_sign_request(&sign_request) {
|
||||||
|
Ok(signature) => {
|
||||||
|
// Create and send response
|
||||||
|
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
|
||||||
|
match serde_json::to_string(&response) {
|
||||||
|
Ok(response_json) => {
|
||||||
|
if let Err(e) = ws.send_with_str(&response_json) {
|
||||||
|
web_sys::console::error_1(&format!("Failed to send response: {:?}", e).into());
|
||||||
|
} else {
|
||||||
|
web_sys::console::log_1(&format!("Sent signature response for request {}", response.id).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::error_1(&format!("Failed to serialize response: {}", e).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::warn_1(&format!("Sign request rejected: {}", e).into());
|
||||||
|
// Optionally send an error response to the server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
web_sys::console::error_1(&"Failed to borrow sign handler".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
web_sys::console::warn_1(&"No sign request handler registered, ignoring request".into());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
web_sys::console::warn_1(&format!("Failed to parse message: {}", text).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the WebSocket server
|
||||||
|
pub async fn disconnect(&mut self) -> Result<()> {
|
||||||
|
if let Some(ws) = &self.websocket {
|
||||||
|
ws.close()
|
||||||
|
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.connected.borrow_mut() = false;
|
||||||
|
self.websocket = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a sign response to the server
|
||||||
|
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||||
|
if let Some(ws) = &self.websocket {
|
||||||
|
let response_json = serde_json::to_string(response)?;
|
||||||
|
ws.send_with_str(&response_json)
|
||||||
|
.map_err(|e| SigSocketError::Send(format!("{:?}", e)))?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SigSocketError::NotConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if connected
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
*self.connected.borrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WasmClient {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Close WebSocket connection if it exists
|
||||||
|
if let Some(ws) = self.websocket.take() {
|
||||||
|
ws.close().unwrap_or_else(|e| {
|
||||||
|
web_sys::console::warn_1(&format!("Failed to close WebSocket: {:?}", e).into());
|
||||||
|
});
|
||||||
|
web_sys::console::log_1(&"🔌 WebSocket connection closed on drop".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM-specific utilities
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(js_namespace = console)]
|
||||||
|
fn log(s: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper macro for logging in WASM
|
||||||
|
#[allow(unused_macros)]
|
||||||
|
macro_rules! console_log {
|
||||||
|
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||||
|
}
|
||||||
162
sigsocket_client/tests/integration_test.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
//! Integration tests for sigsocket_client
|
||||||
|
|
||||||
|
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||||
|
|
||||||
|
/// Test sign request handler
|
||||||
|
struct TestSignHandler {
|
||||||
|
should_approve: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestSignHandler {
|
||||||
|
fn new(should_approve: bool) -> Self {
|
||||||
|
Self { should_approve }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignRequestHandler for TestSignHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
if self.should_approve {
|
||||||
|
// Create a test signature
|
||||||
|
let signature = format!("test_signature_for_{}", request.id);
|
||||||
|
Ok(signature.into_bytes())
|
||||||
|
} else {
|
||||||
|
Err(SigSocketError::Other("User rejected request".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_request_creation() {
|
||||||
|
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
|
||||||
|
assert_eq!(request.id, "test-123");
|
||||||
|
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_request_message_decoding() {
|
||||||
|
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||||
|
|
||||||
|
let bytes = request.message_bytes().unwrap();
|
||||||
|
assert_eq!(bytes, b"test message");
|
||||||
|
|
||||||
|
let hex = request.message_hex().unwrap();
|
||||||
|
assert_eq!(hex, hex::encode(b"test message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_response_creation() {
|
||||||
|
let response = SignResponse::new("test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||||
|
assert_eq!(response.id, "test-123");
|
||||||
|
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||||
|
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_response_from_request() {
|
||||||
|
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
|
||||||
|
let signature = b"test_signature";
|
||||||
|
|
||||||
|
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||||
|
assert_eq!(response.id, request.id);
|
||||||
|
assert_eq!(response.message, request.message);
|
||||||
|
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_protocol_serialization() {
|
||||||
|
// Test SignRequest serialization
|
||||||
|
let request = SignRequest::new("req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
|
||||||
|
let json = serde_json::to_string(&request).unwrap();
|
||||||
|
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(request, deserialized);
|
||||||
|
|
||||||
|
// Test SignResponse serialization
|
||||||
|
let response = SignResponse::new("req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(response, deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_creation() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
|
||||||
|
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||||
|
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_invalid_url() {
|
||||||
|
let public_key = vec![1, 2, 3];
|
||||||
|
let result = SigSocketClient::new("invalid-url", public_key);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_empty_public_key() {
|
||||||
|
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let Err(error) = result {
|
||||||
|
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_handler_approval() {
|
||||||
|
let handler = TestSignHandler::new(true);
|
||||||
|
let request = SignRequest::new("test-789", "dGVzdA==");
|
||||||
|
|
||||||
|
let result = handler.handle_sign_request(&request);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let signature = result.unwrap();
|
||||||
|
assert_eq!(signature, b"test_signature_for_test-789");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_handler_rejection() {
|
||||||
|
let handler = TestSignHandler::new(false);
|
||||||
|
let request = SignRequest::new("test-789", "dGVzdA==");
|
||||||
|
|
||||||
|
let result = handler.handle_sign_request(&request);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_display() {
|
||||||
|
let error = SigSocketError::NotConnected;
|
||||||
|
assert_eq!(error.to_string(), "Client is not connected");
|
||||||
|
|
||||||
|
let error = SigSocketError::Connection("test error".to_string());
|
||||||
|
assert_eq!(error.to_string(), "Connection error: test error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that demonstrates the expected usage pattern
|
||||||
|
#[test]
|
||||||
|
fn test_usage_pattern() {
|
||||||
|
// 1. Create client
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||||
|
.unwrap();
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// 2. Set handler
|
||||||
|
client.set_sign_handler(TestSignHandler::new(true));
|
||||||
|
|
||||||
|
// 3. Verify state
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
|
||||||
|
// 4. Create a test request/response cycle
|
||||||
|
let request = SignRequest::new("test-request", "dGVzdCBtZXNzYWdl");
|
||||||
|
let handler = TestSignHandler::new(true);
|
||||||
|
let signature = handler.handle_sign_request(&request).unwrap();
|
||||||
|
let response = SignResponse::from_request_and_signature(&request, &signature);
|
||||||
|
|
||||||
|
// 5. Verify the response
|
||||||
|
assert_eq!(response.id, request.id);
|
||||||
|
assert_eq!(response.message, request.message);
|
||||||
|
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||||
|
}
|
||||||
92
sigsocket_client/tests/request_management_test.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
//! Tests for the enhanced request management functionality
|
||||||
|
|
||||||
|
use sigsocket_client::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_request_management() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// Initially no requests
|
||||||
|
assert_eq!(client.pending_request_count(), 0);
|
||||||
|
assert!(client.get_pending_requests().is_empty());
|
||||||
|
|
||||||
|
// Add a request
|
||||||
|
let request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl");
|
||||||
|
let public_key_hex = "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
|
||||||
|
client.add_pending_request(request.clone(), public_key_hex.to_string());
|
||||||
|
|
||||||
|
// Check request was added
|
||||||
|
assert_eq!(client.pending_request_count(), 1);
|
||||||
|
assert!(client.get_pending_request("test-1").is_some());
|
||||||
|
|
||||||
|
// Check filtering by public key
|
||||||
|
let filtered = client.get_requests_for_public_key(public_key_hex);
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
assert_eq!(filtered[0].id(), "test-1");
|
||||||
|
|
||||||
|
// Add another request for different public key
|
||||||
|
let request2 = SignRequest::new("test-2", "dGVzdCBtZXNzYWdlMg==");
|
||||||
|
let other_public_key = "03f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
|
||||||
|
client.add_pending_request(request2, other_public_key.to_string());
|
||||||
|
|
||||||
|
// Check total count
|
||||||
|
assert_eq!(client.pending_request_count(), 2);
|
||||||
|
|
||||||
|
// Check filtering still works
|
||||||
|
let filtered = client.get_requests_for_public_key(public_key_hex);
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
|
||||||
|
let filtered_other = client.get_requests_for_public_key(other_public_key);
|
||||||
|
assert_eq!(filtered_other.len(), 1);
|
||||||
|
|
||||||
|
// Remove a request
|
||||||
|
let removed = client.remove_pending_request("test-1");
|
||||||
|
assert!(removed.is_some());
|
||||||
|
assert_eq!(removed.unwrap().id(), "test-1");
|
||||||
|
assert_eq!(client.pending_request_count(), 1);
|
||||||
|
|
||||||
|
// Clear all requests
|
||||||
|
client.clear_pending_requests();
|
||||||
|
assert_eq!(client.pending_request_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_request_validation() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// Valid request
|
||||||
|
let valid_request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl");
|
||||||
|
assert!(client.can_handle_request_for_key(&valid_request, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid request - empty ID
|
||||||
|
let invalid_request = SignRequest::new("", "dGVzdCBtZXNzYWdl");
|
||||||
|
assert!(!client.can_handle_request_for_key(&invalid_request, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid request - empty message
|
||||||
|
let invalid_request2 = SignRequest::new("test-1", "");
|
||||||
|
assert!(!client.can_handle_request_for_key(&invalid_request2, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid request - invalid base64
|
||||||
|
let invalid_request3 = SignRequest::new("test-1", "invalid-base64!");
|
||||||
|
assert!(!client.can_handle_request_for_key(&invalid_request3, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid public key
|
||||||
|
assert!(!client.can_handle_request_for_key(&valid_request, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_connection_state() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// Initially disconnected
|
||||||
|
assert_eq!(client.state(), ConnectionState::Disconnected);
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
assert!(client.connected_public_key().is_none());
|
||||||
|
|
||||||
|
// Public key should be available
|
||||||
|
assert_eq!(client.public_key_hex(), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9");
|
||||||
|
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||||
|
}
|
||||||
181
sigsocket_client/tests/wasm_tests.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
//! WASM/browser tests for sigsocket_client using wasm-bindgen-test
|
||||||
|
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||||
|
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
/// Test sign request handler for WASM tests
|
||||||
|
struct TestWasmSignHandler {
|
||||||
|
should_approve: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestWasmSignHandler {
|
||||||
|
fn new(should_approve: bool) -> Self {
|
||||||
|
Self { should_approve }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignRequestHandler for TestWasmSignHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
if self.should_approve {
|
||||||
|
// Create a test signature
|
||||||
|
let signature = format!("wasm_test_signature_for_{}", request.id);
|
||||||
|
Ok(signature.into_bytes())
|
||||||
|
} else {
|
||||||
|
Err(SigSocketError::Other("User rejected request in WASM test".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_sign_request_creation_wasm() {
|
||||||
|
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
|
||||||
|
assert_eq!(request.id, "wasm-test-123");
|
||||||
|
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_sign_request_message_decoding_wasm() {
|
||||||
|
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||||
|
|
||||||
|
let bytes = request.message_bytes().unwrap();
|
||||||
|
assert_eq!(bytes, b"test message");
|
||||||
|
|
||||||
|
let hex = request.message_hex().unwrap();
|
||||||
|
assert_eq!(hex, hex::encode(b"test message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_sign_response_creation_wasm() {
|
||||||
|
let response = SignResponse::new("wasm-test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||||
|
assert_eq!(response.id, "wasm-test-123");
|
||||||
|
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||||
|
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_sign_response_from_request_wasm() {
|
||||||
|
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
|
||||||
|
let signature = b"wasm_test_signature";
|
||||||
|
|
||||||
|
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||||
|
assert_eq!(response.id, request.id);
|
||||||
|
assert_eq!(response.message, request.message);
|
||||||
|
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_protocol_serialization_wasm() {
|
||||||
|
// Test SignRequest serialization
|
||||||
|
let request = SignRequest::new("wasm-req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
|
||||||
|
let json = serde_json::to_string(&request).unwrap();
|
||||||
|
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(request, deserialized);
|
||||||
|
|
||||||
|
// Test SignResponse serialization
|
||||||
|
let response = SignResponse::new("wasm-req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(response, deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_client_creation_wasm() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
|
||||||
|
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||||
|
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_client_invalid_url_wasm() {
|
||||||
|
let public_key = vec![1, 2, 3];
|
||||||
|
let result = SigSocketClient::new("invalid-url", public_key);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_client_empty_public_key_wasm() {
|
||||||
|
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let Err(error) = result {
|
||||||
|
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_sign_handler_approval_wasm() {
|
||||||
|
let handler = TestWasmSignHandler::new(true);
|
||||||
|
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
|
||||||
|
|
||||||
|
let result = handler.handle_sign_request(&request);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let signature = result.unwrap();
|
||||||
|
assert_eq!(signature, b"wasm_test_signature_for_wasm-test-789");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_sign_handler_rejection_wasm() {
|
||||||
|
let handler = TestWasmSignHandler::new(false);
|
||||||
|
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
|
||||||
|
|
||||||
|
let result = handler.handle_sign_request(&request);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_error_display_wasm() {
|
||||||
|
let error = SigSocketError::NotConnected;
|
||||||
|
assert_eq!(error.to_string(), "Client is not connected");
|
||||||
|
|
||||||
|
let error = SigSocketError::Connection("wasm test error".to_string());
|
||||||
|
assert_eq!(error.to_string(), "Connection error: wasm test error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that demonstrates the expected WASM usage pattern
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_wasm_usage_pattern() {
|
||||||
|
// 1. Create client
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||||
|
.unwrap();
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// 2. Set handler
|
||||||
|
client.set_sign_handler(TestWasmSignHandler::new(true));
|
||||||
|
|
||||||
|
// 3. Verify state
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
|
||||||
|
// 4. Create a test request/response cycle
|
||||||
|
let request = SignRequest::new("wasm-test-request", "dGVzdCBtZXNzYWdl");
|
||||||
|
let handler = TestWasmSignHandler::new(true);
|
||||||
|
let signature = handler.handle_sign_request(&request).unwrap();
|
||||||
|
let response = SignResponse::from_request_and_signature(&request, &signature);
|
||||||
|
|
||||||
|
// 5. Verify the response
|
||||||
|
assert_eq!(response.id, request.id);
|
||||||
|
assert_eq!(response.message, request.message);
|
||||||
|
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test WASM-specific console logging (if needed)
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_wasm_console_logging() {
|
||||||
|
// This test verifies that WASM console logging works
|
||||||
|
web_sys::console::log_1(&"SigSocket WASM test logging works!".into());
|
||||||
|
|
||||||
|
// Test that we can create and log protocol messages
|
||||||
|
let request = SignRequest::new("log-test", "dGVzdA==");
|
||||||
|
let json = serde_json::to_string(&request).unwrap();
|
||||||
|
web_sys::console::log_1(&format!("Sign request JSON: {}", json).into());
|
||||||
|
|
||||||
|
// This test always passes - it's just for verification that logging works
|
||||||
|
assert!(true);
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ use error::VaultError;
|
|||||||
pub use kvstore::traits::KVStore;
|
pub use kvstore::traits::KVStore;
|
||||||
|
|
||||||
use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
|
use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
|
||||||
use signature::SignatureEncoding;
|
|
||||||
// TEMP: File-based debug logger for crypto troubleshooting
|
// TEMP: File-based debug logger for crypto troubleshooting
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
@@ -217,7 +216,7 @@ impl<S: KVStore> Vault<S> {
|
|||||||
|
|
||||||
// --- Keypair Management APIs ---
|
// --- Keypair Management APIs ---
|
||||||
|
|
||||||
/// Create a default Ed25519 keypair for client identity
|
/// Create a default Secp256k1 keypair for client identity
|
||||||
/// This keypair is deterministically generated from the password and salt
|
/// This keypair is deterministically generated from the password and salt
|
||||||
/// and will always be the first keypair in the keyspace
|
/// and will always be the first keypair in the keyspace
|
||||||
async fn create_default_keypair(
|
async fn create_default_keypair(
|
||||||
@@ -229,26 +228,32 @@ impl<S: KVStore> Vault<S> {
|
|||||||
// 1. Derive a deterministic seed using standard PBKDF2
|
// 1. Derive a deterministic seed using standard PBKDF2
|
||||||
let seed = kdf::keyspace_key(password, salt);
|
let seed = kdf::keyspace_key(password, salt);
|
||||||
|
|
||||||
// 2. Generate Ed25519 keypair from the seed
|
// 2. Generate Secp256k1 keypair from the seed
|
||||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
use k256::ecdsa::{SigningKey, VerifyingKey};
|
||||||
|
|
||||||
// Use the seed to create a deterministic keypair
|
// Use the seed as the private key directly (32 bytes)
|
||||||
let signing = SigningKey::from_bytes(seed.as_slice().try_into().unwrap());
|
let mut secret_key_bytes = [0u8; 32];
|
||||||
let verifying: VerifyingKey = (&signing).into();
|
secret_key_bytes.copy_from_slice(&seed[..32]);
|
||||||
|
|
||||||
let priv_bytes = signing.to_bytes().to_vec();
|
// Create signing key
|
||||||
let pub_bytes = verifying.to_bytes().to_vec();
|
let signing_key = SigningKey::from_bytes(&secret_key_bytes.into())
|
||||||
|
.map_err(|e| VaultError::Crypto(format!("Failed to create signing key: {}", e)))?;
|
||||||
|
|
||||||
// Create an ID for the default keypair
|
// Get verifying key
|
||||||
|
let verifying_key = VerifyingKey::from(&signing_key);
|
||||||
|
|
||||||
|
// Convert keys to bytes
|
||||||
|
let priv_bytes = signing_key.to_bytes().to_vec();
|
||||||
|
let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
|
||||||
let id = hex::encode(&pub_bytes);
|
let id = hex::encode(&pub_bytes);
|
||||||
|
|
||||||
// 3. Unlock the keyspace to get its data
|
// 3. Unlock keyspace to add the keypair
|
||||||
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
|
||||||
// 4. Add to keypairs (as the first entry)
|
// 4. Create key entry
|
||||||
let entry = KeyEntry {
|
let entry = KeyEntry {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
key_type: KeyType::Ed25519,
|
key_type: KeyType::Secp256k1,
|
||||||
private_key: priv_bytes,
|
private_key: priv_bytes,
|
||||||
public_key: pub_bytes,
|
public_key: pub_bytes,
|
||||||
metadata: Some(KeyMetadata {
|
metadata: Some(KeyMetadata {
|
||||||
@@ -460,14 +465,15 @@ impl<S: KVStore> Vault<S> {
|
|||||||
Ok(sig.to_bytes().to_vec())
|
Ok(sig.to_bytes().to_vec())
|
||||||
}
|
}
|
||||||
KeyType::Secp256k1 => {
|
KeyType::Secp256k1 => {
|
||||||
use k256::ecdsa::{signature::Signer, SigningKey};
|
use k256::ecdsa::{signature::Signer, SigningKey, Signature};
|
||||||
let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| {
|
let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| {
|
||||||
VaultError::Crypto("Invalid secp256k1 private key length".to_string())
|
VaultError::Crypto("Invalid secp256k1 private key length".to_string())
|
||||||
})?;
|
})?;
|
||||||
let sk = SigningKey::from_bytes(arr.into())
|
let sk = SigningKey::from_bytes(arr.into())
|
||||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
let sig: k256::ecdsa::DerSignature = sk.sign(message);
|
let sig: Signature = sk.sign(message);
|
||||||
Ok(sig.to_vec())
|
// Return compact signature (64 bytes) instead of DER format
|
||||||
|
Ok(sig.to_bytes().to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,7 +517,11 @@ impl<S: KVStore> Vault<S> {
|
|||||||
use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
|
use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
|
||||||
let pk = VerifyingKey::from_sec1_bytes(&key.public_key)
|
let pk = VerifyingKey::from_sec1_bytes(&key.public_key)
|
||||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
let sig = Signature::from_der(signature)
|
// Use compact format (64 bytes) instead of DER
|
||||||
|
let sig_array: &[u8; 64] = signature.try_into().map_err(|_| {
|
||||||
|
VaultError::Crypto("Invalid secp256k1 signature length".to_string())
|
||||||
|
})?;
|
||||||
|
let sig = Signature::from_bytes(sig_array.into())
|
||||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
Ok(pk.verify(message, &sig).is_ok())
|
Ok(pk.verify(message, &sig).is_ok())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::session::SessionManager;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static>(
|
pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
session_manager: std::sync::Arc<std::sync::Mutex<SessionManager<S>>>,
|
_session_manager: std::sync::Arc<std::sync::Mutex<SessionManager<S>>>,
|
||||||
) {
|
) {
|
||||||
engine.register_type::<RhaiSessionManager<S>>();
|
engine.register_type::<RhaiSessionManager<S>>();
|
||||||
engine.register_fn("select_keypair", RhaiSessionManager::<S>::select_keypair);
|
engine.register_fn("select_keypair", RhaiSessionManager::<S>::select_keypair);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
//! All state is local to the SessionManager instance. No global state.
|
//! All state is local to the SessionManager instance. No global state.
|
||||||
|
|
||||||
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
|
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
|
||||||
use std::collections::HashMap;
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
vault.create_keyspace(keyspace, password, None).await.unwrap();
|
vault.create_keyspace(keyspace, password, None).await.unwrap();
|
||||||
|
|
||||||
debug!("after create_keyspace: keyspace={} password={}", keyspace, hex::encode(password));
|
debug!("after create_keyspace: keyspace={} password={}", keyspace, hex::encode(password));
|
||||||
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
|
assert_eq!(keys.len(), 1); // should be 1 because we added a default keypair on create_keyspace
|
||||||
debug!("before add Ed25519 keypair");
|
debug!("before add Ed25519 keypair");
|
||||||
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Ed25519), Some(KeyMetadata { name: Some("edkey".into()), created_at: None, tags: None })).await;
|
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Ed25519), Some(KeyMetadata { name: Some("edkey".into()), created_at: None, tags: None })).await;
|
||||||
match &key_id {
|
match &key_id {
|
||||||
@@ -38,7 +40,7 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
|
|
||||||
debug!("before list_keypairs");
|
debug!("before list_keypairs");
|
||||||
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
assert_eq!(keys.len(), 2);
|
assert_eq!(keys.len(), 3);
|
||||||
|
|
||||||
debug!("before export Ed25519 keypair");
|
debug!("before export Ed25519 keypair");
|
||||||
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
|
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
|
||||||
@@ -65,5 +67,5 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
// Remove a keypair
|
// Remove a keypair
|
||||||
vault.remove_keypair(keyspace, password, &key_id).await.unwrap();
|
vault.remove_keypair(keyspace, password, &key_id).await.unwrap();
|
||||||
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
assert_eq!(keys.len(), 1);
|
assert_eq!(keys.len(), 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ async fn session_manager_end_to_end() {
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
let tmp_dir = TempDir::new().expect("create temp dir");
|
let tmp_dir = TempDir::new().expect("create temp dir");
|
||||||
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
|
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
|
||||||
let mut vault = Vault::new(store);
|
let vault = Vault::new(store);
|
||||||
let keyspace = "personal";
|
let keyspace = "personal";
|
||||||
let password = b"testpass";
|
let password = b"testpass";
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ async fn test_session_manager_lock_unlock_keypairs_persistence() {
|
|||||||
|
|
||||||
// 3. List, store keys and names
|
// 3. List, store keys and names
|
||||||
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||||
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
assert_eq!(keypairs_before.len(), 3);
|
||||||
assert_eq!(keypairs_before.len(), 2);
|
|
||||||
assert!(keypairs_before.iter().any(|k| k.0 == id1 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-one")));
|
assert!(keypairs_before.iter().any(|k| k.0 == id1 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-one")));
|
||||||
assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two")));
|
assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two")));
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ web-sys = { version = "0.3", features = ["console"] }
|
|||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
kvstore = { path = "../kvstore" }
|
kvstore = { path = "../kvstore" }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
gloo-utils = "0.1"
|
gloo-utils = "0.1"
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ wasm-bindgen-futures = "0.4"
|
|||||||
once_cell = "1.21"
|
once_cell = "1.21"
|
||||||
vault = { path = "../vault" }
|
vault = { path = "../vault" }
|
||||||
evm_client = { path = "../evm_client" }
|
evm_client = { path = "../evm_client" }
|
||||||
|
sigsocket_client = { path = "../sigsocket_client" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
|
|||||||
@@ -24,8 +24,13 @@ pub use vault::session_singleton::SESSION_MANAGER;
|
|||||||
|
|
||||||
// Include the keypair bindings module
|
// Include the keypair bindings module
|
||||||
mod vault_bindings;
|
mod vault_bindings;
|
||||||
|
mod sigsocket_bindings;
|
||||||
pub use vault_bindings::*;
|
pub use vault_bindings::*;
|
||||||
|
|
||||||
|
// Include the sigsocket module
|
||||||
|
mod sigsocket;
|
||||||
|
pub use sigsocket::*;
|
||||||
|
|
||||||
/// Initialize the scripting environment (must be called before run_rhai)
|
/// Initialize the scripting environment (must be called before run_rhai)
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn init_rhai_env() {
|
pub fn init_rhai_env() {
|
||||||
|
|||||||
168
wasm_app/src/sigsocket/connection.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
//! SigSocket connection wrapper for WASM
|
||||||
|
//!
|
||||||
|
//! This module provides a WASM-bindgen compatible wrapper around the
|
||||||
|
//! SigSocket client that can be used from JavaScript in the browser extension.
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use sigsocket_client::{SigSocketClient, SignResponse};
|
||||||
|
use crate::sigsocket::handler::JavaScriptSignHandler;
|
||||||
|
|
||||||
|
/// WASM-bindgen wrapper for SigSocket client
|
||||||
|
///
|
||||||
|
/// This provides a clean JavaScript API for the browser extension to:
|
||||||
|
/// - Connect to SigSocket servers
|
||||||
|
/// - Send responses to sign requests
|
||||||
|
/// - Manage connection state
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct SigSocketConnection {
|
||||||
|
client: Option<SigSocketClient>,
|
||||||
|
connected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl SigSocketConnection {
|
||||||
|
/// Create a new SigSocket connection
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: None,
|
||||||
|
connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to a SigSocket server
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
/// * `public_key_hex` - Client's public key as hex string
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Successfully connected
|
||||||
|
/// * `Err(error)` - Connection failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn connect(&mut self, server_url: &str, public_key_hex: &str) -> Result<(), JsValue> {
|
||||||
|
web_sys::console::log_1(&format!("SigSocketConnection::connect called with URL: {}", server_url).into());
|
||||||
|
web_sys::console::log_1(&format!("Public key (first 16 chars): {}", &public_key_hex[..16]).into());
|
||||||
|
|
||||||
|
// Decode public key from hex
|
||||||
|
let public_key = hex::decode(public_key_hex)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid public key hex: {}", e)))?;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"Creating SigSocketClient...".into());
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
let mut client = SigSocketClient::new(server_url, public_key)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to create client: {}", e)))?;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"SigSocketClient created, attempting connection...".into());
|
||||||
|
|
||||||
|
// Set up JavaScript handler
|
||||||
|
client.set_sign_handler(JavaScriptSignHandler);
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
web_sys::console::log_1(&"Calling client.connect()...".into());
|
||||||
|
client.connect().await
|
||||||
|
.map_err(|e| {
|
||||||
|
web_sys::console::error_1(&format!("Client connection failed: {}", e).into());
|
||||||
|
JsValue::from_str(&format!("Failed to connect: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"Client connection successful!".into());
|
||||||
|
|
||||||
|
self.client = Some(client);
|
||||||
|
self.connected = true;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&"SigSocketConnection state updated to connected".into());
|
||||||
|
|
||||||
|
// Notify JavaScript of connection state change
|
||||||
|
super::handler::on_connection_state_changed(true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a response to a sign request
|
||||||
|
///
|
||||||
|
/// This should be called by the extension after the user has approved
|
||||||
|
/// a sign request and the message has been signed.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - ID of the original request
|
||||||
|
/// * `message_base64` - Original message (base64-encoded)
|
||||||
|
/// * `signature_hex` - Signature as hex string
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Response sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send response
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn send_response(&self, request_id: &str, message_base64: &str, signature_hex: &str) -> Result<(), JsValue> {
|
||||||
|
let client = self.client.as_ref()
|
||||||
|
.ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||||
|
|
||||||
|
// Decode signature from hex
|
||||||
|
let signature = hex::decode(signature_hex)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid signature hex: {}", e)))?;
|
||||||
|
|
||||||
|
// Create response
|
||||||
|
let response = SignResponse::new(request_id, message_base64,
|
||||||
|
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature));
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
client.send_sign_response(&response).await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to send response: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a rejection for a sign request
|
||||||
|
///
|
||||||
|
/// This should be called when the user rejects a sign request.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - ID of the request to reject
|
||||||
|
/// * `reason` - Reason for rejection (optional)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Rejection sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send rejection
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn send_rejection(&self, request_id: &str, reason: &str) -> Result<(), JsValue> {
|
||||||
|
// For now, we'll just log the rejection
|
||||||
|
// In a full implementation, the server might support rejection messages
|
||||||
|
web_sys::console::log_1(&format!("Sign request {} rejected: {}", request_id, reason).into());
|
||||||
|
|
||||||
|
// TODO: If the server supports rejection messages, send them here
|
||||||
|
// For now, we just ignore the request (timeout on server side)
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the SigSocket server
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn disconnect(&mut self) {
|
||||||
|
if let Some(_client) = self.client.take() {
|
||||||
|
// Note: We can't await in a non-async function, so we'll just drop the client
|
||||||
|
// The Drop implementation should handle cleanup
|
||||||
|
self.connected = false;
|
||||||
|
|
||||||
|
// Notify JavaScript of connection state change
|
||||||
|
super::handler::on_connection_state_changed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if connected to the server
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
// Check if we have a client and if it reports as connected
|
||||||
|
if let Some(ref client) = self.client {
|
||||||
|
client.is_connected()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SigSocketConnection {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
51
wasm_app/src/sigsocket/handler.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//! JavaScript bridge handler for SigSocket sign requests
|
||||||
|
//!
|
||||||
|
//! This module provides a sign request handler that delegates to JavaScript
|
||||||
|
//! callbacks, allowing the browser extension to handle the actual signing
|
||||||
|
//! and user approval flow.
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use sigsocket_client::{SignRequest, SignRequestHandler, Result, SigSocketError};
|
||||||
|
|
||||||
|
/// JavaScript sign handler that delegates to extension
|
||||||
|
///
|
||||||
|
/// This handler receives sign requests from the SigSocket server and
|
||||||
|
/// calls JavaScript callbacks to notify the extension. The extension
|
||||||
|
/// handles the user approval flow and signing, then responds via
|
||||||
|
/// the SigSocketConnection.send_response() method.
|
||||||
|
pub struct JavaScriptSignHandler;
|
||||||
|
|
||||||
|
impl SignRequestHandler for JavaScriptSignHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||||
|
// Call JavaScript callback to notify extension of incoming request
|
||||||
|
on_sign_request_received(&request.id, &request.message);
|
||||||
|
|
||||||
|
// Return error - JavaScript handles response via send_response()
|
||||||
|
// This is intentional as the signing happens asynchronously in the extension
|
||||||
|
Err(SigSocketError::Other("Handled by JavaScript extension".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// External JavaScript functions that the extension must implement
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
/// Called when a sign request is received from the server
|
||||||
|
///
|
||||||
|
/// The extension should:
|
||||||
|
/// 1. Store the request details
|
||||||
|
/// 2. Show notification/badge to user
|
||||||
|
/// 3. Handle user approval flow when popup is opened
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - Unique identifier for the request
|
||||||
|
/// * `message_base64` - Message to be signed (base64-encoded)
|
||||||
|
#[wasm_bindgen(js_name = "onSignRequestReceived")]
|
||||||
|
pub fn on_sign_request_received(request_id: &str, message_base64: &str);
|
||||||
|
|
||||||
|
/// Called when connection state changes
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `connected` - True if connected, false if disconnected
|
||||||
|
#[wasm_bindgen(js_name = "onConnectionStateChanged")]
|
||||||
|
pub fn on_connection_state_changed(connected: bool);
|
||||||
|
}
|
||||||
11
wasm_app/src/sigsocket/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//! SigSocket integration module for WASM app
|
||||||
|
//!
|
||||||
|
//! This module provides a clean transport API for SigSocket communication
|
||||||
|
//! that can be used by the browser extension. It handles connection management
|
||||||
|
//! and delegates signing to the extension through JavaScript callbacks.
|
||||||
|
|
||||||
|
pub mod connection;
|
||||||
|
pub mod handler;
|
||||||
|
|
||||||
|
pub use connection::SigSocketConnection;
|
||||||
|
pub use handler::JavaScriptSignHandler;
|
||||||