diff --git a/docs-developers/architecture/cross-repo-contract.md b/docs-developers/architecture/cross-repo-contract.md index 45298e61..81c682f7 100644 --- a/docs-developers/architecture/cross-repo-contract.md +++ b/docs-developers/architecture/cross-repo-contract.md @@ -260,8 +260,15 @@ home only after a tokenless `GET /api/v1/server` answered with this connec its `server_id` matched.** The web player never switches (it is served same-origin). Details: [Connect and home/away addresses](../frontend/connect-and-addresses.md#home-and-away-addresses). +**Listen in player.** The admin console's book page mints a token for its own user +(`POST /auth/pair`) and opens the same-origin +`/web/connect?token=…&sid=…&uid=…&book=&path=` in a new tab; the +player skips the exchange when it is already signed in as `uid` on `sid`, and ends the flow +on that book (rebuilt with `bookHref`, never a URL from the link). Details: +[Connect and onboarding](../frontend/connect-and-addresses.md#a-link-that-names-a-book). + **A change requires:** the scheme, the query keys (`server`, `token`, and the optional -`home`, `away`), and the `/web/connect` route path are a contract - change the server +`home`, `away`, `sid`, and `uid`, `book`, `path`), and the `/web/connect` route path are a contract - change the server emitter, the parser, and `app.json` together, or pairing breaks. ## 8. Capability flags: `GET /api/v1/server` diff --git a/docs-developers/frontend/connect-and-addresses.md b/docs-developers/frontend/connect-and-addresses.md index 9d497e82..0ee9ef0a 100644 --- a/docs-developers/frontend/connect-and-addresses.md +++ b/docs-developers/frontend/connect-and-addresses.md @@ -61,7 +61,8 @@ 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()`; and whenever the person listening is in kids mode afterwards (a +`leaveOnboarding()`; a link that names a book ends on its page instead +([below](#a-link-that-names-a-book)); 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 @@ -69,6 +70,36 @@ refused link (expired, used up, unknown, malformed) "This pairing link has expir was already used...", a 5xx "The server couldn't finish pairing...", and no answer "Could not reach the server to finish pairing.". +### A link that names a book + +A connect link can name a book to end on: `book=&path=`, with +`uid=`, the user its token was minted for, beside `token` and `sid`. The admin +console's **Listen in player** builds one (`/web/connect?token=…&sid=…&uid=…&book=…&path=…`, +same origin); nothing else does yet. `parseConnectBook` (`src/lib/paths.ts`) reads it: `book` +must be a positive integer and `path` non-empty, else the link names no book and the flow +ends as it always does. It travels on the `PairingScan` as `book` and `userId`, filled the +same way from the route's params and from a pasted link (`parsePairingScan`), so a pasted +link behaves as the opened one. The destination is rebuilt with `bookHref`, never followed +as a URL from the link, so there is no open redirect. Then: + +- **Already signed in as that user there** (`signedInAs` in `connect-model.ts`: a + connection on the `sid` server whose `user.id` is `uid`, its token not refused, no + `needsReconnect`): no exchange. `useLinkPairing.pair` checks this first (reading the + connections again when the session read failed at launch, as `finishConnect` does), + then asks `GET /me` with the stored token at the connection's own address (never the + link's), and ends on the book; the unused token expires by itself, and no second + session (or "new device" notice) is made at each press. A 401 there (the token was + revoked) falls through to the exchange; a server that doesn't answer still opens the + book. "Add someone" never skips, so the exchange switches to that person. +- **Otherwise** the token is exchanged as for any link, and `finishConnect`'s `book` ends + the flow on the book's page (`router.dismissTo(bookHref(…))`) instead of + `/connect/ready` or Home, first connection or not. + +Both end through `endConnect` (`finish-connect.ts`), the flow's one ending, so kids mode +still wins (`/kids`) either way. + +Native is unchanged: no link it receives carries `book`. + **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 diff --git a/docs-developers/server/api/reference.md b/docs-developers/server/api/reference.md index 9ec69520..14b17cca 100644 --- a/docs-developers/server/api/reference.md +++ b/docs-developers/server/api/reference.md @@ -366,7 +366,8 @@ on a server with that capability - the same shape as `/auth/exchange`. device" from an existing session. No request body. Response `200`: a `PairingPayload` (same shape as `/auth/redeem`, without the invite budget fields). Its token is single-use and lasts 10 minutes (`pairingTTL`), so the player counts down from -when the payload arrived. A kid's account (a [household member](#household-members) +when the payload arrived. The admin console's **Listen in player** uses it too, to sign the +web player in as the admin in a new tab. An API key gets `403`. A kid's account (a [household member](#household-members) with `member_kind: "kid"`) gets `403` `code: "kid_account"`: its owner sets up its devices with [`POST /me/household/members/{id}/pair`](#post-apiv1mehouseholdmembersidpair). diff --git a/docs-developers/server/web-ui.md b/docs-developers/server/web-ui.md index a4de8af2..5e882f9d 100644 --- a/docs-developers/server/web-ui.md +++ b/docs-developers/server/web-ui.md @@ -408,7 +408,16 @@ CSP-sensitive work against a real build served by Go. The console's own gate is leaves the dialog open), listeners, shares and the "Files on disk" section with the disabled on-disk rename. The more - menu's "Read the files again" calls `POST …/book/rescan`. `?match=1` opens the + menu's "Read the files again" calls `POST …/book/rescan`. **Listen in player** + (shown while `GET /server`'s `web_player` is on) opens the book in the web + player in a new tab as the same user: the click opens the tab at once + (`window.open('', '_blank')`, so no pop-up blocker stops it; its `opener` is + cleared), `POST /auth/pair` mints a single-use, 10-minute pairing token, and + the tab goes to the same-origin `/web/connect?token=&sid=&uid=&book=&path=` + (`playerConnectUrl` in `book-model.ts`; never the payload's `web_url`, whose + base may be the `public_url`, another origin). A failed call closes the tab + and toasts. What the player does with the link is in + [Connect and onboarding](../frontend/connect-and-addresses.md#a-link-that-names-a-book). `?match=1` opens the match dialog on arrival (Health's "Review match"); closing it drops the param. The Chapters card opens with the **Community chapters** panel (`features/book/community-chapters.tsx`; its words and its one action come diff --git a/docs-users/admin/books.md b/docs-users/admin/books.md index e8cb0559..8779e08b 100644 --- a/docs-users/admin/books.md +++ b/docs-users/admin/books.md @@ -114,6 +114,11 @@ The buttons there are: - **Match with community metadata** (it reads **Compare with community** once the book has an ASIN or ISBN) - see [Matching with community metadata](#matching-with-community-metadata). +- **Listen in player** - opens the book in the web player in a new tab, + already signed in as you, so you can check how it sounds or test its + chapters without signing in again. If the browser blocks the new tab, the + console says so: allow pop-ups for your server's address and click it again. + It only shows when your server has the web player. - **Change cover** - see [Covers](#covers). - The **⋯** menu (**More book actions**) - **Copy path**, **Add to a share...** and **Read the files again**, which re-reads this book's files