Define Cover and record hosting its bytes (#47) #64

Merged
sulthan merged 1 commits from docs/cover-ownership into main 2026-08-09 23:19:38 +07:00
Owner

Defines Cover in the glossary and records ADR-0007, the decision behind #47's fix.

Why these two files, and why now

CONTEXT.md named Cover inside the Series entry — "facts true regardless of who is reading — title, cover, Latest Chapter" — but never said what one is. That gap is the bug. Nothing in the model distinguished "an address on a Site" from "an image a Reader's browser can display", so both clients were left to work it out independently, and one of them got it wrong. kagane serves covers with cross-origin-resource-policy: same-origin, the web UI rewrote them to a proxy in its templates, the JSON API did not, and the panel rendered a broken-image glyph. The new entry closes the ambiguity: an address no client can load is not a Cover, it is a missing one.

ADR-0007 records what follows from that — the backend fetches, stores and serves every Site's cover bytes — plus the alternatives that were rejected and, more importantly, the two places this deliberately departs from existing precedent:

  • Destination-class control instead of a host allowlist. fetchableSeriesURL sets the allowlist precedent for series_url, and covers do not follow it. Cover hosts are CDNs that move independently of their Site — demonicscans serves its covers from readermc.org — so an allowlist would stop producing Covers the day a Site switched CDN, and that failure would look exactly like #47. The resolve-then-classify step is what actually stops the SSRF.
  • A public cover route where the kagane proxy is session-gated. An <img> cannot send a bearer token, and it cannot be given one either: the panel's shadow root is mode: "open", so the host page's JavaScript can read any src the script sets.

Both are security-adjacent departures, which is precisely why they are written down rather than left in a commit message.

Scope

Documentation only — no code, no schema, no behaviour. The implementation is #56–#63.

Why this should merge promptly rather than sit

All eight implementation tickets cite docs/adr/0007-backend-hosts-cover-bytes.md as the authority for decisions they must not relitigate, and they are written in the vocabulary this glossary entry defines. An agent picking up #56 reads both from main. Until this lands they get a 404 and either invent a rationale or stall — so this PR gates the tickets, not the other way round.

Related: #47 (bug), #55 (spec), #54 (deferred admin refetch).

Defines **Cover** in the glossary and records ADR-0007, the decision behind #47's fix. ## Why these two files, and why now `CONTEXT.md` named Cover inside the **Series** entry — "facts true regardless of who is reading — title, cover, Latest Chapter" — but never said *what* one is. That gap is the bug. Nothing in the model distinguished "an address on a Site" from "an image a Reader's browser can display", so both clients were left to work it out independently, and one of them got it wrong. kagane serves covers with `cross-origin-resource-policy: same-origin`, the web UI rewrote them to a proxy in its templates, the JSON API did not, and the panel rendered a broken-image glyph. The new entry closes the ambiguity: *an address no client can load is not a Cover, it is a missing one.* ADR-0007 records what follows from that — the backend fetches, stores and serves every Site's cover bytes — plus the alternatives that were rejected and, more importantly, the two places this deliberately departs from existing precedent: - **Destination-class control instead of a host allowlist.** `fetchableSeriesURL` sets the allowlist precedent for `series_url`, and covers do not follow it. Cover hosts are CDNs that move independently of their Site — demonicscans serves its covers from `readermc.org` — so an allowlist would stop producing Covers the day a Site switched CDN, and that failure would look exactly like #47. The resolve-then-classify step is what actually stops the SSRF. - **A public cover route where the kagane proxy is session-gated.** An `<img>` cannot send a bearer token, and it cannot be given one either: the panel's shadow root is `mode: "open"`, so the host page's JavaScript can read any `src` the script sets. Both are security-adjacent departures, which is precisely why they are written down rather than left in a commit message. ## Scope Documentation only — no code, no schema, no behaviour. The implementation is #56–#63. ## Why this should merge promptly rather than sit All eight implementation tickets cite `docs/adr/0007-backend-hosts-cover-bytes.md` as the authority for decisions they must not relitigate, and they are written in the vocabulary this glossary entry defines. An agent picking up #56 reads both from `main`. Until this lands they get a 404 and either invent a rationale or stall — so this PR gates the tickets, not the other way round. Related: #47 (bug), #55 (spec), #54 (deferred admin refetch).
sulthan added 1 commit 2026-08-09 23:18:48 +07:00
Cover was named in the Series entry but never defined, and the gap is
what #47 is: nothing said whether a Cover is an address on a Site or an
image a Reader's browser can actually display. Kagane proved they are
not the same thing.

The ADR records the decision that follows — the backend fetches, stores
and serves every Site's cover bytes — along with the alternatives that
were rejected and the two knowing relaxations: destination-class control
instead of the host allowlist fetchableSeriesURL sets as precedent, and
a public cover route where the kagane proxy is session-gated.
sulthan merged commit 30c57bd39c into main 2026-08-09 23:19:38 +07:00
Sign in to join this conversation.