docs: approve spec 001; self-hosted Bun server, defer password gate
The user signed off the spec. The server runs self-hosted under Bun; the password gate and the Cloudflare Workers entry stay designed but deferred until abuse or traffic calls for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54cd626f21
commit
dd2d5a94bc
6 files changed
+80
-57
No files matched your search
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: draft
|
||||
status: approved
|
||||
---
|
||||
|
||||
# 001 — Webtoons covers
|
||||
@@ -23,6 +23,10 @@ the runtime is Bun, and images go to a temporary host with a 30-day expiry.
|
||||
Revised again the same day: catbox.moe and litterbox are also supported as
|
||||
image hosts, the server can be password-gated, and the same server code runs
|
||||
on Cloudflare Workers or under Bun on our own machine.
|
||||
Signed off by the user 2026-10-14, with two changes applied here: the server
|
||||
is **self-hosted** (Bun), and the **password gate is deferred** until abuse or
|
||||
traffic calls for it (§4.11 keeps the design). The Cloudflare entry is
|
||||
deferred too; the handler stays web-standard so it can be added later.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,11 +43,10 @@ on Cloudflare Workers or under Bun on our own machine.
|
||||
2. a **userscript** (Tampermonkey/Violentmonkey) on mangabaka.org, which needs
|
||||
no proxy because `GM_xmlhttpRequest` is not bound by CORS.
|
||||
A Bun CLI on the same core comes later.
|
||||
- **One server, two deployments.** The server is a single `fetch(Request) →
|
||||
Response` handler that runs on Cloudflare Workers or under `Bun.serve` on
|
||||
our own machine (e.g. behind our own reverse proxy) (§4.10).
|
||||
- **Lockable.** An optional password gates the whole site, the page and the API
|
||||
alike (§4.11).
|
||||
- **Self-hosted server.** The server is a single web-standard `fetch(Request) →
|
||||
Response` handler run by `Bun.serve` on our own machine, behind our reverse
|
||||
proxy (§4.10). Because it is web-standard, a Cloudflare Workers entry can be
|
||||
added later without touching it.
|
||||
- **Choice of image host:** our own storage (30-day expiry), litterbox (up to
|
||||
72h), catbox.moe (permanent), or no rehosting (§4.6).
|
||||
- **Find historical covers**, not just the current one.
|
||||
@@ -66,7 +69,10 @@ on Cloudflare Workers or under Bun on our own machine.
|
||||
- Running our own permanent image host. Our storage only needs to outlive the
|
||||
MangaBaka review, so it expires after 30 days; catbox.moe is the option for
|
||||
anyone who wants permanent links (§4.6).
|
||||
- User accounts. The gate is one shared password, not per-user logins.
|
||||
- Access control, for now. The site is open; the password gate (§4.11) is
|
||||
designed but deferred until abuse or traffic calls for it.
|
||||
- A Cloudflare Workers deployment, for now (§4.10).
|
||||
- User accounts, ever: if a gate is added, it is one shared password.
|
||||
|
||||
---
|
||||
|
||||
@@ -249,9 +255,9 @@ src/
|
||||
web/ the static page (HTML + TS bundled by `bun build`) + login page
|
||||
server/
|
||||
app.ts the portable fetch(Request) handler: gate, assets, /api/*
|
||||
gate.ts password check, signed session cookie, login rate limit
|
||||
storage.ts Storage interface: R2 binding or S3-compatible bucket
|
||||
worker.ts Cloudflare Workers entry (R2 binding, static assets)
|
||||
gate.ts (deferred) password check, signed session cookie, login rate limit
|
||||
storage.ts Storage interface: S3-compatible bucket (R2 binding later)
|
||||
worker.ts (deferred) Cloudflare Workers entry (R2 binding, static assets)
|
||||
bun.ts Bun.serve entry for self-hosting (S3 storage, assets from dist/)
|
||||
userscript/ entry + metadata banner, bundled by `bun build` into mbseed.user.js
|
||||
cli/ later: Bun CLI on the same core
|
||||
@@ -431,7 +437,7 @@ export interface ImageHost {
|
||||
|
||||
| Host | Page | Userscript | Lifetime |
|
||||
|---|---|---|---|
|
||||
| `server` (default when the server has storage) | `POST /api/upload { url, host: "store" }` | same, with the gate password (§4.11) | 30 days (bucket lifecycle rule) |
|
||||
| `server` (default when the server has storage) | `POST /api/upload { url, host: "store" }` | same | 30 days (bucket lifecycle rule) |
|
||||
| `litterbox` (default otherwise) | multipart upload of the inspected bytes **directly from the browser** (CORS open), `time=72h` unless the user picks less | same, via `GM_xmlhttpRequest` | 1h / 12h / 24h / 72h |
|
||||
| `catbox` | `POST /api/upload { url, host: "catbox", userhash? }` — the server fetches the image and uploads it, because catbox has no CORS | **directly** via `GM_xmlhttpRequest`, no server needed | permanent |
|
||||
| `direct` | no upload: the normalised source URL | same | as long as Webtoons keeps it |
|
||||
@@ -522,7 +528,7 @@ language: series.language, type, note, content_rating, is_default }`.
|
||||
(`@connect webtoons.com`, `pstatic.net`, `web.archive.org`, `catbox.moe`,
|
||||
and our server's host). Same review cards as the page.
|
||||
- Rehosts to litterbox or catbox directly, or to our server's storage
|
||||
(`/api/upload`, with the gate password; no storage keys in the script). With
|
||||
(`/api/upload`; no storage keys in the script). With
|
||||
litterbox or catbox it works with **no server at all**.
|
||||
- Submits through the API when a token is set (stored with `GM_setValue`).
|
||||
- **Without a token**, on the cover form page it can fill the form fields
|
||||
@@ -533,20 +539,19 @@ language: series.language, type, note, content_rating, is_default }`.
|
||||
|
||||
| Route | Behaviour |
|
||||
|---|---|
|
||||
| `GET /login`, `POST /login`, `POST /logout` | The gate (§4.11). Always reachable. |
|
||||
| `GET /*` | Static assets of the page. |
|
||||
| `GET /api/config` | `{ storage: boolean, gated: boolean }`, so the page knows which hosts to offer. |
|
||||
| `GET /api/config` | `{ storage: boolean }`, so the page knows which hosts to offer. |
|
||||
| `GET /api/fetch?url=` | Fetches `url` only if its host is in the allowlist (union of `Source.hosts` plus `web.archive.org`), with a browser User-Agent; streams the response back. Anything else: 403. |
|
||||
| `POST /api/upload` | `{ url, host: "store" \| "catbox", userhash? }` (§4.6). `url` must pass the same allowlist. |
|
||||
|
||||
The API routes reject other origins (except the userscript, which is not bound
|
||||
by CORS), cap response size at 20 MB, and are rate-limited per IP.
|
||||
|
||||
**Two deployments, one handler.** `app.ts` exports
|
||||
**One handler; Bun now, Cloudflare later.** `app.ts` exports
|
||||
`handle(req: Request, env: ServerEnv): Promise<Response>` using only
|
||||
web-standard APIs (`fetch`, `Request`, `Response`, `crypto.subtle`):
|
||||
|
||||
| | Cloudflare (`worker.ts`) | Self-hosted (`bun.ts`) |
|
||||
| | Cloudflare (`worker.ts`, deferred) | Self-hosted (`bun.ts`, **now**) |
|
||||
|---|---|---|
|
||||
| Run | `wrangler dev` / `wrangler deploy` | `bun run src/server/bun.ts` behind our reverse proxy |
|
||||
| Static assets | Workers static assets, with `run_worker_first = true` so the gate also covers assets (confirm the key when scaffolding) | read from the built `dist/web/` |
|
||||
@@ -554,9 +559,16 @@ web-standard APIs (`fetch`, `Request`, `Response`, `crypto.subtle`):
|
||||
| Password | `wrangler secret put MBSEED_PASSWORD` | `MBSEED_PASSWORD` env var |
|
||||
| Client IP for rate limits | `CF-Connecting-IP` | `server.requestIP(req)`, or `X-Forwarded-For` only when `MBSEED_TRUST_PROXY=1` |
|
||||
|
||||
Self-hosting also removes the Cloudflare-egress risk (§7).
|
||||
Self-hosting also avoids the Cloudflare-egress risk. Our reverse proxy
|
||||
terminates TLS; how `X-Forwarded-For` is trusted is set by `MBSEED_TRUST_PROXY`.
|
||||
|
||||
### 4.11 Access gate (`server/gate.ts`)
|
||||
### 4.11 Access gate (`server/gate.ts`) — deferred
|
||||
|
||||
**Not built in the first version.** Decided 2026-10-14: the self-hosted site
|
||||
starts open, and we add this if it gets abused or traffic grows too large.
|
||||
Until then the server's protections are the host allowlist, the 20 MB cap and
|
||||
the per-IP rate limit. The design below is kept for when it's needed; until
|
||||
then there is no `/login`, and `/api/config` has no `gated` field.
|
||||
|
||||
An optional shared password locks the **whole** service: page, assets and API.
|
||||
|
||||
@@ -583,15 +595,16 @@ An optional shared password locks the **whole** service: page, assets and API.
|
||||
|
||||
- **Secrets.** The MangaBaka token stays in the user's browser (or userscript
|
||||
storage) and is only sent to `api.mangabaka.org`. The server's only secrets
|
||||
are `MBSEED_PASSWORD` and, when self-hosted, the `MBSEED_S3_*` credentials
|
||||
(on Cloudflare, R2 is a binding). The catbox userhash is forwarded, never
|
||||
are the `MBSEED_S3_*` credentials (and `MBSEED_PASSWORD` once the gate
|
||||
exists). The catbox userhash is forwarded, never
|
||||
stored.
|
||||
- **Errors.** The UI shows a per-stage status; Wayback failure is a warning,
|
||||
not a failure.
|
||||
- **Cost.** On Cloudflare, free tiers cover expected use: a run is ~60–100 Worker
|
||||
requests and ≤ ~6 MB stored for 30 days. Abuse of the proxy is the main cost
|
||||
risk, hence the allowlist, the size cap, the per-IP rate limit and the
|
||||
password gate.
|
||||
- **Load.** A run is ~60–100 proxied requests and ≤ ~6 MB stored for 30 days.
|
||||
Abuse of the open proxy is the main risk, hence the allowlist, the size cap
|
||||
and the per-IP rate limit; the gate (§4.11) is the next step if needed.
|
||||
(Earlier estimate for Cloudflare: $0 on free tiers; ~$5/month for a busy
|
||||
public instance.)
|
||||
|
||||
## 6. Testing
|
||||
|
||||
@@ -611,8 +624,8 @@ An optional shared password locks the **whole** service: page, assets and API.
|
||||
|
||||
## 7. Open questions and risks
|
||||
|
||||
1. **Cloudflare egress.** Webtoons or Wayback may block or throttle Cloudflare
|
||||
IPs. The userscript and a self-hosted server don't have this risk.
|
||||
1. **Server egress.** Webtoons or Wayback may throttle our server's IP under
|
||||
load. (Cloudflare IPs would be riskier; the userscript uses the user's IP.)
|
||||
2. **When MangaBaka copies images** (submission or approval) and whether it
|
||||
accepts our storage URLs, `litter.catbox.moe` / `files.catbox.moe` URLs, or
|
||||
`swebtoon-phinf.pstatic.net` (`direct`). If it copies only on approval,
|
||||
@@ -620,10 +633,11 @@ An optional shared password locks the **whole** service: page, assets and API.
|
||||
3. **"Main cover" = `is_default: true`** is inferred from the API spec.
|
||||
4. **Where users create a personal access token** in the MangaBaka UI.
|
||||
5. **Whether the cover form accepts fields filled by a userscript.**
|
||||
6. **Whether catbox accepts uploads from Cloudflare IPs** (catbox-via-server).
|
||||
7. **Hosting details:** Cloudflare account and domains, or which of our own
|
||||
servers and which S3-compatible bucket; whether the gate or the reverse
|
||||
proxy's auth locks it.
|
||||
6. **Whether catbox accepts uploads from our server** (catbox-via-server).
|
||||
7. **Hosting details:** which of our servers and domain, and the storage
|
||||
behind `server` — an S3-compatible bucket (MinIO, R2, B2…) as specified, or,
|
||||
if simpler on our box, a local directory with a daily 30-day sweep (decide
|
||||
in the plan; it would be a `Storage` implementation, nothing else changes).
|
||||
|
||||
## 8. Test data
|
||||
|
||||
|
||||
Reference in new issue
Block a user