import type { Context, Next } from "hono"; import { validateApiKey } from "./api-key"; import { verifyWorkerJwt } from "./jwt"; import { resolveToken } from "./token"; const WS_AUTH_PROTOCOL_PREFIX = "rcs.auth."; /** Encode a bearer token for WebSocket clients that cannot send auth headers. */ export function encodeWebSocketAuthProtocol(token: string): string { return `${WS_AUTH_PROTOCOL_PREFIX}${Buffer.from(token, "utf8").toString("base64url")}`; } function decodeWebSocketAuthProtocol(protocolHeader: string | undefined): string | undefined { if (!protocolHeader) { return undefined; } for (const protocol of protocolHeader.split(",")) { const trimmed = protocol.trim(); if (!trimmed.startsWith(WS_AUTH_PROTOCOL_PREFIX)) { continue; } const encoded = trimmed.slice(WS_AUTH_PROTOCOL_PREFIX.length); if (!encoded) { return undefined; } try { const token = Buffer.from(encoded, "base64url").toString("utf8"); return token.length > 0 ? token : undefined; } catch { return undefined; } } return undefined; } /** Extract a Bearer token from the Authorization header only. */ export function extractBearerToken(c: Context): string | undefined { const authHeader = c.req.header("Authorization"); return authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : undefined; } /** Extract auth for WebSocket upgrades without putting secrets in query strings. */ export function extractWebSocketAuthToken(c: Context): string | undefined { return extractBearerToken(c) ?? decodeWebSocketAuthProtocol(c.req.header("Sec-WebSocket-Protocol")); } /** * Unified authentication middleware — supports two modes: * * 1. **Token mode** (Web UI): Bearer token resolved via server-side lookup → username injected * 2. **API Key mode** (CLI bridge): Valid API key + X-Username header → username injected */ export async function apiKeyAuth(c: Context, next: Next) { const token = extractBearerToken(c); // Try token authentication (Web UI) const tokenUsername = resolveToken(token); if (tokenUsername) { c.set("username", tokenUsername); await next(); return; } // Try API Key authentication (CLI bridge) if (validateApiKey(token)) { // Extract username from X-Username header or ?username= query param const username = c.req.header("X-Username") || c.req.query("username"); if (username) { c.set("username", username); } await next(); return; } return c.json({ error: { type: "unauthorized", message: "Invalid or missing auth token" } }, 401); } /** * Session ingress authentication — accepts both API key and worker JWT. * * Used for SSE stream, CCR worker events, and WebSocket ingress endpoints. * On JWT validation, stores the decoded payload in c.set("jwtPayload") for * downstream handlers to inspect session_id if needed. */ export async function sessionIngressAuth(c: Context, next: Next) { const token = extractWebSocketAuthToken(c); if (!token) { return c.json({ error: { type: "unauthorized", message: "Missing auth token" } }, 401); } // Try API key first (backward compatible) if (validateApiKey(token)) { await next(); return; } // Try JWT verification — validate session_id matches route param const payload = verifyWorkerJwt(token); if (payload) { const routeSessionId = c.req.param("id") || c.req.param("sessionId"); if (routeSessionId && payload.session_id !== routeSessionId) { return c.json({ error: { type: "forbidden", message: "JWT session_id does not match target session" } }, 403); } c.set("jwtPayload", payload); await next(); return; } return c.json({ error: { type: "unauthorized", message: "Invalid API key or JWT" } }, 401); } /** Accept CLI headers but don't validate them */ export async function acceptCliHeaders(c: Context, next: Next) { await next(); } /** * Extract UUID from request — query param ?uuid= or header X-UUID */ export function getUuidFromRequest(c: Context): string | undefined { return c.req.query("uuid") || c.req.header("X-UUID"); } /** * UUID-based auth for Web UI routes (no-login mode). * Accepts UUID in query param/header, OR a valid API key via Authorization header. */ export async function uuidAuth(c: Context, next: Next) { // Try API key auth via Authorization header const bearer = extractBearerToken(c); if (bearer && validateApiKey(bearer)) { // Valid API key — generate a stable UUID from the key for downstream use const uuid = getUuidFromRequest(c); c.set("uuid", uuid || bearer); await next(); return; } // Fall back to UUID auth const uuid = getUuidFromRequest(c); if (!uuid) { return c.json({ error: { type: "unauthorized", message: "Missing UUID" } }, 401); } c.set("uuid", uuid); await next(); }