/** * Synor Public API Gateway * * Provides rate-limited, authenticated access to Synor blockchain RPC. * * Endpoints: * - POST /rpc - JSON-RPC proxy to node (requires API key) * - GET /health - Health check * - GET /v1/keys - List API keys (admin) * - POST /v1/keys - Create API key (admin) * - GET /v1/keys/:key/stats - Get usage stats * - DELETE /v1/keys/:key - Revoke API key (admin) * - GET /v1/analytics - Global analytics (admin) */ import express, { Request, Response, NextFunction } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import helmet from 'helmet'; import cors from 'cors'; import { RateLimiter, RateLimitTier } from './rate-limiter.js'; import { ApiKeyManager } from './api-keys.js'; // Configuration const PORT = parseInt(process.env.PORT || '3100'); const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; const RPC_TARGET = process.env.RPC_TARGET || 'http://localhost:16110'; const ADMIN_KEY = process.env.ADMIN_KEY || 'admin-secret-key'; // Initialize services const rateLimiter = new RateLimiter(REDIS_URL); const apiKeyManager = new ApiKeyManager(REDIS_URL); const app = express(); // Security middleware app.use(helmet()); app.use(cors()); app.use(express.json()); // Request timing middleware app.use((req: Request, res: Response, next: NextFunction) => { (req as any).startTime = Date.now(); next(); }); // Health check (no auth required) app.get('/health', (req: Request, res: Response) => { res.json({ status: 'healthy', timestamp: Date.now() }); }); // API key extraction middleware const extractApiKey = (req: Request): string | null => { // Check Authorization header const authHeader = req.headers.authorization; if (authHeader?.startsWith('Bearer ')) { return authHeader.substring(7); } // Check X-API-Key header const apiKeyHeader = req.headers['x-api-key']; if (typeof apiKeyHeader === 'string') { return apiKeyHeader; } // Check query parameter if (typeof req.query.api_key === 'string') { return req.query.api_key; } return null; }; // Rate limiting and auth middleware for RPC const rpcAuthMiddleware = async ( req: Request, res: Response, next: NextFunction ) => { const apiKey = extractApiKey(req); let tier: RateLimitTier = 'free'; let keyIdentifier: string; if (apiKey) { const keyData = await apiKeyManager.validateKey(apiKey); if (keyData) { tier = keyData.tier; keyIdentifier = apiKey; } else { return res.status(401).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Invalid API key' }, id: null, }); } } else { // Anonymous access - rate limit by IP keyIdentifier = `ip:${req.ip}`; } // Check rate limit const result = await rateLimiter.checkLimit(keyIdentifier, tier); // Set rate limit headers res.set('X-RateLimit-Limit', tier === 'enterprise' ? 'unlimited' : String( tier === 'developer' ? 1000 : 100 )); res.set('X-RateLimit-Remaining', String(result.remaining)); res.set('X-RateLimit-Reset', String(Math.ceil(result.resetAt / 1000))); res.set('X-RateLimit-Tier', tier); if (!result.allowed) { return res.status(429).json({ jsonrpc: '2.0', error: { code: -32005, message: 'Rate limit exceeded', data: { retryAfter: Math.ceil((result.resetAt - Date.now()) / 1000), tier, upgrade: tier === 'free' ? 'Upgrade to Developer tier for 1000 req/min' : tier === 'developer' ? 'Contact us for Enterprise tier' : null, }, }, id: null, }); } // Store for analytics (req as any).apiKey = keyIdentifier; (req as any).tier = tier; next(); }; // RPC proxy app.post( '/rpc', rpcAuthMiddleware, createProxyMiddleware({ target: RPC_TARGET, changeOrigin: true, pathRewrite: { '^/rpc': '/' }, onProxyRes: async (proxyRes, req: any) => { // Record usage analytics const responseTime = Date.now() - req.startTime; const body = req.body || {}; const method = body.method || 'unknown'; await apiKeyManager.recordUsage(req.apiKey, method, responseTime); }, onError: (err, req, res: any) => { res.status(502).json({ jsonrpc: '2.0', error: { code: -32603, message: 'RPC node unavailable' }, id: null, }); }, }) ); // Admin middleware const adminAuth = (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (authHeader !== `Bearer ${ADMIN_KEY}`) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; // Admin: Create API key app.post('/v1/keys', adminAuth, async (req: Request, res: Response) => { const { tier, name } = req.body; if (!tier || !name) { return res.status(400).json({ error: 'tier and name are required' }); } if (!['free', 'developer', 'enterprise'].includes(tier)) { return res.status(400).json({ error: 'Invalid tier' }); } const key = await apiKeyManager.createKey({ tier, name }); res.status(201).json(key); }); // Admin: List API keys app.get('/v1/keys', adminAuth, async (req: Request, res: Response) => { const keys = await apiKeyManager.listKeys(); res.json({ keys }); }); // Get API key stats (key owner or admin) app.get('/v1/keys/:key/stats', async (req: Request, res: Response) => { const { key } = req.params; const authHeader = req.headers.authorization; // Allow key owner or admin if (authHeader !== `Bearer ${key}` && authHeader !== `Bearer ${ADMIN_KEY}`) { return res.status(403).json({ error: 'Forbidden' }); } const stats = await apiKeyManager.getKeyWithStats(key); if (!stats) { return res.status(404).json({ error: 'Key not found' }); } const analytics = await apiKeyManager.getAnalytics(key); res.json({ ...stats, analytics }); }); // Admin: Revoke API key app.delete('/v1/keys/:key', adminAuth, async (req: Request, res: Response) => { const { key } = req.params; const deleted = await apiKeyManager.revokeKey(key); if (!deleted) { return res.status(404).json({ error: 'Key not found' }); } res.json({ success: true }); }); // Admin: Global analytics app.get('/v1/analytics', adminAuth, async (req: Request, res: Response) => { // Get global statistics res.json({ message: 'Analytics endpoint - implement as needed', timestamp: Date.now(), }); }); // Error handler app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error('Error:', err.message); res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null, }); }); // Start server app.listen(PORT, () => { console.log(`Synor API Gateway running on port ${PORT}`); console.log(`Proxying RPC to: ${RPC_TARGET}`); }); // Graceful shutdown process.on('SIGTERM', async () => { console.log('Shutting down...'); await rateLimiter.close(); await apiKeyManager.close(); process.exit(0); });