v1.4.0: Fix Vercel deployment, security headers, rate limiter, cache cleanup

This commit is contained in:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-25 00:52:26 +05:30
parent 68f7ea7cf4
commit 3a28c4d182
7 changed files with 42 additions and 15 deletions
+9
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
module.exports = require("../server");
+1 -1
View File
@@ -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": {
+15 -2
View File
@@ -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();
});
+9
View File
@@ -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 });
};
+2 -2
View File
@@ -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: [
+5 -10
View File
@@ -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" }
]
}