mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-08 05:18:49 +03:00
feat(webui): bypass tokens for trusted proxy auth
This commit is contained in:
+11
-7
@@ -70,7 +70,7 @@ type BootState =
|
||||
status: "ready";
|
||||
client: NanobotClient;
|
||||
token: string;
|
||||
tokenExpiresAt: number;
|
||||
tokenExpiresAt: number | null;
|
||||
modelName: string | null;
|
||||
ingressLimits: BootstrapResponse["limits"] | null;
|
||||
runtimeSurface: RuntimeSurface;
|
||||
@@ -733,7 +733,9 @@ export default function App() {
|
||||
? toRuntimeSurface(boot.runtime_surface)
|
||||
: fallbackSurface;
|
||||
const runtimeHost = createRuntimeHost(runtimeSurface, boot.runtime_capabilities);
|
||||
const tokenExpiresAt = bootstrapTokenExpiresAt(boot.expires_in);
|
||||
const tokenExpiresAt = boot.expires_in
|
||||
? bootstrapTokenExpiresAt(boot.expires_in)
|
||||
: null;
|
||||
if (runtimeHost.socketFactory) {
|
||||
client.updateUrl(url, runtimeHost.socketFactory);
|
||||
} else {
|
||||
@@ -744,7 +746,7 @@ export default function App() {
|
||||
current.status === "ready" && current.client === client
|
||||
? {
|
||||
...current,
|
||||
token: boot.api_token,
|
||||
token: boot.api_token ?? "",
|
||||
tokenExpiresAt,
|
||||
modelName: boot.model_name ?? current.modelName,
|
||||
ingressLimits: boot.limits ?? current.ingressLimits,
|
||||
@@ -752,7 +754,7 @@ export default function App() {
|
||||
}
|
||||
: current,
|
||||
);
|
||||
return { token: boot.api_token, url };
|
||||
return { token: boot.api_token ?? "", url };
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -787,8 +789,10 @@ export default function App() {
|
||||
setState({
|
||||
status: "ready",
|
||||
client,
|
||||
token: boot.api_token,
|
||||
tokenExpiresAt: bootstrapTokenExpiresAt(boot.expires_in),
|
||||
token: boot.api_token ?? "",
|
||||
tokenExpiresAt: boot.expires_in
|
||||
? bootstrapTokenExpiresAt(boot.expires_in)
|
||||
: null,
|
||||
modelName: boot.model_name ?? null,
|
||||
ingressLimits: boot.limits ?? null,
|
||||
runtimeSurface,
|
||||
@@ -813,7 +817,7 @@ export default function App() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status !== "ready") return;
|
||||
if (state.status !== "ready" || state.tokenExpiresAt === null) return;
|
||||
const client = state.client;
|
||||
const timer = window.setTimeout(async () => {
|
||||
try {
|
||||
|
||||
@@ -87,13 +87,8 @@ export async function fetchBootstrap(
|
||||
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");
|
||||
}
|
||||
if (!body.api_token) {
|
||||
throw new BootstrapAuthRequiredError(
|
||||
"bootstrap authentication required: missing api_token",
|
||||
);
|
||||
if (!body.ws_path) {
|
||||
throw new Error("bootstrap response missing ws_path");
|
||||
}
|
||||
return body;
|
||||
}
|
||||
@@ -107,10 +102,10 @@ export async function fetchBootstrap(
|
||||
*/
|
||||
export function deriveWsUrl(
|
||||
wsPath: string,
|
||||
token: string,
|
||||
token: string | null | undefined,
|
||||
wsUrl?: string | null,
|
||||
): string {
|
||||
const query = `?token=${encodeURIComponent(token)}`;
|
||||
const query = token ? `?token=${encodeURIComponent(token)}` : "";
|
||||
const path = wsPath && wsPath.startsWith("/") ? wsPath : `/${wsPath || ""}`;
|
||||
if (typeof window !== "undefined" && window.location.port === "5173") {
|
||||
const host = window.location.hostname.includes(":")
|
||||
@@ -127,6 +122,7 @@ export function deriveWsUrl(
|
||||
return `${scheme}://${authority}${path}${query}`;
|
||||
}
|
||||
if (wsUrl && /^(wss?|nanobot-host):\/\//i.test(wsUrl)) {
|
||||
if (!token) return wsUrl;
|
||||
const join = wsUrl.includes("?") ? "&" : "?";
|
||||
return `${wsUrl}${join}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
@@ -390,11 +390,11 @@ export interface SidebarStatePayload {
|
||||
}
|
||||
|
||||
export interface BootstrapResponse {
|
||||
token: string;
|
||||
api_token: string;
|
||||
token?: string;
|
||||
api_token?: string;
|
||||
ws_path: string;
|
||||
ws_url?: string | null;
|
||||
expires_in: number;
|
||||
expires_in?: number;
|
||||
limits?: WebUIIngressLimits;
|
||||
model_name?: string | null;
|
||||
runtime_surface?: RuntimeSurface;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
BootstrapAuthRequiredError,
|
||||
consumeUrlBootstrapSecret,
|
||||
deriveWsUrl,
|
||||
fetchBootstrap,
|
||||
@@ -57,6 +56,12 @@ describe("bootstrap helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not append a token for trusted-proxy websocket URLs", () => {
|
||||
expect(deriveWsUrl("/", undefined, "wss://proxy.example/")).toBe(
|
||||
"wss://proxy.example/",
|
||||
);
|
||||
});
|
||||
|
||||
it("times out when the bootstrap endpoint never responds", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal("fetch", vi.fn(() => new Promise<Response>(() => {})));
|
||||
@@ -69,21 +74,19 @@ describe("bootstrap helpers", () => {
|
||||
await pending;
|
||||
});
|
||||
|
||||
it("treats bootstrap responses without an API token as auth-required", async () => {
|
||||
it("accepts tokenless trusted-proxy bootstrap responses", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ token: "ws-token", ws_path: "/", expires_in: 300 }),
|
||||
json: async () => ({ ws_path: "/", ws_url: "wss://proxy.example/" }),
|
||||
})),
|
||||
);
|
||||
|
||||
const promise = fetchBootstrap();
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
name: "BootstrapAuthRequiredError",
|
||||
message: "bootstrap authentication required: missing api_token",
|
||||
await expect(fetchBootstrap()).resolves.toMatchObject({
|
||||
ws_path: "/",
|
||||
ws_url: "wss://proxy.example/",
|
||||
});
|
||||
await expect(promise).rejects.toBeInstanceOf(BootstrapAuthRequiredError);
|
||||
});
|
||||
|
||||
it("consumes bootstrap secrets from the URL fragment", () => {
|
||||
|
||||
Reference in New Issue
Block a user