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).
55 lines
2.2 KiB
Markdown
55 lines
2.2 KiB
Markdown
# 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`):
|
|
|
|
```sql
|
|
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.
|