b9f9aea82c
Two doc commits: a new secure-coding rules section, plus a fix for top-matter the rebrand left stale.
## `9156525` — secure-coding rules
`AGENTS.md` carried two security invariants (bearer auth, CORS) but nothing about the code an agent actually writes here. That is the gap worth closing: measured rates for AI-generated web/backend code are ~40% vulnerable (Pearce et al.), 45% failing security tests (Veracode 2025), and users *with* assistants shipped SQLi at 36% vs 7% for the control group (Perry et al., Stanford). The failure classes cluster on broken access control, injection, session/error handling and invented dependencies — all live surfaces in this repo.
Rules were **extracted, not pasted**. Every one names a guard that already exists in-tree, so the instruction is *match this*, not *invent something*:
| Rule | Existing anchor |
| --- | --- |
| parameterized SQL only; constants may concatenate | `store.go` — all queries use `?` |
| `html/template` only; no `template.HTML` on stored data | `web.go:85` |
| client-supplied URLs pass the fetch gate | `poller.go:144 fetchableSeriesURL` |
| cap remote bodies | `fetch.go:16 maxBodyBytes` |
| `subtle.ConstantTimeCompare`, never `==` | `middleware.go:22`, `session.go:61` |
| generic error out, detail to log, never log the token | `web.go:151` |
| `X-Forwarded-Proto` for Secure; **rightmost** XFF for IP | `session.go:68,101` |
| cookie flags; expiry checked before signature | `session.go:72-94`, `Verify` |
| validate at handler boundary | `handlers.go:48` `MaxBytesReader` 64 KB, 400 on bad key/status/kind |
| site strings via `el({text})`, never `{html}` | `el()` in both userscripts |
| `fetch()`/`authHeaders()` → `API_BASE` only | existing `authHeaders` |
| `localStorage` = cache/queue, never credentials | shared with site JS |
Plus a dependency rule (stdlib first; verify a package exists before adding — ~20% of LLM-proposed packages don't resolve, which is the slopsquatting vector) and a review gate marking auth/CORS/session/crypto/fetch-gate as security-critical.
Deliberately **excluded**: container signing, k8s admission control, IaC scanning, PII/HIPAA/PCI, C/C++ memory safety. Per OpenSSF's guide for AI assistant instructions, irrelevant rules make a model generate code compensating for attacks that cannot happen. None of those apply to a single-user Go + SQLite + userscript stack.
Sources: OWASP AISVS 1.0 Appendix C, OWASP Top 10 / ASVS v5, OpenSSF *Security-Focused Guide for AI Code Assistant Instructions* (2025-08-01).
## `5d4d890` — stale top-matter
The rebrand rewrote root `AGENTS.md` as a compression pass and switched Bromite -> Violentmonkey, but left the project described as a manga-only tracker over two sites. Six sites, two libraries and two userscripts now exist.
Root `AGENTS.md`:
- *What this is* names both scripts with their site lists, the `kind` column, and the `<site>:<series_id>` key shape.
- Origins constraint generalised past Asura/Demonic.
- Records that **kagane and novelfull are reliably Cloudflare-challenged** and browser-polled over CDP. Without it that bullet list reads as contradicting the code, since the paragraph above asserts blocking is "not universal — and not reliably reproducible".
- Diagram says two userscripts.
`backend/AGENTS.md` — two instances of the same defect, found while verifying the above:
- Store key list gained `novelfull|lightnovelworld` and the `kind` column.
- **`NOVEL_USERSCRIPT_PATH` documented** — it shipped in `main.go:150` undocumented.
The dated Cloudflare paragraph is left verbatim: it is a timestamped observation ("Verified 2026-07-26"), so rewriting it would falsify a record rather than update it. `userscript/AGENTS.md` is untouched; it already documents both novel adapters and the `LIBRARY`/`STORE_PREFIX` split.
## Verification
Docs-only, no code touched. Every code reference above was read at `4229c17` before being cited — the fetch gate, body cap, constant-time compares, cookie flags, handler validation, `el()` helper and both route registrations. No invented line numbers.
## Not addressed here
The `API_TOKEN` literal is committed in plaintext in both userscripts and in their `@downloadURL`/`@updateURL` lines. The new rules say not to propagate it, but the actual remedy is rotation plus build-time substitution, since the value is already in git history. Separate change; flagging it so it does not get lost.
Reviewed-on: #16
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
6.4 KiB
6.4 KiB
Guidance for OpenCode (and Claude Code) working under backend/. See root AGENTS.md for the project-wide architecture diagram, hard constraints, and design system.
- Backend (
backend/): stdlibnet/http(handful routes, no framework) +modernc.org/sqlite(pure Go,CGO_ENABLED=0-> static binary -> distroless/scratch image). Reverse proxy terminates TLS; Go service listens plain:8080. Single binary, split into packages underbackend/internal/:store(Bookmark type, SQLite persistence, migrations),latest(background poller, site parsers, TLS fetcher),session(cookie signing, login rate limiter),httpmw(Auth/Gzip/CORS middleware),api(JSON bookmark handlers),userscript(userscript-serving handler),web(browser UI handler +templates/+static/,go:embed-ed).backend/main.gois the composition root — the only place that wires packages together intonewRouter. Root-level*_test.gohold integration tests that exercise the full router; unit tests for a package live beside it underinternal/. - Single-user store. One
bookmarkstable keyed<site>:<series_id>(asura|demonic|comix|kagane|novelfull|lightnovelworld), with akindcolumn (manga|novel) splitting the two libraries. Sync last-write-wins. Schema and endpoint list in plan. - Endpoints:
GET /bookmarks,PUT /bookmarks/{key}(upsert; seeupdated_atrule below),DELETE /bookmarks/{key},GET /healthz(no auth). - Web UI: same binary serve password-gated browser UI on second
hostname —
GET /(list, or login page when no session),POST /login,POST /logout,GET /static/*, htmx fragment endpoints under/ui/*. Templates + assetsgo:embed-ed underbackend/internal/web/, sobackend/Dockerfilemust copy the wholeinternal/tree, not just*.go. Sessions stateless HMAC cookies keyed offAPI_TOKEN;WEB_PASSWORDgates them, and when empty, web routes not registered at all. UI mutations read-modify-write throughStore.Get+Store.Upsertsoupdated_atrule stays one place. Seedocs/superpowers/specs/2026-07-25-web-ui-design.md. Design-tool caveat: templates link/static/style.cssroot-absolutely (correct — served from/), but impeccable detector resolves stylesheet href withpath.resolve(fileDir, href), drops directory on leading/and silently skip file. Relative href don't help either: template's directory isn't its served path. Sodetect.mjs backend/internal/web/templatesreports false clean — always passbackend/internal/web/statictoo. One finding there,overused-fonton "Instrument Serif", deliberate identity choice, not debt. - Every action that moves series out of list is confirm-gated.
Archive, finish, remove each open own
.confirm-rowdisclosure (toggleConfirmRow(key, kind)infilter.js,kind∈archive|finish|remove); restore fire instantly since it's the reversal. Remove's row wear ember wash, two reversible ones wear.calmgrey.--emberstay reserved for new-chapter signal: busy bar and inline error use--mute. - Latest-chapter poller: ticker goroutine in same binary re-check
each bookmarked series' newest published chapter from backend's own
network access, so
latest_chapterstay fresh when user not browsing. Second, parallel signal — userscript keep ownmaybeCaptureLatestOnSeriesPage/backgroundRefreshLatestlogic unchanged. Two independent clocks: per-bookmark cooldown (latest_checked_atcolumn, enforced byStore.DueForLatestCheck's WHERE clause) and wake interval. Row stamped before fetch so broken series wait out full cooldown instead of retrying every tick, and writes go throughStore.Get+Store.Upsertso new chapter never reorders list. Fetches usebogdanfinn/tls-clientwith Chrome profile as defence in depth against fingerprint-based blocking; any failure log and skip. kagane and novelfull sit behind Cloudflare JavaScript challenges the TLS client can't clear, so they are browser-only: fetched over CDP viaBROWSER_WS_URL, and simply not polled when that's unset. Seedocs/superpowers/specs/2026-07-26-server-latest-chapter-polling-design.md. Poller'sStore.Get+Store.Upsertnot wrapped in transaction, so userscriptPUTthat commits between the two can get overwritten by poller's stale re-read — reverting that read progress and, since stored value now differs, movingupdated_atand reordering list. Known, accepted limitation for single-user deployment, not bug to fix. updated_atdrives list order, so moves only on real reading progress: server apply its timestamp when row new orlast_chapter_numchanges, else keep stored value — favouriting series or recording newly published chapter must not reorder list.PUTtherefore returns row as stored, clients must adopt that response rather than own payload. Seeplans/2026-07-25-bookmark-list-favorites-design.md§4.- Lifecycle buckets:
statuson each bookmark isreading|archived|finished, orthogonal tofavorite. Archived and finished appear only in own tab — not in All, Updated, Favourites, or recent strip. Poller keeps checking archived series and skip finished ones.finishedsettable only from web UI;PUT /bookmarks/{key}reject it with 400. Empty incoming status means "keep stored one" — resolved on theVALUESside ofStore.Upsert, not conflict clause, sinceexcluded.*is post-evaluation row and default applied there would wipe bucket on every PUT from client that predates column. Seedocs/superpowers/specs/2026-07-27-status-buckets-design.md. - Config via env:
API_TOKEN,ALLOWED_ORIGINS(comma list),DB_PATH(default/data/bookmarks.db),PORT(default8080),WEB_PASSWORD(gates browser UI; unset disable it),LATEST_CHAPTER_POLL_ENABLED/_COOLDOWN/_INTERVAL/_BATCH/_STAGGER(background latest-chapter poller; defaults on,1h/10m/14/20s).USERSCRIPT_PATHandNOVEL_USERSCRIPT_PATH(files served at/u/{token}/manga-bookmark.user.jsand/u/{token}/novel-bookmark.user.js, defaults/userscript/manga-bookmark.user.jsand/userscript/novel-bookmark.user.js, both supplied by bindmount).BROWSER_WS_URL(headless-shell CDP endpoint for kagane and novelfull; unset disables browser polling and leaves those sites to the userscript alone).