import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { Hono } from "hono"; import { serveStatic } from "hono/bun"; import type { Store } from "./store/store.ts"; import { normalizeDashboardHost } from "./config.ts"; import { classifyApiAccessPath, isAllowedPublicApiPath, isPublicTunnelRequestHost } from "./services/publicIngressAccess.ts"; import { attachAuthRoutes, hasValidDashboardSessionCookie, isDashboardAuthSessionApiPath } from "./dashboard/routesAuth.ts"; import { attachSettingsRoutes } from "./dashboard/routesSettings.ts"; import { attachOAuthRoutes } from "./dashboard/routesOAuth.ts"; import { attachMetricsRoutes } from "./dashboard/routesMetrics.ts"; import { attachVoiceRoutes } from "./dashboard/routesVoice.ts"; import { BonjourAdvertiser } from "./services/bonjourAdvertiser.ts"; import { createDashboardServerHandle, DashboardHttpError, getRequestIp, isApiPath, type DashboardApp, type DashboardEnv, type DashboardServerHandle, type DashboardSseClient, stripApiPrefix, STREAM_INGEST_API_PATH } from "./dashboard/shared.ts";
const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(_filename); const PUBLIC_FRAME_REQUEST_WINDOW_MS = 60_000; const PUBLIC_FRAME_REQUEST_MAX_PER_WINDOW = 1200; const PUBLIC_FRAME_DECLARED_BYTES_MAX = 6_000_000; const PUBLIC_SHARE_FRAME_PATH_RE = /^/api/voice/share-session/[a-z0-9-]{16,}/frame/?$/i;
function isLocalDashboardHost(value: string) { const normalized = String(value || "").trim().toLowerCase(); return ( normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "[::1]" ); }
export interface DashboardAppConfig { dashboardPort: number; dashboardHost: string; dashboardToken: string; dashboardSettingsSaveDebug?: boolean | null; publicApiToken: string; elevenLabsApiKey?: string | null; anthropicApiKey?: string | null; openaiApiKey?: string | null; claudeOAuthRefreshToken?: string | null; openaiOAuthRefreshToken?: string | null; xaiApiKey?: string | null; }
export interface DashboardBot { applyRuntimeSettings(settings: unknown): Promise; reloadOAuthProviders?(): Promise<{ claudeOAuth: boolean; codexOAuth: boolean }>; getRuntimeState(): Record<string, unknown> & { voice?: { activeCount?: unknown; sessions?: Array<Record<string, unknown>>; }; }; getGuilds(): Array<{ id: string; name: string }>; getGuildChannels(guildId: string): unknown; purgeGuildMemoryRuntime?(guildId: string): Promise | unknown; requestVoiceJoinFromDashboard?(payload: { guildId: string | null; requesterUserId: string | null; textChannelId: string | null; source: string; }): Promise; ingestVoiceStreamFrame(payload: { guildId: string; streamerUserId: string | null; mimeType: string; dataBase64: string; source: string; }): Promise; }
export interface DashboardMemory { readMemoryMarkdown(opts?: { guildId?: string | null; }): Promise; refreshMemoryMarkdown(): Promise; purgeGuildMemory?(opts?: { guildId?: string | null; }): Promise<{ ok: boolean; reason?: string; guildId?: string | null; durableFactsDeleted?: number; durableFactVectorsDeleted?: number; conversationMessagesDeleted?: number; conversationVectorsDeleted?: number; reflectionEventsDeleted?: number; sessionSummariesDeleted?: number; journalEntriesDeleted?: number; journalFilesTouched?: number; summaryRefreshed?: boolean; }>; loadFactProfile?(payload: { userId?: string | null; guildId?: string | null; participantIds?: string[]; participantNames?: Record<string, string>; includeOwner?: boolean; }): { participantProfiles?: unknown[]; selfFacts?: unknown[]; loreFacts?: unknown[]; ownerFacts?: unknown[]; userFacts?: unknown[]; relevantFacts?: unknown[]; guidanceFacts?: unknown[]; }; loadUserFactProfile?(payload: { userId?: string | null; guildId?: string | null; }): { userFacts?: unknown[]; }; loadGuildFactProfile?(payload: { guildId?: string | null; }): { selfFacts?: unknown[]; loreFacts?: unknown[]; }; loadOwnerFactProfile?(): { ownerFacts?: unknown[]; guidanceFacts?: unknown[]; }; loadBehavioralFactsForPrompt?(payload: { guildId: string; channelId?: string | null; queryText: string; participantIds?: string[]; settings?: unknown; trace?: Record<string, unknown>; limit?: number; }): Promise<unknown[]>; searchDurableFacts(payload: { guildId?: string | null; scope?: "user" | "guild" | "owner" | "owner_private" | "all"; queryText: string; settings?: Record<string, unknown>; channelId?: string | null; subjectIds?: string[] | null; factTypes?: string[] | null; trace?: Record<string, unknown>; limit?: number; }): Promise<unknown[]>; searchConversationHistory?(payload: { guildId?: string | null; channelId?: string | null; queryText: string; settings?: Record<string, unknown>; trace?: Record<string, unknown>; limit?: number; maxAgeHours?: number; before?: number; after?: number; }): Promise<unknown[]>; getRecentVoiceSessionSummariesForPrompt?(payload: { guildId: string; channelId?: string | null; referenceAtMs?: number | null; limit?: number; windowMinutes?: number; }): Array<Record<string, unknown>>; }
export interface DashboardPublicHttpsState { enabled?: boolean; publicUrl?: string; [key: string]: unknown; }
export interface DashboardPublicHttpsEntrypoint { getState?(): DashboardPublicHttpsState | null; }
export interface DashboardScreenShareSessionManager { getRuntimeState?(): unknown; renderSharePage(token: string): { statusCode?: number | null; html?: string | null; }; createSession(payload: { guildId: string; channelId: string; requesterUserId: string; requesterDisplayName?: string; targetUserId?: string | null; source?: string; }): Promise<Record<string, unknown>>; ingestFrameByToken(payload: { token: string; mimeType: string; dataBase64: string; source?: string; }): Promise<Record<string, unknown>>; stopSessionByToken(payload: { token: string; reason: string; }): Promise | unknown; }
interface DashboardDeps { appConfig: DashboardAppConfig; store: Store; bot: DashboardBot; memory: DashboardMemory; publicHttpsEntrypoint?: DashboardPublicHttpsEntrypoint | null; screenShareSessionManager?: DashboardScreenShareSessionManager | null; }
export function createDashboardServer({ appConfig, store, bot, memory, publicHttpsEntrypoint = null, screenShareSessionManager = null }: DashboardDeps): { app: DashboardApp; server: DashboardServerHandle; } { const dashboardHost = normalizeDashboardHost(appConfig.dashboardHost); const dashboardToken = String(appConfig.dashboardToken || "").trim(); if (!dashboardToken && !isLocalDashboardHost(dashboardHost)) { throw new Error("DASHBOARD_TOKEN is required when DASHBOARD_HOST is not loopback-only."); }
const app = new Hono(); const publicFrameIngressRateLimit = new Map<string, FixedWindowBucket>(); const getStatsPayload = (guildId: string | null = null) => { const botRuntime = bot.getRuntimeState(); return { stats: store.getStats({ guildId }), runtime: { ...botRuntime, publicHttps: publicHttpsEntrypoint?.getState?.() || null, screenShare: screenShareSessionManager?.getRuntimeState?.() || null } }; };
app.onError((error, c) => { if (error instanceof DashboardHttpError) { if (error.responseKind === "text") { return new Response(String(error.responseBody), { status: error.status, headers: { "content-type": "text/plain; charset=UTF-8" } }); } return Response.json(error.responseBody, { status: error.status }); }
console.error("Dashboard request failed:", error);
if (isApiPath(c.req.path)) {
return c.json(
{
error: String(error instanceof Error ? error.message : error)
},
500
);
}
return c.text("Internal Server Error", 500);
});
app.notFound((c) => { if (isApiPath(c.req.path)) { return c.json({ error: "Not found." }, 404); } return c.text("Not found.", 404); });
app.use("*", async (c, next) => { if (!isPublicFrameIngressPath(c.req.path)) { await next(); return; }
const contentLengthHeader = String(c.req.header("content-length") || "").trim();
if (contentLengthHeader) {
const declaredBytes = Number(contentLengthHeader);
if (Number.isFinite(declaredBytes) && declaredBytes > PUBLIC_FRAME_DECLARED_BYTES_MAX) {
return c.json(
{
accepted: false,
reason: "payload_too_large"
},
413
);
}
}
const callerIp = getRequestIp(c);
const rateKey = `${callerIp}|${c.req.path}`;
const allowed = consumeFixedWindowRateLimit({
buckets: publicFrameIngressRateLimit,
key: rateKey,
nowMs: Date.now(),
windowMs: PUBLIC_FRAME_REQUEST_WINDOW_MS,
maxRequests: PUBLIC_FRAME_REQUEST_MAX_PER_WINDOW
});
if (!allowed) {
return c.json(
{
accepted: false,
reason: "ingest_rate_limited"
},
429
);
}
await next();
});
app.use("*", async (c, next) => { if (!isApiPath(c.req.path)) { await next(); return; }
const apiPath = stripApiPrefix(c.req.path);
if (isDashboardAuthSessionApiPath(apiPath)) {
await next();
return;
}
const apiAccessKind = classifyApiAccessPath(apiPath);
const isPublicApiRoute = isAllowedPublicApiPath(apiPath);
const dashboardToken = String(appConfig.dashboardToken || "").trim();
const publicApiToken = String(appConfig.publicApiToken || "").trim();
const presentedDashboardToken = c.req.header("x-dashboard-token") || "";
const presentedPublicToken = c.req.header("x-public-api-token") || "";
const isDashboardSessionAuthorized = await hasValidDashboardSessionCookie(c, dashboardToken);
const isDashboardAuthorized =
(Boolean(dashboardToken) && presentedDashboardToken === dashboardToken) || isDashboardSessionAuthorized;
const isPublicApiAuthorized = Boolean(publicApiToken) && presentedPublicToken === publicApiToken;
const isPublicTunnelRequest = isRequestFromPublicTunnel(c, publicHttpsEntrypoint);
const publicHttpsEnabled = Boolean(publicHttpsEntrypoint?.getState?.()?.enabled);
if (isDashboardAuthorized) {
await next();
return;
}
if (apiAccessKind === "public_session_token") {
await next();
return;
}
if (apiAccessKind === "public_header_token" && isPublicApiAuthorized) {
await next();
return;
}
if (isPublicTunnelRequest && !isPublicApiRoute) {
return c.json({ error: "Not found." }, 404);
}
if (apiAccessKind === "public_header_token") {
if (!dashboardToken && !publicApiToken) {
return c.json(
{
accepted: false,
reason: "dashboard_or_public_api_token_required"
},
503
);
}
if (publicApiToken && !isPublicApiAuthorized) {
return c.json(
{
accepted: false,
reason: "unauthorized_public_api_token"
},
401
);
}
return c.json(
{
accepted: false,
reason: "unauthorized_dashboard_token"
},
401
);
}
if (!dashboardToken) {
if (publicHttpsEnabled) {
return c.json(
{
error: "dashboard_token_required_when_public_https_enabled"
},
503
);
}
await next();
return;
}
return c.json({ error: "Unauthorized. Provide x-dashboard-token." }, 401);
});
const voiceSseClients = new Set(); const activitySseClients = new Set(); const writeSseEvent = async (client: DashboardSseClient, eventName: string, payload: unknown) => { const wirePayload = `event: ${String(eventName || "message")} data: ${JSON.stringify(payload)}
`; await client.write(wirePayload); }; const broadcastSseEvent = ( clients: Set, eventName: string, payload: unknown ) => { if (clients.size === 0) return; for (const client of clients) { void writeSseEvent(client, eventName, payload).catch(() => { clients.delete(client); }); } };
attachAuthRoutes(app, { appConfig, publicHttpsEntrypoint }); attachSettingsRoutes(app, { store, bot, appConfig }); attachOAuthRoutes(app, { store, appConfig, bot }); attachMetricsRoutes(app, { store, publicHttpsEntrypoint, getStatsPayload, activitySseClients, writeSseEvent }); attachVoiceRoutes(app, { store, bot, memory, screenShareSessionManager, voiceSseClients });
const previousActionListener = typeof store.onActionLogged === "function" ? store.onActionLogged : null; store.onActionLogged = (action) => { if (previousActionListener) { try { previousActionListener(action); } catch { // keep dashboard listener resilient } }
if (activitySseClients.size > 0) {
broadcastSseEvent(activitySseClients, "action_event", action);
broadcastSseEvent(activitySseClients, "stats_update", getStatsPayload());
}
if (action?.kind?.startsWith("voice_") && voiceSseClients.size > 0) {
broadcastSseEvent(voiceSseClients, "voice_event", action);
}
};
const staticDir = path.resolve(__dirname, "../dashboard/dist"); const staticRoot = path.relative(process.cwd(), staticDir) || "."; const indexPath = path.join(staticDir, "index.html");
if (!fs.existsSync(indexPath)) {
throw new Error("React dashboard build missing at dashboard/dist. Run bun run build:ui.");
}
const indexHtml = fs.readFileSync(indexPath, "utf8"); const serveDashboardStatic = serveStatic({ root: staticRoot });
app.use("*", async (c, next) => { if (isRequestFromPublicTunnel(c, publicHttpsEntrypoint) && !c.req.path.startsWith("/share/")) { return c.text("Not found.", 404); }
await next();
});
app.get("/share/:token", (c) => { if (!screenShareSessionManager) { return c.text("Screen share link unavailable.", 503); } const rendered = screenShareSessionManager.renderSharePage(String(c.req.param("token") || "").trim()); return new Response(String(rendered.html || ""), { status: rendered.statusCode || 200, headers: { "content-type": "text/html; charset=UTF-8" } }); });
app.use("/assets/*", serveDashboardStatic);
app.get("*", (c) => { if (isApiPath(c.req.path) || c.req.path.startsWith("/share/")) { return c.text("Not found.", 404); } c.header("Cache-Control", "no-store"); return c.html(indexHtml); });
app.on("HEAD", "*", (c) => { if (isApiPath(c.req.path) || c.req.path.startsWith("/share/")) { return c.body(null, 404); } return c.body(null, 200, { "Cache-Control": "no-store", "content-type": "text/html; charset=UTF-8" }); });
const bunServer = Bun.serve({ hostname: dashboardHost, port: appConfig.dashboardPort, fetch(request, server) { return app.fetch(request, { server }); } }); const server = createDashboardServerHandle(bunServer, dashboardHost);
console.log(Dashboard running on http://${dashboardHost}:${bunServer.port});
// Bonjour: advertise dashboard on local network for iOS app discovery const bonjour = new BonjourAdvertiser(bunServer.port); const tunnelUrl = publicHttpsEntrypoint?.getState?.()?.publicUrl || ""; bonjour.start(tunnelUrl); if (!tunnelUrl && publicHttpsEntrypoint) { console.log("Bonjour: waiting for tunnel URL before advertising"); }
// Re-advertise quickly once the tunnel becomes ready so discovery clients
// can catch the new TXT record during initial setup.
if (publicHttpsEntrypoint) {
let lastTunnelUrl = tunnelUrl;
setInterval(() => {
const currentUrl = publicHttpsEntrypoint.getState?.()?.publicUrl || "";
if (currentUrl !== lastTunnelUrl) {
lastTunnelUrl = currentUrl;
bonjour.updateTunnelUrl(currentUrl);
if (currentUrl) {
console.log(Bonjour: updated tunnel URL → ${currentUrl});
} else {
console.log("Bonjour: cleared tunnel URL advertisement");
}
}
}, 1_000);
}
return { app, server }; }
function isRequestFromPublicTunnel( c: { req: { header(name: string): string | undefined } }, publicHttpsEntrypoint: DashboardPublicHttpsEntrypoint | null | undefined ) { const requestHost = String(c.req.header("x-forwarded-host") || c.req.header("host") || "").trim(); if (!requestHost) return false; const publicState = publicHttpsEntrypoint?.getState?.() || null; return isPublicTunnelRequestHost(requestHost, publicState); }
function isPublicFrameIngressPath(rawPath: string) {
const normalizedPath = String(rawPath || "").trim();
if (!normalizedPath) return false;
if (normalizedPath === /api${STREAM_INGEST_API_PATH} || normalizedPath === /api${STREAM_INGEST_API_PATH}/) {
return true;
}
return PUBLIC_SHARE_FRAME_PATH_RE.test(normalizedPath);
}
interface FixedWindowBucket { windowStartedAt: number; count: number; lastSeenAt: number; }
function consumeFixedWindowRateLimit({ buckets, key, nowMs, windowMs, maxRequests }: { buckets: Map<string, FixedWindowBucket>; key: string; nowMs: number; windowMs: number; maxRequests: number; }) { if (!key) return false; const now = Number.isFinite(Number(nowMs)) ? Number(nowMs) : Date.now(); const windowSpan = Math.max(1, Number(windowMs) || 1); const maxInWindow = Math.max(1, Number(maxRequests) || 1);
let bucket = buckets.get(key) || null; if (!bucket || now - Number(bucket.windowStartedAt || 0) >= windowSpan) { bucket = { windowStartedAt: now, count: 0, lastSeenAt: now }; buckets.set(key, bucket); }
if (Number(bucket.count || 0) >= maxInWindow) { bucket.lastSeenAt = now; pruneRateLimitBuckets(buckets, now, windowSpan); return false; }
bucket.count = Number(bucket.count || 0) + 1; bucket.lastSeenAt = now; pruneRateLimitBuckets(buckets, now, windowSpan); return true; }
function pruneRateLimitBuckets(buckets: Map<string, FixedWindowBucket>, nowMs: number, windowMs: number) { if (buckets.size <= 2500) return; const staleBefore = nowMs - windowMs * 3; for (const [key, bucket] of buckets.entries()) { if (Number(bucket.lastSeenAt || 0) < staleBefore) { buckets.delete(key); } if (buckets.size <= 1500) break; } }
