Files
Shade/packages/shade-observer/src/auth.ts

48 lines
1.5 KiB
TypeScript
Raw Normal View History

import { UnauthorizedError, ConfigurationError } from '@shade/core';
import type { Context, Next } from 'hono';
/**
* Bearer token middleware for the observer.
*
* Reads token from `Authorization: Bearer <token>` header.
* For SSE endpoints (where browsers can't set headers), also accepts
* `?token=<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;
}