feat: production readiness improvements
## Error Handling - Add GlobalErrorBoundary with error classification and recovery - Add custom error types (SecurityError, ConnectionError, TimeoutError) - Fix ErrorAlert component syntax errors ## Offline Mode - Add offlineStore for offline state management - Implement message queue with localStorage persistence - Add exponential backoff reconnection (1s→60s) - Add OfflineIndicator component with status display - Queue messages when offline, auto-retry on reconnect ## Security Hardening - Add AES-256-GCM encryption for chat history storage - Add secure API key storage with OS keychain integration - Add security audit logging system - Add XSS prevention and input validation utilities - Add rate limiting and token generation helpers ## CI/CD (Gitea Actions) - Add .gitea/workflows/ci.yml for continuous integration - Add .gitea/workflows/release.yml for release automation - Support Windows Tauri build and release ## UI Components - Add LoadingSpinner, LoadingOverlay, LoadingDots components - Add MessageSkeleton, ConversationListSkeleton skeletons - Add EmptyMessages, EmptyConversations empty states - Integrate loading states in ChatArea and ConversationList ## E2E Tests - Fix WebSocket mock for streaming response tests - Fix approval endpoint route matching - Add store state exposure for testing - All 19 core-features tests now passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
*
|
||||
* Extracted from gateway-client.ts for modularity.
|
||||
* Manages WSS configuration, URL normalization, and
|
||||
* localStorage persistence for gateway URL and token.
|
||||
* secure storage persistence for gateway URL and token.
|
||||
*
|
||||
* Security: Token is now stored using secure storage (keychain or encrypted localStorage)
|
||||
*/
|
||||
|
||||
import { secureStorage } from './secure-storage';
|
||||
import { logKeyEvent, logSecurityEvent } from './security-audit';
|
||||
|
||||
// === WSS Configuration ===
|
||||
|
||||
/**
|
||||
@@ -95,18 +100,104 @@ export function setStoredGatewayUrl(url: string): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function getStoredGatewayToken(): string {
|
||||
/**
|
||||
* Get the stored gateway token from secure storage
|
||||
* Uses OS keychain when available, falls back to encrypted localStorage
|
||||
*
|
||||
* @returns The stored token or empty string if not found
|
||||
*/
|
||||
export async function getStoredGatewayTokenAsync(): Promise<string> {
|
||||
try {
|
||||
return localStorage.getItem(GATEWAY_TOKEN_STORAGE_KEY) || '';
|
||||
const token = await secureStorage.get(GATEWAY_TOKEN_STORAGE_KEY);
|
||||
if (token) {
|
||||
logKeyEvent('key_accessed', 'Retrieved gateway token', { source: 'secure_storage' });
|
||||
}
|
||||
return token || '';
|
||||
} catch (error) {
|
||||
console.error('[GatewayStorage] Failed to get gateway token:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version for backward compatibility
|
||||
* @deprecated Use getStoredGatewayTokenAsync() instead
|
||||
*/
|
||||
export function getStoredGatewayToken(): string {
|
||||
// This returns empty string and logs a warning in dev mode
|
||||
// Real code should use the async version
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('[GatewayStorage] Using synchronous token access - consider using async version');
|
||||
}
|
||||
|
||||
// Try to get from localStorage as fallback (may be encrypted)
|
||||
try {
|
||||
const stored = localStorage.getItem(GATEWAY_TOKEN_STORAGE_KEY);
|
||||
if (stored) {
|
||||
// Check if it's encrypted (has iv and data fields)
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed && typeof parsed.iv === 'string' && typeof parsed.data === 'string') {
|
||||
// Data is encrypted - cannot decrypt synchronously
|
||||
console.warn('[GatewayStorage] Token is encrypted - use async version');
|
||||
return '';
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, so it's plaintext (legacy format)
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function setStoredGatewayToken(token: string): string {
|
||||
/**
|
||||
* Store the gateway token securely
|
||||
* Uses OS keychain when available, falls back to encrypted localStorage
|
||||
*
|
||||
* @param token - The token to store
|
||||
* @returns The normalized token
|
||||
*/
|
||||
export async function setStoredGatewayTokenAsync(token: string): Promise<string> {
|
||||
const normalized = token.trim();
|
||||
|
||||
try {
|
||||
if (normalized) {
|
||||
await secureStorage.set(GATEWAY_TOKEN_STORAGE_KEY, normalized);
|
||||
logKeyEvent('key_stored', 'Stored gateway token', { source: 'secure_storage' });
|
||||
} else {
|
||||
await secureStorage.delete(GATEWAY_TOKEN_STORAGE_KEY);
|
||||
logKeyEvent('key_deleted', 'Deleted gateway token', { source: 'secure_storage' });
|
||||
}
|
||||
|
||||
// Clear legacy localStorage token if it exists
|
||||
localStorage.removeItem(GATEWAY_TOKEN_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error('[GatewayStorage] Failed to store gateway token:', error);
|
||||
logSecurityEvent('security_violation', 'Failed to store gateway token securely', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version for backward compatibility
|
||||
* @deprecated Use setStoredGatewayTokenAsync() instead
|
||||
*/
|
||||
export function setStoredGatewayToken(token: string): string {
|
||||
const normalized = token.trim();
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('[GatewayStorage] Using synchronous token storage - consider using async version');
|
||||
}
|
||||
|
||||
try {
|
||||
if (normalized) {
|
||||
// Store in localStorage as fallback (not secure, but better than nothing)
|
||||
localStorage.setItem(GATEWAY_TOKEN_STORAGE_KEY, normalized);
|
||||
} else {
|
||||
localStorage.removeItem(GATEWAY_TOKEN_STORAGE_KEY);
|
||||
@@ -114,5 +205,6 @@ export function setStoredGatewayToken(token: string): string {
|
||||
} catch {
|
||||
/* ignore localStorage failures */
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user