UNPKG

userdo

Version:

A Durable Object base class for building applications on Cloudflare Workers.

179 lines (178 loc) 6.04 kB
import jwt from '@tsndr/cloudflare-worker-jwt'; /** * Simple JWT decoding without verification * @param token - JWT token to decode * @returns Decoded payload or null if invalid */ export function decodeJWT(token) { try { const parts = token.split('.'); if (parts.length !== 3) return null; const payload = JSON.parse(atob(parts[1])); return payload; } catch { return null; } } /** * JWT verification with secret - extracted from UserDO implementation * @param token - JWT token to verify * @param secret - JWT secret for verification * @returns Verification result with payload if valid */ export async function verifyJWT(token, secret) { try { const isValid = await jwt.verify(token, secret); if (!isValid) { return { ok: false, error: 'Invalid token' }; } const decoded = jwt.decode(token); if (!decoded || !decoded.payload) { return { ok: false, error: 'Invalid token payload' }; } return { ok: true, payload: decoded.payload }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : 'Token verification failed' }; } } /** * Hash email for ID generation - matches UserDO internal implementation * @param email - Email to hash * @returns Promise resolving to hex hash string */ export async function hashEmailForId(email) { const encoder = new TextEncoder(); const data = encoder.encode(email.toLowerCase()); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = new Uint8Array(hashBuffer); const hashHex = Array.from(hashArray) .map(b => b.toString(16).padStart(2, '0')) .join(''); return hashHex; } /** * Check if token is expired * @param payload - JWT payload * @returns true if token is expired */ export function isTokenExpired(payload) { if (!payload.exp) return false; return payload.exp < Math.floor(Date.now() / 1000); } /** * Extract email from token * @param token - JWT token * @returns Email string or null if not found */ export function getEmailFromToken(token) { const payload = decodeJWT(token); return payload?.email?.toLowerCase() || null; } /** * Sign a JWT token with the given payload and secret * @param payload - JWT payload * @param secret - JWT secret * @returns Promise resolving to signed token */ export async function signJWT(payload, secret) { return await jwt.sign(payload, secret); } /** * Generate a UserDO-compatible access token * @param userId - User ID * @param email - User email * @param secret - JWT secret * @param expiresInMinutes - Token expiration in minutes (default: 15) * @returns Promise resolving to signed access token */ export async function generateAccessToken(userId, email, secret, expiresInMinutes = 15) { const exp = Math.floor(Date.now() / 1000) + expiresInMinutes * 60; return await signJWT({ sub: userId, email: email.toLowerCase(), exp }, secret); } /** * Generate a UserDO-compatible refresh token * @param userId - User ID * @param secret - JWT secret * @param expiresInDays - Token expiration in days (default: 7) * @returns Promise resolving to signed refresh token */ export async function generateRefreshToken(userId, secret, expiresInDays = 7) { const exp = Math.floor(Date.now() / 1000) + expiresInDays * 24 * 60 * 60; return await signJWT({ sub: userId, type: 'refresh', exp }, secret); } /** * Verify tokens with automatic refresh - decodes email from expired access token * @param token - Current access token (may be expired) * @param refreshToken - Refresh token (optional) * @param secret - JWT secret * @returns Verification result with payload and new token if refreshed */ export async function verifyTokens(token, refreshToken, secret) { // Try current token first const result = await verifyJWT(token, secret); if (result.ok && result.payload) { return { ok: true, payload: result.payload }; } // If token failed and we have refresh token, try refresh if (refreshToken) { try { // Decode (but don't verify) the expired access token to get email const decodedToken = decodeJWT(token); const email = decodedToken?.email; if (!email) { return { ok: false, error: 'Cannot decode email from access token' }; } const refreshResult = await verifyJWT(refreshToken, secret); if (refreshResult.ok && refreshResult.payload) { // Ensure it's a refresh token if (refreshResult.payload.type !== 'refresh') { return { ok: false, error: 'Invalid refresh token type' }; } const userId = refreshResult.payload.sub; // Generate new access token using decoded email const newToken = await generateAccessToken(userId, email, secret); return { ok: true, payload: { sub: userId, email }, newToken }; } } catch (e) { return { ok: false, error: 'Token refresh failed' }; } } return { ok: false, error: 'Token verification failed' }; } /** * Generate a UserDO-compatible password reset token * @param userId - User ID * @param email - User email * @param secret - JWT secret * @param expiresInMinutes - Token expiration in minutes (default: 60) * @returns Promise resolving to signed password reset token */ export async function generatePasswordResetToken(userId, email, secret, expiresInMinutes = 60) { const exp = Math.floor(Date.now() / 1000) + expiresInMinutes * 60; return await signJWT({ sub: userId, email: email.toLowerCase(), type: 'password_reset', exp }, secret); }