Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions wallbreaker/dashboard/web/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { withAuth } from "./auth";


export interface ConfigInfo {
has_target: boolean;
target: string | null;
Expand Down Expand Up @@ -229,7 +232,7 @@ export interface FireResult extends ComposeResult {
}

async function j<T>(url: string, init?: RequestInit): Promise<T> {
const r = await fetch(url, init);
const r = await fetch(url, await withAuth(init));
if (!r.ok) {
let detail = r.statusText;
try {
Expand Down Expand Up @@ -328,12 +331,12 @@ export async function runAgent(
onEvent: (ev: AgentEvent) => void,
signal?: AbortSignal
): Promise<void> {
const r = await fetch("/api/agent/run", {
const r = await fetch("/api/agent/run", await withAuth({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal,
});
}));
if (!r.ok || !r.body) {
let detail = r.statusText;
try { detail = (await r.json()).detail || detail; } catch { /* ignore */ }
Expand Down
40 changes: 40 additions & 0 deletions wallbreaker/dashboard/web/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Shared dashboard authentication helper.
//
// The backend (dashboard/auth.py) requires an `X-WB-Token` header on every /api/* route
// except the exempt bootstrap paths (/api/health, /api/session). The token is minted per
// launch and exposed to the same-origin SPA via GET /api/session. This module fetches it
// once, caches the in-flight promise, and attaches it to outgoing requests via `withAuth`.
//
// Both the root API client (src/api.ts) and the V2 client (src/v2/api.ts) import from here
// so the whole app shares ONE token cache and one source of truth for auth.

let tokenPromise: Promise<string> | null = null;

/** Fetch the per-launch dashboard token once and cache the promise. */
export async function ensureToken(): Promise<string> {
if (!tokenPromise) {
tokenPromise = fetch("/api/session")
.then((r) => (r.ok ? r.json() : { token: "" }))
.then((b: { token?: string }) => b.token ?? "")
.catch(() => "");
}

return tokenPromise;
}

/** Return a RequestInit with the X-WB-Token header set (merging any existing headers). */
export async function withAuth(init?: RequestInit): Promise<RequestInit> {
const token = await ensureToken();

if (!token) {
return init ?? {};
}

const headers = new Headers(init?.headers);
headers.set("X-WB-Token", token);

return {
...init,
headers,
};
}
5 changes: 3 additions & 2 deletions wallbreaker/dashboard/web/src/v2/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
SettingsRecord,
} from "./types";
import { inferEventActor } from "./eventProjection";
import { withAuth } from "../auth";

class HttpError extends Error {
constructor(public status: number, message: string) {
Expand All @@ -24,7 +25,7 @@ class HttpError extends Error {
}

async function request<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init);
const response = await fetch(url, await withAuth(init));
if (!response.ok) {
let message = response.statusText || `Request failed (${response.status})`;
try {
Expand Down Expand Up @@ -231,7 +232,7 @@ export const v2Api = {
signal: AbortSignal,
): Promise<void> {
const url = `/api/v2/executions/${encodeURIComponent(executionId)}/events?after=${after}`;
const response = await fetch(url, { signal, headers: { Accept: "text/event-stream" } });
const response = await fetch(url, await withAuth({ signal, headers: { Accept: "text/event-stream" } }));
if (!response.ok || !response.body) throw new HttpError(response.status, response.statusText);
const reader = response.body.getReader();
const decoder = new TextDecoder();
Expand Down