Delete the kagane-specific cover path (#63) #73

Merged
sulthan merged 2 commits from feat/63-delete-kagane-cover-path into main 2026-08-10 18:02:47 +07:00
Owner

Closes #63

Deletes the second way to reach a Cover. Since #62, every Site's cover bytes land in the content-addressed store at creation or on the poll, and the one public route serves them all — nothing needs the kagane proxy anymore.

What went

  • Template-level rewrite: Bookmark.CoverURL() and both templates' use of it. Cards and chrome now render .Cover — the wire value — and nothing else. Bookmark.CoverSource was dead once CoverURL went, so it and its bookmarkColumns entry are gone too.
  • Kagane-only cover route and its identifier validation: GET /img/kagane/{id}, web.CoverFetcher, coverIDRe, and the whole internal/web/cover.go.
  • The proxy's persistence: store.KaganeImageID, GetKaganeCover, PutKaganeCover, kaganeCoverSourceURL, kaganeCoverRe.
  • The kagane-shaped branch in the byte-fetch routing: fetchCoverBytes no longer takes a site argument and no longer names a Site. The URL shape kagane's API publishes is claimed by the browser module itself — kaganeImageURLRe + browserCoverURL live in latest/browser.go with the rest of the per-Site knowledge — and BrowserFetcher.Image is now URL-driven (it validates the URL it will navigate to, same SSRF discipline as before). The no-plain-TLS-fallback rule for a claimed URL is preserved: a claimed address with no browser is an error, never a challenge-page fetch.

What stayed (deliberately)

  • BrowserFetcher.Image and the browser-backed acquisition path: kagane genuinely serves cover bytes behind the challenge + cross-origin-resource-policy: same-origin, so the sidecar remains the only fetcher for them — it just routes by URL claim now instead of by Site name.
  • fetcherFor's per-Site page routing (kagane/novelfull page fetches) — that is the page path, not a cover path.

Acceptance criteria

  • Template-level kagane cover rewrite gone
  • Kagane-only cover route and its identifier validation gone
  • Tests removed/rewritten against the general route, guarantees kept: unstored + traversal-shaped addresses serve nothing (TestPublicCoverRejectsUnknownAddress), non-image content types never echoed (TestPublicCoverNeverEchoesNonImage — new; the store-side gate was already pinned by TestCoverStoreAcceptsAnySourceURL). Store reopen-persistence and filesystem content-addressing tests rewritten against PutCover/GetCover, no guarantee lost.
  • No Site name in a cover code path outside the acquisition module (grep kagane backend: store/web/templates/api are clean; remaining hits are latest/browser.go + latest/sites.go, tests, docs)
  • Web UI and panel render Covers for all six Sites (templates render the wire address; panel renders b.cover — untouched, it never had a kagane path)
  • go test ./... green

Verification

  • go vet ./... clean
  • go test ./... — all packages pass (root 16.9s, latest 12.7s, store 12.7s, web 0.004s)
  • CGO_ENABLED=0 go build produces the static binary
  • Cover-path tests run verbosely: TestPublicCoverServesStoredBytesUnauthenticated, TestPublicCoverRejectsUnknownAddress (unknown/malformed/traversal/empty), TestPublicCoverNeverEchoesNonImage, TestListRendersAcquiredCover, TestAcquireKaganeCoverThroughBrowser, TestRunOncePrefetchesKaganeCover, TestRunOnceRoutesNonKaganeCoverToPublicFetcher all pass; the three SMOKE_* tests skip without the browser sidecar, as designed

Live browser verification of the "web UI and panel render Covers for all six Sites" criterion is being run separately with Playwright against real Site pages and a locally mocked backend.

Closes #63 Deletes the second way to reach a Cover. Since #62, every Site's cover bytes land in the content-addressed store at creation or on the poll, and the one public route serves them all — nothing needs the kagane proxy anymore. ## What went - **Template-level rewrite:** `Bookmark.CoverURL()` and both templates' use of it. Cards and chrome now render `.Cover` — the wire value — and nothing else. `Bookmark.CoverSource` was dead once `CoverURL` went, so it and its `bookmarkColumns` entry are gone too. - **Kagane-only cover route and its identifier validation:** `GET /img/kagane/{id}`, `web.CoverFetcher`, `coverIDRe`, and the whole `internal/web/cover.go`. - **The proxy's persistence:** `store.KaganeImageID`, `GetKaganeCover`, `PutKaganeCover`, `kaganeCoverSourceURL`, `kaganeCoverRe`. - **The kagane-shaped branch in the byte-fetch routing:** `fetchCoverBytes` no longer takes a `site` argument and no longer names a Site. The URL shape kagane's API publishes is claimed by the browser module itself — `kaganeImageURLRe` + `browserCoverURL` live in `latest/browser.go` with the rest of the per-Site knowledge — and `BrowserFetcher.Image` is now URL-driven (it validates the URL it will navigate to, same SSRF discipline as before). The no-plain-TLS-fallback rule for a claimed URL is preserved: a claimed address with no browser is an error, never a challenge-page fetch. ## What stayed (deliberately) - `BrowserFetcher.Image` and the browser-backed acquisition path: kagane genuinely serves cover bytes behind the challenge + `cross-origin-resource-policy: same-origin`, so the sidecar remains the only fetcher for them — it just routes by URL claim now instead of by Site name. - `fetcherFor`'s per-Site page routing (kagane/novelfull page fetches) — that is the page path, not a cover path. ## Acceptance criteria - [x] Template-level kagane cover rewrite gone - [x] Kagane-only cover route and its identifier validation gone - [x] Tests removed/rewritten against the general route, guarantees kept: unstored + traversal-shaped addresses serve nothing (`TestPublicCoverRejectsUnknownAddress`), non-image content types never echoed (`TestPublicCoverNeverEchoesNonImage` — new; the store-side gate was already pinned by `TestCoverStoreAcceptsAnySourceURL`). Store reopen-persistence and filesystem content-addressing tests rewritten against `PutCover`/`GetCover`, no guarantee lost. - [x] No Site name in a cover code path outside the acquisition module (`grep kagane backend`: store/web/templates/api are clean; remaining hits are `latest/browser.go` + `latest/sites.go`, tests, docs) - [x] Web UI and panel render Covers for all six Sites (templates render the wire address; panel renders `b.cover` — untouched, it never had a kagane path) - [x] `go test ./...` green ## Verification - `go vet ./...` clean - `go test ./...` — all packages pass (root 16.9s, latest 12.7s, store 12.7s, web 0.004s) - `CGO_ENABLED=0 go build` produces the static binary - Cover-path tests run verbosely: `TestPublicCoverServesStoredBytesUnauthenticated`, `TestPublicCoverRejectsUnknownAddress` (unknown/malformed/traversal/empty), `TestPublicCoverNeverEchoesNonImage`, `TestListRendersAcquiredCover`, `TestAcquireKaganeCoverThroughBrowser`, `TestRunOncePrefetchesKaganeCover`, `TestRunOnceRoutesNonKaganeCoverToPublicFetcher` all pass; the three `SMOKE_*` tests skip without the browser sidecar, as designed Live browser verification of the "web UI and panel render Covers for all six Sites" criterion is being run separately with Playwright against real Site pages and a locally mocked backend.
sulthan added 1 commit 2026-08-10 11:26:51 +07:00
The web proxy for kagane covers is dead: since #62 every Site's cover
bytes land in the content-addressed store at creation or on the poll,
and the one public route serves them all. Remove the second way to
reach a Cover:

- Bookmark.CoverURL() and the templates' use of it: templates render
  the wire value (.Cover) and nothing else.
- GET /img/kagane/{id}, web.CoverFetcher, coverIDRe: the route and its
  identifier validation are gone, and with them web/cover.go.
- store.KaganeImageID, GetKaganeCover, PutKaganeCover,
  kaganeCoverSourceURL: the proxy's persistence.
- Bookmark.CoverSource: dead once CoverURL is gone.

Acquisition keeps the browser where kagane genuinely needs it, but the
Site name leaves the routing: kaganeImageURLRe lives in browser.go with
the rest of the per-Site knowledge, browserCoverURL claims the URLs the
sidecar alone can fetch, and fetchCoverBytes routes by URL shape with
no Site argument. No plain-TLS fallback for a claimed URL — that would
only retrieve a challenge page.

Cover tests: kagane route tests removed, the general-route guarantees
they pinned kept and re-pinned — unstored and traversal-shaped
addresses serve nothing (TestPublicCoverRejectsUnknownAddress), non-image
content types are never echoed back (TestPublicCoverNeverEchoesNonImage
+ TestCoverStoreAcceptsAnySourceURL). Store content-addressing and
reopen-persistence tests rewritten against PutCover/GetCover.

No Site name remains in a cover code path outside the acquisition
module; go test ./... green.
sulthan added 1 commit 2026-08-10 11:36:58 +07:00
- Move the kagane cover URL shape into the extraction module (sites.go):
  browserOnlyCoverURL + kaganeImageURLRe now own the claim; the byte-fetch
  router and BrowserFetcher.Image reference it. One shape gate for producer
  and fetcher (the id regex is folded into the full-URL match), so no Site
  name appears in a cover path outside the extraction module and the
  producer cannot emit an address the fetch would refuse.
- Restore the serving-boundary guarantee: GET /covers/{addr} re-checks the
  stored media type via store.CoverContentType and 404s a poisoned row;
  TestPublicCoverNeverEchoesNonImage now seeds one directly behind the
  write gate and pins the refusal where bytes leave.
- Restore the SSRF rationale (client-supplied stored URL, headless browser
  as a strong primitive) on the URL regex.
sulthan merged commit 7c7d597019 into main 2026-08-10 18:02:47 +07:00
Sign in to join this conversation.