/* * ======= • ======= • ======= • ======= • =======• ======= * MiruroAPI — worker.js (Cloudflare Worker) * Repository: https://github.com/Shineii86/MiruroAPI * * @description * Full API running on Cloudflare's edge network. * Streaming endpoints work via edge-to-edge requests that bypass * Cloudflare bot detection. Zero cold starts, 100K free requests/day. * No npm dependencies — uses native fetch + DecompressionStream. * * Why Workers Work: * Vercel (datacenter) → miruro.to → Cloudflare blocks ❌ * Worker (edge) → miruro.to → Cloudflare trusts itself ✅ * * @exports * default — Cloudflare Worker fetch handler * * @deploy * npm install -g wrangler * wrangler login * wrangler deploy worker.js --name miruroapi * * @author Shinei Nouzen * @license MIT * ======= • ======= • ======= • ======= • =======• ======= */ // ══════════════════════════════════════════════════════════════ // WORKER CONFIGURATION // ══════════════════════════════════════════════════════════════ // ---- FEATURE: API version — synced across server.js, apiRoutes.js, health endpoint ---- /** * Current Worker API version string. * Must match package.json version for consistency. * * @type {string} */ const VERSION = "2.3.2"; // ---- FEATURE: AniList GraphQL endpoint ---- /** * AniList GraphQL API endpoint. * All metadata queries (search, trending, info, filter) go here. * * @type {string} */ const ANILIST_URL = "https://graphql.anilist.co"; // ---- FEATURE: Miruro pipe mirror rotation list ---- /** * Ordered list of Miruro mirror origins for pipe requests. * Worker rotates through these on failure (exponential backoff). * NOTE: .ru is tried first as it has the softest Cloudflare rules. * * @type {string[]} */ const MIRURO_ORIGINS = [ "https://www.miruro.ru", "https://www.miruro.to", "https://www.miruro.bz", "https://www.miruro.tv", ]; // ---- FEATURE: Canonical Miruro origin for headers ---- /** * Primary Miruro origin used in Referer/Origin headers. * Cloudflare expects requests to come from this domain. * * @type {string} */ const CANONICAL_ORIGIN = "https://www.miruro.to"; // ---- FEATURE: Pipe API path — where episode/source data lives ---- /** * Miruro pipe endpoint path. Appended to each origin during rotation. * Format: {origin}/api/secure/pipe?e={base64url-encoded-payload} * * @type {string} */ const PIPE_PATH = "/api/secure/pipe"; // ---- FEATURE: Browser-accurate request headers ---- /** * Full set of browser-accurate headers to pass Cloudflare bot detection. * Includes sec-ch-ua, sec-fetch-* family, and proper Referer/Origin. * TIP: Cloudflare checks the consistency of these headers — missing or * mismatched values trigger bot detection. * * @type {object} */ const HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", Accept: "*/*", "Accept-Language": "en-US,en;q=0.9", "sec-ch-ua": '"Chromium";v="137", "Not?A_Brand";v="24"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Referer: CANONICAL_ORIGIN + "/", Origin: CANONICAL_ORIGIN, }; // ══════════════════════════════════════════════════════════════ // GRAPHQL FRAGMENTS // ══════════════════════════════════════════════════════════════ // ---- FEATURE: Media list fields fragment (used in collections/search) ---- /** * Standard set of fields for anime list responses. * Used in trending, popular, search, filter, and schedule endpoints. * Includes titles, cover, format, scores, genres, studios, and airing info. * * @type {string} */ const MEDIA_LIST_FIELDS = ` id title { romaji english native } coverImage { large extraLarge } bannerImage format season seasonYear episodes duration status averageScore meanScore popularity favourites genres source countryOfOrigin isAdult studios(isMain: true) { nodes { name isAnimationStudio } } nextAiringEpisode { episode airingAt timeUntilAiring } startDate { year month day } endDate { year month day } `; // ---- FEATURE: Media full fields fragment (used in info endpoint) ---- /** * Extended set of fields for detailed anime info responses. * Includes everything in MEDIA_LIST_FIELDS plus characters, staff, * relations, recommendations, trailer, stats, and external links. * * @type {string} */ const MEDIA_FULL_FIELDS = ` id idMal title { romaji english native } description(asHtml: false) coverImage { large extraLarge color } bannerImage format season seasonYear episodes duration status averageScore meanScore popularity favourites trending genres tags { name rank isMediaSpoiler } source countryOfOrigin isAdult hashtag synonyms siteUrl trailer { id site thumbnail } studios { nodes { id name isAnimationStudio siteUrl } } nextAiringEpisode { episode airingAt timeUntilAiring } startDate { year month day } endDate { year month day } characters(sort: [ROLE, RELEVANCE], perPage: 25) { edges { role node { id name { full native } image { large } } voiceActors(language: JAPANESE) { id name { full native } image { large } languageV2 } } } staff(sort: RELEVANCE, perPage: 25) { edges { role node { id name { full native } image { large } } } } relations { edges { relationType(version: 2) node { id title { romaji english native } coverImage { large } format type status episodes meanScore } } } recommendations(sort: RATING_DESC, perPage: 10) { nodes { rating mediaRecommendation { id title { romaji english native } coverImage { large } format episodes status meanScore averageScore } } } externalLinks { url site type } streamingEpisodes { title thumbnail url site } stats { scoreDistribution { score amount } statusDistribution { status amount } } `; // ══════════════════════════════════════════════════════════════ // RESPONSE HELPERS // ══════════════════════════════════════════════════════════════ // ---- FEATURE: Success response — wraps data in standard { success, results } format ---- /** * Wraps data in the standard API success response format. * All successful responses follow: { success: true, results: data } * * NOTE: Includes CORS header for browser playback. * * @param {any} data - The data to wrap in the response * @param {number} [status=200] - HTTP status code * @returns {Response} Web API Response with JSON body and CORS headers * * @example * jsonResponse({ anime: [...] }); * // => { "success": true, "results": { "anime": [...] } } */ const jsonResponse = (data, status = 200) => new Response(JSON.stringify({ success: true, results: data }), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, }); // ---- FEATURE: Error response — wraps message in standard { success, message } format ---- /** * Wraps an error message in the standard API error response format. * All error responses follow: { success: false, message: "..." } * * @param {string} [message="Internal server error"] - Error description * @param {number} [status=500] - HTTP status code * @returns {Response} Web API Response with JSON error body * * @example * jsonError("Anime not found", 404); * // => { "success": false, "message": "Anime not found" } */ const jsonError = (message = "Internal server error", status = 500) => new Response(JSON.stringify({ success: false, message }), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, }); // ---- FEATURE: CORS preflight response — handles OPTIONS requests ---- /** * Returns a 204 No Content response with CORS preflight headers. * Used to handle OPTIONS requests before actual API calls. * * @returns {Response} 204 response with CORS headers */ const corsResponse = () => new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Max-Age": "86400", }, }); // ══════════════════════════════════════════════════════════════ // INPUT SANITIZATION // ══════════════════════════════════════════════════════════════ // ---- FEATURE: Integer ID validator ---- /** * Validates and parses a positive integer ID from a string. * Returns null for invalid, zero, or negative values. * * @param {string} val - The value to validate * @returns {number|null} Parsed integer or null if invalid * * @example * sanitizeId("123"); // => 123 * sanitizeId("-1"); // => null * sanitizeId("abc"); // => null */ const sanitizeId = (val) => { const n = parseInt(val); return isNaN(n) || n <= 0 ? null : n; }; // ---- FEATURE: String sanitizer with length limit ---- /** * Sanitizes a string by stripping dangerous characters and enforcing length. * Removes <, >, ", ', `, ;, \ characters to prevent injection. * * @param {string} val - The string to sanitize * @param {number} [maxLen=100] - Maximum allowed length * @returns {string|null} Cleaned string or null if invalid/empty * * @example * sanitizeString("Action"); // => "Action" * sanitizeString("