Files
bookhoard/docs/developer/library-visibility.md
John O'Keefe f42720ac64 docs: retire PROJECT_GUIDELINES.md — library-visibility rule now in AGENTS.md + dev docs
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).
2026-10-13 20:18:42 -04:00

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:

  1. Filter by the requesting user's visible-library set.
  2. Reject an explicit library_id outside that set with 403 — never silently ignore or fall back.
  3. 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 SQL ANY()).
  • router.visibleLibraryIDsFor — same for SSR routes.
  • resolveLibrary (router/helpers.go) only honors a requested library_id that 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.