# Decisions Append-only. Never edit or delete an entry — supersede it with a new one that links back. One entry per decision a future maintainer would otherwise have to reverse-engineer. No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry (date, title, line number). ## 2026-10-14 — Adopt AGENTS.md as single source of truth **Status:** accepted **Context:** Agents had no durable project context and no consistent workflow between sessions. The design context lived in `HANDOVER.md`, which is now split into this file, the spec and `docs/TODO.md`. **Decision:** `AGENTS.md` is canonical; `CLAUDE.md`/`GEMINI.md` are pointers. Specs are permanent in `docs/specs/`. Plans live in `docs/plans/` only while work is in progress and are removed before the merge request. Both override the superpowers default paths. **Alternatives:** Per-tool rule files (drift, synchronisation debt); no rules file (agents re-explore the repo every session — measurably slower and more tokens). **Consequences:** Rule changes happen in one file; pointer files must stay thin. Plans are working artefacts: kept after merge they go stale and contradict the code, so the spec is the record. ## 2026-10-15 — Harden the server proxy and uploads (final review) **Status:** accepted (amends "Implementation details: server API" below) **Context:** The whole-branch review found that `/api/fetch` passed the upstream content type through. Anyone can archive an HTML page on Wayback and link a user to our `/api/fetch?url=…` for it: the script would run on our origin and could read a MangaBaka token saved with "remember on this device". `/api/upload` also accepted any Wayback URL and any bytes starting with `GIF`, so our domain could be used as a 30-day file drop and the store filled. **Decision:** - Proxied bodies are always `application/octet-stream` with `nosniff`, `Content-Security-Policy: sandbox; default-src 'none'` and `Content-Disposition: attachment`. Nothing in our clients reads the content type. - `/api/fetch` and `/api/upload` require the header `x-mbseed: 1`, which our page and userscript send and a link or form cannot. - `/api/upload` rehosts only from a source's image hosts (`Source.imageHosts`) or Wayback's copy of one, and only images 1–10 000 px a side. - The origin check compares hosts, so `https://host` passes behind a TLS proxy that Bun sees as `http://host`. **Alternatives:** A session cookie or the password gate (deferred, spec §4.11); serving the page and the proxy on different origins (more deployment work). **Consequences:** A new source must list its image hosts. Clients other than ours must send `x-mbseed: 1`. ## 2026-10-14 — Implementation details: server API, Webtoons fetch forms **Status:** accepted **Context:** Decisions made while implementing plan 001 that the spec leaves open. **Decision:** - `createApp(env)` returns the handler (spec §4.10 says `handle(req, env)`) so rate-limit state is bound once. - `/api/*` accepts `Origin: https://mangabaka.org` besides our own, for the userscript. - Behind the proxy, the rate limiter keys on the **last** `X-Forwarded-For` entry; earlier ones come from the client. - `/api/fetch` buffers responses up to 20 MB instead of streaming: a simple cap; pages and covers are far smaller. - Webtoons is fetched as `/-/-/-/list?title_no=N` (official) or `/-/canvas/-/list?title_no=N` (canvas); the wrong form is a 404, so the other is tried. A made-up slug made Webtoons hang, so the user's own path is never fetched. - Test fixtures are marked `-text` in `.gitattributes` to stay byte-exact. **Alternatives:** Stream `/api/fetch` (needs a counting stream for the cap); trust the first `X-Forwarded-For` entry (spoofable). **Consequences:** Reverse proxies must append to `X-Forwarded-For` (see `docs/ARCHITECTURE.md`). ## 2026-10-14 — Spec amendments from testing the real services **Status:** accepted (amends spec 001 §2.2–§4.5) **Context:** Before planning, we parsed real pages and downloaded every cover for Tower of God (current + Wayback every 6 months, 2015–2026) and the canvas example; the user reviewed them in `samples/` (gitignored). Findings: Wayback answered `429` "suspected abusive bot traffic" to a fake Chrome User-Agent but nothing at all to an honest one at one request per 10 s; most historical `og:image`s are character or landing-page art, not covers; MangaBaka requires exactly one `is_default` per series; the public images endpoint is paginated and lacks `is_default`. **Decision:** - History granularity (every 1 / 3 / 6 / 12 months, default 6) over the span of captures, one snapshot per bucket — the user's idea, replacing "at most 60 snapshots". - Wayback gets `mangabaka-seeder/ (+repo URL)` as User-Agent and one request per 10 s; a snapshot that fails after retries stops the history and keeps partial results. - Images taller than 1.6 : 1 are "landing-page art": type `other`, not selected by default. - "Make this the series' main cover" is a checkbox, on by default, warning that it replaces the main cover for every language. - Retries only for `GET`/`HEAD`, never `POST`. **Alternatives:** A fixed snapshot limit (arbitrary; either misses years or wastes requests); spoofing a browser more convincingly (dishonest, and the honest UA simply works); dropping tall art entirely (it is genuine Webtoons art some editors may want). **Consequences:** A default run costs ~4–5 minutes of Wayback time, so the UI needs progress and cancel. Our server makes every user's Wayback requests from one IP — if that gets blocked, the userscript (user's own IP) still works. ## 2026-10-14 — Size cap on the covers folder; refuse when full, never evict **Status:** accepted (extends "Server storage is a plain folder" below) **Context:** The user asked for storage limits on the local folder. The site is open (no gate), so uploads must not be able to fill the server's disk. **Decision:** `MBSEED_STORAGE_MAX_BYTES` (default 1 GiB) caps the folder. Uploads past it get 507 and the page offers litterbox/catbox instead. Files are never deleted before their 30 days. **Alternatives:** Evict the oldest files (rejected: their links may be in requests still awaiting review, which would break them silently); per-IP upload quotas (deferred until abuse shows up). **Consequences:** Under heavy use the server host becomes unavailable until the sweep frees space. ## 2026-10-14 — Server storage is a plain folder on our server **Status:** accepted (replaces the S3/R2 storage in the entries below) **Context:** Self-hosted, a bucket is extra infrastructure. The user chose: "For now, store images in a plain folder on the server." **Decision:** `MBSEED_STORAGE_DIR` holds `.`; our server serves it at `/covers/…` under `MBSEED_PUBLIC_URL`. `bun.ts` deletes files older than 30 days (by mtime) at startup and every 24 hours. Writes are temp file + rename. It sits behind the `Storage` interface. **Alternatives:** S3-compatible bucket (MinIO, R2, B2) with a lifecycle rule — kept as a later option. **Consequences:** The server must be publicly reachable for MangaBaka to fetch covers, and its uptime now matters until each review is done. Folder access needs Bun APIs, so it lives in `storage-dir.ts`, not in the web-standard `app.ts`. ## 2026-10-14 — Spec signed off; self-hosted only; password gate deferred **Status:** accepted (narrows the entry below) **Context:** The user signed off spec 001 and said the app will run self-hosted, and that password-gating should wait: "if it gets abused or we see too big amounts of traffic, we will deal with it then." **Decision:** First version runs `server/app.ts` under `Bun.serve` on our own machine behind our reverse proxy. No gate, no Cloudflare entry; both stay designed in the spec (§4.10, §4.11) and are listed under Someday in `docs/TODO.md`. The handler stays web-standard so either can be added later. **Alternatives:** Build the gate now (cost without a demonstrated need). **Consequences:** The site is open: the host allowlist, the 20 MB cap and the per-IP rate limit are the only protections. The rate limit needs the real client IP from our reverse proxy (`MBSEED_TRUST_PROXY`). ## 2026-10-14 — Also support catbox/litterbox; optional password gate; self-hostable server **Status:** accepted (extends the two entries below) **Context:** The user asked for catbox/litterbox support as well, and for a way to password-gate the static site "such that we could lock the service behind our own server". **Decision:** Image hosts are `server` (our storage, 30 days, default when configured), `litterbox` (≤ 72h; uploaded straight from the browser, which works because litterbox sends CORS headers), `catbox` (permanent; through our server for the page, direct for the userscript) and `direct`. The server is one web-standard `fetch` handler with two thin entries: Cloudflare Workers (R2 binding) and `Bun.serve` (S3-compatible bucket) for our own machine. An optional shared password (`MBSEED_PASSWORD`) gates every route, static assets included, with an HMAC-signed session cookie, plus `Authorization: Bearer` for the userscript. Self-hosted instances may use the reverse proxy's auth instead. "Behind our own server" was read as covering both the built-in gate and self-hosting. **Alternatives:** Cloudflare Access in front of the Worker (free for small teams, but Cloudflare-only and no help when self-hosting); per-user accounts (not needed for a small group); Basic auth (can't log out, and the browser prompt is clumsy for the userscript). **Consequences:** On Workers, assets must run through the Worker (`run_worker_first`) or the gate leaks the page. The userscript works without any server when using litterbox or catbox. Litterbox links may die before a slow review; the UI warns about it. ## 2026-10-14 — Temporary image hosting: Cloudflare R2 with a 30-day expiry **Status:** accepted (supersedes "Rehosting on catbox.moe" below) **Context:** A static page can't upload to catbox.moe (no CORS). The user said temporary hosting is fine with an expiry of at most a month: if moderators take longer, the user reruns. **Decision:** The Worker rehosts into R2 at `covers/.`, with a bucket lifecycle rule deleting objects after 30 days. `direct` (no rehost) stays as an option. **Alternatives:** litterbox (CORS works, but 72h maximum — likely shorter than a review); 0x0.st (CORS and MangaBaka acceptance unverified); catbox (no CORS, permanent). **Consequences:** Needs a Cloudflare account with a card on file (R2). Expected cost is $0 on free tiers; abuse of the Worker is the cost risk, hence the allowlist and the per-IP rate limit. ## 2026-10-14 — Static page + Cloudflare Worker, and a userscript; Bun; links given directly **Status:** accepted (supersedes "Stack: TypeScript / Node CLI" and "Automatic series matching" below) **Context:** The user wants a static web page first, with the CLI as secondary, Bun instead of Node, and to pass both the MangaBaka and Webtoons links. Checked 2026-10-14: `api.mangabaka.org` sends `access-control-allow-origin: *` (submissions too); Webtoons HTML, pstatic images and Wayback do not. **Decision:** One pure core with an injected `Transport`. The page uses a Worker proxy (allowlisted hosts) for Webtoons/Wayback and calls MangaBaka directly. The userscript uses `GM_xmlhttpRequest` and needs no proxy for fetching. The user chose to pursue both front ends. Tooling is Bun (`bun test`, `bun build`). No automatic MangaBaka search; a `title_no` link check only warns. **Alternatives:** Local Bun server only (not a hosted page); userscript only. **Consequences:** Two front ends to keep in step; the Worker is public infrastructure to protect. Cloudflare IPs may be throttled by Webtoons/Wayback — the userscript doesn't have that risk. ## 2026-10-14 — Original design decisions (from the design conversation) **Status:** partly superseded — see the two entries above **Context:** Recorded in the former `HANDOVER.md`. **Decision:** - Stack: TypeScript, chosen over Go (yambs is Go) to keep a browser extension or userscript possible. *Runtime superseded: Bun, not Node.* - Submission path: the API when a token is available, otherwise help with manual entry. - Which images: all — poster, square thumbnail, banner/background and foreground art. - Historical covers through the Wayback Machine. - Main cover: the current poster (square for canvas) gets `is_default: true`, existing covers `false`. - Rehosting on catbox.moe by default. *Superseded: R2, 30 days.* - Content rating chosen by the user, default `safe`; mature titles show a warning. - Image bytes kept in memory; sizes up to ~650 KB seen; 20 MB cap. - Extensibility: new sources and image hosts are one module plus one registry line. - Safety: nothing uploaded or sent without explicit user action; `save_mode: "review"`. - Not now: fields other than images; following language editions. - Automatic series matching via `/v2/series/match`. *Superseded: links given directly.* **Alternatives:** not recorded **Consequences:** See the spec.