Covers render in the userscript panel, from a public route #60
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
Spec: #55. Originating bug: #47. Architecture and rejected alternatives:
docs/adr/0007-backend-hosts-cover-bytes.md. Domain vocabulary:CONTEXT.md.Do not close #47 or #55 from this ticket.
What to build
A Reader opens the panel on any Site and sees Covers — the same images the web UI shows, loaded from the deployment's own origin.
Two things have to be true for that, and the first explains the second. An
<img>cannot send anAuthorizationheader, and it cannot be handed a token in its URL either: the panel's shadow root is open, so the host page's own JavaScript can read anysrcattribute the script sets, and a credential in an image URL is a credential handed to a third-party site. The cover route therefore becomes public — no session, no credential. That is defensible rather than a concession: it serves public artwork from public Sites, and its address reveals nothing about which Reader holds what. It does knowingly relax the session gate the kagane proxy carries today, which the ADR records. Responses keep the long-lived immutable cache directive, which content addressing makes safe.On the client side, both userscripts lose their cover scraping entirely — the DOM scan, the metadata reads, and the
coverfield in the request body. A scraped address now has no rendering path left, and keeping it would put third-party URLs back into exactly the place this bug came from. This is a deletion, so the corresponding cases in the Node logic tests are deleted with it and the exported symbol lists shrink accordingly.Both userscripts gain an error handler on the cover image that swaps in the existing placeholder element when a load fails. This is the part worth doing even in isolation: it is the difference between a Reader seeing a designed empty state and a Reader seeing the browser's broken-image glyph, which is half of what #47 actually reported.
The error handler is DOM behaviour and is not testable in the userscript harness by design — that harness covers pure logic under a hand-written stub and must not grow a DOM. Verify it on-device and say so in the report rather than inventing coverage. Run the parse check and the logic tests for both scripts.
Acceptance criteria
go test ./...is greenBlocked by
PR: #69 (
feat/60-public-cover-route, closes this issue on merge).Nine of the ten boxes are ticked. What each rests on:
92eba07) and were verified here rather than re-implemented:GET /covers/{address}is registered outsidehttpmw.Authand outside the Discord session (backend/main.go:210-214), the handler reads no cookie or header (internal/api/handlers.go:142-158), the^[0-9a-f]{64}$check runs before the address becomes a path, and the response carriesCache-Control: public, max-age=604800, immutable. Asserted bybackend/cover_test.go:231-278.cover:field,coverFromPage()(the comiximg[alt]scan),metaName()and the orphaned novelmeta(). Nothing underuserscript/readsog:image,meta[name=image]orimg[alt]any more, anddelete body.coverinapiPutkeeps a stale pre-upgradelocalStoragerow from putting a third-party URL back on the wire.onerror: (e) => e.target.replaceWith(el("div", { class: "cover ph" }))in both card renderers — the same element the no-cover branch already builds, so it inherits the designed empty state instead of the broken-image glyph.origin/mainrather than assumed: both deleted helpers were module-private and no cover symbol was ever exported.go test -count=1 ./...green;node --checkclean on both scripts; 46/46 logic tests pass.As the ticket instructs, the
onerrorhandler got no fabricated harness coverage — the Node stub must not grow a DOM. It was exercised ad hoc instead: theel()helper and the exact render expression loaded into a headless Chromium with an unloadablesrcproduced<div class="cover ph"></div>.The one open box is yours: a comix Series bookmarked mid-chapter, checked on-device against the deployment. That needs a real install and a real panel.
Left alone deliberately: the kagane-specific proxy and its session gate (#63), the poll's blank-Cover fill (#61), browser-backed Sites (#62). #47 and #55 stay open.
Manual verification done in a real Chromium (Playwright), against
mainat8b58019.Setup: local stack (
docker compose up, poller off) withPUBLIC_BASE_URL=https://bookmark-api.violetcrown.my.id; the committedmanga-bookmark.user.jsrendered through/u/<credential>/…so the served copy (credential substituted, version stamped) is what ran, injected as an init script onhttps://comix.to. The API origin was routed to the local container at the network layer, so the page saw a normal HTTPS cross-origin backend.Result — comix Series bookmarked mid-chapter (Ch.66 of 70,
comix:2031"Happy Face: From Slave to Arena Legend"):coverfield; the backend acquired one at creation and stored it.GET /bookmarksreturnedcover: https://bookmark-api.violetcrown.my.id/covers/f88ea4f3…5ce8.img.coverwith thatsrc,naturalWidth×naturalHeight = 280×420,complete: true, no placeholder element — i.e. the image really decoded. The<img>sent noAuthorizationheader, so the public route is what served it.Read: Chapter 66 · Latest: Chapter 70, so the mid-chapter state is the one that rendered.imgat/covers/000…0(never stored → 404) removed the<img>and leftdiv.cover.ph, no broken-image glyph.Incidental, not a defect in this ticket: the acquisition fetch first failed from this network because the ISP hijacks DNS for
comix.to/static.comix.toand answers with an expired MITM certificate. Pinning both hostnames to the real Cloudflare address for the container made the fetch succeed. Deployment egress is unaffected.Local test state (
.env, containers, volumes) has been torn down; nothing in the working tree changed.