chore: drop asuracomic.net from the userscript, CORS allowlist and docs (#96)

The old domain's deep links 301 to the asurascans.com root, discarding the
path, so a page on it never yields a series document and a stored address on
it never yields a series page. #94 pinned each Site to one hostname, which
already rejects such an address server-side; this removes the remaining
references so nothing invites a Reader onto the dead host.

Live deploys still carry the origin in their own .env and must drop it there
too - the allowlist is read from the environment, not from these defaults.
This commit is contained in:
2026-08-12 05:49:35 +07:00
parent 21615be2bd
commit 8ae98816eb
8 changed files with 29 additions and 23 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ OWNER_DISCORD_ID=changeme-your-discord-user-id
# Comma-separated origins allowed to call the API (CORS). Both Asura domains # Comma-separated origins allowed to call the API (CORS). Both Asura domains
# plus Demonic, Comix, Kagane, and the two novel sites. Add/remove as the # plus Demonic, Comix, Kagane, and the two novel sites. Add/remove as the
# sites' hostnames change. # sites' hostnames change.
ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net ALLOWED_ORIGINS=https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net
# Password for the bundled Postgres container, and therefore half of the # Password for the bundled Postgres container, and therefore half of the
# DATABASE_URL compose builds for the backend. Generate one: # DATABASE_URL compose builds for the backend. Generate one:
+1 -1
View File
@@ -6,7 +6,7 @@ Guidance for OpenCode (and Claude Code) working in this repo.
Read-progress tracker for two libraries — manga and novels — behind one self-hosted Go backend. Two separate Violentmonkey userscripts inject on-page UI (floating button + slide-in panel) and sync progress, so bookmarks unify across sites and devices: Read-progress tracker for two libraries — manga and novels — behind one self-hosted Go backend. Two separate Violentmonkey userscripts inject on-page UI (floating button + slide-in panel) and sync progress, so bookmarks unify across sites and devices:
- `manga-bookmark.user.js` — **asurascans.com** (current domain; asuracomic.net 301s here), **demonicscans.org**, **comix.to**, **kagane.to**. - `manga-bookmark.user.js` — **asurascans.com** (asuracomic.net is dropped: its deep links 301 to the asurascans.com root, discarding the path), **demonicscans.org**, **comix.to**, **kagane.to**.
- `novel-bookmark.user.js` — **novelfull.com**, **lightnovelworld.net**. - `novel-bookmark.user.js` — **novelfull.com**, **lightnovelworld.net**.
One backend, one `bookmarks` table: a `kind` column (`manga`|`novel`) splits the libraries and the web UI switches between them. Rows are keyed `<site>:<series_id>`. One backend, one `bookmarks` table: a `kind` column (`manga`|`novel`) splits the libraries and the web UI switches between them. Rows are keyed `<site>:<series_id>`.
+1 -1
View File
@@ -43,7 +43,7 @@ TOKEN_KEY=<paste output of: openssl rand -hex 32>
OWNER_DISCORD_ID=<discord user id> OWNER_DISCORD_ID=<discord user id>
# CORS allowlist — leave as-is unless a site changes hostname. # CORS allowlist — leave as-is unless a site changes hostname.
ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to ALLOWED_ORIGINS=https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net
# Required — password for the bundled Postgres container. Compose builds the # Required — password for the bundled Postgres container. Compose builds the
# backend's DATABASE_URL out of it and has no fallback for either. # backend's DATABASE_URL out of it and has no fallback for either.
+7 -6
View File
@@ -1,6 +1,6 @@
# Manga Bookmark # Manga Bookmark
Track manga read-progress on **asurascans.com** (a.k.a. asuracomic.net), Track manga read-progress on **asurascans.com**,
**demonicscans.org**, **comix.to**, and **kagane.to** from a phone (Bromite / **demonicscans.org**, **comix.to**, and **kagane.to** from a phone (Bromite /
mobile Chromium), synced to a self-hosted Go backend so bookmarks unify across mobile Chromium), synced to a self-hosted Go backend so bookmarks unify across
all four sites and all devices. all four sites and all devices.
@@ -274,11 +274,12 @@ the backend acquires, stores and serves every Cover from its own origin
| **Kagane** (`kagane.to`) | `/series/<uuid>` | `/series/<uuid>/reader/<bookUuid>` | `<uuid>` | | **Kagane** (`kagane.to`) | `/series/<uuid>` | `/series/<uuid>/reader/<bookUuid>` | `<uuid>` |
Notes: Notes:
- **`asuracomic.net` deep links are dead (re-checked 2026-07-25).** They 301 to - **`asuracomic.net` is no longer matched (deep links dead, re-checked
the `asurascans.com` **root**, discarding the path, at the edge — before the 2026-07-25).** They 301 to the `asurascans.com` **root**, discarding the path,
userscript gets a document — so nothing client-side can rescue them. Reach at the edge — before the userscript gets a document — so nothing client-side
series through `asurascans.com`. The host stays matched in case the redirect can rescue them. The backend rejects stored addresses on that host too, since
starts preserving paths again. the poller pins each Site to one hostname. Reach series through
`asurascans.com`.
- Asura `og:title` carries a `Chapter N - Read Online \| Asura Scans` suffix that - Asura `og:title` carries a `Chapter N - Read Online \| Asura Scans` suffix that
the adapter strips; Demonic chapter `og:title` is `<Title> Chapter N`. the adapter strips; Demonic chapter `og:title` is `<Title> Chapter N`.
- Demonic's `<slug>` is identical on `/manga/…` and the canonical `/title/…` - Demonic's `<slug>` is identical on `/manga/…` and the canonical `/title/…`
+6 -6
View File
@@ -33,7 +33,7 @@ const testCoverBaseURL = "https://bookmarks.test"
func testConfig() Config { func testConfig() Config {
return Config{ return Config{
TokenKey: testTokenKey, TokenKey: testTokenKey,
AllowedOrigins: []string{"https://asuracomic.net", "https://demonicscans.org"}, AllowedOrigins: []string{"https://asurascans.com", "https://demonicscans.org"},
Port: "8080", Port: "8080",
} }
} }
@@ -169,7 +169,7 @@ func TestAuthAccepted(t *testing.T) {
func TestCORSPreflight(t *testing.T) { func TestCORSPreflight(t *testing.T) {
srv := newTestServer(t) srv := newTestServer(t)
req := httptest.NewRequest(http.MethodOptions, "/bookmarks/asura:foo-1", nil) req := httptest.NewRequest(http.MethodOptions, "/bookmarks/asura:foo-1", nil)
req.Header.Set("Origin", "https://asuracomic.net") req.Header.Set("Origin", "https://asurascans.com")
req.Header.Set("Access-Control-Request-Method", "PUT") req.Header.Set("Access-Control-Request-Method", "PUT")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req) srv.ServeHTTP(rr, req)
@@ -177,7 +177,7 @@ func TestCORSPreflight(t *testing.T) {
if rr.Code != http.StatusNoContent { if rr.Code != http.StatusNoContent {
t.Fatalf("preflight status = %d, want 204", rr.Code) t.Fatalf("preflight status = %d, want 204", rr.Code)
} }
if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://asuracomic.net" { if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://asurascans.com" {
t.Fatalf("Allow-Origin = %q, want reflected origin", got) t.Fatalf("Allow-Origin = %q, want reflected origin", got)
} }
if got := rr.Header().Get("Access-Control-Allow-Methods"); got == "" { if got := rr.Header().Get("Access-Control-Allow-Methods"); got == "" {
@@ -204,11 +204,11 @@ func TestBookmarkRoundTrip(t *testing.T) {
key := "asura:solo-leveling-123" key := "asura:solo-leveling-123"
in := store.Bookmark{ in := store.Bookmark{
Title: "Solo Leveling", Title: "Solo Leveling",
SeriesURL: "https://asuracomic.net/series/solo-leveling-123", SeriesURL: "https://asurascans.com/series/solo-leveling-123",
Cover: "https://asuracomic.net/cover.jpg", Cover: "https://asurascans.com/cover.jpg",
LastChapter: "Chapter 10", LastChapter: "Chapter 10",
LastChapterNum: 10, LastChapterNum: 10,
LastChapterURL: "https://asuracomic.net/series/solo-leveling-123/chapter/10", LastChapterURL: "https://asurascans.com/series/solo-leveling-123/chapter/10",
} }
body, _ := json.Marshal(in) body, _ := json.Marshal(in)
+1 -1
View File
@@ -25,7 +25,7 @@ services:
# Owner's Discord user ID — required. Seeds the owner Reader (the # Owner's Discord user ID — required. Seeds the owner Reader (the
# administrator); every other Reader registers on their first login. # administrator); every other Reader registers on their first login.
OWNER_DISCORD_ID: ${OWNER_DISCORD_ID:?set OWNER_DISCORD_ID in .env} OWNER_DISCORD_ID: ${OWNER_DISCORD_ID:?set OWNER_DISCORD_ID in .env}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to,https://novelfull.com,https://lightnovelworld.net}
# The bookmarks database. Host is the compose service name; the password # The bookmarks database. Host is the compose service name; the password
# comes from .env so it is never committed. # comes from .env so it is never committed.
DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable} DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable}
+4 -7
View File
@@ -6,7 +6,6 @@
// @author you // @author you
// @downloadURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/manga-bookmark.user.js // @downloadURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/manga-bookmark.user.js
// @updateURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/manga-bookmark.user.js // @updateURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/manga-bookmark.user.js
// @match https://asuracomic.net/*
// @match https://asurascans.com/* // @match https://asurascans.com/*
// @match https://demonicscans.org/* // @match https://demonicscans.org/*
// @match https://comix.to/* // @match https://comix.to/*
@@ -112,12 +111,10 @@
const asura = { const asura = {
site: "asura", site: "asura",
// asuracomic.net deep links 301 to the asurascans.com *root*, dropping the // asuracomic.net is not matched: its deep links 301 to the asurascans.com
// path, and that happens at the edge before this script gets a document — // *root* at the edge, discarding the path, so this script never sees a
// so those URLs cannot be handled here at all (checked 2026-07-25). It stays // series document there (re-checked 2026-07-25).
// matched in case the redirect starts preserving paths again; until then, matches: (loc) => /(^|\.)asurascans\.com$/.test(loc.hostname),
// reach series through asurascans.com.
matches: (loc) => /(^|\.)asurascans\.com$|(^|\.)asuracomic\.net$/.test(loc.hostname),
detect(loc) { detect(loc) {
const path = loc.pathname; const path = loc.pathname;
// /comics/<slug-hash>/chapter/<n> // /comics/<slug-hash>/chapter/<n>
+8
View File
@@ -120,6 +120,14 @@ test("asura.detect returns other for non-series paths", () => {
assert.equal(asura.detect(loc("https://asurascans.com/bookmarks")).type, "other"); assert.equal(asura.detect(loc("https://asurascans.com/bookmarks")).type, "other");
}); });
test("asura.matches accepts only asurascans.com", () => {
assert.equal(asura.matches({ hostname: "asurascans.com" }), true);
assert.equal(asura.matches({ hostname: "www.asurascans.com" }), true);
// Dead domain: deep links 301 to the asurascans.com root, discarding the path.
assert.equal(asura.matches({ hostname: "asuracomic.net" }), false);
assert.equal(asura.matches({ hostname: "asurascans.com.evil.example" }), false);
});
test("asura.latestChapterFromAnchors takes the highest and skips the First Chapter shortcut", () => { test("asura.latestChapterFromAnchors takes the highest and skips the First Chapter shortcut", () => {
const best = asura.latestChapterFromAnchors([ const best = asura.latestChapterFromAnchors([
{ href: "/comics/solo-leveling-059befe1/chapter/1", text: "Chapter 1" }, { href: "/comics/solo-leveling-059befe1/chapter/1", text: "Chapter 1" },