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>
11 KiB
AGENTS.md
Guidance for OpenCode (and Claude Code) working in this repo.
What this is
Read-progress tracker for two libraries — manga and novels — behind one self-hosted Go backend. Two separate Violentmonkey userscripts inject on-page UI (floating button + slide-in panel) and sync progress, so bookmarks unify across sites and devices:
manga-bookmark.user.js— asurascans.com (current domain; asuracomic.net 301s here), demonicscans.org, comix.to, kagane.to.novel-bookmark.user.js— novelfull.com, lightnovelworld.net.
One backend, one bookmarks table: a kind column (manga|novel) splits the libraries and the web UI switches between them. Rows are keyed <site>:<series_id>.
Hard constraints (drive design — don't violate)
Userscript targets Violentmonkey, so GM_* APIs available, but stay GM-free where plain web APIs suffice — keeps portability across engines:
- Avoid
GM_*unless needed. Prefer pagelocalStorageoverGM_setValue/GM_getValue, on-page UI overGM_registerMenuCommand, plainfetch()overGM_xmlhttpRequestfor cross-origin. - Cross-origin
fetch()work only against CORS-enabled backend. Manga siteshttps://, so backend must be HTTPS (else mixed-content block). - Every site is its own origin with its own
localStorage— a shared remote store is the only way to unify bookmarks. Cloud sync required, not optional. - Userscript run in isolated world, so embedded API token safe from site's JS.
- Cloudflare's block on manga sites IP-reputation-based, not universal — and not reliably reproducible. Verified 2026-07-26: plain
curlfrom both CGNAT dev machine and deployed VPS got clean 200s with real HTML on both asurascans.com and demonicscans.org (homepage, series, chapter pages) — no interactive Turnstile challenge from either IP at test time. Contradicts earlier untested assumption CGNAT dev IP blocked; wasn't, at least this date. Treat "does curl work right now" as live, time-varying fact to re-check, not fixed property of machine — Cloudflare's bot scoring can flip previously-clean IP without notice. Backend fetcher still needs graceful-degrade path for when challenged, and adapters should be verified against live pages (Playwright MCP, on-device devtools, direct probe) before finalizing, not assumed from single earlier test. - kagane.to and novelfull.com are the exception to the above — both sit behind a Cloudflare JavaScript challenge no TLS fingerprint clears, so the backend polls them over CDP (
BROWSER_WS_URL) and skips them entirely when that's unset. The four other sites poll fine over plain TLS.
Architecture
Two Violentmonkey userscripts (isolated world, per-site adapters, localStorage cache)
-- fetch() HTTPS --> reverse proxy (TLS + CORS) --> Go net/http --> SQLite (volume)
Backend-specific architecture (packages, endpoints, poller, config env vars) lives in backend/AGENTS.md. Userscript-specific structure (adapters, retry queue, UI, live URL shapes) lives in userscript/AGENTS.md.
Commands
Backend (cd backend):
- Test all:
go test ./... - Single test:
go test -run TestName ./... - Build static binary:
CGO_ENABLED=0 go build
Local stack: docker compose up (named volume mounted at /data, restart: unless-stopped).
Smoke test: curl endpoints with Authorization: Bearer <token>; confirm OPTIONS preflight return CORS headers and /healthz return 200.
Forge: Gitea, not GitHub
origin is self-hosted Gitea instance (gitea.violetcrown.my.id), so gh don't work here — use tea (Gitea CLI) for anything past plain git. Common ones:
- Open PR:
tea pr create --head <branch> --base main --title "..." --description "..." - List / view / check out:
tea pr list,tea pr <n>,tea pr checkout <n> - Issues:
tea issue create,tea issue list - Auth lives in
tea login, notGH_TOKENenv var.
tea print output as rendered boxes rather than plain text; PR URL lands on last line.
Design system
Web UI + userscript panel follow Cinder, rules in docs/design-system.md
— source of truth Claude Design project BookmarkManager Web UI
(969ac210-fe02-4c01-ae1b-9a271dcc779a). Read it before touching
backend/internal/web/static/style.css, backend/internal/web/templates/*, or userscript
TEMPLATE/CSS. Core law: ember means new chapter only — no other
state (busy, error, destruction) may use --ember; destruction gets
--danger. No cards/corners/shadows, one --measure: 760px column, tokens
only (never hardcode hex outside :root), both colour branches touched
together. Any move that pulls series out of list (archive/finish/remove)
must be confirm-gated via its own .confirm-row; only restore fires
instantly.
Security invariants
Existing guarantees — don't regress:
- Auth on
/bookmarks*: requireAuthorization: Bearer <API_TOKEN>, constant-time compare, 401 otherwise. - CORS: reflect
Originonly when inALLOWED_ORIGINS; allowGET,PUT,DELETE,OPTIONS+ headersAuthorization,Content-Type; answer preflightOPTIONSwith204.
Secure coding rules (code you write here)
Anchored to OWASP Top 10 / ASVS. Every rule below already has a working example in-tree — match it, don't start a second convention. AI-written backends fail on exactly these: broken access control, injection, weak session/error handling, invented dependencies.
Go backend:
- SQL always parameterized (
?). Only compile-time constants (bookmarkColumns) may be concatenated into query text — never a request value, not even a validated one. html/templateonly for anything a browser parses, nevertext/template. Never wrap stored or fetched strings intemplate.HTML/JS/URL; that switches off the escaping every template depends on.- Any outbound fetch of a client-supplied URL passes
fetchableSeriesURL(site +https+ host check) first.series_urlarrives in a PUT body, so without the gate the poller will probe arbitrary hosts from the server's own network position. New fetch path reuses the gate rather than re-deriving one. - Cap every remote body with
io.LimitReader(maxBodyBytes). An unbounded read is an OOM handed to whatever is on the other end. - Compare secrets with
hmac.Equal/subtle.ConstantTimeCompare, never==. Covers API token, web password, session MAC. - Errors: generic text to the client (
http.Error(w, "internal error", 500)), detail tolog.Printf. Never logAPI_TOKEN,WEB_PASSWORD, a session cookie value, or a wholeAuthorizationheader. - Proxy headers are trusted only where they already are:
X-Forwarded-Protofor the Secure cookie flag, rightmostX-Forwarded-Forfor client IP (leftmost is attacker-supplied). Don't read either anywhere else. - Session cookies keep
HttpOnly,SameSite,Secure-when-HTTPS, and expiry checked before signature. - Stdlib crypto only. No hand-rolled hashing, no MD5/SHA-1 anywhere security-bearing.
- Validate at the handler boundary before storing: body capped by
http.MaxBytesReader(64 KB), emptykeyand unknownstatus/kindrejected with400. A bad value that reaches the store becomes every later reader's problem.
Userscript:
- Site-derived and stored strings render via
el(..., {text})/textContent.{html}andinnerHTMLare for author-written literal markup only (TEMPLATE,CSS) — never a title, chapter label, or API response field. The page DOM belongs to a third-party site; treat it as attacker-controlled. - Isolated world protects the token from the site's JS. It does not protect anything from an
innerHTMLsink you add yourself. - The
API_TOKENliteral sits in both userscripts and must equal backendAPI_TOKEN. Never copy it into logs, docs, commit messages, issues, or a new file. Rotation touches three places: backend env plus both scripts. fetch()targetsAPI_BASEonly — no dynamic origin, no site-supplied URL.authHeaders()goes nowhere but the backend.localStorageis shared with the site's own JS: cache and queue live there, credentials never do.- Wrap every
localStorageread/write andJSON.parsein try/catch (quota, private mode, corrupt entry), as the existing helpers do.
Dependencies: stdlib first; a new module needs a stated reason. Confirm a package actually exists before adding it — a plausible name may be fiction (~20% of LLM-proposed packages don't resolve, which is how slopsquatting lands). Pin exact versions.
Review gate: auth, CORS, session, crypto, and the fetch gate are security-critical. Editing one is not a drive-by change — say which invariant you preserved and run go test ./... before calling it done.
Comments
Comment only if code alone can't carry info. Cost per read — must earn spot.
Write for:
- Why not what. Tradeoffs, non-obvious decisions.
- Load-bearing detail looking incidental — say so if "simplify" breaks it.
- Non-local consequence, invisible from function alone.
- Wire format / encoding / interface contract — save callers re-deriving.
- Gotcha/workaround, with ref if exists.
- Domain/business rule not derivable from code.
Skip:
- Restating code (no
// increment iabovei++). - Trivial getter/setter/pass-through.
- Banners, dividers,
// helpers. - Change narration (
// fix bug,// as requested,// new impl) — git's job. - Commented-out code — delete.
- TODO without concrete action.
Style: one dense comment over function beats one per line inside. Tight, no worked example unless bug subtle. Wrong comment worse than none — update/delete on change. Default fewer — sparse+high-signal beats comprehensive.
Test: "competent reader get this from code in few sec?" Yes → skip. Needs detour through another file/spec/git-blame → write it.
Relevant skills
multi-stage-dockerfile and docker-compose-orchestration for container work (referenced in plan).
golang-code-style, golang-error-handling, golang-performance, golang-testing for backend Go work.
graphify
Project has knowledge graph at graphify-out/ with god nodes, community structure, cross-file relationships.
Rules:
- For codebase questions, first run
graphify query "<question>"when graphify-out/graph.json exists. Usegraphify path "<A>" "<B>"for relationships andgraphify explain "<concept>"for focused concepts. Return scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - If graphify-out/wiki/index.md exists, use for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain don't surface enough context.
- After modifying code, run
graphify update .to keep graph current (AST-only, no API cost).
Notes
- Keep comms terse — drop articles, fluff, pleasantries. Code/commits/security written normally.