diff --git a/CHANGELOG.md b/CHANGELOG.md index 894ca05..5490832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.4.0 +- Fixed Vercel deployment: replaced legacy `builds`/`routes` config with modern `functions` config +- Added `api/index.js` serverless entry point (Vercel requires this for proper function detection) +- Security headers: removed deprecated `X-XSS-Protection`, added `Strict-Transport-Security` (HSTS) +- Rate limiter: uses `x-forwarded-for` header for correct IP detection on Vercel serverless +- Rate limiter: added periodic IP entry pruning when >200 IPs tracked (prevents memory leak) +- Cache: added periodic expired entry cleanup when cache reaches 80% capacity (prevents memory leak) +- Version synced to 1.4.0 across all files (server.js, package.json, health endpoint, OpenAPI spec) + ## v1.3.0 - Full landing page upgrade to match AniKotoAPI style - Added canvas particle effects with cyan-colored floating particles diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..97d3b5f --- /dev/null +++ b/api/index.js @@ -0,0 +1 @@ +module.exports = require("../server"); diff --git a/package.json b/package.json index ab107d4..ef6616c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "miruro-api", - "version": "1.3.0", + "version": "1.4.0", "description": "RESTful API for miruro.tv - Anime streaming data powered by AniList GraphQL and Miruro streaming providers", "main": "server.js", "scripts": { diff --git a/server.js b/server.js index 20c8af0..e782617 100644 --- a/server.js +++ b/server.js @@ -58,7 +58,7 @@ app.use((req, res, next) => { app.use((req, res, next) => { res.setHeader("X-Frame-Options", "DENY"); res.setHeader("X-Content-Type-Options", "nosniff"); - res.setHeader("X-XSS-Protection", "1; mode=block"); + res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); next(); @@ -128,7 +128,10 @@ const jsonError = (res, message = "Internal server error", status = 500) => // ---- FEATURE: Rate limiting (100 requests per minute per IP) ---- const requestCounts = new Map(); app.use((req, res, next) => { - const ip = req.ip || req.connection.remoteAddress; + const ip = + req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || + req.ip || + req.connection.remoteAddress; const now = Date.now(); const windowMs = 60000; const maxRequests = 100; @@ -148,6 +151,16 @@ app.use((req, res, next) => { } timestamps.push(now); + + // NOTE: Periodic cleanup — prune expired IP entries every 60s to prevent memory leak + if (requestCounts.size > 200) { + for (const [key, val] of requestCounts) { + if (val.filter((t) => now - t < windowMs).length === 0) { + requestCounts.delete(key); + } + } + } + next(); }); diff --git a/src/helpers/cache.js b/src/helpers/cache.js index 6f32e4f..ea7bbb3 100644 --- a/src/helpers/cache.js +++ b/src/helpers/cache.js @@ -104,6 +104,15 @@ const setCache = (key, data, ttl = DEFAULT_TTL) => { cache.delete(firstKey); } + // NOTE: Periodic cleanup — prune all expired entries when cache reaches 80% capacity + // Prevents unbounded memory growth from stale entries that were never accessed + if (cache.size >= MAX_CACHE_SIZE * 0.8) { + const now = Date.now(); + for (const [k, v] of cache) { + if (now > v.expiry) cache.delete(k); + } + } + cache.set(key, { data, expiry: Date.now() + ttl }); }; diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index 052151e..e5b6583 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -355,7 +355,7 @@ const createApiRoutes = (jsonResponse, jsonError) => { success: true, results: { status: "healthy", - version: "1.2.0", + version: "1.4.0", uptime: `${hours}h ${minutes}m ${seconds}s`, uptimeSeconds: uptime, timestamp: new Date().toISOString(), @@ -404,7 +404,7 @@ const createApiRoutes = (jsonResponse, jsonError) => { info: { title: "MiruroAPI", description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers", - version: "1.2.0", + version: "1.4.0", contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" }, }, servers: [ diff --git a/vercel.json b/vercel.json index 1440fb0..30202ac 100644 --- a/vercel.json +++ b/vercel.json @@ -1,13 +1,8 @@ { - "version": 2, - "builds": [ - { - "src": "server.js", - "use": "@vercel/node" + "functions": { + "api/index.js": { + "includeFiles": "src/**", + "maxDuration": 30 } - ], - "routes": [ - { "src": "/api/(.*)", "dest": "server.js" }, - { "src": "/(.*)", "dest": "server.js" } - ] + } }