diff --git a/hub/src/api/api-keys.ts b/hub/src/api/api-keys.ts index 24f17bf..6cde13f 100644 --- a/hub/src/api/api-keys.ts +++ b/hub/src/api/api-keys.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono' import { createApiKey, listApiKeys, revokeApiKey } from '../db/dal' -import { hashToken } from '../ws/channel' +import { hashToken } from '../lib/crypto' import { generateToken } from '../utils/token' const apiKeys = new Hono() diff --git a/hub/src/api/plugin.ts b/hub/src/api/plugin.ts index 94b4fbe..33bf29c 100644 --- a/hub/src/api/plugin.ts +++ b/hub/src/api/plugin.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono' import { z } from 'zod' import { createPluginSession } from '../db/dal' -import { hashToken } from '../ws/channel' +import { hashToken } from '../lib/crypto' import { generateToken } from '../utils/token' const plugin = new Hono() diff --git a/hub/src/api/sessions.ts b/hub/src/api/sessions.ts index c2c0062..9f4e27b 100644 --- a/hub/src/api/sessions.ts +++ b/hub/src/api/sessions.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono' import { z } from 'zod' import { createSession, listSessions, getSession, deleteSession, updateSessionToken, markSessionDisconnected } from '../db/dal' -import { hashToken } from '../ws/channel' +import { hashToken } from '../lib/crypto' import { getChannel } from '../ws/registry' import { generateToken } from '../utils/token' diff --git a/hub/src/auth/api-key-middleware.ts b/hub/src/auth/api-key-middleware.ts index 8a23da4..e7ae83f 100644 --- a/hub/src/auth/api-key-middleware.ts +++ b/hub/src/auth/api-key-middleware.ts @@ -1,6 +1,6 @@ import type { Context, Next } from 'hono' import { verifyApiKey } from '../db/dal' -import { hashToken } from '../ws/channel' +import { hashToken } from '../lib/crypto' export async function apiKeyMiddleware(c: Context, next: Next) { const authHeader = c.req.header('Authorization') diff --git a/hub/src/lib/crypto.ts b/hub/src/lib/crypto.ts new file mode 100644 index 0000000..5b1c893 --- /dev/null +++ b/hub/src/lib/crypto.ts @@ -0,0 +1,12 @@ +// Crypto helpers shared across hub modules. +// Kept dependency-free (Web Crypto only) so it can be imported from API routes, +// WS handlers, and middleware without pulling in channel/session code. + +export async function hashToken(token: string): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(token) + const hash = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') +} diff --git a/hub/src/ws/agent.ts b/hub/src/ws/agent.ts index d60712d..e94a68f 100644 --- a/hub/src/ws/agent.ts +++ b/hub/src/ws/agent.ts @@ -1,7 +1,7 @@ import type { ServerWebSocket } from 'bun' import { AgentInbound } from './agent-protocol' import { verifyApiKey, findOrCreateAgentSession, updateSessionStatus as setSessionStatus, insertMessage, insertAssistantPlaceholder, appendToMessage, finalizeMessage, listSessions, getUserSystemPrompt, recentlyDisconnectedForProjectDir, updateSessionAgentInfo } from '../db/dal' -import { hashToken } from './channel' +import { hashToken } from '../lib/crypto' import { generateToken } from '../utils/token' import { registerChannel, unregisterChannel, getChannel, broadcastToSubscribers, broadcastToUser } from './registry' import { verifyApiKeyWithCapability, upsertSupervisor, endRun, replaceSupervisorCommands } from '../db/supervisor-dal' diff --git a/hub/src/ws/channel.ts b/hub/src/ws/channel.ts index 745ea92..18dc591 100644 --- a/hub/src/ws/channel.ts +++ b/hub/src/ws/channel.ts @@ -4,6 +4,7 @@ import { ChannelInbound } from './protocol' import { verifyChannelToken, updateSessionStatus as setSessionStatus, insertMessage } from '../db/dal' import { registerChannel, unregisterChannel, broadcastToSubscribers, broadcastToUser } from './registry' import { listSessions } from '../db/dal' +import { hashToken } from '../lib/crypto' const AUTH_TIMEOUT_MS = 5_000 const HEARTBEAT_INTERVAL_MS = 30_000 @@ -32,13 +33,6 @@ export function createChannelWsData(): ChannelWsData { } } -export async function hashToken(token: string): Promise { - const encoder = new TextEncoder() - const data = encoder.encode(token) - const hash = await crypto.subtle.digest('SHA-256', data) - return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('') -} - export function handleChannelOpen(ws: ServerWebSocket) { const data = ws.data console.log('[channel] connection opened') diff --git a/web/src/App.tsx b/web/src/App.tsx index 649b34f..1f47e72 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -13,6 +13,14 @@ import type { AuthUser } from './lib/auth.ts' type Route = 'chat' | 'settings' | 'schedules' +function LoadingScreen() { + return ( +
+
Loading...
+
+ ) +} + function getRoute(): Route { const hash = window.location.hash // Legacy /#/supervisor → settings with supervisor tab @@ -55,11 +63,7 @@ export default function App() { }, []) if (loading || needsSetup === null) { - return ( -
-
Loading...
-
- ) + return } if (needsSetup) { @@ -72,11 +76,7 @@ export default function App() { // Wait for profile to load before rendering gated routes if (profileLoading || !profile) { - return ( -
-
Loading...
-
- ) + return } return (