import type { IncomingMessage } from 'http'; export interface AuthConfig { /** Static bearer token. If set, all connections must provide it. */ token?: string; /** Trust Tailscale-User-Login header for identity. */ tailscaleIdentity?: boolean; } export interface AuthResult { authenticated: boolean; identity?: string; error?: string; } /** * Authenticates a WebSocket upgrade request. * * Auth is checked in this order: * 1. If token is configured, validate Authorization header (Bearer token) * 2. If tailscaleIdentity is enabled, extract identity from Tailscale-User-Login header * 3. If no auth is configured, allow all connections */ export function authenticateRequest(req: IncomingMessage, config: AuthConfig): AuthResult { // If token auth is configured, it's required if (config.token) { const authHeader = req.headers['authorization']; if (!authHeader) { return { authenticated: false, error: 'Authorization header required' }; } const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { return { authenticated: false, error: 'Invalid Authorization format (expected: Bearer )' }; } if (parts[1] !== config.token) { return { authenticated: false, error: 'Invalid token' }; } // Token is valid — check for Tailscale identity too const identity = extractTailscaleIdentity(req, config); return { authenticated: true, identity: identity ?? 'token-user' }; } // If Tailscale identity is configured (no token), use it if (config.tailscaleIdentity) { const identity = extractTailscaleIdentity(req, config); if (identity) { return { authenticated: true, identity }; } // Tailscale identity configured but header not present — still allow (might be local) return { authenticated: true, identity: 'anonymous' }; } // No auth configured — allow all return { authenticated: true, identity: 'anonymous' }; } function extractTailscaleIdentity(req: IncomingMessage, config: AuthConfig): string | undefined { if (!config.tailscaleIdentity) return undefined; const header = req.headers['tailscale-user-login']; if (typeof header === 'string' && header.length > 0) { return header; } return undefined; }