'use strict'; const net = require('node:net'); function sourceUrl(value, prefix) { const url = new URL(value); if (url.protocol !== 'https:' || url.hostname !== 'weebcentral.com' || url.username || url.password || !url.pathname.startsWith(prefix)) { throw new Error('Source plugin returned an unexpected WeebCentral URL'); } return url.href; } function imageUrl(value) { const url = new URL(value); const hostname = url.hostname.toLowerCase(); if (url.protocol !== 'https:' || url.username || url.password || net.isIP(hostname) || hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local') || !hostname.includes('.')) { throw new Error('Source plugin returned an unsafe image URL'); } return url.href; } function mangaDexUrl(value, kind) { const url = new URL(value); if (url.protocol !== 'https:' || url.username || url.password || url.port) throw new Error('Invalid MangaDex plugin URL'); if (kind === 'chapter' && (url.hostname !== 'mangadex.org' || !url.pathname.startsWith('/chapter/'))) { throw new Error('Unexpected MangaDex chapter URL'); } if (kind === 'page' && !(url.hostname === 'uploads.mangadex.org' || url.hostname.endsWith('.mangadex.network'))) { throw new Error('Unexpected MangaDex page host'); } return url.href; } function mangaPillUrl(value, kind) { const url = new URL(value); if (url.protocol !== 'https:' || url.username || url.password || url.port) throw new Error('Invalid MangaPill plugin URL'); if (kind === 'chapter' && (url.hostname !== 'mangapill.com' || !/^\/chapters\/\d+-\d+\//.test(url.pathname))) { throw new Error('Unexpected MangaPill chapter URL'); } if (kind === 'page' && (url.hostname !== 'cdn.readdetectiveconan.com' || !url.pathname.startsWith('/file/mangap/'))) { throw new Error('Unexpected MangaPill page host'); } return url.href; } function validate(plugin, operation, output) { if (!['weebcentral', 'mangadex', 'mangapill'].includes(plugin) || !output || typeof output !== 'object' || Array.isArray(output)) { throw new Error('Invalid source plugin output'); } if (plugin === 'mangadex') { if (operation === 'metadata') { if (typeof output.title !== 'string' || !output.availabilityAttributes || typeof output.specialLabel !== 'string') throw new Error('Invalid MangaDex metadata'); } else if (operation === 'feed') { if (!Array.isArray(output.releases) || output.releases.some(ref => typeof ref?.id !== 'string' || !ref.attributes)) throw new Error('Invalid MangaDex feed'); } else if (operation === 'catalog') { if (!Array.isArray(output.chapters) || !output.availability) throw new Error('Invalid MangaDex catalog'); for (const chapter of output.chapters) mangaDexUrl(chapter.url, 'chapter'); } else if (operation === 'atHome') { if (!Array.isArray(output.pages) || !output.pages.length) throw new Error('Invalid MangaDex pages'); for (const page of output.pages) mangaDexUrl(page.url, 'page'); } else throw new Error('Unknown source plugin operation'); return output; } if (plugin === 'mangapill') { if (operation === 'catalog') { if (typeof output.title !== 'string' || !Array.isArray(output.chapters) || !Number.isInteger(output.chapterLinks) || output.chapterLinks < 0) throw new Error('Invalid MangaPill catalog'); for (const chapter of output.chapters) { if (typeof chapter?.id !== 'string' || !/^\d+-\d+$/.test(chapter.id) || !/^\d+(?:\.\d+)?$/.test(chapter.number)) throw new Error('Invalid MangaPill chapter'); mangaPillUrl(chapter.url, 'chapter'); } } else if (operation === 'pages') { if (!Array.isArray(output.pages)) throw new Error('Invalid MangaPill pages'); for (const page of output.pages) mangaPillUrl(page, 'page'); } else throw new Error('Unknown MangaPill operation'); return output; } if (operation === 'series') { if (typeof output.title !== 'string' || typeof output.listUrl !== 'string' && output.listUrl !== null) throw new Error('Invalid series parse'); if (output.listUrl !== null) sourceUrl(output.listUrl, '/series/'); } else if (operation === 'catalog') { if (!Array.isArray(output.chapters) || !Number.isInteger(output.chapterLinks) || output.chapterLinks < 0) throw new Error('Invalid catalog parse'); for (const chapter of output.chapters) { if (!chapter || typeof chapter.id !== 'string' || typeof chapter.number !== 'string') throw new Error('Invalid chapter reference'); sourceUrl(chapter.url, '/chapters/'); } } else if (operation === 'chapter') { sourceUrl(output.manifestUrl, '/chapters/'); if (!new URL(output.manifestUrl).pathname.endsWith('/images')) throw new Error('Invalid chapter image manifest'); } else if (operation === 'pages') { if (!Array.isArray(output.pages)) throw new Error('Invalid page parse'); for (const page of output.pages) imageUrl(page); } else throw new Error('Unknown source plugin operation'); return output; } module.exports = { validate };