Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion docs-developers/architecture/cross-repo-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ version.
**Server:** `handleServerInfo` advertises `admin_ui`, `web_player`, `upload`,
`transcode`, `websocket`, `api_keys`, `export`, `metadata`, `meta_bundle`,
`browse_people`, `cover_sizes`, `next_book` and the flags added after it (the
latest, `household` and `cast`, gate [§15](#15-listening-in-the-house-and-casting)), plus the
latest: `household` and `cast`, which gate [§15](#15-listening-in-the-house-and-casting),
and `household_members`, which gates [§16](#16-household-members-profiles-and-kids-mode-stay-client-side)), plus the
server version
(`api.Version`, stamped from the release tag via ldflags). `transcode` reflects
ffmpeg availability; `web_player` reflects whether `/web` is populated; `api_keys`
Expand Down Expand Up @@ -554,6 +555,57 @@ address the receiver's URLs use: [Chromecast](../frontend/chromecast.md).
must never filter someone in); any widening of where a cast token is accepted is a
security-critical change with allowed and denied tests on the server.

## 16. Household members (profiles and kids mode stay client-side)

**Profiles and kids mode are client-side; household members are server-side.** A
profile (a person on this device), its PIN and its kids mode never reach a server
([Profiles](../frontend/profiles.md)). A **household member** (capability
`household_members`) is a real account an owner creates from the player, and
everything about what it may do is the server's.

**The wire** (all additive; shapes in
[the reference](../server/api/reference.md#household-members)):

- `/me/household/members` (list, create) and `/{id}` (`PATCH`, `DELETE`),
`/{id}/session`, `/{id}/pair`, `/{id}/devices` and `/{id}/devices/{device_id}`;
mirrored in `types.ts`, `client.ts` and `hooks.ts`
([State and data](../frontend/state-and-data.md#household-members)).
- The `user` object (`/me` and every session envelope) gains `managed_by` (`{ id,
username }` or `null`) and `member_kind` (`"kid"`, `"grownup"` or `null`), and, for a
member only, `display_name` (the name the owner typed; the player names a new profile
for the account after it). The member's access is not on it: the player can't see it
and never needs it.
- `/me/devices` rows gain `member_sign_ins`, and `DELETE /me/devices/{id}?also_members=1`
answers `{ current, members_signed_out }`.
- The admin console's fields and settings stay inside the server repo
([Admin: users](../server/api/reference.md#get-apiv1adminusers)).

**Server rules** (each with an allowed and a denied test;
[Auth & security](../server/auth-and-security.md#household-members)): what a member may
listen to (the owner's access, or the collections the owner shares, plus their own
grants), one level and never an admin, the kid refusals (`403 kid_account`), kids out
of share targets and Listening in the house, someone else's member `404` like no such account, a
disabled member's session and pairing code `409 member_disabled`.

**Client rules** ([Household page and members](../frontend/household.md)): a member
the owner signs in on this device from the Household page is placed by `addMemberHere`
(the owner stays the person listening, the default connection doesn't move); `member_kind === 'kid'` turns
kids mode on by default and allows it with no grown-up on the device; `managed_by`
picks the kids shelf's collections. The player never filters a kid's access itself.
Refusals are read by `code`, never by the server's message
([Error mapping](../frontend/state-and-data.md#the-api-client-srcapiclientts)), so a new
`code` needs a case in `API_ERROR_CODES` and its words in the player, or it reads by
its status.

**Old clients:** a shipped player ignores the new `user` fields and sees a member as
an ordinary account; a kid signed in to an old player is still limited by the
server. An older server has no flag, so the player offers no "Add someone to your
household" and profiles and kids mode work as before.

**A change requires:** any change to the member rule is security-critical (allowed and
denied tests on the server); a new member field is the usual wire change in both
repos, gated on the flag.

## The wire-change checklist

Every change to the wire format follows the same shape (the worked example in
Expand Down
4 changes: 3 additions & 1 deletion docs-developers/architecture/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ audiobooks" into "remote file read on your NAS".
- **Path safety**: `library.SafeJoin` rejects traversal outside the library root;
every filesystem access derived from user input goes through it. Share scoping
(`Scope.Allows`, `Scope.VisibleInBrowse`, `pathFilterSQL`) authorizes every
content path against the caller's grants (`authorizedPath` in the handlers).
content path against the caller's grants (`authorizedPath` in the handlers),
a household member's derived from their owner's by the
[member rule](../server/auth-and-security.md#household-members).
- **Scoped CSP**: the admin/connect pages keep a strict same-origin CSP with no
inline script/style at all; the web player at `/web` gets a per-document CSP
whose `script-src` carries a sha256 hash of that document's inline scripts
Expand Down
5 changes: 3 additions & 2 deletions docs-developers/contributing/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ mappings:
| Smart speed, Voice boost, time saved (`effects.ts`, `time-saved.ts`, `effects-settings.tsx`, the Android `effects/` processors, `VoiceBoostTap.swift`, `VoiceBoostDSP.swift`) | [frontend/audio-effects.md](../frontend/audio-effects.md) (+ [frontend/book-page.md](../frontend/book-page.md) for `smartSpeedSaved`) + `/users/listening/full-player` (Smart speed and Voice boost), the Playback settings in `/users/listening/settings`, the Your listening row in `/users/listening/book-page` + the `web-player/phone-speed-sheet.png`, `settings.png` and `phone-settings.png` screenshots |
| CarPlay, Android Auto (`src/car/`, the module's car code, `plugins/withCarPlay.js`), the after-first-unlock token store, the iOS widgets and Live Activity (`src/widgets/`) | [frontend/native-integrations.md](../frontend/native-integrations.md) (+ [testing.md](../frontend/testing.md#native-checks) for how to run them) + `/users/listening/in-the-car`, `/users/listening/mobile-apps` (Availability, In the car, widgets), `/users/listening/sleep-timer` (the Live Activity). The web can't show these surfaces: device screenshots, when there are any, are added by hand to the manifest |
| Connection ids, the identity migration (`src/lib/connection-id.ts`, `src/lib/identity-migration.ts`, a new document keyed by a connection id) | [frontend/state-and-data.md](../frontend/state-and-data.md#connection-identity-server_iduser_id) (+ `ID_DOCUMENTS` when a new document is keyed by a connection id) |
| Profiles, the PIN, Settings > Household (`src/stores/profiles.ts`, `src/lib/profile-pin.ts`, `src/components/profiles/`) | [frontend/profiles.md](../frontend/profiles.md) + `/users/listening/profiles`, the Household and sharing section of `/users/listening/settings` |
| Kids mode (`src/components/kids/`, the bedtime rule in `auto-sleep-controller.ts`) | [frontend/kids-mode.md](../frontend/kids-mode.md) + `/users/listening/kids-mode` |
| Profiles, the PIN, the Household page's On this device (`src/stores/profiles.ts`, `src/lib/profile-pin.ts`, `src/components/profiles/`, `src/components/household/device-people.tsx`) | [frontend/profiles.md](../frontend/profiles.md) + `/users/listening/profiles`, `/users/listening/household` |
| Household members: the Household page, the add sheet, member cards, `addMemberHere` (`src/components/household/`, `src/components/connect/add-member-here.ts`; server `auth/members.go`, `catalog/members.go`, `handlers_members.go`, the `household.*` settings; the console's `people/household.tsx`) | [frontend/household.md](../frontend/household.md) + [state-and-data.md](../frontend/state-and-data.md#household-members) + [kids-mode.md](../frontend/kids-mode.md) + [you-and-settings.md](../frontend/you-and-settings.md#account-srccomponentsaccount) (a member's account page, the lost-phone tick) + the [reference](../server/api/reference.md#household-members) + [api/index.md](../server/api/index.md#error-envelope-and-status-conventions) (the codes and the capability) + [auth-and-security.md](../server/auth-and-security.md#household-members) + [data-model.md](../server/data-model.md#accounts--credentials) + [server/configuration.md](../server/configuration.md#household-members-household) + [backups-and-notifications.md](../server/backups-and-notifications.md#audit-log) (the `member` audit area) + the [cross-repo contract](../architecture/cross-repo-contract.md#16-household-members-profiles-and-kids-mode-stay-client-side) + `/users/listening/household`, `/users/listening/kids-mode`, `/users/listening/account`, `/users/listening/profiles`, `/users/admin/users-and-invites#household-members`, `/users/admin/server` (Household, the audit log) |
| Kids mode (`src/components/kids/`, the bedtime rule and the kids 1× speed in `src/playback/listener-settings.ts`, applied by `auto-sleep-controller.ts`, the kids car snapshot in `src/car/`) | [frontend/kids-mode.md](../frontend/kids-mode.md) (+ [frontend/sleep-timer.md](../frontend/sleep-timer.md) for the auto sleep rule) + `/users/listening/kids-mode` (+ `/users/listening/in-the-car` for the car) |
| Listening in the house (`src/components/house/`, the household hooks; server `catalog/household.go`) | [frontend/listening-in-the-house.md](../frontend/listening-in-the-house.md) + the [reference](../server/api/reference.md#listening-in-the-house) + [auth-and-security.md](../server/auth-and-security.md#listening-in-the-house-what-other-people-see) + `/users/listening/in-the-house`, `/users/admin/activity` (what the server records) |
| Chromecast (`src/cast/`, `plugins/withCast.js`; server `auth/cast.go`, `handlers_cast.go`, the web CSP) | [frontend/chromecast.md](../frontend/chromecast.md) + the [reference](../server/api/reference.md#casting) + [auth-and-security.md](../server/auth-and-security.md#cast-tokens) + [server/web-ui.md](../server/web-ui.md#per-document-csp-htmlcsp) for the web CSP (`castSenderScripts`, the policy `TestHTMLCSPCastSender` pins) + `/users/listening/chromecast`, `/users/listening/playback` (other speakers), `/users/getting-started/remote-access` (the certificate note) |
| Downloads / PWA, the offline companion (`offline-meta.ts`) | [frontend/offline.md](../frontend/offline.md) + `/users/listening/offline-downloads` |
Expand Down
3 changes: 2 additions & 1 deletion docs-developers/contributing/gates-and-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ the repo:
- **Security-critical code requires both an allowed *and* a denied regression
test** (the denied one is the point). That covers anything touching
`library.SafeJoin`, `Scope.Allows` / `VisibleInBrowse` / `pathFilterSQL`, the
rate limiters, `auth.ResolveRequest`/`lookupToken`, `web.htmlCSP`, or `bearerToken`. See
household member rule (`catalog/members.go`) and member routes
(`handlers_members.go`), the rate limiters, `auth.ResolveRequest`/`lookupToken`, `web.htmlCSP`, or `bearerToken`. See
[auth and security](../server/auth-and-security.md).

### Frontend (TypeScript)
Expand Down
17 changes: 16 additions & 1 deletion docs-developers/frontend/connect-and-addresses.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,22 @@ the server's own name when the flow knows it (the probe, the redeem payload's
addresses the link (`linkAddresses`) and the answer taught, merged there once
(`mergeAddresses`). The device's **first**
connection then shows `/connect/ready`; an added server goes straight back with
`leaveOnboarding()`.
`leaveOnboarding()`; and whenever the person listening is in kids mode afterwards (a
kid's account signing in, below) it goes straight to `/kids` instead, with no "Your
library is ready.". A link's exchange that fails is said in the app's words, never the
server's (`apiErrorKind`): 429 "Too many tries. Wait a few minutes and try again.", a
refused link (expired, used up, unknown, malformed) "This pairing link has expired or
was already used...", a 5xx "The server couldn't finish pairing...", and no answer
"Could not reach the server to finish pairing.".

**Kids mode never runs the flow**: `src/app/connect/_layout.tsx` is the one gate (see
[Kids mode](kids-mode.md#the-route-and-its-guard)). A kid's account placed for the
first time on this device starts in kids mode (`kidsOnForKidAccount`: see
[Kids mode](kids-mode.md)). Two sign-ins skip `finishConnect`: the public demo
landing (`src/app/demo.tsx`, which calls `setSession` itself so its QR stays on
screen), and a household member signed in beside the owner (`addMemberHere`, or
`renewMemberHere` when signed in again; see
[Signing a member in here](household.md#signing-a-member-in-here)).

`repairPlan` decides what a sign-in stores:

Expand Down
Loading
Loading