Files
zclaw_openfang/desktop/src/lib/gateway-storage.ts
iven f79560a911 refactor(desktop): split kernel_commands/pipeline_commands into modules, add SaaS client libs and gateway modules
Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines)
into focused sub-modules under kernel_commands/ and pipeline_commands/ directories.
Add gateway module (commands, config, io, runtime), health_check, and 15 new
TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel
sub-systems (a2a, agent, chat, hands, skills, triggers).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:12:47 +08:00

218 lines
6.8 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';
import { createLogger } from './logger';
const logger = createLogger('GatewayStorage');
// === 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 (e) {
logger.debug('URL parsing failed in isLocalhost', { error: e });
return false;
}
}
// === 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:50051/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:4200/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 (e) {
logger.debug('localStorage unavailable for gateway URL read', { error: e });
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 (e) { logger.debug('localStorage unavailable for gateway URL write', { error: e }); }
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 (e) {
// Not JSON, so it's plaintext (legacy format)
logger.debug('Legacy plaintext token format detected', { error: e });
return stored;
}
}
return '';
} catch (e) {
logger.warn('Failed to read gateway token from localStorage', { error: e });
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 (e) {
logger.warn('Failed to write gateway token to localStorage', { error: e });
}
return normalized;
}