UNPKG

mya-cli

Version:

MYA - AI-Powered Stock & Options Analysis CLI Tool

326 lines 12.6 kB
/** * Module: Worker Authentication * Purpose: Handle authentication flows directly in the worker using Stytch for OTP delivery * Dependencies: jose (JWT), stytch (OTP email delivery), Cloudflare Workers API * Used by: worker.ts * * Auth Flows Handled Locally: * - POST /auth: Generate OTP and send via Stytch email * - POST /verify-otp: Verify OTP code and create JWT session * - POST /auth/verify: Validate JWT token * * Why Local Handling: * Authentication should not depend on backend availability * Worker is the auth boundary for the system * OTP and JWT can be generated and validated in the worker * Stytch handles secure email delivery */ import * as jose from 'jose'; import * as stytch from 'stytch'; /** * Create a Stytch client configured for Cloudflare Workers * Handles User-Agent issues that cause authentication failures in Workers */ function createStytchClient(env) { // Validate required environment variables if (!env.STYTCH_PROJECT_ID || !env.STYTCH_SECRET) { throw new Error('Missing Stytch project ID or secret in environment variables'); } // Create the base client const client = new stytch.Client({ project_id: env.STYTCH_PROJECT_ID, secret: env.STYTCH_SECRET, }); const clientWithRequest = client; const originalRequest = clientWithRequest.request; if (originalRequest) { clientWithRequest.request = async function (method, path, data, opts) { const options = { ...opts, headers: { ...(opts?.headers ?? {}), 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', 'X-Stytch-Client': 'stytch-js/browser', }, }; return originalRequest.call(this, method, path, data, options); }; } return client; } /** * Generate a 6-digit OTP code */ function generateOtpCode() { return Math.floor(100000 + Math.random() * 900000).toString(); } /** * Generate a JWT token for authenticated sessions */ async function generateSessionJwt(userId, env) { const secret = new TextEncoder().encode(env.JWT_SECRET); const token = await new jose.SignJWT({ userId, sub: userId, type: 'session', }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('24h') .sign(secret); return token; } /** * Store OTP in KV with expiration */ async function storeOtp(email, code, kv) { const methodId = `method_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const expiresAt = Date.now() + (15 * 60 * 1000); // 15 minutes const otpData = { email, code, timestamp: Date.now(), expiresAt, }; await kv.put(`otp:${methodId}`, JSON.stringify(otpData), { expirationTtl: 15 * 60, // 15 minutes }); return methodId; } /** * Retrieve and verify OTP */ async function verifyOtp(methodId, code, kv) { const otpJson = await kv.get(`otp:${methodId}`); if (!otpJson) { return { valid: false }; } const otpData = JSON.parse(otpJson); if (Date.now() > otpData.expiresAt) { await kv.delete(`otp:${methodId}`); return { valid: false }; } if (otpData.code !== code) { return { valid: false }; } // OTP is valid - delete it so it can't be reused await kv.delete(`otp:${methodId}`); return { valid: true, email: otpData.email }; } /** * Handle authentication request (POST /auth) * Generates OTP and sends it via Stytch email service */ export async function handleAuth(c, env) { try { const body = await c.req.json(); const email = body.email; if (!email || !email.includes('@')) { return c.json({ success: false, error: 'Valid email required' }, 400); } const kv = env.KV_NAMESPACE; if (!kv) { console.error('[AUTH] KV_NAMESPACE not configured in environment'); return c.json({ success: false, error: 'Auth service unavailable', details: 'KV_NAMESPACE binding is not configured. Ensure wrangler.toml has KV namespace binding and wrangler secret put JWT_SECRET was run.', }, 503); } // Initialize Stytch client let stytchClient; try { stytchClient = createStytchClient(env); } catch (error) { console.error('[AUTH] Failed to initialize Stytch client:', error); return c.json({ success: false, error: 'Email service unavailable', details: 'Stytch authentication service is not configured properly.', }, 503); } // Generate OTP code for local storage (in case we need it for verification) const otpCode = generateOtpCode(); try { // Send OTP via Stytch email const params = { email: email, expiration_minutes: 10, }; const response = await stytchClient.otps.email.loginOrCreate(params); if (!response || typeof response !== 'object') { throw new Error('Invalid response from Stytch'); } const stytchResponse = response; // Store OTP in KV for verification (using Stytch's email_id as methodId) const methodId = stytchResponse.email_id || `stytch_${Date.now()}`; const expiresAt = Date.now() + (10 * 60 * 1000); // 10 minutes to match Stytch expiration const otpData = { email, code: otpCode, // Store our generated code for fallback verification timestamp: Date.now(), expiresAt, }; await kv.put(`otp:${methodId}`, JSON.stringify(otpData), { expirationTtl: 10 * 60, // 10 minutes }); console.log(`[AUTH] OTP sent to ${email} via Stytch (methodId: ${methodId})`); return c.json({ success: true, methodId, email, message: 'OTP sent to email via Stytch', }); } catch (stytchError) { console.error('[AUTH] Stytch OTP send failed:', stytchError); // Fallback: Store OTP locally and log it (for development/testing) console.log(`[AUTH FALLBACK] OTP for ${email}: ${otpCode} (Stytch failed)`); const methodId = `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const expiresAt = Date.now() + (15 * 60 * 1000); // 15 minutes fallback const otpData = { email, code: otpCode, timestamp: Date.now(), expiresAt, }; await kv.put(`otp:${methodId}`, JSON.stringify(otpData), { expirationTtl: 15 * 60, // 15 minutes }); return c.json({ success: true, methodId, email, message: 'OTP sent to email (check console in dev mode - Stytch unavailable)', }); } } catch (error) { console.error('[AUTH ERROR]', error); return c.json({ success: false, error: 'Authentication failed', details: error instanceof Error ? error.message : String(error) }, 500); } } /** * Handle OTP verification (POST /verify-otp) * Verifies OTP code with Stytch and creates JWT session */ export async function handleVerifyOtp(c, env) { try { const body = await c.req.json(); const { email, otpCode, methodId } = body; if (!email || !otpCode || !methodId) { return c.json({ success: false, error: 'Email, OTP code, and method ID required' }, 400); } const kv = env.KV_NAMESPACE; if (!kv) { return c.json({ success: false, error: 'Auth service unavailable' }, 503); } // Initialize Stytch client let stytchClient; try { stytchClient = createStytchClient(env); } catch (error) { console.error('[VERIFY_OTP] Failed to initialize Stytch client:', error); // Fall back to local KV verification if Stytch is unavailable return await verifyOtpLocally(c, methodId, otpCode, email, env); } try { // Try Stytch verification first const params = { method_id: methodId, code: otpCode, session_duration_minutes: 60 * 24 * 30, // 30 days }; const response = await stytchClient.otps.authenticate(params); if (!response || typeof response !== 'object') { throw new Error('Invalid response from Stytch'); } const stytchResponse = response; // Clean up local OTP record if it exists await kv.delete(`otp:${methodId}`).catch(() => { }); // Generate our own JWT for the worker (using Stytch user_id) const userId = stytchResponse.user_id || `stytch_${Date.now()}`; const sessionJwt = await generateSessionJwt(userId, env); const machineId = `machine_${Math.random().toString(36).substr(2, 9)}`; const sessionToken = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; console.log(`[VERIFY_OTP] Stytch verification successful for ${email} (userId: ${userId})`); return c.json({ success: true, userId, machineId, sessionToken, sessionJwt, email, expiresAt: Date.now() + (24 * 60 * 60 * 1000), }); } catch (stytchError) { console.error('[VERIFY_OTP] Stytch verification failed:', stytchError); // Fall back to local KV verification console.log('[VERIFY_OTP] Falling back to local OTP verification'); return await verifyOtpLocally(c, methodId, otpCode, email, env); } } catch (error) { console.error('[VERIFY_OTP ERROR]', error); return c.json({ success: false, error: 'OTP verification failed' }, 500); } } /** * Fallback OTP verification using local KV storage * Used when Stytch is unavailable or for development */ async function verifyOtpLocally(c, methodId, otpCode, email, env) { const kv = env.KV_NAMESPACE; const { valid, email: storedEmail } = await verifyOtp(methodId, otpCode, kv); if (!valid || storedEmail !== email) { return c.json({ success: false, error: 'Invalid OTP code' }, 401); } // Generate session JWT const userId = `user_${email.split('@')[0]}_${Date.now()}`; const sessionJwt = await generateSessionJwt(userId, env); const machineId = `machine_${Math.random().toString(36).substr(2, 9)}`; const sessionToken = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; console.log(`[VERIFY_OTP] Local verification successful for ${email} (userId: ${userId})`); return c.json({ success: true, userId, machineId, sessionToken, sessionJwt, email, expiresAt: Date.now() + (24 * 60 * 60 * 1000), }); } /** * Handle token verification (POST /auth/verify) * Validates JWT token from Authorization header */ export async function handleAuthVerify(c, env) { try { const authHeader = c.req.header('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return c.json({ valid: false, error: 'Missing or invalid Authorization header' }, 401); } const token = authHeader.substring(7); try { const secret = new TextEncoder().encode(env.JWT_SECRET); const verified = await jose.jwtVerify(token, secret); const userId = (verified.payload.userId || verified.payload.sub); return c.json({ valid: true, userId, }); } catch { return c.json({ valid: false, error: 'Token verification failed' }, 401); } } catch (error) { console.error('[AUTH_VERIFY ERROR]', error); return c.json({ valid: false, error: 'Token verification failed' }, 500); } } //# sourceMappingURL=auth.js.map