import { createHttpClient, type HttpClient } from "@ltt/shared";
import browser from "webextension-polyfill";
/** Server settings persisted in storage.local. */
export interface Settings {
serverUrl: string;
/** web panel base URL; falls back to serverUrl when unset (same origin in prod) */
panelUrl: string | null;
token: string | null;
tokenExpiresAt: string | null;
defaultProjectId: string | null;
user: { id: string; nickname: string; email: string } | null;
/** replay CSS :hover through the Chrome debugger API (shows an infobar) */
hoverReplay: boolean;
/** record the screen around the cursor via the debugger API during recordings (shows an infobar) */
videoCapture: boolean;
/** attach console.error/warn and uncaught exceptions to recordings */
consoleCapture: boolean;
}
const DEFAULTS: Settings = {
serverUrl: "http://localhost:8001",
// dev stack serves the panel on Vite's port; in prod panel and API share an origin
panelUrl: "http://localhost:5173",
token: null,
tokenExpiresAt: null,
defaultProjectId: null,
user: null,
hoverReplay: true,
videoCapture: true,
consoleCapture: true,
};
export async function getSettings(): Promise<Settings> {
const stored = await browser.storage.local.get(Object.keys(DEFAULTS));
return { ...DEFAULTS, ...stored } as Settings;
}
export async function saveSettings(patch: Partial<Settings>): Promise<void> {
await browser.storage.local.set(patch);
}
export async function clearSession(): Promise<void> {
await saveSettings({ token: null, tokenExpiresAt: null, user: null, defaultProjectId: null });
}
export async function login(email: string, password: string): Promise<{ nickname: string; email: string }> {
const settings = await getSettings();
const response = await fetch(`${settings.serverUrl}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Client": "extension" },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const detail = await response.json().catch(() => null);
throw new Error(detail?.detail ?? `Login failed (${response.status})`);
}
const { token, expires_at } = (await response.json()) as { token: string; expires_at: string };
// fetch the profile with the new token
const meResponse = await fetch(`${settings.serverUrl}/api/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!meResponse.ok) throw new Error("Login succeeded but profile fetch failed");
const user = (await meResponse.json()) as { id: string; nickname: string; email: string };
await saveSettings({ token, tokenExpiresAt: expires_at, user });
return user;
}
export async function logout(): Promise<void> {
const settings = await getSettings();
if (settings.token) {
await fetch(`${settings.serverUrl}/api/auth/logout`, {
method: "POST",
headers: { Authorization: `Bearer ${settings.token}` },
}).catch(() => {});
}
await clearSession();
}
/** Client with the current stored token; refreshes settings each call. */
export async function getHttpClient(): Promise<HttpClient> {
const settings = await getSettings();
return createHttpClient({ baseUrl: settings.serverUrl, token: settings.token });
}
/** Base URL of the web panel for share links and the "open panel" shortcut. */
export function panelBase(settings: Settings): string {
return (settings.panelUrl ?? settings.serverUrl).replace(/\/+$/, "");
}