From a2ba1df831e9905f87c22f4bdb67e955280aebba Mon Sep 17 00:00:00 2001 From: Shinei Nouzen <157171073+Shineii86@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:39:31 +0530 Subject: [PATCH] feat: add Cloudflare Worker implementation for edge-to-edge streaming bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add worker.js: full API on Cloudflare's edge network (1720 lines) - Add wrangler.toml: deployment configuration for wrangler CLI - Streaming endpoints now bypass Cloudflare bot detection via edge-to-edge - Zero cold starts, 100K free requests/day, no npm dependencies - 15 endpoints: search, trending, info, episodes, watch, genres, random, etc. - Self-healing fallback: direct → ScraperAPI → FlareSolverr - Proper code style: box headers, JSDoc, feature markers, inline notes - Bump version to 2.3.2 across package.json, server.js, apiRoutes.js Fixes #2 --- CHANGELOG.md | 42 + package.json | 2 +- server.js | 2 +- src/routes/apiRoutes.js | 4 +- worker.js | 1720 +++++++++++++++++++++++++++++++++++++++ wrangler.toml | 22 + 6 files changed, 1788 insertions(+), 4 deletions(-) create mode 100644 worker.js create mode 100644 wrangler.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 923f23d..667510d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## v2.3.2 +### Cloudflare Worker Implementation — Issue #2 Fix + +#### New: `worker.js` (Cloudflare Worker Edge Runtime) +- **Full Cloudflare Worker implementation** created at project root +- Edge-to-edge requests bypass Cloudflare bot detection (Worker → miruro.to → Cloudflare trusts itself) +- Zero cold starts, 100K free requests/day on Cloudflare free tier +- No npm dependencies — uses native `fetch`, `DecompressionStream`, `TextEncoder`/`TextDecoder` +- 15 API endpoints: search, suggestions, filter, trending, trending/daily, trending/weekly, popular, top, upcoming, recent, random, info, episodes, watch, stream, genres, tags, providers, health + +#### New: `wrangler.toml` +- Cloudflare Workers deployment configuration +- Compatible with `wrangler deploy` CLI +- Node.js compatibility flag enabled for buffer/text encoding support +- CPU time limit: 30 seconds + +#### Architecture +``` +Browser → Cloudflare Worker (edge) → miruro.to pipe → Cloudflare trusts itself ✅ +Vercel (datacenter) → miruro.to → Cloudflare blocks ❌ +``` + +#### Deploy +```bash +npm install -g wrangler +wrangler login +wrangler deploy worker.js --name miruroapi +``` + +#### Files Changed +- `worker.js` — Cloudflare Worker implementation (complete rewrite for edge runtime) +- `wrangler.toml` — Cloudflare Workers deployment configuration +- `CHANGELOG.md` — This entry + +#### Notes +- This fixes Issue #2: Cloudflare Worker/Pages deployment support +- Streaming endpoints (`/episodes`, `/watch`, `/stream`) now work via edge-to-edge requests +- Environment variables `SCRAPER_API_KEY` and `FLARESOLVERR_URL` are optional for fallback methods +- The worker runs on Cloudflare's edge network with zero cold starts + +--- + ## v2.3.1 ### Cloudflare Worker Edition — Streaming Bypasses Cloudflare diff --git a/package.json b/package.json index 1e57add..a8eecac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "miruro-api", - "version": "2.3.0", + "version": "2.3.2", "description": "RESTful API for Miruro - Anime streaming data powered by AniList GraphQL and Miruro streaming providers", "main": "server.js", "scripts": { diff --git a/server.js b/server.js index d1d3f39..3dcfe67 100644 --- a/server.js +++ b/server.js @@ -301,7 +301,7 @@ app.use((req, res) => { if (!process.env.VERCEL) { app.listen(PORT, () => { - console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.0 listening at ${PORT}`); + console.log(`[${new Date().toISOString()}] MiruroAPI v2.3.2 listening at ${PORT}`); }); } diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index 07c3a71..6928fad 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -1124,7 +1124,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { success: true, results: { status: "healthy", - version: "2.3.0", + version: "2.3.2", uptime: `${hours}h ${minutes}m ${seconds}s`, uptimeSeconds: uptime, timestamp: new Date().toISOString(), @@ -1211,7 +1211,7 @@ const createApiRoutes = (jsonResponse, jsonError, startTime) => { info: { title: "MiruroAPI", description: "Free REST API for anime streaming data — AniList GraphQL + Miruro streaming providers", - version: "2.3.0", + version: "2.3.2", contact: { name: "Shineii86", url: "https://github.com/Shineii86/MiruroAPI" }, }, servers: [ diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..95476ba --- /dev/null +++ b/worker.js @@ -0,0 +1,1720 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * 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("