import jwt from 'jsonwebtoken'; import crypto from 'crypto'; export function parseExpiryToSeconds(expiry: string): number { const match = expiry.match(/^(\d+)([smhd])$/); if (!match) return 15 * 60; const [, num, unit] = match; const n = parseInt(num, 10); switch (unit) { case 's': return n; case 'm': return n * 60; case 'h': return n * 3600; case 'd': return n * 86400; default: return 15 * 60; } } function requireSecret(name: string): string { const value = process.env[name]; if (!value) { throw new Error( `FATAL: JWT secrets not configured — missing ${name}.\n` + 'Set JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, JWT_SUPERADMIN_SECRET in environment variables.' ); } return value; } const ACCESS_TOKEN_SECRET = requireSecret('JWT_ACCESS_SECRET'); const REFRESH_TOKEN_SECRET = requireSecret('JWT_REFRESH_SECRET'); const SUPERADMIN_TOKEN_SECRET = requireSecret('JWT_SUPERADMIN_SECRET'); const BOT_TOKEN_SECRET = process.env.JWT_BOT_SECRET || ACCESS_TOKEN_SECRET; const ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_EXPIRES || '14d'; const REFRESH_TOKEN_EXPIRY_REMEMBER = process.env.JWT_REFRESH_EXPIRES_REMEMBER || '90d'; const REFRESH_TOKEN_EXPIRY_SESSION = process.env.JWT_REFRESH_EXPIRES_SESSION || '30d'; const SUPERADMIN_TOKEN_EXPIRY = process.env.JWT_SUPERADMIN_EXPIRES || '1h'; const BOT_TOKEN_EXPIRY = process.env.JWT_BOT_EXPIRES || '10m'; export interface TokenPayload { userId: number; organizationId: number; appRole: string; role?: string; // legacy fallback for old tokens } export interface BotServiceTokenPayload { botId: number; organizationId: number; type: 'bot_service'; } export interface SuperAdminTokenPayload { superAdminId: number; email: string; isSuperAdmin: true; } export interface TokenPair { accessToken: string; refreshToken: string; expiresIn: number; familyId?: string; } export function generateTokens(payload: TokenPayload, remember: boolean = false, familyId?: string): TokenPair { // Ensure we don't include legacy role in new tokens const { role, ...cleanPayload } = payload as any; const accessToken = jwt.sign(cleanPayload, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-users' }); const refreshToken = jwt.sign(cleanPayload, REFRESH_TOKEN_SECRET, { expiresIn: (remember ? REFRESH_TOKEN_EXPIRY_REMEMBER : REFRESH_TOKEN_EXPIRY_SESSION) as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-users' }); return { accessToken, refreshToken, expiresIn: parseExpiryToSeconds(ACCESS_TOKEN_EXPIRY), familyId }; } export function verifyAccessToken(token: string): TokenPayload { const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET, { algorithms: ['HS256'], issuer: 'workflow-system', audience: 'workflow-users', }) as TokenPayload; // Fallback for old tokens that used 'role' instead of 'appRole' if (!decoded.appRole && decoded.role) { decoded.appRole = decoded.role; } return decoded; } export function verifyRefreshToken(token: string): TokenPayload { const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET, { algorithms: ['HS256'], issuer: 'workflow-system', audience: 'workflow-users', }) as TokenPayload; // Fallback for old tokens that used 'role' instead of 'appRole' if (!decoded.appRole && decoded.role) { decoded.appRole = decoded.role; } return decoded; } export function generateVerificationToken(): string { return crypto.randomBytes(32).toString('hex'); } export function generateResetToken(): string { return crypto.randomBytes(32).toString('hex'); } export function generateBotServiceToken(botId: number, organizationId: number): string { const payload: BotServiceTokenPayload = { botId, organizationId, type: 'bot_service' }; return jwt.sign(payload, BOT_TOKEN_SECRET, { expiresIn: BOT_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-bots' }); } export function verifyBotServiceToken(token: string): BotServiceTokenPayload { const payload = jwt.verify(token, BOT_TOKEN_SECRET, { issuer: 'workflow-system', audience: 'workflow-bots' }) as BotServiceTokenPayload; if (payload.type !== 'bot_service') { throw new Error('Invalid token type'); } return payload; } export function generateSuperAdminToken(payload: SuperAdminTokenPayload): string { return jwt.sign(payload, SUPERADMIN_TOKEN_SECRET, { expiresIn: SUPERADMIN_TOKEN_EXPIRY as jwt.SignOptions['expiresIn'], issuer: 'workflow-system', audience: 'workflow-superadmin' }); } export function verifySuperAdminToken(token: string): SuperAdminTokenPayload { const payload = jwt.verify(token, SUPERADMIN_TOKEN_SECRET, { issuer: 'workflow-system', audience: 'workflow-superadmin' }) as SuperAdminTokenPayload; if (!payload.isSuperAdmin) { throw new Error('Invalid super admin token'); } return payload; }