mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
219 lines
6.7 KiB
TypeScript
219 lines
6.7 KiB
TypeScript
/**
|
|
* Config persistence utilities for runtime settings.
|
|
*
|
|
* Handles storing/retrieving user preferences like RPC URL using
|
|
* localStorage (web) or Tauri store (desktop).
|
|
*/
|
|
import { CORE_RPC_URL } from './config';
|
|
import { isTauri } from './tauriCommands';
|
|
|
|
// Storage key for RPC URL preference
|
|
const RPC_URL_STORAGE_KEY = 'openhuman_core_rpc_url';
|
|
|
|
// Storage key for cloud-mode bearer token. Pre-login and per-device, parallel
|
|
// to the URL key. Held in plain localStorage because the cloud picker runs
|
|
// before any user session exists.
|
|
const CORE_TOKEN_STORAGE_KEY = 'openhuman_core_rpc_token';
|
|
|
|
// Storage key for the user-chosen core mode ('local' | 'cloud'). Mirrors the
|
|
// redux-persist `coreMode` blob synchronously so reloads (notably the dev-mode
|
|
// `window.location.reload()` triggered by `handleIdentityFlip`) can recover
|
|
// the chosen mode before redux-persist's async flush completes — without this
|
|
// the BootCheckGate flips back to the picker after every reload, producing an
|
|
// infinite picker → flip → reload loop in cloud mode.
|
|
const CORE_MODE_STORAGE_KEY = 'openhuman_core_mode';
|
|
|
|
// Default RPC URL — canonical value from config.ts so they can never drift
|
|
const DEFAULT_RPC_URL = CORE_RPC_URL;
|
|
|
|
/**
|
|
* Check if we're running in a Tauri environment.
|
|
* Used to determine storage backend.
|
|
*/
|
|
export function isTauriEnvironment(): boolean {
|
|
return isTauri();
|
|
}
|
|
|
|
/**
|
|
* Get the stored RPC URL preference.
|
|
*
|
|
* @returns The stored RPC URL or the default if none stored
|
|
*/
|
|
export function getStoredRpcUrl(): string {
|
|
try {
|
|
const stored = localStorage.getItem(RPC_URL_STORAGE_KEY);
|
|
if (stored && stored.trim().length > 0) {
|
|
return stored.trim();
|
|
}
|
|
} catch {
|
|
// localStorage might be unavailable in some environments
|
|
console.warn('[configPersistence] Unable to access localStorage');
|
|
}
|
|
return DEFAULT_RPC_URL;
|
|
}
|
|
|
|
/**
|
|
* Peek at the stored RPC URL **without** falling back to the build-time
|
|
* default — returns `null` when nothing is stored.
|
|
*
|
|
* Use this to distinguish "user has explicitly chosen a URL" from "nothing
|
|
* stored yet, you're seeing the default". The masked-by-default behavior of
|
|
* `getStoredRpcUrl` makes that distinction impossible: when a user chooses a
|
|
* URL that happens to equal `CORE_RPC_URL` (e.g. the build-time fallback in
|
|
* `app/.env.local` matches their cloud picker input), `getStoredRpcUrl` and
|
|
* the default are indistinguishable, so callers that want to honour the
|
|
* explicit choice unambiguously must read this instead.
|
|
*/
|
|
export function peekStoredRpcUrl(): string | null {
|
|
try {
|
|
const stored = localStorage.getItem(RPC_URL_STORAGE_KEY);
|
|
if (stored && stored.trim().length > 0) {
|
|
return stored.trim();
|
|
}
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to access localStorage');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Store the RPC URL preference.
|
|
*
|
|
* @param url - The RPC URL to store
|
|
*/
|
|
export function storeRpcUrl(url: string): void {
|
|
try {
|
|
if (url && url.trim().length > 0) {
|
|
localStorage.setItem(RPC_URL_STORAGE_KEY, url.trim());
|
|
console.debug('[configPersistence] Stored RPC URL:', { url: url.trim() });
|
|
} else {
|
|
// Allow clearing the stored URL to reset to default
|
|
localStorage.removeItem(RPC_URL_STORAGE_KEY);
|
|
console.debug('[configPersistence] Cleared stored RPC URL');
|
|
}
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to store RPC URL in localStorage');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the stored RPC URL preference.
|
|
* This will cause the app to use the default RPC URL.
|
|
*/
|
|
export function clearStoredRpcUrl(): void {
|
|
storeRpcUrl('');
|
|
}
|
|
|
|
/**
|
|
* Validate an RPC URL format.
|
|
*
|
|
* @param url - The URL to validate
|
|
* @returns true if the URL is valid, false otherwise
|
|
*/
|
|
export function isValidRpcUrl(url: string): boolean {
|
|
if (!url || url.trim().length === 0) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(url);
|
|
// Must be http or https
|
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize an RPC URL by trimming whitespace and trailing slashes.
|
|
*
|
|
* @param url - The URL to normalize
|
|
* @returns The normalized URL
|
|
*/
|
|
export function normalizeRpcUrl(url: string): string {
|
|
return url.trim().replace(/\/+$/, '');
|
|
}
|
|
|
|
/**
|
|
* Get the default RPC URL.
|
|
*
|
|
* @returns The default RPC URL
|
|
*/
|
|
export function getDefaultRpcUrl(): string {
|
|
return CORE_RPC_URL;
|
|
}
|
|
|
|
/**
|
|
* Get the stored cloud-mode bearer token, if any.
|
|
*
|
|
* Returns null when no token is stored (the common case for local-mode users)
|
|
* so the caller can fall back to the local sidecar's per-process token.
|
|
*/
|
|
export function getStoredCoreToken(): string | null {
|
|
try {
|
|
const stored = localStorage.getItem(CORE_TOKEN_STORAGE_KEY);
|
|
if (stored && stored.trim().length > 0) {
|
|
return stored.trim();
|
|
}
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to access localStorage');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Store the cloud-mode bearer token. An empty string clears the stored value
|
|
* so the caller can flip back to local-sidecar auth without manual cleanup.
|
|
*/
|
|
export function storeCoreToken(token: string): void {
|
|
try {
|
|
if (token && token.trim().length > 0) {
|
|
localStorage.setItem(CORE_TOKEN_STORAGE_KEY, token.trim());
|
|
console.debug('[configPersistence] Stored core token (cloud mode)');
|
|
} else {
|
|
localStorage.removeItem(CORE_TOKEN_STORAGE_KEY);
|
|
console.debug('[configPersistence] Cleared stored core token');
|
|
}
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to store core token in localStorage');
|
|
}
|
|
}
|
|
|
|
/** Clear the stored cloud-mode bearer token. */
|
|
export function clearStoredCoreToken(): void {
|
|
storeCoreToken('');
|
|
}
|
|
|
|
/**
|
|
* Read the synchronous core-mode marker. Returns `null` when nothing has
|
|
* been written yet (first launch, or after `clearStoredCoreMode`).
|
|
*/
|
|
export function getStoredCoreMode(): 'local' | 'cloud' | null {
|
|
try {
|
|
const stored = localStorage.getItem(CORE_MODE_STORAGE_KEY)?.trim();
|
|
if (stored === 'local' || stored === 'cloud') return stored;
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to access localStorage');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Persist the synchronous core-mode marker. */
|
|
export function storeCoreMode(mode: 'local' | 'cloud'): void {
|
|
try {
|
|
localStorage.setItem(CORE_MODE_STORAGE_KEY, mode);
|
|
console.debug('[configPersistence] Stored core mode:', { mode });
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to store core mode in localStorage');
|
|
}
|
|
}
|
|
|
|
/** Remove the synchronous core-mode marker (returns the picker to first-launch state). */
|
|
export function clearStoredCoreMode(): void {
|
|
try {
|
|
localStorage.removeItem(CORE_MODE_STORAGE_KEY);
|
|
} catch {
|
|
console.warn('[configPersistence] Unable to clear core mode in localStorage');
|
|
}
|
|
}
|