UNPKG

mya-cli

Version:

MYA - AI-Powered Stock & Options Analysis CLI Tool

284 lines 9.31 kB
/** * Module: MYA API Gateway * Purpose: Cloudflare Worker proxy gateway for MYA CLI requests to mya-LLM backend * Dependencies: hono (web framework), middleware (JWT, rate limiting), proxy (request forwarding) * Used by: MYA CLI (cli-http.ts), directly accessed by end users * * Simplified Cloudflare Worker that acts as a gateway/proxy for CLI requests. * Forwards all requests to the mya-LLM Python agent for processing. * Handles authentication, rate limiting, and CORS headers. * * Configuration: Required Secrets * - JWT_SECRET: Secret key for JWT token verification. Set with: wrangler secret put JWT_SECRET --env [env] * - MYA_LLM_URL: Backend LLM service URL. Set with: wrangler secret put MYA_LLM_URL --env [env] * * Architecture: * - JWT validation via middleware from worker/middleware.ts * - Rate limiting per user based on KV storage * - Request forwarding to MYA_LLM_URL backend via worker/proxy.ts * - CORS headers for CLI client * * All AI processing, market data fetching, and analysis happens in mya-llm. */ import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { getJwtMiddleware, getRateLimitMiddleware } from './worker/middleware.js'; import { proxyToBackend, proxyToBackendQueued } from './worker/proxy.js'; import { RequestQueue } from './worker/queue.js'; import { handleAuth, handleVerifyOtp, handleAuthVerify } from './worker/auth.js'; const app = new Hono(); app.use(logger()); app.use(cors({ origin: '*', allowHeaders: ['Content-Type', 'Authorization'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: false, })); // Apply middleware using factory functions app.use('*', async (c, next) => { const env = c.env; const jwtMiddleware = getJwtMiddleware(env); return jwtMiddleware(c, next); }); app.use('*', async (c, next) => { const env = c.env; const rateLimitMiddleware = getRateLimitMiddleware(env); return rateLimitMiddleware(c, next); }); /** * Health check endpoint */ app.get('/health', (c) => { return c.json({ status: 'healthy', service: 'mya-gateway', version: '2.0.0', }); }); /** * Test endpoint - Get OTP for testing (dev only) * Only available in dev environment for testing * In production, OTP is sent via email */ app.get('/test/otp/:methodId', async (c) => { const env = c.env; // Only allow in dev environment if (env.ENVIRONMENT !== 'dev') { return c.json({ error: 'Not available in production' }, 404); } try { const methodId = c.req.param('methodId'); const kv = env.KV_NAMESPACE; if (!kv) { return c.json({ error: 'KV not available' }, 503); } const otpJson = await kv.get(`otp:${methodId}`); if (!otpJson) { return c.json({ error: 'OTP not found', methodId }, 404); } const otpData = JSON.parse(otpJson); return c.json({ methodId, email: otpData.email, code: otpData.code, expiresAt: otpData.expiresAt, }); } catch (error) { return c.json({ error: 'Failed to retrieve OTP' }, 500); } }); /** * API Routes - All forward to mya-llm backend via proxy module * Heavy operations (analyze, forecast) use request queue for better throughput * Light operations (health, auth, status checks) proxy directly */ /** * Analyze endpoint - uses queue for POST (submit analysis) * Direct proxy for GET (check results) */ app.post('/analyze', async (c) => { const env = c.env; return proxyToBackendQueued(c, env, 'POST', '/api/v1/analyze'); }); app.get('/analyze/:jobId', async (c) => { const env = c.env; const jobId = c.req.param('jobId'); return proxyToBackend(c, env, 'GET', `/api/v1/analyze/${jobId}`); }); app.post('/forecast', async (c) => { const env = c.env; return proxyToBackendQueued(c, env, 'POST', '/api/v1/forecast'); }); app.get('/daily-report', async (c) => { const env = c.env; return proxyToBackend(c, env, 'GET', '/api/v1/daily-report'); }); app.get('/learning-metrics', async (c) => { const env = c.env; return proxyToBackend(c, env, 'GET', '/api/v1/learning-metrics'); }); /** * Authentication Endpoints - Handled Locally by Worker * No backend call needed for auth flows */ app.post('/auth', async (c) => { const env = c.env; return handleAuth(c, env); }); app.post('/auth/verify', async (c) => { const env = c.env; return handleAuthVerify(c, env); }); app.post('/verify-otp', async (c) => { const env = c.env; return handleVerifyOtp(c, env); }); app.get('/recommendations/open', async (c) => { const env = c.env; return proxyToBackend(c, env, 'GET', '/api/v1/recommendations/open'); }); app.post('/announcements', async (c) => { const env = c.env; return proxyToBackend(c, env, 'POST', '/api/v1/announcements'); }); app.post('/double', async (c) => { const env = c.env; return proxyToBackendQueued(c, env, 'POST', '/api/v1/double'); }); app.post('/cmt', async (c) => { const env = c.env; return proxyToBackendQueued(c, env, 'POST', '/api/v1/cmt'); }); app.post('/benchmark', async (c) => { const env = c.env; return proxyToBackendQueued(c, env, 'POST', '/api/v1/benchmark'); }); /** * Agent API routes - forward to mya-llm agent endpoints */ app.post('/agent/ingest', async (c) => { const env = c.env; return proxyToBackend(c, env, 'POST', '/api/v1/agent/ingest'); }); app.post('/agent/predict', async (c) => { const env = c.env; return proxyToBackend(c, env, 'POST', '/api/v1/agent/predict'); }); app.post('/agent/benchmark', async (c) => { const env = c.env; return proxyToBackend(c, env, 'POST', '/api/v1/agent/benchmark'); }); app.get('/agent/status', async (c) => { const env = c.env; return proxyToBackend(c, env, 'GET', '/api/v1/agent/status'); }); /** * Queue Management Endpoints * Provide access to request queue status and operations */ /** * Check status of a queued request * Returns current status (pending, processing, completed, failed) * and result if completed */ app.get('/queue/status/:queueId', async (c) => { const env = c.env; const userId = c.get('userId') || 'anonymous'; const queueId = c.req.param('queueId'); try { const queue = new RequestQueue(env); const request = await queue.getRequestStatus(userId, queueId); if (!request) { return c.json({ error: 'Request not found', queueId }, 404); } return c.json({ queueId: request.id, status: request.status, result: request.status === 'completed' ? request.result : null, error: request.status === 'failed' ? request.error : null, timestamp: request.timestamp, }); } catch (error) { console.error('[QUEUE STATUS ERROR]', error); return c.json({ error: 'Failed to get queue status', details: error instanceof Error ? error.message : String(error), }, 500); } }); /** * Get queue statistics for current user * Returns counts of pending, processing, completed, and failed requests */ app.get('/queue/stats', async (c) => { const env = c.env; const userId = c.get('userId') || 'anonymous'; try { const queue = new RequestQueue(env); const stats = await queue.getQueueStats(userId); return c.json(stats); } catch (error) { console.error('[QUEUE STATS ERROR]', error); return c.json({ error: 'Failed to get queue stats', details: error instanceof Error ? error.message : String(error), }, 500); } }); /** * Clear completed and failed requests from user's queue * Helps maintain queue efficiency */ app.post('/queue/cleanup', async (c) => { const env = c.env; const userId = c.get('userId') || 'anonymous'; try { const queue = new RequestQueue(env); const removed = await queue.clearCompleted(userId); return c.json({ message: 'Cleanup completed', removedCount: removed, }); } catch (error) { console.error('[QUEUE CLEANUP ERROR]', error); return c.json({ error: 'Failed to cleanup queue', details: error instanceof Error ? error.message : String(error), }, 500); } }); /** * Catch-all route for /api/v1/* paths * Strips /api/v1 prefix and forwards to backend * Allows CLI to call either /analyze or /api/v1/analyze */ app.all('/api/v1/*', async (c) => { const env = c.env; const method = c.req.method; const pathname = c.req.path.replace(/^\/api\/v1/, '') || '/'; console.log(`[CATCHALL] ${method} /api/v1${pathname} -> /api/v1${pathname}`); return proxyToBackend(c, env, method, `/api/v1${pathname}`); }); /** * Default 404 handler */ app.notFound((c) => { return c.json({ error: 'Not found' }, 404); }); /** * Error handler */ app.onError((err, c) => { console.error('[ERROR]', err); return c.json({ error: 'Internal server error', details: err instanceof Error ? err.message : String(err), }, 500); }); export default app; //# sourceMappingURL=worker.js.map