From 765e9b326f3a1cc164a24e93bdc2441d77c91b67 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Sun, 18 Oct 2026 09:01:18 +0800 Subject: [PATCH 1/4] fix(english/fenrirrealm): handle verification-gated chapters Fenrir Realm now gates free chapters behind a Cloudflare Turnstile check and the chapter API returns empty content with content_format "locked" until the reader has a verified session, so the plugin silently returned empty chapters. - When a chapter is locked, mint a read token with the existing (WebView verified) session and refetch it, as the site's own reader does. - Otherwise throw a clear error asking to complete the check in WebView instead of returning an empty chapter, carrying the read-token HTTP status (403 without a session); also throw on non-2xx chapter responses. - Volume chapter slugs now include the volume, so resolveUrl collapses the repeated volume segment; the WebView URL no longer 404s. --- plugins/english/fenrirrealm.ts | 85 ++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/plugins/english/fenrirrealm.ts b/plugins/english/fenrirrealm.ts index a758f4db4..d496dac8c 100644 --- a/plugins/english/fenrirrealm.ts +++ b/plugins/english/fenrirrealm.ts @@ -31,6 +31,21 @@ type APIChapter = { created_at: string; }; +type APIChapterContent = { + id: number; + uuid: string | null; + content: string | null; + content_format?: string; + remint_chapter_id: number | null; +}; + +type ReadTokenResult = { + token?: string; + status?: number; +}; + +type HttpError = Error & { status?: number }; + type ChapterInfo = { name: string; path: string; @@ -75,7 +90,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { name = 'Fenrir Realm'; icon = 'src/en/fenrirrealm/icon.png'; site = 'https://fenrirealm.com'; - version = '1.1.3'; + version = '1.1.4'; imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; hideLocked = storage.get('hideLocked'); @@ -218,8 +233,27 @@ class FenrirRealmPlugin implements Plugin.PluginBase { const chapterId = chapterPath.split('~~')[1]; if (chapterId) { const url = `${this.site}/api/new/v2/chapters/${chapterId}`; - const res = await fetchApi(url); - const json = await res.json(); + let json = await this.fetchChapterContent(url); + + if (json.content_format === 'locked') { + // Free chapters are gated behind a Cloudflare Turnstile check. Once it + // has been passed in WebView, the session can mint a read token for + // this chapter the same way the site's own reader does. + const readToken = await this.mintReadToken(json); + if (readToken.token) { + json = await this.fetchChapterContent(url, readToken.token); + } + if (json.content_format === 'locked') { + const error: HttpError = new Error( + 'Fenrir Realm requires a human verification check to read this chapter. Open it in WebView, complete the check, then try again.', + ); + // Keep the read-token status (403 without a verified session) so + // callers can tell an anti-bot refusal from a parsing failure. + error.status = readToken.status; + throw error; + } + } + const content = json.content; if (content) { @@ -303,7 +337,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { } // Fallback or old method - const url = `${this.site}/series/${chapterPath.split('~~')[0]}`; + const url = this.resolveUrl(chapterPath); const result = await fetchApi(url); const body = await result.text(); @@ -380,6 +414,42 @@ class FenrirRealmPlugin implements Plugin.PluginBase { return chapterText; } + async fetchChapterContent( + url: string, + readToken?: string, + ): Promise { + const res = await fetchApi(url, { + headers: readToken ? { Authorization: `Bearer ${readToken}` } : {}, + }); + if (!res.ok) { + const error: HttpError = new Error( + `Failed to load chapter (HTTP ${res.status}). Open it in WebView and try again.`, + ); + error.status = res.status; + throw error; + } + return res.json(); + } + + async mintReadToken(chapter: APIChapterContent): Promise { + const chapterId = chapter.remint_chapter_id || chapter.uuid || chapter.id; + try { + const res = await fetchApi(`${this.site}/api/read-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ chapter_id: String(chapterId) }), + }); + if (!res.ok) return { status: res.status }; + const json = await res.json(); + return json.active && json.token ? { token: json.token } : {}; + } catch { + return {}; + } + } + async searchNovels( searchTerm: string, pageNo: number, @@ -417,7 +487,12 @@ class FenrirRealmPlugin implements Plugin.PluginBase { } resolveUrl(path: string): string { - return `${this.site}/series/${path.split('~~')[0]}`; + // Volume chapter slugs now include the volume (e.g. "vol1/1"), so paths + // built as novel/vol1/vol1/1 must drop the repeated volume segment. + const chapterPath = path + .split('~~')[0] + .replace(/^([^/]+)\/([^/]+)\/\2\//, '$1/$2/'); + return `${this.site}/series/${chapterPath}`; } filters = { From b5705fbe2cd68b99235364778fe6e98f18550315 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Sun, 18 Oct 2026 09:04:47 +0800 Subject: [PATCH 2/4] no-mistakes(review): fix(english/fenrirrealm): fall back to HTML on non-blocking API errors --- plugins/english/fenrirrealm.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/english/fenrirrealm.ts b/plugins/english/fenrirrealm.ts index d496dac8c..341a29c36 100644 --- a/plugins/english/fenrirrealm.ts +++ b/plugins/english/fenrirrealm.ts @@ -422,11 +422,14 @@ class FenrirRealmPlugin implements Plugin.PluginBase { headers: readToken ? { Authorization: `Bearer ${readToken}` } : {}, }); if (!res.ok) { - const error: HttpError = new Error( - `Failed to load chapter (HTTP ${res.status}). Open it in WebView and try again.`, - ); - error.status = res.status; - throw error; + if ([401, 403, 429, 503].includes(res.status)) { + const error: HttpError = new Error( + `Failed to load chapter (HTTP ${res.status}). Open it in WebView and try again.`, + ); + error.status = res.status; + throw error; + } + return { id: 0, uuid: null, content: null, remint_chapter_id: null }; } return res.json(); } From ce1d001c1886d3dc3ef4d1db5415907f6d0642b1 Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Sun, 18 Oct 2026 09:13:33 +0800 Subject: [PATCH 3/4] no-mistakes(ci): The CI failure was in `popularNovels`, not `parseChapter`. On the runner, the series list request came back as a non-2xx block page, and the code replaced it with a generic error that had no status. The live check couldn't classify that, so it reported FAIL. I changed `popularNovels` in `plugins/english/fenrirrealm.ts` so that a non-OK response now throws the same user-facing "open it in WebView" message with `error.status` set to the real HTTP status. A JSON parse failure on a 2xx response still throws the same message with no status, so a genuine parsing problem still reports as a failure. I didn't edit `scripts/live-check-plugin.js` or the workflow. The `parseChapter` paths already carried real statuses. The read-token 403 and the chapter API's 401/403/429/503 responses both set `error.status`. In the earlier run `parseChapter` never ran, because the check stopped at `popularNovels`. Prettier and ESLint pass. A local `node scripts/live-check-plugin.js plugins/english/fenrirrealm.ts` gave PASS for `popularNovels`, `searchNovels` and `parseNovel`, and INCONCLUSIVE for `parseChapter` with HTTP 403. Locally the list request wasn't blocked, so I couldn't reproduce the runner's `popularNovels` failure. I haven't confirmed that the runner's response was a 403 or 503. If it was a different non-2xx status, the check will still report FAIL. I made no version change here; the file is at 1.1.4 --- plugins/english/fenrirrealm.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/english/fenrirrealm.ts b/plugins/english/fenrirrealm.ts index 341a29c36..64644478b 100644 --- a/plugins/english/fenrirrealm.ts +++ b/plugins/english/fenrirrealm.ts @@ -124,13 +124,21 @@ class FenrirRealmPlugin implements Plugin.PluginBase { const res = await fetchApi( `${this.site}/api/new/v2/series?${params.toString()}`, - ).then(r => - r.json().catch(() => { - throw new Error( + ).then(r => { + const failure = () => { + const error: HttpError = new Error( 'There was an error fetching the data from the server. Please try to open it in WebView', ); - }), - ); + // Keep the real HTTP status so an anti-bot refusal is distinguishable + // from a parsing failure. + if (!r.ok) error.status = r.status; + return error; + }; + if (!r.ok) throw failure(); + return r.json().catch(() => { + throw failure(); + }); + }); return (res.data || []).map((r: APINovel) => this.parseNovelFromApi(r)); } From e0f3c3973440b51d74a3343afe6dd21be712a40e Mon Sep 17 00:00:00 2001 From: Raiyn Aydin Date: Sun, 18 Oct 2026 09:20:24 +0800 Subject: [PATCH 4/4] fix(english/fenrirrealm): use corrected chapter path for SvelteKit fallback The __data.json fallback still built its URL from the raw chapter path, so volume chapters requested novel/vol1/vol1/1. Build it from resolveUrl so it hits the same chapter page as the HTML fallback. --- plugins/english/fenrirrealm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/english/fenrirrealm.ts b/plugins/english/fenrirrealm.ts index 64644478b..9c7438c66 100644 --- a/plugins/english/fenrirrealm.ts +++ b/plugins/english/fenrirrealm.ts @@ -373,7 +373,7 @@ class FenrirRealmPlugin implements Plugin.PluginBase { // Fallback to SvelteKit JSON if HTML parsing fails or is empty try { - const jsonUrl = `${this.site}/series/${chapterPath.split('~~')[0]}/__data.json?x-sveltekit-invalidated=001`; + const jsonUrl = `${this.resolveUrl(chapterPath)}/__data.json?x-sveltekit-invalidated=001`; const jsonRes = await fetchApi(jsonUrl); const json: Nodes = await jsonRes.json();