Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | 1x 1x 1x 1x 14x 14x 14x 10x 7x 14x 14x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* 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;
}
}
// === URL Constants ===
// OpenFang 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 {
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;
}
|