synor/apps/api-gateway/src/index.ts
Gulshan Yadav 4a2825f516 feat(api): add public API gateway with rate limiting
- Create API gateway service with Express.js
- Implement sliding window rate limiting via Redis
- Add API key management with tiered access (free/developer/enterprise)
- Track usage analytics per key and globally
- Add RPC proxy to blockchain nodes
- Configure Docker Compose with api-gateway and redis services
- Free tier: 100 req/min, Developer: 1000 req/min, Enterprise: unlimited
2026-01-10 06:19:08 +05:30

258 lines
6.9 KiB
TypeScript

/**
* 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);
});