refactor: 重构数据库连接使用PostgreSQL替代SQLite feat(auth): 增加JWT验证的audience和issuer检查 feat(crypto): 添加AES-256-GCM字段加密支持 feat(api): 集成utoipa实现OpenAPI文档 fix(admin): 修复配置项表单验证逻辑 style: 统一代码格式与类型定义 docs: 更新技术栈文档说明PostgreSQL
243 lines
7.3 KiB
TypeScript
243 lines
7.3 KiB
TypeScript
/**
|
|
* gateway-storage.ts - Gateway URL/Token Storage & Normalization
|
|
*
|
|
* Extracted from gateway-client.ts for modularity.
|
|
* Manages WSS configuration, URL normalization, and
|
|
* 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 ===
|
|
|
|
/**
|
|
* Whether to use WSS (WebSocket Secure) instead of WS.
|
|
* - Production: defaults to WSS for security
|
|
* - Development: defaults to WS for convenience
|
|
* - Override: set VITE_USE_WSS=false to force WS in production
|
|
*/
|
|
const USE_WSS = import.meta.env.VITE_USE_WSS !== 'false' && import.meta.env.PROD;
|
|
|
|
/**
|
|
* Default protocol based on WSS configuration.
|
|
*/
|
|
const DEFAULT_WS_PROTOCOL = USE_WSS ? 'wss://' : 'ws://';
|
|
|
|
/**
|
|
* Check if a URL points to localhost.
|
|
*/
|
|
export function isLocalhost(url: string): boolean {
|
|
try {
|
|
const parsed = new URL(url);
|
|
return parsed.hostname === 'localhost' ||
|
|
parsed.hostname === '127.0.0.1' ||
|
|
parsed.hostname === '[::1]';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// === Port Constants ===
|
|
|
|
/** Default gRPC/HTTP port used by the ZCLAW kernel */
|
|
export const ZCLAW_GRPC_PORT = 50051;
|
|
|
|
/** Legacy/alternative port used in development or older configurations */
|
|
export const ZCLAW_LEGACY_PORT = 4200;
|
|
|
|
// === Connection Mode ===
|
|
|
|
/**
|
|
* Determines how the client connects to the ZCLAW gateway.
|
|
* - `rest`: Kernel exposes an HTTP REST API (gRPC-gateway). Used when the
|
|
* URL contains a known kernel port.
|
|
* - `ws`: Direct WebSocket connection to the kernel.
|
|
*/
|
|
export type ConnectionMode = 'rest' | 'ws';
|
|
|
|
/**
|
|
* Decide the connection mode based on the gateway URL.
|
|
*
|
|
* When the URL contains a known kernel port (gRPC or legacy), the client
|
|
* routes requests through the REST adapter instead of opening a raw
|
|
* WebSocket.
|
|
*/
|
|
export function detectConnectionMode(url: string): ConnectionMode {
|
|
if (url.includes(`:${ZCLAW_GRPC_PORT}`) || url.includes(`:${ZCLAW_LEGACY_PORT}`)) {
|
|
return 'rest';
|
|
}
|
|
return 'ws';
|
|
}
|
|
|
|
// === URL Constants ===
|
|
|
|
// ZCLAW endpoints (port 50051 - actual running port)
|
|
// Note: REST API uses relative path to leverage Vite proxy for CORS bypass
|
|
export const DEFAULT_GATEWAY_URL = `${DEFAULT_WS_PROTOCOL}127.0.0.1:${ZCLAW_GRPC_PORT}/ws`;
|
|
export const REST_API_URL = ''; // Empty = use relative path (Vite proxy)
|
|
export const FALLBACK_GATEWAY_URLS = [
|
|
DEFAULT_GATEWAY_URL,
|
|
`${DEFAULT_WS_PROTOCOL}127.0.0.1:${ZCLAW_LEGACY_PORT}/ws`,
|
|
];
|
|
|
|
const GATEWAY_URL_STORAGE_KEY = 'zclaw_gateway_url';
|
|
const GATEWAY_TOKEN_STORAGE_KEY = 'zclaw_gateway_token';
|
|
|
|
// === URL Normalization ===
|
|
|
|
/**
|
|
* Normalize a gateway URL to ensure correct protocol and path.
|
|
* - Ensures ws:// or wss:// protocol based on configuration
|
|
* - Ensures /ws path suffix
|
|
* - Handles both localhost and IP addresses
|
|
*/
|
|
export function normalizeGatewayUrl(url: string): string {
|
|
let normalized = url.trim();
|
|
|
|
// Remove trailing slashes except for protocol
|
|
normalized = normalized.replace(/\/+$/, '');
|
|
|
|
// Ensure protocol
|
|
if (!normalized.startsWith('ws://') && !normalized.startsWith('wss://')) {
|
|
normalized = USE_WSS ? `wss://${normalized}` : `ws://${normalized}`;
|
|
}
|
|
|
|
// Ensure /ws path
|
|
if (!normalized.endsWith('/ws')) {
|
|
normalized = `${normalized}/ws`;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
// === LocalStorage Helpers ===
|
|
|
|
export function getStoredGatewayUrl(): string {
|
|
try {
|
|
const stored = localStorage.getItem(GATEWAY_URL_STORAGE_KEY);
|
|
return normalizeGatewayUrl(stored || DEFAULT_GATEWAY_URL);
|
|
} catch {
|
|
return DEFAULT_GATEWAY_URL;
|
|
}
|
|
}
|
|
|
|
export function setStoredGatewayUrl(url: string): string {
|
|
const normalized = normalizeGatewayUrl(url || DEFAULT_GATEWAY_URL);
|
|
try {
|
|
localStorage.setItem(GATEWAY_URL_STORAGE_KEY, normalized);
|
|
} catch { /* ignore localStorage failures */ }
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
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 '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
} catch {
|
|
/* ignore localStorage failures */
|
|
}
|
|
|
|
return normalized;
|
|
}
|