import jwt from 'jsonwebtoken'; import { CONFIG } from '../config'; export interface JWTPayload { uid: string; iat?: number; exp?: number; } /** * Verifies a JWT and returns the decoded payload. * Throws if the token is invalid or expired. * The uid is ALWAYS extracted from the verified token — never from client input. */ export function verifyToken(token: string): JWTPayload { const decoded = jwt.verify(token, CONFIG.JWT_SECRET) as JWTPayload; if (!decoded.uid || typeof decoded.uid !== 'string') { throw new Error('JWT payload missing uid field'); } // Sanitize uid to prevent Redis key injection if (!/^[a-zA-Z0-9_-]{1,64}$/.test(decoded.uid)) { throw new Error('JWT uid contains invalid characters'); } return decoded; } /** Extract Bearer token from Upgrade/Authorization header or query param */ export function extractToken( authHeader: string | undefined, queryToken: string | undefined ): string { if (authHeader?.startsWith('Bearer ')) { return authHeader.slice(7).trim(); } if (queryToken) { return queryToken.trim(); } throw new Error('No authorization token provided'); }