feat(serve)!: add server-held accounts for remote sessions

serve.server_accounts lends region-tagged server credentials to remote sessions (round-robin per region, per-key opt-in, per-account cache). Remote sessions no longer fall back to the server's local credentials when the client sends none.
This commit is contained in:
imSp4rky
2026-08-28 16:36:39 -06:00
parent e3f4e52444
commit d9b21229c3
12 changed files with 517 additions and 48 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ All are `GET`.
| Path | Returns |
|------|---------|
| `/api/dashboard/status` | Version, `code_hash`, bind host and port, `mode` (`full`, `api_only`, `remote_only`), uptime, `requests_total`, `requests_rejected` (401s), `requests_by_key` (label → count), loaded service count, session count, `max_sessions`, `session_ttl`, job counts by status. |
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the first resolved title's display name), `auth_status`, `auth_error`, `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`. `client` is whatever the remote client sent as `client` in its session create request (the current CLI sends nothing, so it is `{}`). `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the first resolved title's display name), `auth_status`, `auth_error`, `server_account` (the server profile lent to the remote session, else `null`), `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`. `client` is whatever the remote client sent as `client` in its session create request (the current CLI sends nothing, so it is `{}`). `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
| `/api/dashboard/jobs` | Every download job with full detail, regardless of owner. Empty in `--remote-only` mode. |
| `/api/dashboard/logs` | `{"seq": N, "records": [...]}`: the last 1000 log records. `?since=<seq>` returns only newer records; `?level=WARNING` sets the minimum level; `?logger=serve` keeps one logger and its children (`aiohttp.access` is the noisiest). `ts` is a Unix epoch in seconds. |
| `/api/dashboard/events` | Server-Sent Events stream. With `?since=<seq>` it returns `{"seq", "stats", "events": [...]}` instead: every event after `seq` (the server keeps the last 20000, log records included, so a poll every few seconds is safe even under a busy access log) as one JSON burst, so a UI that polls every few seconds still sees a remote session that opened and closed in between. |
+6 -2
View File
@@ -120,6 +120,7 @@ Field notes:
| `has_search` | Whether the service supports `POST /api/search`. |
| `has_drm` | Whether the service overrides a Widevine or PlayReady license hook. |
| `pending_update` | Set to `true` while a service repository refresh waits for this service's jobs to finish before it swaps in the new code. Absent otherwise. |
| `server_accounts` | Present when the server lends its own accounts for this service: `{regions: ["ca", "gb"], global: bool}`, the regions those accounts cover. A client sends no credentials, cookies, or cache for such a service. |
| `auth_methods` | Declared or inferred auth methods, e.g. `["cookies"]`, `["credentials"]`. |
| Status | Meaning |
@@ -833,7 +834,7 @@ DELETE /api/session/{id} → tear down, harvest updated cache
### `POST /api/session/create`
Make a remote session for a service and title. Authentication runs asynchronously in the background. This call returns immediately with `status: "authenticating"`, and you then poll the prompt endpoint. The body accepts `service` and `title_id` (both required). It also accepts a broad set of optional keys, because the body allows `additionalProperties`. These are `credentials` (`{username, password, extra?}`), `cookies` (base64 of zlib-compressed Netscape cookie file), `proxy`, `no_proxy`, `profile`, `cache` (map of forwarded cache files), `client_region`, `cdm_type`, `range_`, `vcodec`, `quality`, `best_available`, and any service CLI options.
Make a remote session for a service and title. Authentication runs asynchronously in the background. This call returns immediately with `status: "authenticating"`, and you then poll the prompt endpoint. The body accepts `service` and `title_id` (both required). It also accepts a broad set of optional keys, because the body allows `additionalProperties`. These are `credentials` (`{username, password, extra?}`), `cookies` (base64 of zlib-compressed Netscape cookie file), `proxy`, `no_proxy`, `profile`, `cache` (map of forwarded cache files), `client_region`, `proxy_region`, `cdm_type`, `range_`, `vcodec`, `quality`, `best_available`, and any service CLI options. `proxy_region` is the country code the client resolved `proxy` from; the server matches it against its own accounts.
The `proxy` value must be a full proxy URI, unless the operator gives your API key `server_proxy` in the `serve.users` config. Without it, the server does not resolve country codes with its own proxy providers. It rejects the request with `INVALID_PROXY` when no `proxy` is set and the reported `client_region` differs from the server's region. A request that reports no `client_region` is not blocked. Pass `proxy` with your own proxy, or `no_proxy` to accept the server's own connection. With `server_proxy`, the server resolves a country code and picks a proxy for your `client_region` itself.
@@ -843,10 +844,13 @@ The `proxy` value must be a full proxy URI, unless the operator gives your API k
{
"session_id": "d4e5f6a7-...",
"service": "EXAMPLE",
"status": "authenticating"
"status": "authenticating",
"server_account": false
}
```
`server_account` is `true` when the server authenticated with one of its own accounts (`serve.server_accounts`). Otherwise the server uses only what the client sent, and never falls back to its own credentials.
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | - | Session created; authentication in progress. |
+15 -1
View File
@@ -126,6 +126,18 @@ hood the client walks a remote session through its lifecycle.
the server fetches the right manifests
- Your local per-service **cache files** (e.g. refreshed tokens)
The server uses its own accounts only when the operator lists the service in
`serve.server_accounts` and gives your API key `server_accounts` in `serve.users`.
For any other service, a client that sends no credentials or cookies authenticates
with nothing. For a listed service the client sends no credentials, cookies, or
cache at all: the server picks one of its own accounts that works in your region and
rotates through them. `--proxy ca` picks a `ca` account.
`GET /api/services` advertises the regions those accounts cover. When your own
region is not one of them and you set no `--proxy`, the client resolves a proxy for
the first advertised region itself, so both sides sit in a region the account
works in.
The server responds immediately with a session ID and a status. Authentication
runs in the background on the server.
@@ -247,7 +259,9 @@ serve:
- **Namespaced, isolated cache.** Each remote session gets its own cache directory,
namespaced by a hash of the API key and the remote session ID, so remote sessions
cannot read each other's cached tokens. The server deletes the directory when the
remote session ends.
remote session ends. A remote session that uses a server account (`serve.server_accounts`)
gets a persistent per-account cache instead, so the account's tokens outlive the
remote session.
- **The server redacts secrets in logs.** It redacts remote session IDs, service tags,
and other user values before it writes them to the log.
+50
View File
@@ -128,6 +128,7 @@ is the [REST API](../../dev/rest-api/index.md) section. These are the config key
| `global_speed_limit` | str | *(unlimited)* | Server-wide download speed cap, e.g. `10M`, `1.5G` or plain bytes/sec (same format as `speed_limit`). One shared budget across all concurrent jobs; per-job speed limits are ignored while it is set. |
| `cdm_overrides` | list or bool | *(unset)* | Allowed per-request CDM overrides: a list of permitted device names, or `true` for any. Unset rejects every override. |
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
| `server_accounts` | dict | `{}` | Services whose remote sessions use the server's own `credentials` and cookie files instead of the client's (see below). |
| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. |
| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. |
@@ -145,6 +146,54 @@ client that asks anyway gets a `FORBIDDEN` error. Because a download job always
an API key without `server_cdm` for that service also cannot submit or retry `/api/download`
jobs. Keys that have no `users` entry, such as `api_secret`, keep server CDM access.
`server_accounts` lists, per service, the accounts the server lends to remote sessions. A remote
session for a listed service ignores the credentials, cookies, and cache the client sends and
uses one of the server's own profiles from `credentials`. For a service that is not listed the
server never uses its own accounts: a client that sends nothing authenticates with nothing.
```yaml
credentials:
EXAMPLE1:
ca_main: user1@example.com:pw1
eu_box: user2@example.com:pw2
shared: user3@example.com:pw3
serve:
server_accounts:
EXAMPLE1:
ca_main: ca # one country
eu_box: [gb, fr, de] # a list of countries
shared: global # any region
EXAMPLE2: true # every profile (or the single credential), any region
```
Each entry under a service names a profile from `credentials`, or a cookie file
`cookies/<TAG>/<profile>.txt` for a service that has no credentials. Its value is the region or
regions that account works in: a two-letter country code, a list of them, or `global`. Quote
`no` (Norway), because YAML reads the bare word as `false`.
The region of a remote session is the country the client asked for with `--proxy` (`ca`, `ca1`,
`provider:ca`), the server's own region with `--no-proxy`, or the client's own region otherwise.
A full proxy URI carries no country, so with one only a `global` account matches. The server
picks the profiles that cover that region (or are `global`) and rotates through them
round-robin, one account per remote session. With no matching account it rejects the remote
session. These services ignore the client's `-p/--profile`.
Cookies come only from the profile's own file (`cookies/EXAMPLE1/ca_main.txt`; the server does
not read a bare `cookies/EXAMPLE1.txt`), and each account keeps its own persistent cache under
`cache/_accounts/EXAMPLE1/<profile>/`, so refreshed tokens survive the remote session but never
cross accounts. `serve` refuses to start when a listed profile has neither an entry under
`credentials` nor a cookie file.
`/api/search`, `/api/list-titles` and `/api/list-tracks` use the same pool, matched on the
client's reported region, and return `FORBIDDEN` to an API key without the opt-in. Rotation is
per process and does not track which accounts are in use, so concurrent remote sessions can
share one account.
A `users` entry opts in to those accounts with `server_accounts: true` (every listed service)
or a list of service tags. It is off unless set: an API key without it authenticates with the
credentials the client sends, as for any other service, and `/api/services` does not advertise
the server's accounts to it. API keys with no `users` entry, such as `api_secret`, keep access.
`server_proxy` decides whether that API key may use the proxy providers the server has
configured. There is no implicit access: only the boolean `true` on the API key's `users` entry
grants it, and an API key without a `users` entry has no access either. Without the opt-in the
@@ -173,6 +222,7 @@ serve:
c9d0e1f2:
services: [EXAMPLE1, EXAMPLE2]
server_cdm: [EXAMPLE1] # the server licenses only EXAMPLE1; EXAMPLE2 needs a local CDM
server_accounts: [EXAMPLE1] # may authenticate EXAMPLE1 with one of the server's own accounts
server_proxy: true # this key may also use the server's proxy providers
```