import { UnauthorizedError, ConfigurationError } from '@shade/core'; import type { Context, Next } from 'hono'; /** * Bearer token middleware for the observer. * * Reads token from `Authorization: Bearer ` header. * For SSE endpoints (where browsers can't set headers), also accepts * `?token=` query parameter. * * Throws ConfigurationError if SHADE_OBSERVER_TOKEN is empty (refuses to start). */ export function createAuthMiddleware(token: string) { if (!token || token.length < 16) { throw new ConfigurationError( 'SHADE_OBSERVER_TOKEN must be set and at least 16 characters. Refusing to start.', ); } return async (c: Context, next: Next) => { const header = c.req.header('Authorization'); let provided: string | null = null; if (header && header.startsWith('Bearer ')) { provided = header.slice(7); } else { // Allow query string for SSE (EventSource can't set headers) provided = c.req.query('token') ?? null; } if (!provided || !constantTimeStringEqual(provided, token)) { throw new UnauthorizedError('Invalid or missing observer token'); } await next(); }; } /** Constant-time string comparison (avoids timing attacks on token check) */ function constantTimeStringEqual(a: string, b: string): boolean { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) { diff |= a.charCodeAt(i) ^ b.charCodeAt(i); } return diff === 0; }