PROJECT_GUIDELINES.md had drifted out of reality (podman-only rule while the stack runs docker; verifier scripts wired to Makefile targets nobody runs). Trashed it, its two verifier scripts, and the Makefile targets; the rules that still matched reality (docker runtime, schema in place until release, server binary only via Docker, services layer, integration-test pool rules) are absorbed into the new AGENTS.md. The library-visibility family-lens rule now has a real home: - AGENTS.md: the standing rule agents auto-discover (new media endpoints MUST thread the visible-library set; 403, never silently pass; history of the 2026-10-13 leak). - docs/developer/library-visibility.md: the full explanation (helpers, SQL pattern, per-item content gates, series per-library), linked from the developer docs index. Stale doc links updated (docs index, contributing, development).
2.2 KiB
Library Visibility (family-lens rule)
Every endpoint that returns media metadata or content must respect
per-user library visibility: the library_visibility table maps
(user_id, library_id, is_visible) and defaults to ALLOW — every
user sees every library unless an admin explicitly hides one from them.
The point of the system is the family case: an admin hides the adult
library from a child's account, and no trace of that library (titles,
covers, series, page images) may reach the child's account.
The rule for new endpoints
Any endpoint that lists, filters, or details media/series/books:
- Filter by the requesting user's visible-library set.
- Reject an explicit
library_idoutside that set with 403 — never silently ignore or fall back. - Content endpoints (file bytes, page images) gate per item, like
ReaderHandler.ensureItemVisible.
If you forget, hidden libraries leak. This exact leak shipped: the
series endpoints were name-only across ALL libraries and the PDF
thumbnail endpoint served page images of hidden books (fixed 2026-10-13,
bookhoard 431fec0).
Helpers — do not hand-roll
(*MediaHandler).visibleLibrarySet/(*SeriesHandler).visibleLibrarySet— returns a membership map plus the id slice (map for param checks, slice for SQLANY()).router.visibleLibraryIDsFor— same for SSR routes.resolveLibrary(router/helpers.go) only honors a requestedlibrary_idthat is inside the caller's visible set.- SQL pattern (see
ListMediaItems,GetDistinctSeries,SearchMediaItems):
AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL
OR library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))
Known-safe surfaces
- OPDS device catalogs filter by the device owner's visible libraries.
- Search (
SearchMediaItems) is visibility-scoped. - File download, reader page render, and page content gate per item.
Series are per-library
Series are grouped by (library_id, series) — the same series name in
two libraries is two entries (manga adaptations, LN splits). Do not
re-merge them cross-library. /api/series/books REQUIRES a visible
library_id; /api/series without one aggregates only the caller's
visible libraries.