v1.9.0: Full diagnostic sweep - 42 issues fixed

Critical fixes (7):
- Added global Express error handler (returns JSON instead of HTML)
- Removed caching from random endpoint (defeated purpose)
- Renamed share.jpg to share.png (MIME type mismatch)
- Fixed OG image dimensions (630 → 601)
- Fixed manifest.json icon sizes (192x192 → 512x512)
- Fixed implicit global event variable in console
- Added 9 missing endpoints to docs/endpoints.md

High fixes (12):
- Imported version from package.json (no hardcoded drift)
- Added dynamic endpoint count in stats
- Added input sanitization for mapper API (SSRF prevention)
- Added homepage caching (reduced 5 duplicate HTTP requests)
- Added rate limiting (100 requests/minute per IP)
- Added rel="noopener noreferrer" to all target="_blank" links
- Fixed trailing slash inconsistency in console
- Added noindex meta to 404.html
- Fixed docs/architecture.md controller filenames and dependency versions
- Fixed README.md dependency versions
- Added 12 missing endpoint tests

Medium fixes (22):
- Added escapeHtml helper for error messages
- Consistent favicon across all pages
- Added meta descriptions to privacy.html and tos.html
- Added OG/Twitter meta tags to privacy.html and tos.html
- Added aria-label to playground inputs
- Added aria-hidden to decorative SVGs
- Synced Twitter description with OG description
- Fixed robots.txt to disallow /api
- Added PWA fields to manifest.json
- Removed 5 unused imports
- Removed unused cookie-parser dependency
- Fixed inconsistent error handling in extractors
- Fixed resolvedSlug returning numeric ID
- Added /watch endpoint to streaming docs
- Improved test assertions and status code validation
- Deleted orphaned og-image.svg and logo.png

Low fixes (11):
- Removed redundant try/catch from extractPages
- Added max size eviction to cache
- Added cache key sorting in filter controller
- Added author meta tag
- Fixed favicon.svg font

Version bumped to 1.9.0
This commit is contained in:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-09 06:00:37 +05:30
parent 84c4877669
commit 100b9f9414
30 changed files with 404 additions and 139 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ import { getCache, setCache } from "../helper/cache.helper.js";
const getFilter = async (req, res, next) => {
try {
const params = req.query;
const cacheKey = `filter_${JSON.stringify(params)}`;
const cacheKey = `filter_${JSON.stringify(params, Object.keys(params).sort())}`;
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
+1 -6
View File
@@ -16,7 +16,7 @@
*/
import { extractRandom } from "../extractors/random.extractor.js";
import { getCache, setCache } from "../helper/cache.helper.js";
// ══════════════════════════════════════════════════════════════
// CONTROLLER: RANDOM
@@ -38,13 +38,8 @@ import { getCache, setCache } from "../helper/cache.helper.js";
*/
const getRandom = async (req, res, next) => {
try {
const cacheKey = "random";
const cached = getCache(cacheKey);
if (cached) {
return res.json({ success: true, results: cached });
}
const data = await extractRandom();
setCache(cacheKey, data);
res.json({ success: true, results: data });
} catch (error) {
next(error);
-1
View File
@@ -15,7 +15,6 @@
* ======= • ======= • ======= • ======= • =======• =======
*/
import { headers } from "../configs/header.config.js";
import { URLS } from "../configs/dataUrl.js";
import { countPages } from "../helper/countPages.helper.js";
import { extractPages } from "../helper/extractPages.helper.js";
+3 -3
View File
@@ -56,7 +56,7 @@ const extractEpisodeList = async (slugOrId) => {
}
if (!animeId) {
return { animeId: 0, slug: resolvedSlug, totalEpisodes: 0, episodes: [] };
return { animeId: 0, slug: slugOrId, totalEpisodes: 0, episodes: [] };
}
try {
@@ -104,7 +104,7 @@ const extractEpisodeList = async (slugOrId) => {
return {
animeId,
slug: resolvedSlug,
slug: slugOrId,
totalEpisodes: episodes.length,
episodes
};
@@ -112,7 +112,7 @@ const extractEpisodeList = async (slugOrId) => {
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
return {
animeId,
slug: resolvedSlug,
slug: slugOrId,
totalEpisodes: 0,
episodes: []
};
-1
View File
@@ -16,7 +16,6 @@
* ======= • ======= • ======= • ======= • =======• =======
*/
import * as cheerio from "cheerio";
import { headers } from "../configs/header.config.js";
import { URLS } from "../configs/dataUrl.js";
import { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS } from "../configs/ids.config.js";
+3
View File
@@ -19,6 +19,7 @@ import * as cheerio from "cheerio";
import axios from "axios";
import { headers } from "../configs/header.config.js";
import { URLS } from "../configs/dataUrl.js";
import { getCache, setCache } from "../helper/cache.helper.js";
// ══════════════════════════════════════════════════════════════
// HOME INFO EXTRACTOR
@@ -36,6 +37,8 @@ import { URLS } from "../configs/dataUrl.js";
* console.log(home.spotlights.length); // number of spotlight items
*/
const extractHomeInfo = async () => {
const cached = getCache("home");
if (cached) return cached;
try {
const { data } = await axios.get(URLS.home, { headers });
const $ = cheerio.load(data);
-1
View File
@@ -15,7 +15,6 @@
* ======= • ======= • ======= • ======= • =======• =======
*/
import * as cheerio from "cheerio";
import { headers } from "../configs/header.config.js";
import { URLS } from "../configs/dataUrl.js";
import { countPages } from "../helper/countPages.helper.js";
+1 -1
View File
@@ -42,7 +42,7 @@ const extractRandom = async () => {
try {
// NOTE: maxRedirects=5 ensures we follow the random redirect chain
// NOTE: validateStatus allows any 2xx/3xx response without throwing
const { data, request, response } = await axios.get(URLS.random, {
const { data, request } = await axios.get(URLS.random, {
headers,
maxRedirects: 5,
validateStatus: (status) => status < 400
+1 -1
View File
@@ -49,7 +49,7 @@ const extractSeasons = async (animeId) => {
return data || [];
} catch (error) {
// NOTE: Return empty array on error to maintain consistent API
return [];
throw error;
}
};
+5 -1
View File
@@ -139,7 +139,11 @@ const extractServerList = async (episodeIds) => {
*/
const extractMapperServers = async (malId, slug, timestamp) => {
try {
const url = `https://mapper.nekostream.site/api/mal/${malId}/${slug}/${timestamp}`;
// Validate and sanitize input to prevent SSRF
if (!Number.isFinite(Number(malId))) throw new Error("Invalid malId");
if (!/^[a-zA-Z0-9-]+$/.test(slug)) throw new Error("Invalid slug");
if (!Number.isFinite(Number(timestamp))) throw new Error("Invalid timestamp");
const url = `https://mapper.nekostream.site/api/mal/${encodeURIComponent(malId)}/${encodeURIComponent(slug)}/${encodeURIComponent(timestamp)}`;
const { data } = await axios.get(url, { headers });
const servers = [];
+1 -1
View File
@@ -49,7 +49,7 @@ const extractWatchOrder = async (animeId) => {
return data || [];
} catch (error) {
// NOTE: Return empty array on error to maintain consistent API
return [];
throw error;
}
};
+5
View File
@@ -83,6 +83,11 @@ const getCache = (key) => {
* setCache("search-one-piece", animeResults);
*/
const setCache = (key, data) => {
// Evict oldest entries if cache is full
if (cache.size >= MAX_CACHE_SIZE) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
cache.set(key, { data, timestamp: Date.now() });
};
+15 -2
View File
@@ -1,5 +1,8 @@
/*
* ======= • ======= • ======= • ======= • =======• =======
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { version } = require("../package.json");
* AniKotoAPI — apiRoutes.js
* Repository: https://github.com/Shineii86/AniKotoAPI
*
@@ -361,7 +364,7 @@ app.get("/api/health", (req, res) => {
success: true,
results: {
status: "healthy",
version: "1.8.0",
version: version,
uptime: `${hours}h ${minutes}m ${seconds}s`,
uptimeSeconds: uptime,
timestamp: new Date().toISOString(),
@@ -407,7 +410,7 @@ app.get("/api/openapi", (req, res) => {
info: {
title: "AniKotoAPI",
description: "Free REST API for anime data from anikototv.to",
version: "1.8.0",
version: version,
contact: {
name: "Shinei Nouzen",
url: "https://github.com/Shineii86/AniKotoAPI",
@@ -437,6 +440,16 @@ app.get("/api/openapi", (req, res) => {
"/health": { get: { summary: "Health check", tags: ["System"] } },
"/stats": { get: { summary: "API statistics", tags: ["System"] } },
"/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } },
"/watch": { get: { summary: "Watch page data", tags: ["Streaming"] } },
"/search/suggest": { get: { summary: "Search suggestions", tags: ["Search"] } },
"/episodes-ajax/{id}": { get: { summary: "AJAX episode list", tags: ["Episodes"] } },
"/mapper-servers": { get: { summary: "Mapper servers", tags: ["Streaming"] } },
"/newly-added": { get: { summary: "Newly added", tags: ["Releases"] } },
"/trending-sidebar": { get: { summary: "Trending sidebar", tags: ["Discovery"] } },
"/seasons/{id}": { get: { summary: "Season data", tags: ["Anime"] } },
"/watch-order/{id}": { get: { summary: "Watch order", tags: ["Anime"] } },
"/az-list/{letter}": { get: { summary: "A-Z listing", tags: ["Discovery"] } },
"/suggestions": { get: { summary: "Anime suggestions", tags: ["Search"] } },
},
tags: [
{ name: "Home", description: "Homepage data" },