← vibesdk
import { jwtVerify, SignJWT } from 'jose';
import { TokenPayload } from '../types/auth-types';
import { SecurityError, SecurityErrorType } from 'shared/types/errors';
import { createLogger } from '../logger';
import { SessionService } from 'worker/database/services/SessionService';
const logger = createLogger('JWTUtils');
export class JWTUtils {
private static instance: JWTUtils | null = null;
private jwtSecret: Uint8Array;
private readonly algorithm = 'HS256';
private constructor(jwtSecret: string) {
this.validateJWTSecret(jwtSecret);
this.jwtSecret = new TextEncoder().encode(jwtSecret);
}
static getInstance(env: { JWT_SECRET: string }): JWTUtils {
if (!env.JWT_SECRET) {
throw new Error('JWT_SECRET not configured');
}
if (!JWTUtils.instance) {
JWTUtils.instance = new JWTUtils(env.JWT_SECRET);
}
return JWTUtils.instance;
}
private validateJWTSecret(secret: string): void {
if (secret.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters long for security');
}
const weakSecrets = ['default', 'secret', 'password', 'changeme', 'admin', 'test'];
if (weakSecrets.includes(secret.toLowerCase())) {
throw new Error('JWT_SECRET contains a weak/default value. Please use a cryptographically secure random string');
}
const hasLowercase = /[a-z]/.test(secret);
const hasUppercase = /[A-Z]/.test(secret);
const hasNumbers = /[0-9]/.test(secret);
const hasSpecial = /[^a-zA-Z0-9]/.test(secret);
const characterTypes = [hasLowercase, hasUppercase, hasNumbers, hasSpecial].filter(Boolean).length;
if (characterTypes < 3) {
throw new Error('JWT_SECRET must contain at least 3 different character types');
}
const hasRepeatingChars = /(.)\1{3,}/.test(secret);
if (hasRepeatingChars) {
throw new Error('JWT_SECRET contains repetitive patterns');
}
}
async signPayload(payload: Record<string, unknown>, expiresIn: number): Promise<string> {
const now = Math.floor(Date.now() / 1000);
return new SignJWT({ ...payload })
.setProtectedHeader({ alg: this.algorithm })
.setIssuedAt(now)
.setExpirationTime(now + expiresIn)
.sign(this.jwtSecret);
}
async verifyPayload(token: string): Promise<Record<string, unknown> | null> {
try {
const { payload } = await jwtVerify(token, this.jwtSecret, { algorithms: [this.algorithm] });
return payload as Record<string, unknown>;
} catch {
return null;
}
}
async createToken(payload: Omit<TokenPayload, 'iat' | 'exp'>, expiresIn: number = 24 * 3600): Promise<string> {
try {
return await this.signPayload(payload as Record<string, unknown>, expiresIn);
} catch (error) {
logger.error('Error creating token', error);
throw new SecurityError(
SecurityErrorType.INVALID_TOKEN,
'Failed to create token',
500
);
}
}
async verifyToken(token: string): Promise<TokenPayload | null> {
const payload = await this.verifyPayload(token);
if (!payload) return null;
if (!payload.sub || !payload.email || !payload.type || !payload.exp || !payload.iat) {
return null;
}
return {
sub: payload.sub as string,
email: payload.email as string,
type: payload.type as 'access' | 'refresh',
exp: payload.exp as number,
iat: payload.iat as number,
jti: payload.jti as string | undefined,
sessionId: payload.sessionId as string
};
}
// -- Convenience methods --
async createAccessToken(userId: string, email: string, sessionId: string): Promise<{
accessToken: string;
expiresIn: number;
}> {
const accessTokenExpiry = SessionService.config.sessionTTL;
const payload = { sub: userId, email, sessionId };
const accessToken = await this.createToken({
...payload,
type: 'access' as const,
}, accessTokenExpiry);
return { accessToken, expiresIn: accessTokenExpiry };
}
async hashToken(token: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(token);
const hash = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(hash)));
}
}