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>
218 lines
6.8 KiB
TypeScript
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;
|
|
}
|