diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..13627b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:18-slim +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm install --only=production +COPY . . +EXPOSE 8080 +CMD [ "npm", "start" ] \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..1b633db --- /dev/null +++ b/index.js @@ -0,0 +1,80 @@ +const express = require('express'); +const { createProxyMiddleware } = require('http-proxy-middleware'); +const cors = require('cors'); +const rateLimit = require('express-rate-limit'); + +const app = express(); +const PORT = process.env.PORT || 8080; + +// CORS middleware +app.use(cors({ + origin: true, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['*'] +})); + +// Handle preflight BEFORE proxy +app.options('*', cors()); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +// Rate limiting +const limiter = rateLimit({ + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60_000, + max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100, + standardHeaders: true, + legacyHeaders: false, + message: { error: 'Too many requests, please try again later.' }, +}); +app.use(limiter); + +// Require Authorization header +app.use('/api/v1', (req, res, next) => { + const auth = req.headers['authorization']; + if (!auth || !auth.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Missing or invalid Authorization header.' }); + } + next(); +}); + +// Request logging +app.use('/api/v1', (req, res, next) => { + const start = Date.now(); + res.on('finish', () => { + const duration = Date.now() - start; + console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`); + }); + next(); +}); + +// Proxy — only /api/v1/* traffic +app.use('/api/v1', createProxyMiddleware({ + target: 'https://openrouter.ai/api/v1', + changeOrigin: true, + pathRewrite: { + '^/api/v1': '', + }, + onProxyReq: (proxyReq, req, res) => { + if (!req.headers['http-referer']) { + proxyReq.setHeader('HTTP-Referer', 'https://your-app-domain.com'); + } + }, + onError: (err, req, res) => { + console.error(`Proxy error: ${err.message}`); + res.status(502).json({ error: 'Bad gateway — upstream request failed.' }); + }, + buffer: false, +})); + +// 404 for everything else +app.use((req, res) => { + res.status(404).json({ error: 'Not found. Use /api/v1/* to access the OpenRouter API.' }); +}); + +app.listen(PORT, () => { + console.log(`CORS Proxy running on port ${PORT}`); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..01c9c75 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "openrouter-cors-proxy", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "express": "^4.18.2", + "http-proxy-middleware": "^2.0.6", + "cors": "^2.8.5", + "express-rate-limit": "^7.1.5" + } +} \ No newline at end of file