feat: one registry entry per Site, one shared Series-page read #95

Merged
sulthan merged 7 commits from feat/94-site-registry into main 2026-08-12 05:43:39 +07:00
Owner

Closes #94.

What

Two phases per the spec, in three feature commits plus two review-fix commits:

Phase one — one registry entry per Site (2d134fb)
The six per-site comparison points that used to live across three files collapse
into one sites map in backend/internal/latest/sites.go: Latest Chapter parse,
Cover parse, browser-backed list, fetcher route, host pins, and the browser
payload read all become lookups into it. browserBackedSites() is derived from
the registry (sorted, deterministic); fetcherFor and fetchableSeriesURL keep
their signatures and become lookups; BrowserFetcher.Get dispatches through the
entries' Read/Done while the tab lifecycle stays in BrowserFetcher.run.

Phase two — one shared Series-page read (f215130)
readSeriesPage (new read.go) performs the read the Poll and the Acquisition
have in common: gate, route, fetch, parse Latest Chapter, parse Cover address.
It returns facts only — polling and persistence policies (stamp order, cooldowns,
cover policy) stay with the callers; acquire.go gained the comment naming the
deliberate post-fetch stamp order. The poll's legacy cover heal and the
no-chapter byte-count diagnostic were restored after review (d998f87) so the
claims "the Poll keeps its own Cover policy" and "pinning is the only
behavioural change" both hold.

Behaviour

  • All six Sites now pin their host exactly; asura/demonic/comix previously
    accepted any https host. For asura this is a strict improvement: its dead old
    domain redirects deep links to the site root and would parse the wrong
    document.
  • Everything else is unchanged: existing parse tables, the challenge-body table
    and the gate table pass unmodified except the one deliberate exception — the
    gate table gains the three new pin cases.

Security invariants preserved

  • The address gate is recognisably the same rule, now a single registry lookup:
    https + exact hostname match, all callers route through it. No fetch path
    was widened; asura/demonic/comix were narrowed.
  • The second host pin inside each browser entry's Read is retained deliberately
    (browser = strong SSRF primitive, series_url is client-supplied) and is not
    deduplicated against the shared gate.
  • Review hardening: fetcherFor now fails closed for unknown site strings
    (previously fell through to the TLS fetcher on an unreachable path), and the
    browser dispatch iterates a sorted list so outcomes cannot depend on map order.
  • The security review's log-injection finding was checked against Go's
    url.Parse and does not hold: control characters are rejected anywhere in a
    URL, so a client-supplied value in a log line cannot carry a newline.

Review

Reviewed on three axes (spec, standards, security) by read-only subagents over
672c16f..f1b26f4. No blocking findings; all minor/nit findings addressed in
d998f87 and 700de20. Verified end to end with go test ./... (Docker
Postgres per test package) on every commit.

Out of scope (tracked separately)

  • Dropping asuracomic.net (CORS allowlist, userscript match, API fixtures,
    live env) — separate issue, per spec.
Closes #94. ## What Two phases per the spec, in three feature commits plus two review-fix commits: **Phase one — one registry entry per Site** (`2d134fb`) The six per-site comparison points that used to live across three files collapse into one `sites` map in `backend/internal/latest/sites.go`: Latest Chapter parse, Cover parse, browser-backed list, fetcher route, host pins, and the browser payload read all become lookups into it. `browserBackedSites()` is derived from the registry (sorted, deterministic); `fetcherFor` and `fetchableSeriesURL` keep their signatures and become lookups; `BrowserFetcher.Get` dispatches through the entries' `Read`/`Done` while the tab lifecycle stays in `BrowserFetcher.run`. **Phase two — one shared Series-page read** (`f215130`) `readSeriesPage` (new `read.go`) performs the read the Poll and the Acquisition have in common: gate, route, fetch, parse Latest Chapter, parse Cover address. It returns facts only — polling and persistence policies (stamp order, cooldowns, cover policy) stay with the callers; `acquire.go` gained the comment naming the deliberate post-fetch stamp order. The poll's legacy cover heal and the no-chapter byte-count diagnostic were restored after review (`d998f87`) so the claims "the Poll keeps its own Cover policy" and "pinning is the only behavioural change" both hold. ## Behaviour - All six Sites now pin their host exactly; asura/demonic/comix previously accepted any https host. For asura this is a strict improvement: its dead old domain redirects deep links to the site root and would parse the wrong document. - Everything else is unchanged: existing parse tables, the challenge-body table and the gate table pass unmodified except the one deliberate exception — the gate table gains the three new pin cases. ## Security invariants preserved - The address gate is recognisably the same rule, now a single registry lookup: `https` + exact hostname match, all callers route through it. No fetch path was widened; asura/demonic/comix were narrowed. - The second host pin inside each browser entry's Read is retained deliberately (browser = strong SSRF primitive, `series_url` is client-supplied) and is not deduplicated against the shared gate. - Review hardening: `fetcherFor` now fails closed for unknown site strings (previously fell through to the TLS fetcher on an unreachable path), and the browser dispatch iterates a sorted list so outcomes cannot depend on map order. - The security review's log-injection finding was checked against Go's `url.Parse` and does not hold: control characters are rejected anywhere in a URL, so a client-supplied value in a log line cannot carry a newline. ## Review Reviewed on three axes (spec, standards, security) by read-only subagents over `672c16f..f1b26f4`. No blocking findings; all minor/nit findings addressed in `d998f87` and `700de20`. Verified end to end with `go test ./...` (Docker Postgres per test package) on every commit. ## Out of scope (tracked separately) - Dropping asuracomic.net (CORS allowlist, userscript match, API fixtures, live env) — separate issue, per spec.
sulthan added 7 commits 2026-08-12 00:34:53 +07:00
Collapse the six per-site comparison points into a sites map in sites.go:
Latest Chapter parse, Cover parse, browser-backed list, fetcher route,
host pins, and the browser payload read all become lookups into it. All
six hosts are now pinned in fetchableSeriesURL; asura/demonic/comix were
previously accepted on any https host.
Extract gate, route, fetch and both parses into readSeriesPage, which
returns facts only; checkOne and acquire keep their scheduling, stamp
order and persistence policies. The poller also stops logging the fetched
body length, which only made sense while the body lived at the call site.
Restore two review findings against spec claims 'the Poll keeps its own
Cover policy' and 'pinning is the only behavioural change': prefetchCover
now also runs on ticks where the page fetch itself fails (the heal is
independent of the page read, and its source may answer while the origin
does not), and the no-chapter log surfaces the fetched body length again
via a BodyLen fact on seriesRead. Browser dispatch iterates the sorted
browser site list so its outcome cannot depend on map order.
Security review found the route's old default handed the TLS fetcher to
any unrecognised site string; unreachable today because the shared read
gates first, but a trap for a future caller that skips the gate. Returns
nil now, matching the registry's zero-entry handling and the function's
own docstring. Review's log-injection finding (unquoted series_url in
read.go error wraps) was verified against Go's url.Parse and does not
hold: control characters are rejected anywhere in the URL, so a
client-supplied value reaching the log cannot carry a newline.
sulthan merged commit 21615be2bd into main 2026-08-12 05:43:39 +07:00
Sign in to join this conversation.