@nekzus/tokenly
Version:
Secure JWT token management with advanced device fingerprinting
1 lines • 35.9 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/utils/errorHandler.ts","../src/tokenManager.ts","../src/utils/ipHelper.ts"],"sourcesContent":["export enum ErrorCode {\n INVALID_TOKEN = 'INVALID_TOKEN',\n TOKEN_EXPIRED = 'TOKEN_EXPIRED',\n TOKEN_REVOKED = 'TOKEN_REVOKED',\n INVALID_FINGERPRINT = 'INVALID_FINGERPRINT',\n MAX_DEVICES_REACHED = 'MAX_DEVICES_REACHED',\n MAX_ROTATION_EXCEEDED = 'MAX_ROTATION_EXCEEDED',\n\n INVALID_PAYLOAD = 'INVALID_PAYLOAD',\n EMPTY_PAYLOAD = 'EMPTY_PAYLOAD',\n MISSING_USER_ID = 'MISSING_USER_ID',\n INVALID_USER_ID = 'INVALID_USER_ID',\n\n INVALID_CONTEXT = 'INVALID_CONTEXT',\n\n MISSING_ENV_VAR = 'MISSING_ENV_VAR'\n}\n\nexport class TokenlyError extends Error {\n constructor(\n public code: ErrorCode,\n message: string,\n public details?: any\n ) {\n super(message);\n this.name = 'TokenlyError';\n }\n}\n\nexport const ErrorMessages = {\n [ErrorCode.INVALID_TOKEN]: 'Invalid token format or signature',\n [ErrorCode.TOKEN_EXPIRED]: 'Token has expired',\n [ErrorCode.TOKEN_REVOKED]: 'Token has been revoked',\n [ErrorCode.INVALID_FINGERPRINT]: 'Invalid token fingerprint',\n [ErrorCode.MAX_DEVICES_REACHED]: 'Maximum number of devices reached',\n [ErrorCode.MAX_ROTATION_EXCEEDED]: 'Maximum rotation count exceeded',\n [ErrorCode.INVALID_PAYLOAD]: 'Invalid payload format',\n [ErrorCode.EMPTY_PAYLOAD]: 'Payload cannot be empty',\n [ErrorCode.MISSING_USER_ID]: 'Payload must contain a userId',\n [ErrorCode.INVALID_USER_ID]: 'Invalid userId format or value',\n [ErrorCode.INVALID_CONTEXT]: 'Invalid or empty context values',\n [ErrorCode.MISSING_ENV_VAR]: 'Missing required environment variable'\n};\n\nexport function throwError(code: ErrorCode, details?: any): never {\n throw new TokenlyError(code, ErrorMessages[code], details);\n} ","import crypto from 'crypto';\nimport jwt from 'jsonwebtoken';\nimport { ErrorCode, throwError, TokenlyError } from './utils/errorHandler.js';\n\ninterface TokenlyOptions {\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: 'strict' | 'lax' | 'none';\n domain?: string;\n path?: string;\n maxAge?: number;\n}\n\ninterface TokenlyConfig {\n accessTokenExpiry?: string;\n refreshTokenExpiry?: string;\n cookieOptions?: TokenlyOptions;\n jwtOptions?: {\n algorithm?: jwt.Algorithm;\n audience?: string | string[];\n issuer?: string;\n jwtid?: string;\n subject?: string;\n notBefore?: string | number;\n maxAge?: string | number;\n };\n rotationConfig?: {\n enableAutoRotation?: boolean;\n rotationInterval?: number;\n maxRotationCount?: number;\n };\n securityConfig?: {\n enableFingerprint?: boolean;\n enableBlacklist?: boolean;\n maxDevices?: number;\n revokeOnSecurityBreach?: boolean;\n };\n}\n\ninterface TokenlyToken {\n iat: number;\n exp: number;\n [key: string]: any;\n}\n\ninterface TokenlyResponse {\n raw: string;\n payload: {\n [key: string]: any;\n iat?: Date;\n exp?: Date;\n };\n cookieConfig?: {\n name: string;\n value: string;\n options: TokenlyOptions;\n };\n}\n\n/**\n * Tokenly - A secure JWT token manager with HttpOnly cookie support\n * Implements best security practices for JWT token handling in modern web applications\n */\nexport class Tokenly {\n private secretAccess: string;\n private secretRefresh: string;\n private accessTokenExpiry: string;\n private refreshTokenExpiry: string;\n private cookieOptions: TokenlyOptions;\n private jwtOptions: jwt.SignOptions;\n private verifyOptions: jwt.VerifyOptions;\n private currentToken: string | null = null;\n private blacklistedTokens: Set<string> = new Set();\n private rotationConfig: Required<NonNullable<TokenlyConfig['rotationConfig']>>;\n private securityConfig: Required<NonNullable<TokenlyConfig['securityConfig']>>;\n private deviceTokens: Map<string, Set<string>> = new Map();\n private rotationCounts: Map<string, number> = new Map();\n private revokedTokens: Set<string> = new Set();\n private tokenCache: Map<string, TokenlyResponse>;\n private eventListeners: Map<string, Function[]>;\n private autoRotationInterval: NodeJS.Timeout | null = null;\n private fingerprintCache: Map<string, string> = new Map();\n private readonly instanceId: string;\n\n /**\n * Initialize Tokenly with custom configuration\n * @param config Optional configuration for token management\n */\n constructor(config?: TokenlyConfig) {\n this.instanceId = crypto.randomBytes(16).toString('hex');\n\n if (!process.env.JWT_SECRET_ACCESS || !process.env.JWT_SECRET_REFRESH) {\n console.warn(\n '\\x1b[33m%s\\x1b[36m%s\\x1b[0m',\n `WARNING: Using auto-generated secrets. This is secure but tokens will be invalidated on server restart. \n For production, please set JWT_SECRET_ACCESS and JWT_SECRET_REFRESH environment variables.\n Instance ID: ${this.instanceId}\\n Documentation: `,\n 'https://nekzus.github.io/tokenly/guide/security.html#environment-variables'\n );\n }\n\n this.secretAccess = process.env.JWT_SECRET_ACCESS || this.generateSecret('access');\n this.secretRefresh = process.env.JWT_SECRET_REFRESH || this.generateSecret('refresh');\n\n this.accessTokenExpiry = config?.accessTokenExpiry || process.env.ACCESS_TOKEN_EXPIRY || '15m';\n this.refreshTokenExpiry = config?.refreshTokenExpiry || process.env.REFRESH_TOKEN_EXPIRY || '7d';\n\n this.cookieOptions = {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n path: '/',\n maxAge: 7 * 24 * 60 * 60 * 1000,\n ...config?.cookieOptions,\n };\n\n this.jwtOptions = {\n algorithm: 'HS512',\n issuer: 'tokenly-auth',\n audience: 'tokenly-client',\n ...config?.jwtOptions,\n };\n\n this.verifyOptions = {\n algorithms: [this.jwtOptions.algorithm as jwt.Algorithm],\n issuer: this.jwtOptions.issuer,\n audience: this.jwtOptions.audience,\n clockTolerance: 30,\n };\n\n this.rotationConfig = {\n enableAutoRotation: true,\n rotationInterval: 60,\n maxRotationCount: 100,\n ...config?.rotationConfig,\n };\n\n this.securityConfig = {\n enableFingerprint: true,\n enableBlacklist: true,\n maxDevices: 5,\n revokeOnSecurityBreach: true,\n ...config?.securityConfig,\n };\n\n this.eventListeners = new Map();\n this.tokenCache = new Map();\n }\n\n private generateSecret(type: 'access' | 'refresh'): string {\n return crypto\n .createHash('sha256')\n .update(`${this.instanceId}-${type}-${Date.now()}`)\n .digest('hex');\n }\n\n /**\n * Format Unix timestamp to ISO date string\n * @param timestamp Unix timestamp in seconds\n * @returns ISO 8601 formatted date string\n */\n private formatDate(timestamp: number): string {\n return new Date(timestamp * 1000).toISOString();\n }\n\n /**\n * Decode token and add readable dates\n * @param token JWT token string\n * @param decoded Decoded token payload\n * @param cookieConfig Optional cookie configuration\n * @returns Formatted token response\n */\n private decodeWithReadableDates(\n token: string,\n decoded?: any\n ): TokenlyResponse {\n if (!decoded) {\n decoded = jwt.decode(token) as TokenlyToken;\n }\n\n const { iat, exp, ...payloadWithoutDates } = decoded;\n\n const result: TokenlyResponse = {\n raw: token,\n payload: {\n ...payloadWithoutDates,\n iat: iat ? this.formatDate(iat) : undefined,\n exp: exp ? this.formatDate(exp) : undefined,\n }\n };\n\n return result;\n }\n\n /**\n * Genera una huella digital del dispositivo/navegador\n */\n private generateFingerprint(context: { userAgent: string; ip: string }): string {\n if (!context?.userAgent?.trim() || !context?.ip?.trim()) {\n throwError(ErrorCode.INVALID_CONTEXT, 'Invalid or empty context values');\n }\n\n const normalizedUA = context.userAgent\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, ' ');\n\n const normalizedIP = context.ip\n .trim()\n .toLowerCase()\n .replace(/[^0-9.]/g, '');\n\n const uaHash = crypto\n .createHash('sha256')\n .update(`ua:${this.instanceId}:${normalizedUA}`)\n .digest('hex');\n\n const ipHash = crypto\n .createHash('sha256')\n .update(`ip:${this.instanceId}:${normalizedIP}`)\n .digest('hex');\n\n const combinedData = `ua=${uaHash}|ip=${ipHash}`;\n\n return crypto\n .createHash('sha256')\n .update(combinedData)\n .digest('hex');\n }\n\n /**\n * Revoca un token específico\n */\n public revokeToken(token: string): void {\n if (!token) return;\n\n try {\n const decoded = jwt.decode(token) as jwt.JwtPayload;\n this.revokedTokens.add(token);\n\n this.emit('tokenRevoked', {\n token,\n userId: decoded?.userId,\n timestamp: Date.now()\n });\n } catch (error) {\n console.error('Error al revocar token:', error);\n }\n }\n\n /**\n * Verifica si un token está en la lista negra\n */\n private isTokenBlacklisted(token: string): boolean {\n return this.securityConfig.enableBlacklist && this.blacklistedTokens.has(token);\n }\n\n private validatePayload(payload: any): void {\n if (payload === null || typeof payload !== 'object') {\n throwError(ErrorCode.INVALID_PAYLOAD);\n }\n\n if (Object.keys(payload).length === 0) {\n throwError(ErrorCode.EMPTY_PAYLOAD);\n }\n\n if (!Object.prototype.hasOwnProperty.call(payload, 'userId')) {\n throwError(ErrorCode.MISSING_USER_ID);\n }\n\n if (payload.userId === null || payload.userId === undefined) {\n throwError(ErrorCode.INVALID_USER_ID);\n }\n\n if (typeof payload.userId !== 'string' || !payload.userId.trim()) {\n throwError(ErrorCode.INVALID_USER_ID);\n }\n\n Object.entries(payload).forEach(([key, value]) => {\n if (value === null || value === undefined) {\n throwError(ErrorCode.INVALID_PAYLOAD, `Payload property '${key}' cannot be null or undefined`);\n }\n });\n\n const payloadSize = JSON.stringify(payload).length;\n if (payloadSize > 8192) {\n throwError(ErrorCode.INVALID_PAYLOAD, 'Payload size exceeds maximum allowed size');\n }\n }\n\n /**\n * Generate a new access token\n * @param payload Token payload\n * @param options Optional JWT sign options\n * @returns Token response with readable dates\n */\n generateAccessToken(\n payload: object,\n options?: jwt.SignOptions,\n context?: { userAgent: string; ip: string }\n ): TokenlyResponse {\n this.validatePayload(payload);\n const finalPayload: { [key: string]: any } = { ...payload };\n\n if (this.securityConfig.enableFingerprint && context) {\n const fingerprint = this.generateFingerprint(context);\n const userId = (payload as any).userId;\n this.handleDeviceStorage(userId, fingerprint);\n finalPayload.fingerprint = fingerprint;\n }\n\n const token = jwt.sign(finalPayload, this.secretAccess, {\n ...this.jwtOptions,\n ...options,\n expiresIn: this.accessTokenExpiry,\n });\n\n const response = this.decodeWithReadableDates(token);\n this.cacheToken(token, response);\n return response;\n }\n\n /**\n * Verify an access token\n * @param token JWT token string\n * @returns Verified token response\n */\n public verifyAccessToken(\n token: string,\n context?: { userAgent: string; ip: string }\n ): TokenlyResponse {\n if (this.revokedTokens.has(token)) {\n throwError(ErrorCode.TOKEN_REVOKED);\n }\n\n if (this.isTokenBlacklisted(token)) {\n throwError(ErrorCode.TOKEN_REVOKED, 'Token is blacklisted');\n }\n\n try {\n const verified = jwt.verify(token, this.secretAccess, {\n ...this.verifyOptions,\n ignoreExpiration: false,\n clockTolerance: 0\n }) as TokenlyToken;\n\n if (this.securityConfig.enableFingerprint && context) {\n const currentFingerprint = this.generateFingerprint(context);\n if (verified.fingerprint && verified.fingerprint !== currentFingerprint) {\n throwError(ErrorCode.INVALID_FINGERPRINT);\n }\n }\n\n const response = this.decodeWithReadableDates(token, verified);\n this.cacheToken(token, response);\n return response;\n } catch (error: any) {\n if (error instanceof TokenlyError) throw error;\n\n if (error.name === 'TokenExpiredError') {\n throwError(ErrorCode.TOKEN_EXPIRED);\n }\n if (error.name === 'JsonWebTokenError') {\n throwError(ErrorCode.INVALID_TOKEN);\n }\n throw error;\n }\n }\n\n /**\n * Generate a new refresh token with HttpOnly cookie configuration\n * @param payload Token payload\n * @param cookieOptions Optional cookie configuration\n * @returns Token response with cookie configuration\n */\n generateRefreshToken(\n payload: object,\n cookieOptions?: TokenlyOptions\n ): TokenlyResponse {\n this.validatePayload(payload);\n const finalPayload: { [key: string]: any } = { ...payload };\n\n delete (finalPayload as any).aud;\n delete (finalPayload as any).iss;\n delete (finalPayload as any).exp;\n delete (finalPayload as any).iat;\n\n const token = jwt.sign(finalPayload, this.secretRefresh, {\n ...this.jwtOptions,\n expiresIn: this.refreshTokenExpiry,\n });\n\n const response = this.decodeWithReadableDates(token);\n response.cookieConfig = {\n name: 'refresh_token',\n value: token,\n options: {\n ...this.cookieOptions,\n ...cookieOptions,\n }\n };\n\n return response;\n }\n\n /**\n * Verify a refresh token\n * @param token JWT token string\n * @returns Verified token response\n */\n verifyRefreshToken(token: string): TokenlyResponse {\n const decoded = jwt.verify(token, this.secretRefresh, this.verifyOptions) as TokenlyToken;\n return this.decodeWithReadableDates(token, decoded);\n }\n\n /**\n * Rotate access and refresh tokens\n * @param refreshToken Current refresh token\n * @param newPayload Optional new payload for the tokens\n * @returns New access and refresh tokens\n */\n rotateTokens(refreshToken: string): {\n accessToken: TokenlyResponse;\n refreshToken: TokenlyResponse;\n } {\n if (!refreshToken || typeof refreshToken !== 'string') {\n throwError(ErrorCode.INVALID_TOKEN, 'Invalid refresh token format');\n }\n\n const verified = this.verifyRefreshToken(refreshToken);\n const { iat, exp, aud, iss, ...payload } = verified.payload;\n\n const tokenId = refreshToken;\n const currentCount = this.rotationCounts.get(tokenId) || 0;\n\n if (currentCount >= (this.rotationConfig.maxRotationCount || 2)) {\n throwError(ErrorCode.MAX_ROTATION_EXCEEDED);\n }\n\n this.rotationCounts.set(tokenId, currentCount + 1);\n\n // Aseguramos que el payload tenga el formato correcto\n const newPayload = {\n ...payload,\n iat: Math.floor(Date.now() / 1000)\n };\n\n return {\n accessToken: this.generateAccessToken(newPayload),\n refreshToken: this.generateRefreshToken(newPayload)\n };\n }\n\n /**\n * Store a token\n * @param token Token string to store\n */\n setToken(token: string): void {\n this.currentToken = token;\n }\n\n /**\n * Retrieve the stored token\n * @returns The stored token or null if none exists\n */\n getToken(): string | null {\n return this.currentToken;\n }\n\n /**\n * Clear the stored token\n */\n clearToken(): void {\n this.currentToken = null;\n }\n\n /**\n * Helper para verificar si un token está próximo a expirar\n * @param token Token a verificar\n * @param thresholdMinutes Minutos antes de la expiración para considerar como \"próximo a expirar\"\n */\n public isTokenExpiringSoon(token: string, thresholdMinutes: number = 5): boolean {\n try {\n const decoded = jwt.decode(token) as jwt.JwtPayload;\n if (!decoded || !decoded.exp) return false;\n\n const expirationTime = decoded.exp * 1000;\n const currentTime = Date.now();\n const timeUntilExpiry = expirationTime - currentTime;\n\n return timeUntilExpiry < (thresholdMinutes * 60 * 1000);\n } catch {\n return false;\n }\n }\n\n /**\n * Helper para obtener información del token de forma segura\n * @param token Token a decodificar\n */\n public getTokenInfo(token: string): TokenInfo | null {\n try {\n const decoded = jwt.decode(token) as jwt.JwtPayload;\n if (!decoded) return null;\n\n return {\n userId: decoded.userId as string,\n expiresAt: new Date(decoded.exp! * 1000),\n issuedAt: new Date(decoded.iat! * 1000),\n fingerprint: decoded.fingerprint as string | undefined\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Validar un token sin verificar la firma (útil para pre-validaciones)\n * @param token Token a validar\n */\n public validateTokenFormat(token: string): boolean {\n try {\n const parts = token.split('.');\n if (parts.length !== 3) return false;\n\n return parts.every(part => {\n try {\n Buffer.from(part, 'base64').toString();\n return true;\n } catch {\n return false;\n }\n });\n } catch {\n return false;\n }\n }\n\n /**\n * Generar un token temporal de un solo uso\n * @param purpose Propósito del token\n * @param expiresIn Tiempo de expiración\n */\n public generateOneTimeToken(purpose: string, expiresIn: string = '5m'): string {\n const payload = {\n purpose,\n nonce: crypto.randomBytes(16).toString('hex'),\n iat: Math.floor(Date.now() / 1000)\n };\n\n return jwt.sign(payload, this.secretAccess, { expiresIn });\n }\n\n /**\n * Validar un refresh token con verificaciones adicionales de seguridad\n */\n public verifyRefreshTokenEnhanced(token: string): TokenlyResponse {\n if (!this.validateTokenFormat(token)) {\n throwError(ErrorCode.INVALID_TOKEN, 'Invalid token format');\n }\n\n const verified = this.verifyRefreshToken(token);\n\n if (this.isTokenExpiringSoon(token, 60)) {\n throwError(ErrorCode.TOKEN_EXPIRED, 'Refresh token is about to expire');\n }\n\n return verified;\n }\n\n public on(event: string, callback: Function): void {\n if (!this.eventListeners.has(event)) {\n this.eventListeners.set(event, []);\n }\n this.eventListeners.get(event)?.push(callback);\n }\n\n private emit(event: string, data: any): void {\n const listeners = this.eventListeners.get(event);\n if (listeners?.length) {\n listeners.forEach(callback => {\n try {\n callback(data);\n } catch (error) {\n console.error('Error executing event listener:', error);\n }\n });\n }\n }\n\n private cacheToken(key: string, value: TokenlyResponse): void {\n this.tokenCache.set(key, value);\n\n setTimeout(() => {\n this.tokenCache.delete(key);\n }, 5 * 60 * 1000);\n }\n\n public analyzeTokenSecurity(token: string): TokenSecurityAnalysis {\n const decoded = jwt.decode(token, { complete: true }) as {\n header: { alg: string },\n payload: TokenlyToken & { fingerprint?: string }\n };\n if (!decoded) throw new Error('Invalid token');\n\n return {\n algorithm: decoded.header.alg,\n hasFingerprint: !!decoded.payload.fingerprint,\n expirationTime: new Date(decoded.payload.exp * 1000),\n issuedAt: new Date(decoded.payload.iat * 1000),\n timeUntilExpiry: (decoded.payload.exp * 1000) - Date.now(),\n strength: this.calculateTokenStrength(decoded)\n };\n }\n\n private calculateTokenStrength(decodedToken: any): 'weak' | 'medium' | 'strong' {\n let score = 0;\n\n if (decodedToken.header.alg === 'HS512') score += 2;\n else if (decodedToken.header.alg === 'HS256') score += 1;\n\n if (decodedToken.payload.fingerprint) score += 2;\n\n const timeUntilExpiry = (decodedToken.payload.exp * 1000) - Date.now();\n if (timeUntilExpiry < 15 * 60 * 1000) score += 1;\n else if (timeUntilExpiry < 60 * 60 * 1000) score += 2;\n\n return score <= 2 ? 'weak' : score <= 4 ? 'medium' : 'strong';\n }\n\n public enableAutoRotation(options: AutoRotationOptions = {}): NodeJS.Timeout {\n console.log('Enabling auto rotation...');\n const {\n checkInterval = 50,\n rotateBeforeExpiry = 1000\n } = options;\n\n if (this.autoRotationInterval) {\n clearInterval(this.autoRotationInterval);\n }\n\n this.checkTokensExpiration(rotateBeforeExpiry);\n\n this.autoRotationInterval = setInterval(() => {\n this.checkTokensExpiration(rotateBeforeExpiry);\n }, checkInterval);\n\n return this.autoRotationInterval;\n }\n\n public disableAutoRotation(): void {\n if (this.autoRotationInterval) {\n clearInterval(this.autoRotationInterval);\n this.autoRotationInterval = null;\n }\n }\n\n private checkTokensExpiration(rotateBeforeExpiry: number): void {\n Array.from(this.tokenCache.entries()).forEach(([token, _]) => {\n try {\n const decoded = jwt.decode(token) as jwt.JwtPayload;\n if (decoded?.exp) {\n const timeUntilExpiry = (decoded.exp * 1000) - Date.now();\n if (timeUntilExpiry < rotateBeforeExpiry) {\n this.emit('tokenExpiring', {\n token,\n userId: decoded.userId,\n expiresIn: timeUntilExpiry\n });\n }\n }\n } catch (error) {\n console.error('Error checking token expiration:', error);\n }\n });\n }\n\n public enableAutoCleanup(interval: number = 3600000): void {\n setInterval(() => {\n const now = Date.now();\n this.revokedTokens.forEach(token => {\n try {\n const decoded = jwt.decode(token) as jwt.JwtPayload;\n if (decoded && decoded.exp && decoded.exp * 1000 < now) {\n this.revokedTokens.delete(token);\n }\n } catch {\n this.revokedTokens.delete(token);\n }\n });\n }, interval);\n }\n\n private handleDeviceStorage(userId: string, fingerprint: string): void {\n if (!this.deviceTokens.has(userId)) {\n this.deviceTokens.set(userId, new Set());\n }\n\n const userDevices = this.deviceTokens.get(userId)!;\n const deviceKey = `${userId}:${fingerprint}`;\n\n if (!this.fingerprintCache.has(deviceKey)) {\n if (userDevices.size >= this.securityConfig.maxDevices) {\n throwError(ErrorCode.MAX_DEVICES_REACHED, {\n userId,\n currentDevices: userDevices.size,\n maxDevices: this.securityConfig.maxDevices\n });\n }\n this.fingerprintCache.set(deviceKey, fingerprint);\n }\n\n userDevices.add(fingerprint);\n }\n}\n\ninterface TokenInfo {\n userId: string;\n expiresAt: Date;\n issuedAt: Date;\n fingerprint?: string;\n}\n\ninterface TokenSecurityAnalysis {\n algorithm: string;\n hasFingerprint: boolean;\n expirationTime: Date;\n issuedAt: Date;\n timeUntilExpiry: number;\n strength: 'weak' | 'medium' | 'strong';\n}\n\ninterface AutoRotationOptions {\n checkInterval?: number;\n rotateBeforeExpiry?: number;\n}","import { Headers } from '../types.js';\n\n/**\n * Helper function to get the real client IP from various headers\n * @param headers Object containing HTTP headers\n * @param defaultIP Optional default IP if no headers found\n * @returns string Client IP address\n */\nexport function getClientIP(headers: Headers, defaultIP: string = '0.0.0.0'): string {\n if (!headers || typeof headers !== 'object') {\n return defaultIP;\n }\n\n const realIP = headers['x-real-ip'];\n if (typeof realIP === 'string' && realIP.trim()) {\n return realIP.trim();\n }\n\n const forwardedFor = headers['x-forwarded-for'];\n if (typeof forwardedFor === 'string' && forwardedFor.trim()) {\n const ips = forwardedFor.split(',');\n return ips[0].trim() || defaultIP;\n }\n\n return defaultIP;\n}"],"names":["ErrorCode","TokenlyError","Error","constructor","code","message","details","super","this","name","ErrorMessages","INVALID_TOKEN","TOKEN_EXPIRED","TOKEN_REVOKED","INVALID_FINGERPRINT","MAX_DEVICES_REACHED","MAX_ROTATION_EXCEEDED","INVALID_PAYLOAD","EMPTY_PAYLOAD","MISSING_USER_ID","INVALID_USER_ID","INVALID_CONTEXT","MISSING_ENV_VAR","throwError","config","currentToken","blacklistedTokens","Set","deviceTokens","Map","rotationCounts","revokedTokens","autoRotationInterval","fingerprintCache","instanceId","crypto","randomBytes","toString","process","env","JWT_SECRET_ACCESS","JWT_SECRET_REFRESH","console","warn","secretAccess","generateSecret","secretRefresh","accessTokenExpiry","ACCESS_TOKEN_EXPIRY","refreshTokenExpiry","REFRESH_TOKEN_EXPIRY","cookieOptions","httpOnly","secure","NODE_ENV","sameSite","path","maxAge","jwtOptions","algorithm","issuer","audience","verifyOptions","algorithms","clockTolerance","rotationConfig","enableAutoRotation","rotationInterval","maxRotationCount","securityConfig","enableFingerprint","enableBlacklist","maxDevices","revokeOnSecurityBreach","eventListeners","tokenCache","type","createHash","update","Date","now","digest","formatDate","timestamp","toISOString","decodeWithReadableDates","token","decoded","jwt","decode","iat","exp","payloadWithoutDates","raw","payload","undefined","generateFingerprint","context","userAgent","trim","ip","normalizedUA","toLowerCase","replace","normalizedIP","combinedData","revokeToken","add","emit","userId","error","isTokenBlacklisted","has","validatePayload","Object","keys","length","prototype","hasOwnProperty","call","entries","forEach","key","value","JSON","stringify","generateAccessToken","options","finalPayload","fingerprint","handleDeviceStorage","sign","expiresIn","response","cacheToken","verifyAccessToken","verified","verify","ignoreExpiration","currentFingerprint","generateRefreshToken","aud","iss","cookieConfig","verifyRefreshToken","rotateTokens","refreshToken","tokenId","currentCount","get","set","newPayload","Math","floor","accessToken","setToken","getToken","clearToken","isTokenExpiringSoon","thresholdMinutes","expirationTime","currentTime","getTokenInfo","expiresAt","issuedAt","validateTokenFormat","parts","split","every","part","Buffer","from","generateOneTimeToken","purpose","nonce","verifyRefreshTokenEnhanced","on","event","callback","push","data","listeners","setTimeout","delete","analyzeTokenSecurity","complete","header","alg","hasFingerprint","timeUntilExpiry","strength","calculateTokenStrength","decodedToken","score","log","checkInterval","rotateBeforeExpiry","clearInterval","checkTokensExpiration","setInterval","disableAutoRotation","Array","_","enableAutoCleanup","interval","userDevices","deviceKey","size","currentDevices","headers","defaultIP","realIP","forwardedFor"],"mappings":"iBAAYA,iDAAZ,SAAYA,GACVA,EAAA,cAAA,gBACAA,EAAA,cAAA,gBACAA,EAAA,cAAA,gBACAA,EAAA,oBAAA,sBACAA,EAAA,oBAAA,sBACAA,EAAA,sBAAA,wBAEAA,EAAA,gBAAA,kBACAA,EAAA,cAAA,gBACAA,EAAA,gBAAA,kBACAA,EAAA,gBAAA,kBAEAA,EAAA,gBAAA,kBAEAA,EAAA,gBAAA,iBACD,CAhBD,CAAYA,IAAAA,EAgBX,CAAA,IAEK,MAAOC,UAAqBC,MAChC,WAAAC,CACSC,EACPC,EACOC,GAEPC,MAAMF,GAJCG,KAAIJ,KAAJA,EAEAI,KAAOF,QAAPA,EAGPE,KAAKC,KAAO,gBAIT,MAAMC,EAAgB,CAC3B,CAACV,EAAUW,eAAgB,oCAC3B,CAACX,EAAUY,eAAgB,oBAC3B,CAACZ,EAAUa,eAAgB,yBAC3B,CAACb,EAAUc,qBAAsB,4BACjC,CAACd,EAAUe,qBAAsB,oCACjC,CAACf,EAAUgB,uBAAwB,kCACnC,CAAChB,EAAUiB,iBAAkB,yBAC7B,CAACjB,EAAUkB,eAAgB,0BAC3B,CAAClB,EAAUmB,iBAAkB,gCAC7B,CAACnB,EAAUoB,iBAAkB,iCAC7B,CAACpB,EAAUqB,iBAAkB,kCAC7B,CAACrB,EAAUsB,iBAAkB,yCAGf,SAAAC,EAAWnB,EAAiBE,GAC1C,MAAM,IAAIL,EAAaG,EAAMM,EAAcN,GAAOE,EACpD,uBC0CE,WAAAH,CAAYqB,GAjBJhB,KAAYiB,aAAkB,KAC9BjB,KAAAkB,kBAAiC,IAAIC,IAGrCnB,KAAAoB,aAAyC,IAAIC,IAC7CrB,KAAAsB,eAAsC,IAAID,IAC1CrB,KAAAuB,cAA6B,IAAIJ,IAGjCnB,KAAoBwB,qBAA0B,KAC9CxB,KAAAyB,iBAAwC,IAAIJ,IAQlDrB,KAAK0B,WAAaC,EAAOC,YAAY,IAAIC,SAAS,OAE7CC,QAAQC,IAAIC,mBAAsBF,QAAQC,IAAIE,oBACjDC,QAAQC,KACN,qBACA,sOAEenC,KAAK0B,sCACpB,8EAIJ1B,KAAKoC,aAAeN,QAAQC,IAAIC,mBAAqBhC,KAAKqC,eAAe,UACzErC,KAAKsC,cAAgBR,QAAQC,IAAIE,oBAAsBjC,KAAKqC,eAAe,WAE3ErC,KAAKuC,kBAAoBvB,GAAQuB,mBAAqBT,QAAQC,IAAIS,qBAAuB,MACzFxC,KAAKyC,mBAAqBzB,GAAQyB,oBAAsBX,QAAQC,IAAIW,sBAAwB,KAE5F1C,KAAK2C,cAAgB,CACnBC,UAAU,EACVC,OAAiC,eAAzBf,QAAQC,IAAIe,SACpBC,SAAU,SACVC,KAAM,IACNC,OAAQ,UACLjC,GAAQ2B,eAGb3C,KAAKkD,WAAa,CAChBC,UAAW,QACXC,OAAQ,eACRC,SAAU,oBACPrC,GAAQkC,YAGblD,KAAKsD,cAAgB,CACnBC,WAAY,CAACvD,KAAKkD,WAAWC,WAC7BC,OAAQpD,KAAKkD,WAAWE,OACxBC,SAAUrD,KAAKkD,WAAWG,SAC1BG,eAAgB,IAGlBxD,KAAKyD,eAAiB,CACpBC,oBAAoB,EACpBC,iBAAkB,GAClBC,iBAAkB,OACf5C,GAAQyC,gBAGbzD,KAAK6D,eAAiB,CACpBC,mBAAmB,EACnBC,iBAAiB,EACjBC,WAAY,EACZC,wBAAwB,KACrBjD,GAAQ6C,gBAGb7D,KAAKkE,eAAiB,IAAI7C,IAC1BrB,KAAKmE,WAAa,IAAI9C,IAGhB,cAAAgB,CAAe+B,GACrB,OAAOzC,EACJ0C,WAAW,UACXC,OAAO,GAAGtE,KAAK0B,cAAc0C,KAAQG,KAAKC,SAC1CC,OAAO,OAQJ,UAAAC,CAAWC,GACjB,OAAO,IAAIJ,KAAiB,IAAZI,GAAkBC,cAU5B,uBAAAC,CACNC,EACAC,GAEKA,IACHA,EAAUC,EAAIC,OAAOH,IAGvB,MAAMI,IAAEA,EAAGC,IAAEA,KAAQC,GAAwBL,EAW7C,MATgC,CAC9BM,IAAKP,EACLQ,QAAS,IACJF,EACHF,IAAKA,EAAMlF,KAAK0E,WAAWQ,QAAOK,EAClCJ,IAAKA,EAAMnF,KAAK0E,WAAWS,QAAOI,IAUhC,mBAAAC,CAAoBC,GACrBA,GAASC,WAAWC,QAAWF,GAASG,IAAID,QAC/C5E,EAAWvB,EAAUqB,gBAAiB,mCAGxC,MAAMgF,EAAeJ,EAAQC,UAC1BC,OACAG,cACAC,QAAQ,OAAQ,KAEbC,EAAeP,EAAQG,GAC1BD,OACAG,cACAC,QAAQ,WAAY,IAYjBE,EAAe,MAVNtE,EACZ0C,WAAW,UACXC,OAAO,MAAMtE,KAAK0B,cAAcmE,KAChCpB,OAAO,aAEK9C,EACZ0C,WAAW,UACXC,OAAO,MAAMtE,KAAK0B,cAAcsE,KAChCvB,OAAO,SAIV,OAAO9C,EACJ0C,WAAW,UACXC,OAAO2B,GACPxB,OAAO,OAML,WAAAyB,CAAYpB,GACjB,GAAKA,EAEL,IACE,MAAMC,EAAUC,EAAIC,OAAOH,GAC3B9E,KAAKuB,cAAc4E,IAAIrB,GAEvB9E,KAAKoG,KAAK,eAAgB,CACxBtB,QACAuB,OAAQtB,GAASsB,OACjB1B,UAAWJ,KAAKC,QAElB,MAAO8B,GACPpE,QAAQoE,MAAM,0BAA2BA,IAOrC,kBAAAC,CAAmBzB,GACzB,OAAO9E,KAAK6D,eAAeE,iBAAmB/D,KAAKkB,kBAAkBsF,IAAI1B,GAGnE,eAAA2B,CAAgBnB,GACN,OAAZA,GAAuC,iBAAZA,GAC7BvE,EAAWvB,EAAUiB,iBAGa,IAAhCiG,OAAOC,KAAKrB,GAASsB,QACvB7F,EAAWvB,EAAUkB,eAGlBgG,OAAOG,UAAUC,eAAeC,KAAKzB,EAAS,WACjDvE,EAAWvB,EAAUmB,iBAGA,OAAnB2E,EAAQe,aAAsCd,IAAnBD,EAAQe,QACrCtF,EAAWvB,EAAUoB,iBAGO,iBAAnB0E,EAAQe,QAAwBf,EAAQe,OAAOV,QACxD5E,EAAWvB,EAAUoB,iBAGvB8F,OAAOM,QAAQ1B,GAAS2B,SAAQ,EAAEC,EAAKC,MACjCA,SACFpG,EAAWvB,EAAUiB,gBAAiB,qBAAqByG,qCAI3CE,KAAKC,UAAU/B,GAASsB,OAC1B,MAChB7F,EAAWvB,EAAUiB,gBAAiB,6CAU1C,mBAAA6G,CACEhC,EACAiC,EACA9B,GAEAzF,KAAKyG,gBAAgBnB,GACrB,MAAMkC,EAAuC,IAAKlC,GAElD,GAAItF,KAAK6D,eAAeC,mBAAqB2B,EAAS,CACpD,MAAMgC,EAAczH,KAAKwF,oBAAoBC,GACvCY,EAAUf,EAAgBe,OAChCrG,KAAK0H,oBAAoBrB,EAAQoB,GACjCD,EAAaC,YAAcA,EAG7B,MAAM3C,EAAQE,EAAI2C,KAAKH,EAAcxH,KAAKoC,aAAc,IACnDpC,KAAKkD,cACLqE,EACHK,UAAW5H,KAAKuC,oBAGZsF,EAAW7H,KAAK6E,wBAAwBC,GAE9C,OADA9E,KAAK8H,WAAWhD,EAAO+C,GAChBA,EAQF,iBAAAE,CACLjD,EACAW,GAEIzF,KAAKuB,cAAciF,IAAI1B,IACzB/D,EAAWvB,EAAUa,eAGnBL,KAAKuG,mBAAmBzB,IAC1B/D,EAAWvB,EAAUa,cAAe,wBAGtC,IACE,MAAM2H,EAAWhD,EAAIiD,OAAOnD,EAAO9E,KAAKoC,aAAc,IACjDpC,KAAKsD,cACR4E,kBAAkB,EAClB1E,eAAgB,IAGlB,GAAIxD,KAAK6D,eAAeC,mBAAqB2B,EAAS,CACpD,MAAM0C,EAAqBnI,KAAKwF,oBAAoBC,GAChDuC,EAASP,aAAeO,EAASP,cAAgBU,GACnDpH,EAAWvB,EAAUc,qBAIzB,MAAMuH,EAAW7H,KAAK6E,wBAAwBC,EAAOkD,GAErD,OADAhI,KAAK8H,WAAWhD,EAAO+C,GAChBA,EACP,MAAOvB,GACP,GAAIA,aAAiB7G,EAAc,MAAM6G,EAQzC,KANmB,sBAAfA,EAAMrG,MACRc,EAAWvB,EAAUY,eAEJ,sBAAfkG,EAAMrG,MACRc,EAAWvB,EAAUW,eAEjBmG,GAUV,oBAAA8B,CACE9C,EACA3C,GAEA3C,KAAKyG,gBAAgBnB,GACrB,MAAMkC,EAAuC,IAAKlC,UAE1CkC,EAAqBa,WACrBb,EAAqBc,WACrBd,EAAqBrC,WACrBqC,EAAqBtC,IAE7B,MAAMJ,EAAQE,EAAI2C,KAAKH,EAAcxH,KAAKsC,cAAe,IACpDtC,KAAKkD,WACR0E,UAAW5H,KAAKyC,qBAGZoF,EAAW7H,KAAK6E,wBAAwBC,GAU9C,OATA+C,EAASU,aAAe,CACtBtI,KAAM,gBACNkH,MAAOrC,EACPyC,QAAS,IACJvH,KAAK2C,iBACLA,IAIAkF,EAQT,kBAAAW,CAAmB1D,GACjB,MAAMC,EAAUC,EAAIiD,OAAOnD,EAAO9E,KAAKsC,cAAetC,KAAKsD,eAC3D,OAAOtD,KAAK6E,wBAAwBC,EAAOC,GAS7C,YAAA0D,CAAaC,GAINA,GAAwC,iBAAjBA,GAC1B3H,EAAWvB,EAAUW,cAAe,gCAGtC,MAAM6H,EAAWhI,KAAKwI,mBAAmBE,IACnCxD,IAAEA,EAAGC,IAAEA,EAAGkD,IAAEA,EAAGC,IAAEA,KAAQhD,GAAY0C,EAAS1C,QAE9CqD,EAAUD,EACVE,EAAe5I,KAAKsB,eAAeuH,IAAIF,IAAY,EAErDC,IAAiB5I,KAAKyD,eAAeG,kBAAoB,IAC3D7C,EAAWvB,EAAUgB,uBAGvBR,KAAKsB,eAAewH,IAAIH,EAASC,EAAe,GAGhD,MAAMG,EAAa,IACdzD,EACHJ,IAAK8D,KAAKC,MAAM1E,KAAKC,MAAQ,MAG/B,MAAO,CACL0E,YAAalJ,KAAKsH,oBAAoByB,GACtCL,aAAc1I,KAAKoI,qBAAqBW,IAQ5C,QAAAI,CAASrE,GACP9E,KAAKiB,aAAe6D,EAOtB,QAAAsE,GACE,OAAOpJ,KAAKiB,aAMd,UAAAoI,GACErJ,KAAKiB,aAAe,KAQf,mBAAAqI,CAAoBxE,EAAeyE,EAA2B,GACnE,IACE,MAAMxE,EAAUC,EAAIC,OAAOH,GAC3B,IAAKC,IAAYA,EAAQI,IAAK,OAAO,EAErC,MAAMqE,EAA+B,IAAdzE,EAAQI,IACzBsE,EAAclF,KAAKC,MAGzB,OAFwBgF,EAAiBC,EAEI,GAAnBF,EAAwB,IAClD,MACA,OAAO,GAQJ,YAAAG,CAAa5E,GAClB,IACE,MAAMC,EAAUC,EAAIC,OAAOH,GAC3B,OAAKC,EAEE,CACLsB,OAAQtB,EAAQsB,OAChBsD,UAAW,IAAIpF,KAAoB,IAAfQ,EAAQI,KAC5ByE,SAAU,IAAIrF,KAAoB,IAAfQ,EAAQG,KAC3BuC,YAAa1C,EAAQ0C,aANF,KAQrB,MACA,OAAO,MAQJ,mBAAAoC,CAAoB/E,GACzB,IACE,MAAMgF,EAAQhF,EAAMiF,MAAM,KAC1B,OAAqB,IAAjBD,EAAMlD,QAEHkD,EAAME,OAAMC,IACjB,IAEE,OADAC,OAAOC,KAAKF,EAAM,UAAUpI,YACrB,EACP,MACA,OAAO,MAGX,MACA,OAAO,GASJ,oBAAAuI,CAAqBC,EAAiBzC,EAAoB,MAC/D,MAAMtC,EAAU,CACd+E,UACAC,MAAO3I,EAAOC,YAAY,IAAIC,SAAS,OACvCqD,IAAK8D,KAAKC,MAAM1E,KAAKC,MAAQ,MAG/B,OAAOQ,EAAI2C,KAAKrC,EAAStF,KAAKoC,aAAc,CAAEwF,cAMzC,0BAAA2C,CAA2BzF,GAC3B9E,KAAK6J,oBAAoB/E,IAC5B/D,EAAWvB,EAAUW,cAAe,wBAGtC,MAAM6H,EAAWhI,KAAKwI,mBAAmB1D,GAMzC,OAJI9E,KAAKsJ,oBAAoBxE,EAAO,KAClC/D,EAAWvB,EAAUY,cAAe,oCAG/B4H,EAGF,EAAAwC,CAAGC,EAAeC,GAClB1K,KAAKkE,eAAesC,IAAIiE,IAC3BzK,KAAKkE,eAAe4E,IAAI2B,EAAO,IAEjCzK,KAAKkE,eAAe2E,IAAI4B,IAAQE,KAAKD,GAG/B,IAAAtE,CAAKqE,EAAeG,GAC1B,MAAMC,EAAY7K,KAAKkE,eAAe2E,IAAI4B,GACtCI,GAAWjE,QACbiE,EAAU5D,SAAQyD,IAChB,IACEA,EAASE,GACT,MAAOtE,GACPpE,QAAQoE,MAAM,kCAAmCA,OAMjD,UAAAwB,CAAWZ,EAAaC,GAC9BnH,KAAKmE,WAAW2E,IAAI5B,EAAKC,GAEzB2D,YAAW,KACT9K,KAAKmE,WAAW4G,OAAO7D,EAAI,GAC1B,KAGE,oBAAA8D,CAAqBlG,GAC1B,MAAMC,EAAUC,EAAIC,OAAOH,EAAO,CAAEmG,UAAU,IAI9C,IAAKlG,EAAS,MAAM,IAAIrF,MAAM,iBAE9B,MAAO,CACLyD,UAAW4B,EAAQmG,OAAOC,IAC1BC,iBAAkBrG,EAAQO,QAAQmC,YAClC+B,eAAgB,IAAIjF,KAA2B,IAAtBQ,EAAQO,QAAQH,KACzCyE,SAAU,IAAIrF,KAA2B,IAAtBQ,EAAQO,QAAQJ,KACnCmG,gBAAwC,IAAtBtG,EAAQO,QAAQH,IAAcZ,KAAKC,MACrD8G,SAAUtL,KAAKuL,uBAAuBxG,IAIlC,sBAAAwG,CAAuBC,GAC7B,IAAIC,EAAQ,EAEoB,UAA5BD,EAAaN,OAAOC,IAAiBM,GAAS,EACb,UAA5BD,EAAaN,OAAOC,MAAiBM,GAAS,GAEnDD,EAAalG,QAAQmC,cAAagE,GAAS,GAE/C,MAAMJ,EAA8C,IAA3BG,EAAalG,QAAQH,IAAcZ,KAAKC,MAIjE,OAHI6G,EAAkB,IAAgBI,GAAS,EACtCJ,EAAkB,OAAgBI,GAAS,GAE7CA,GAAS,EAAI,OAASA,GAAS,EAAI,SAAW,SAGhD,kBAAA/H,CAAmB6D,EAA+B,IACvDrF,QAAQwJ,IAAI,6BACZ,MAAMC,cACJA,EAAgB,GAAEC,mBAClBA,EAAqB,KACnBrE,EAYJ,OAVIvH,KAAKwB,sBACPqK,cAAc7L,KAAKwB,sBAGrBxB,KAAK8L,sBAAsBF,GAE3B5L,KAAKwB,qBAAuBuK,aAAY,KACtC/L,KAAK8L,sBAAsBF,EAAmB,GAC7CD,GAEI3L,KAAKwB,qBAGP,mBAAAwK,GACDhM,KAAKwB,uBACPqK,cAAc7L,KAAKwB,sBACnBxB,KAAKwB,qBAAuB,MAIxB,qBAAAsK,CAAsBF,GAC5BK,MAAM9B,KAAKnK,KAAKmE,WAAW6C,WAAWC,SAAQ,EAAEnC,EAAOoH,MACrD,IACE,MAAMnH,EAAUC,EAAIC,OAAOH,GAC3B,GAAIC,GAASI,IAAK,CAChB,MAAMkG,EAAiC,IAAdtG,EAAQI,IAAcZ,KAAKC,MAChD6G,EAAkBO,GACpB5L,KAAKoG,KAAK,gBAAiB,CACzBtB,QACAuB,OAAQtB,EAAQsB,OAChBuB,UAAWyD,KAIjB,MAAO/E,GACPpE,QAAQoE,MAAM,mCAAoCA,OAKjD,iBAAA6F,CAAkBC,EAAmB,MAC1CL,aAAY,KACV,MAAMvH,EAAMD,KAAKC,MACjBxE,KAAKuB,cAAc0F,SAAQnC,IACzB,IACE,MAAMC,EAAUC,EAAIC,OAAOH,GACvBC,GAAWA,EAAQI,KAAqB,IAAdJ,EAAQI,IAAaX,GACjDxE,KAAKuB,cAAcwJ,OAAOjG,GAE5B,MACA9E,KAAKuB,cAAcwJ,OAAOjG,MAE5B,GACDsH,GAGG,mBAAA1E,CAAoBrB,EAAgBoB,GACrCzH,KAAKoB,aAAaoF,IAAIH,IACzBrG,KAAKoB,aAAa0H,IAAIzC,EAAQ,IAAIlF,KAGpC,MAAMkL,EAAcrM,KAAKoB,aAAayH,IAAIxC,GACpCiG,EAAY,GAAGjG,KAAUoB,IAE1BzH,KAAKyB,iBAAiB+E,IAAI8F,KACzBD,EAAYE,MAAQvM,KAAK6D,eAAeG,YAC1CjD,EAAWvB,EAAUe,oBAAqB,CACxC8F,SACAmG,eAAgBH,EAAYE,KAC5BvI,WAAYhE,KAAK6D,eAAeG,aAGpChE,KAAKyB,iBAAiBqH,IAAIwD,EAAW7E,IAGvC4E,EAAYlG,IAAIsB,kCChsBQgF,EAAkBC,EAAoB,WAC9D,IAAKD,GAA8B,iBAAZA,EACnB,OAAOC,EAGX,MAAMC,EAASF,EAAQ,aACvB,GAAsB,iBAAXE,GAAuBA,EAAOhH,OACrC,OAAOgH,EAAOhH,OAGlB,MAAMiH,EAAeH,EAAQ,mBAC7B,GAA4B,iBAAjBG,GAA6BA,EAAajH,OAAQ,CAEzD,OADYiH,EAAa7C,MAAM,KACpB,GAAGpE,QAAU+G,EAG5B,OAAOA,CACX"}