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:
iven
2026-03-22 00:03:22 +08:00
parent ce562e8bfc
commit 185763868a
27 changed files with 5725 additions and 268 deletions

View File

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