mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-14 14:54:06 +00:00
* feat(webui): add project workspaces and access controls * feat(webui): add project workspaces and access controls * refactor(tools): centralize workspace access resolution * refactor(webui): remove unused workspace host state * fix(webui): hide estimated file edit label * fix(webui): clarify file edit deletion feedback * fix(webui): label deleted file activity * fix(webui): flatten file edit activity rows * fix(core): remove path-only patch deletion * fix(core): keep apply patch non-destructive * refactor(webui): trim workspace host plumbing * fix(tools): register exec with tools config
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
import type { BootstrapResponse } from "./types";
|
|
|
|
const SECRET_STORAGE_KEY = "nanobot-webui.bootstrap-secret";
|
|
|
|
/** Read a previously saved bootstrap secret from localStorage. */
|
|
export function loadSavedSecret(): string {
|
|
if (typeof window === "undefined") return "";
|
|
try {
|
|
return window.localStorage.getItem(SECRET_STORAGE_KEY) ?? "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/** Persist the bootstrap secret so page reloads don't re-prompt. */
|
|
export function saveSecret(secret: string): void {
|
|
try {
|
|
window.localStorage.setItem(SECRET_STORAGE_KEY, secret);
|
|
} catch {
|
|
// ignore storage errors (private mode, etc.)
|
|
}
|
|
}
|
|
|
|
/** Clear the saved bootstrap secret (sign out). */
|
|
export function clearSavedSecret(): void {
|
|
try {
|
|
window.localStorage.removeItem(SECRET_STORAGE_KEY);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch a short-lived token + the WebSocket path from the gateway's
|
|
* ``/webui/bootstrap`` endpoint.
|
|
*/
|
|
export async function fetchBootstrap(
|
|
baseUrl: string = "",
|
|
secret: string = "",
|
|
): Promise<BootstrapResponse> {
|
|
const headers: Record<string, string> = {};
|
|
if (secret) {
|
|
headers["X-Nanobot-Auth"] = secret;
|
|
}
|
|
const res = await fetch(`${baseUrl}/webui/bootstrap`, {
|
|
method: "GET",
|
|
credentials: "same-origin",
|
|
headers,
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`bootstrap failed: HTTP ${res.status}`);
|
|
}
|
|
const body = (await res.json()) as BootstrapResponse;
|
|
if (!body.token || !body.ws_path) {
|
|
throw new Error("bootstrap response missing token or ws_path");
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/** Derive a WebSocket URL from the current window location and the server-provided path.
|
|
*
|
|
* Keeps the path segment exactly as the server registered it: the root ``/``
|
|
* stays ``/`` and non-root paths are not given an extra trailing slash. This
|
|
* matters because some WS servers dispatch handshakes based on the literal
|
|
* path, not a normalised form.
|
|
*/
|
|
export function deriveWsUrl(
|
|
wsPath: string,
|
|
token: string,
|
|
wsUrl?: string | null,
|
|
): string {
|
|
const query = `?token=${encodeURIComponent(token)}`;
|
|
if (wsUrl && /^(wss?|nanobot-host):\/\//i.test(wsUrl)) {
|
|
const join = wsUrl.includes("?") ? "&" : "?";
|
|
return `${wsUrl}${join}token=${encodeURIComponent(token)}`;
|
|
}
|
|
const path = wsPath && wsPath.startsWith("/") ? wsPath : `/${wsPath || ""}`;
|
|
if (typeof window === "undefined") {
|
|
return `ws://127.0.0.1:8765${path}${query}`;
|
|
}
|
|
if (window.location.port === "5173") {
|
|
const host = window.location.hostname.includes(":")
|
|
? `[${window.location.hostname}]`
|
|
: window.location.hostname;
|
|
return `ws://${host}:8765${path}${query}`;
|
|
}
|
|
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
|
const host = window.location.host;
|
|
return `${scheme}://${host}${path}${query}`;
|
|
}
|