/** * Controller connection state: where the controller lives (env defaults + * browser-stored backend URL) and how we authenticate against it (API key). */import { pickFirstNonEmpty } from "@shared/agent/backend-url";import { getControllerApiKey, normalizeControllerUrl } from "./controllers"; // --- Env-derived defaults --- const LOCAL_BACKEND_FALLBACK = "http://localhost:8080";const CLIENT_PROXY_FALLBACK = "/api/proxy"; /** * Server-side API client base URL. * Used as a first-run fallback when api-settings.json doesn't exist yet. */export const resolveApiServerBaseUrl = (): string => pickFirstNonEmpty( process.env.BACKEND_URL, process.env.NEXT_PUBLIC_BACKEND_URL, process.env.LOCAL_STUDIO_BACKEND_URL, ) ?? LOCAL_BACKEND_FALLBACK; // resolveSettingsDefaultBackendUrl lives in shared/agent/backend-url.ts so the// agent runtime package's settings service can share it; re-exported here for// frontend callers.export { resolveSettingsDefaultBackendUrl } from "@shared/agent/backend-url"; /** * Client-side controller event stream base URL. */export const resolveControllerEventsBaseUrl = (): string => pickFirstNonEmpty( process.env.NEXT_PUBLIC_BACKEND_URL, process.env.LOCAL_STUDIO_BACKEND_URL, process.env.BACKEND_URL, ) ?? CLIENT_PROXY_FALLBACK; // --- Browser-stored backend URL --- export const BACKEND_URL_STORAGE_KEY = "localstudio_backend_url";const LEGACY_BACKEND_URL_STORAGE = [["v", "llmstudio"].join(""), "backend_url"].join("_");const LEGACY_BACKEND_URL_COOKIE = LEGACY_BACKEND_URL_STORAGE;export const BACKEND_URL_CHANGED_EVENT = "vllm:backend-url-changed"; function getCookieValue(name: string): string { if (typeof document === "undefined") return ""; const prefix = `${encodeURIComponent(name)}=`; for (const entry of document.cookie.split(";")) { const trimmed = entry.trim(); if (trimmed.startsWith(prefix)) return decodeURIComponent(trimmed.slice(prefix.length)); } return "";} function setBackendCookie(url: string): void { if (typeof document === "undefined") return; const trimmed = url.trim(); const encoded = encodeURIComponent(trimmed); const maxAge = trimmed ? 60 * 60 * 24 * 365 : 0; // 1 year or delete const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : ""; document.cookie = `${encodeURIComponent(BACKEND_URL_STORAGE_KEY)}=${encoded}; Path=/; Max-Age=${maxAge}; SameSite=Lax${secure}`;} export function getStoredBackendUrl(): string { if (typeof window === "undefined") return ""; try { const stored = normalizeControllerUrl( window.localStorage.getItem(BACKEND_URL_STORAGE_KEY) || getCookieValue(BACKEND_URL_STORAGE_KEY) || window.localStorage.getItem(LEGACY_BACKEND_URL_STORAGE) || getCookieValue(LEGACY_BACKEND_URL_COOKIE) || "", ); if (stored && !window.localStorage.getItem(BACKEND_URL_STORAGE_KEY)) { window.localStorage.setItem(BACKEND_URL_STORAGE_KEY, stored); setBackendCookie(stored); } return stored; } catch { return normalizeControllerUrl( getCookieValue(BACKEND_URL_STORAGE_KEY) || getCookieValue(LEGACY_BACKEND_URL_COOKIE) || "", ); }} export function setStoredBackendUrl(url: string): void { if (typeof window === "undefined") return; const previous = getStoredBackendUrl(); const trimmed = normalizeControllerUrl(url); try { if (trimmed) { window.localStorage.setItem(BACKEND_URL_STORAGE_KEY, trimmed); } else { window.localStorage.removeItem(BACKEND_URL_STORAGE_KEY); } setBackendCookie(trimmed); } catch { // Ignore storage errors setBackendCookie(trimmed); } if (trimmed !== previous) { window.dispatchEvent( new CustomEvent(BACKEND_URL_CHANGED_EVENT, { detail: { url: trimmed, previous } }), ); }} export function clearStoredBackendUrl(): void { if (typeof window === "undefined") return; try { window.localStorage.removeItem(BACKEND_URL_STORAGE_KEY); window.localStorage.removeItem(LEGACY_BACKEND_URL_STORAGE); setBackendCookie(""); document.cookie = `${encodeURIComponent(LEGACY_BACKEND_URL_COOKIE)}=; Path=/; Max-Age=0; SameSite=Lax`; } catch { // Ignore storage errors setBackendCookie(""); }} // --- API key --- let runtimeApiKey = ""; /** * Get the API key from the active browser/controller state. * * Do not read NEXT_PUBLIC_* here. This module is bundled into the renderer, * so public env values become compiled defaults and can outlive key rotation. */export function getApiKey(): string { if (runtimeApiKey) return runtimeApiKey; if (typeof window !== "undefined") { return getControllerApiKey(getStoredBackendUrl()); } return process.env.LOCAL_STUDIO_API_KEY?.trim() || "";} /** * Save API key only for the current browser runtime. */export function setApiKey(key: string): void { runtimeApiKey = key.trim();} /** * Remove the in-memory runtime API key. */export function clearApiKey(): void { runtimeApiKey = "";}
/**
* Controller connection state: where the controller lives (env defaults +
* browser-stored backend URL) and how we authenticate against it (API key).
*/
import { pickFirstNonEmpty } from "@shared/agent/backend-url";
import { getControllerApiKey, normalizeControllerUrl } from "./controllers";
// --- Env-derived defaults ---
const LOCAL_BACKEND_FALLBACK = "http://localhost:8080";
const CLIENT_PROXY_FALLBACK = "/api/proxy";
/**
* Server-side API client base URL.
* Used as a first-run fallback when api-settings.json doesn't exist yet.
*/
export const resolveApiServerBaseUrl = (): string =>
pickFirstNonEmpty(
process.env.BACKEND_URL,
process.env.NEXT_PUBLIC_BACKEND_URL,
process.env.LOCAL_STUDIO_BACKEND_URL,
) ?? LOCAL_BACKEND_FALLBACK;
// resolveSettingsDefaultBackendUrl lives in shared/agent/backend-url.ts so the
// agent runtime package's settings service can share it; re-exported here for
// frontend callers.
export { resolveSettingsDefaultBackendUrl } from "@shared/agent/backend-url";
/**
* Client-side controller event stream base URL.
*/
export const resolveControllerEventsBaseUrl = (): string =>
pickFirstNonEmpty(
process.env.NEXT_PUBLIC_BACKEND_URL,
process.env.LOCAL_STUDIO_BACKEND_URL,
process.env.BACKEND_URL,
) ?? CLIENT_PROXY_FALLBACK;
// --- Browser-stored backend URL ---
export const BACKEND_URL_STORAGE_KEY = "localstudio_backend_url";
const LEGACY_BACKEND_URL_STORAGE = [["v", "llmstudio"].join(""), "backend_url"].join("_");
const LEGACY_BACKEND_URL_COOKIE = LEGACY_BACKEND_URL_STORAGE;
export const BACKEND_URL_CHANGED_EVENT = "vllm:backend-url-changed";
function getCookieValue(name: string): string {
if (typeof document === "undefined") return "";
const prefix = `${encodeURIComponent(name)}=`;
for (const entry of document.cookie.split(";")) {
const trimmed = entry.trim();
if (trimmed.startsWith(prefix)) return decodeURIComponent(trimmed.slice(prefix.length));
}
return "";
}
function setBackendCookie(url: string): void {
if (typeof document === "undefined") return;
const trimmed = url.trim();
const encoded = encodeURIComponent(trimmed);
const maxAge = trimmed ? 60 * 60 * 24 * 365 : 0; // 1 year or delete
const secure =
typeof location !== "undefined" && location.protocol === "https:" ? "; Secure" : "";
document.cookie = `${encodeURIComponent(BACKEND_URL_STORAGE_KEY)}=${encoded}; Path=/; Max-Age=${maxAge}; SameSite=Lax${secure}`;
}
export function getStoredBackendUrl(): string {
if (typeof window === "undefined") return "";
try {
const stored = normalizeControllerUrl(
window.localStorage.getItem(BACKEND_URL_STORAGE_KEY) ||
getCookieValue(BACKEND_URL_STORAGE_KEY) ||
window.localStorage.getItem(LEGACY_BACKEND_URL_STORAGE) ||
getCookieValue(LEGACY_BACKEND_URL_COOKIE) ||
"",
);
if (stored && !window.localStorage.getItem(BACKEND_URL_STORAGE_KEY)) {
window.localStorage.setItem(BACKEND_URL_STORAGE_KEY, stored);
setBackendCookie(stored);
}
return stored;
} catch {
return normalizeControllerUrl(
getCookieValue(BACKEND_URL_STORAGE_KEY) || getCookieValue(LEGACY_BACKEND_URL_COOKIE) || "",
);
}
}
export function setStoredBackendUrl(url: string): void {
if (typeof window === "undefined") return;
const previous = getStoredBackendUrl();
const trimmed = normalizeControllerUrl(url);
try {
if (trimmed) {
window.localStorage.setItem(BACKEND_URL_STORAGE_KEY, trimmed);
} else {
window.localStorage.removeItem(BACKEND_URL_STORAGE_KEY);
}
setBackendCookie(trimmed);
} catch {
// Ignore storage errors
setBackendCookie(trimmed);
}
if (trimmed !== previous) {
window.dispatchEvent(
new CustomEvent(BACKEND_URL_CHANGED_EVENT, { detail: { url: trimmed, previous } }),
);
}
}
export function clearStoredBackendUrl(): void {
if (typeof window === "undefined") return;
try {
window.localStorage.removeItem(BACKEND_URL_STORAGE_KEY);
window.localStorage.removeItem(LEGACY_BACKEND_URL_STORAGE);
setBackendCookie("");
document.cookie = `${encodeURIComponent(LEGACY_BACKEND_URL_COOKIE)}=; Path=/; Max-Age=0; SameSite=Lax`;
} catch {
// Ignore storage errors
setBackendCookie("");
}
}
// --- API key ---
let runtimeApiKey = "";
/**
* Get the API key from the active browser/controller state.
*
* Do not read NEXT_PUBLIC_* here. This module is bundled into the renderer,
* so public env values become compiled defaults and can outlive key rotation.
*/
export function getApiKey(): string {
if (runtimeApiKey) return runtimeApiKey;
if (typeof window !== "undefined") {
return getControllerApiKey(getStoredBackendUrl());
}
return process.env.LOCAL_STUDIO_API_KEY?.trim() || "";
}
/**
* Save API key only for the current browser runtime.
*/
export function setApiKey(key: string): void {
runtimeApiKey = key.trim();
}
/**
* Remove the in-memory runtime API key.
*/
export function clearApiKey(): void {
runtimeApiKey = "";
}