/**
* API Authentication Guard
*
* Validates Bearer token from the Authorization header against GATEWAY_API_KEY.
* When GATEWAY_API_KEY is not set, auth is skipped (local dev mode).
*
* Public endpoints (/.well-known/*) bypass authentication.
*/
import { timingSafeEqual } from 'node:crypto';
import { logger } from '../utils/logger.js';
interface AuthResult {
authenticated: boolean;
error?: string;
}
const PUBLIC_PATHS = [
'/.well-known/ucp',
'/.well-known/agent.json',
];
/**
* Validate the request's Authorization header.
* Returns { authenticated: true } if valid, or { authenticated: false, error } if not.
*/
export function validateApiAuth(
path: string,
headers: Record<string, string | undefined>,
): AuthResult {
// Public endpoints bypass auth
if (PUBLIC_PATHS.includes(path)) {
return { authenticated: true };
}
const apiKey = process.env['GATEWAY_API_KEY'];
// If no API key is configured, skip auth (local dev mode)
if (!apiKey) {
return { authenticated: true };
}
// Extract Bearer token
const authHeader = headers['authorization'] ?? headers['Authorization'];
if (!authHeader) {
logger.warn('API auth: missing Authorization header', { path });
return { authenticated: false, error: 'Missing Authorization header' };
}
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (!match) {
logger.warn('API auth: invalid Authorization format', { path });
return { authenticated: false, error: 'Authorization header must use Bearer scheme' };
}
const token = match[1] ?? '';
// Timing-safe comparison
try {
const tokenBuffer = Buffer.from(token, 'utf-8');
const keyBuffer = Buffer.from(apiKey, 'utf-8');
if (tokenBuffer.length !== keyBuffer.length) {
logger.warn('API auth: invalid token', { path });
return { authenticated: false, error: 'Invalid API key' };
}
if (!timingSafeEqual(tokenBuffer, keyBuffer)) {
logger.warn('API auth: invalid token', { path });
return { authenticated: false, error: 'Invalid API key' };
}
} catch {
logger.warn('API auth: token comparison error', { path });
return { authenticated: false, error: 'Invalid API key' };
}
return { authenticated: true };
}