# Command protocol v1

## Protocol at a glance

The SSH command protocol is UTF-8 and space based. A non-interactive SSH exec
request contains one command; the interactive SSH shell accepts one command
per line. Keywords are case-insensitive. Use double quotes around an argument
that contains whitespace. Quotes must surround an entire, nonempty argument;
quoted segments cannot be concatenated with other text. Unmatched quotes,
empty quoted arguments and quotes inside unquoted arguments return
`bad_command`.

Request backslashes are literal, including before the closing quote. Protocol
v1 has no request escape syntax: neither `\"` nor response-style escaping
represents an embedded double quote. Arguments containing double quotes cannot
be represented in v1. For example, `live create "He said \"hello\""` returns
`bad_command`; `live create "path\"` preserves the trailing backslash. Clients
must keep rejecting embedded double quotes; adding request escapes would need
an explicitly versioned grammar to avoid changing literal-backslash clients.

```text
read BPML Salmos 23
read WEB "Song of Songs" 2 1
live create "Sunday Service"
live abc123 set reference "John 3:16"
```

The grammar notation in this reference is:

| Notation | Meaning |
| --- | --- |
| `<value>` | Required argument |
| `[value]` | Optional argument |
| `a\|b` | Choose one alternative |
| `...` | One or more additional values |

The server parses quoted text as one argument and removes the quotes. Responses
remain newline-delimited records so the CLI can stream multi-record results.

### Session example

```text
translation list
OK translations="BPML,WEB"

read bpml Salmos 23 1
OK translation="bpml" book=19 chapter=23 verse=1 text="Salmos 23:1 O Senhor é o meu pastor..."

quit
OK closing=true
```

## Responses, events, and errors

Each command produces exactly one response envelope.

| Shape | Meaning |
| --- | --- |
| `OK key=value ...` | Successful single-record response |
| `OK ...` then records then `END` | Successful multi-record response |
| `ERR code` | Command was not performed |
| `EVENT ...` | Unsolicited live-subscription update |

String fields are quoted. Backslashes and quotes are escaped; line feed, carriage
return and tab are written as `\n`, `\r` and `\t`. Other ASCII control characters
are written as `\uXXXX`, preserving one physical line per response record.
Booleans are written as
`true` or `false`; counts and timestamps are integers. Clients should accept
additional fields in successful responses so protocol additions remain
compatible.

### Multi-record response example

```text
read bpml Salmos 23
OK translation="bpml" book=19 chapter=23 verses=6
VERSE text="Salmos 23:1 O Senhor é o meu pastor; de nada terei falta."
VERSE text="Salmos 23:2 Em verdes pastagens me faz repousar..."
END
```

### Important error codes

| Error | Meaning and client action |
| --- | --- |
| `bad_command` | Grammar is invalid; correct the command. |
| `forbidden` | Valid command, but the authenticated actor lacks permission. |
| `unauthorized` | The SSH channel has no active account identity. |
| `not_found` | Requested live or resource does not exist; do not retry blindly. |
| `book_not_found` / `invalid_reference` | Translation or reference cannot be resolved. |
| `live_stopped` | Start the live before changing its displayed reading. |
| `subscriber_limit_reached` | The live has reached its audience cap. |

`HELP` and errors are permission-aware. SSH public-key authentication and
account resolution complete before any command channel is accepted. Unknown,
missing, or revoked keys are rejected during the SSH handshake.

## Command summary

| Group | Purpose |
| --- | --- |
| `HELP [topic]` | Discover commands exposed to this session. |
| `SERVER INFO`, `PING`, `WHOAMI`, `QUIT` | Inspect or control the connection. |
| `AUTH ...` | Inspect identity and manage actor hierarchy, roles, tokens, quotas. |
| `READ`, `SEARCH`, `TRANSLATION ...` | Read and manage Bible translations. |
| `LIVE ...` | Create, present, subscribe to, and manage Lives. |

## Server commands

### `HELP [server|auth|translation|live]`

Lists commands available to the connection. Omitting the topic returns the root
summary. Each `COMMAND` record includes a `usage`, authorization category, and
short summary.

```text
help live
OK auth=true commands=...
COMMAND usage="live list" auth=permission summary="List lives owned by the actor or its child actors."
...
END
```

Use `HELP` for feature detection instead of hard-coding a command list in a
client. The README explains semantics; `HELP` explains the currently enabled
surface.

### `SERVER INFO`

Returns the protocol and application version and stable capability names.

```text
server info
OK protocol_version=1 version="0.0.1" capabilities="help,auth,translation,read,search,live"
```

An SSH channel cannot reach this command without an authenticated account.

### `PING`

Returns `OK pong=true`. It is appropriate for a client health check.

### `WHOAMI` and `AUTH INFO`

Both inspect the current authenticated session:

```text
whoami
OK actor="felipe" display_name="Felipe" auth=true ...
```

The response includes the actor, effective permissions, direct
actor count, and how many tokens the actor has issued.

### `QUIT` and `EXIT`

Return `OK closing=true`, then close the SSH session. Neither changes server
state.

## Authentication and authorization

### Model

An **actor** is the protocol-facing compatibility identifier for a persistent
user acting within an account. A **registered OpenSSH public key** authenticates
an SSH connection as that user. A **permission** has a resource and verb, such as
`live.create` or `translation.search`. A **role** is a reusable permission set.

Global permissions authorize server-wide actions. A Live with a secret uses it
for audience access.

Every authenticated actor receives the built-in `default` role:

```text
token.get, help.get, server.get, translation.read
```

It allows help, server discovery, session inspection, and reads. It does
not allow searching, installation, Live management, or actor administration.

Browser-created email, Google, and GitHub accounts receive the `member` role.
It adds search, personal Live management, personal tokens, and SSH-key
management. A member does not administer actors, roles, or server-wide
translation installation.

### Personal account commands

Members use `ACCOUNT` for resources owned by the current account; no internal
actor ID is required:

```text
account info
account quota list
account translation add WEB
account translation list
account translation remove WEB
account token create "Presentation laptop"
account token list
account token revoke <id|all>
account key add ssh-ed25519 AAAA... laptop
account key list
account key revoke SHA256:...
```

### Plans, contributions, and account access

`bibleit_plan` defines Starter and Contributor personal plans. Organizations
provide separate workspace capacity. The read-only `plan_access` view derives
`plan_id`, `activated_at`, and `source` (`signup`, `contribution`, or `approval`)
from Starter accounts, approved `contributor_recognitions`, and `organizations`.
Effective limits merge the plan catalogue with explicit account overrides;
resource APIs and account usage use those same effective limits.
`ACCOUNT INFO` exposes `plan_activated_at` and `plan_source`.

Collaboration slots count accepted collaborators plus unexpired pending links,
excluding the owner. Invitation changes lock the Live row to prevent quota races.
Viewer limits count simultaneous connections, not physical audience members.
All plans remain subject to the server-wide audience safety cap.

Personal token limits exclude CLI login credentials (separate safety cap of 10).
Token rotation in settings gives the old credential a 15-minute overlap. Only one
rotation may be pending per account; the replacement keeps the existing scopes
and expiry. Expired and retiring tokens do not consume additional plan slots.

See [contribution operations](../contribution-operations.md) for recognition grants,
organization and issue review, reset instructions, and validation limits.

### SSH identity and session commands

Authentication is performed entirely by SSH before Bibleit opens a command
channel. The server accepts registered Ed25519, RSA (2048+ bits with SHA-2 signatures),
and NIST ECDSA public keys; password and
keyboard-interactive authentication are disabled. There is no `AUTH LOGIN`,
`AUTH LOGIN PROVE`, or protocol-level logout command.

The separate native CLI delegates the proof to the local OpenSSH client. It
first uses the SSH agent and identities selected by `~/.ssh/config`. To test
the server directly with a specific development key, use OpenSSH:

```sh
ssh -p 2222 -i ~/.ssh/bibleit_local_dev -o IdentitiesOnly=yes bibleit-cli@127.0.0.1
# Then run: auth info
```

SSH commands execute as the identity resolved from the registered key. Bearer
tokens are used by the HTTP command endpoint and are not SSH credentials.
The native CLI is still in development; its release documentation will describe
the final credential storage and transport selection behavior.

#### `AUTH INFO`

Reports the SSH-authenticated actor, effective
roles and permissions, plus the public-key fingerprint used by this
connection. `WHOAMI` is its top-level alias.

### RBAC discovery

#### `AUTH LIST RESOURCE`

Lists resources recognized by the authorization model, including `actor`,
`authorization`, `token`, `live`, `quota`, `role`, and `translation`.
Requires `authorization.list`.

#### `AUTH LIST PERMISSION`

Lists the recognized resource-verb permission pairs. Requires
`authorization.list`.

#### `AUTH LIST ROLE`

Lists roles and their effective permission sets. Requires `role.list`.

Built-in roles are `presenter`, `live_operator`, `translation_manager`,
`identity_manager`, `authorization_manager`, and `server_admin`.

`server_admin` is the all-permissions role. It dynamically includes future
permissions and is the only role permitted to delegate `server_admin` itself.
There is deliberately no global `*.*` permission. A resource wildcard such as
`live.*` is dynamic: it grants every current and future verb for `live`.

### Actors

Actors form a direct creation hierarchy. Outside `server_admin`, an actor may
manage only itself and direct children it created. This scopes ordinary
provisioning to its own tenant.

#### `AUTH ACTOR CREATE <actor>`

Creates an actor with only the immutable `default` role. Requires
`actor.create`; actor quotas can cap the number of direct children.

#### `AUTH ACTOR INFO <actor>`

Shows metadata for a managed actor: creator, creation timestamp, assigned
roles, effective permissions, and token/quota counts. Requires `actor.get`.

#### `AUTH ACTOR LIST [limit [cursor]]`

Lists direct children for ordinary actors or all actors for `server_admin`.
The default page size is 10 and the maximum is 100. A response may include
`next="<actor>"`; supply it as the cursor to continue.

#### `AUTH ACTOR DELETE <actor>`

Deletes a managed actor and revokes its issued tokens. Requires
`actor.delete`. This is irreversible.

### Permission, role, and quota bindings

#### `AUTH ACTOR GRANT <actor> PERMISSION <resource.verb...>`

Adds global permissions. The caller must have `actor.update`, be allowed to
manage the target actor, and already possess every delegated permission.

```text
auth actor grant presenter permission live.create live.update live.subscribe
auth actor grant presenter permission live.*
```

#### `AUTH ACTOR REVOKE <actor> PERMISSION <resource.verb...>`

Removes global permissions under the same actor-management boundary.

#### `AUTH ACTOR GRANT|REVOKE <actor> ROLE <role>`

Adds or removes a role. `default` cannot be bound or removed explicitly.
Delegation requires the caller to hold the role's permissions; delegating
`server_admin` additionally requires `role.bind`.

#### `AUTH ACTOR GRANT <actor> QUOTA <resource.verb> <limit>`

Sets a persistent creation quota. Supported quotas are `actor.create`,
`token.create`, `key.create`, and `live.create`; an actor must effectively have the
corresponding permission before receiving a quota.

#### `AUTH ACTOR QUOTA LIST <actor>`

Lists configured quotas for a managed actor. `AUTH ACTOR REVOKE <actor> QUOTA
<resource.verb>` removes one. Quotas are unlimited unless explicitly set.

### Tokens

#### `AUTH TOKEN CREATE <actor> [label]` (operator)

Issues a token for an existing managed actor. The secret is shown exactly
once:

```text
auth token create presenter "Presentation laptop"
OK actor="presenter" id="..." token="bt_..."
```

Only its SHA-256 hash is persisted. The `bt_` prefix identifies a Bibleit
token; the rest is 256 bits of cryptographically random material encoded
as Base62. The optional label identifies the client or intended use. Requires
`token.create`.

#### `AUTH TOKEN LIST <actor>`

Lists token IDs and metadata, never secrets. Metadata includes its label,
issuance source (`manual`), issue time, issuer, and `last_used_at`
when it has logged in. Requires `token.get`.

`token.create` quotas cap active tokens issued by that actor. Revoking a token
frees one quota slot. Members should prefer `ACCOUNT TOKEN` commands above;
the `AUTH TOKEN` form is reserved for an operator managing an actor.

#### `AUTH TOKEN REVOKE <actor> <id|all>`

Revokes one token by ID or all issued tokens only when `all` is explicit.
Requires `token.delete`. Revocation blocks future login
immediately; already authenticated connections retain their current identity
until they disconnect or log off.

### Custom roles

```text
auth role create service_reader permission live.get live.subscribe
auth role update service_reader permission live.get live.subscribe live.update
auth role delete service_reader
```

Custom roles contain existing permissions only. Creating, replacing, and
deleting them requires `role.create`, `role.update`, and `role.delete`.

## Translation commands

Translations are read from `~/.bibleit` by default. Set `translations_dir` to
override it. A translation consists of `.bt` and `.bidx` data; the server uses a
resource-backed NIF linked with `libbibleit` and reuses native handles by
translation. Search indexes (`.bt.bsearch`) are built beside translations.

### `READ <translation> <book> [chapter] [verse]`

Reads a book, chapter, or verse. `<book>` may be a numeric book ID or localized
name; quote a multi-word name. Book matching is case- and accent-insensitive.
Use `chapter:verse` as a compact alternative to separate chapter and verse
arguments.

```text
read WEB 19
read WEB 19 23
read WEB 43 3 16
read WEB 43 3:16
read BPML Salmos 23
read BPML "o evangelho de joao" 3:16
read WEB "Song of Songs" 2 1
```

One verse returns a single `OK` record. A chapter or book streams `VERSE`
records and terminates with `END`. Requires `translation.read`, included in the
default role. The translation must also be enabled in the account's personal
library.

### `SEARCH <translation> <query>`

Searches verse text only; it does not match book/reference labels. It is
case-insensitive and accent-insensitive for common Latin characters, so `joao`
matches `João` in verse text. The query may contain spaces.

```text
search BPML pastor
search WEB "love your enemies"
```

Results have the `OK` / `VERSE` / `END` shape and are capped by
`search_max_results` (100 by default, at most 1000). Requires
`translation.search` because it is potentially expensive.
The translation must also be enabled in the account's personal library.

### `TRANSLATION LIST [ALL]`

`TRANSLATION LIST` returns installed slugs. `TRANSLATION LIST ALL` returns the
union of installed slugs and translations in Bibleit's local public-domain catalogue.
Requires `translation.list`.

### `TRANSLATION INFO <slug>`

Returns the edition, public-domain status, evidence URL, official source URL,
attribution, and any trademark notice alongside the translation name. Requires
`translation.get`.

### `TRANSLATION CATALOG <slug>`

Streams an installed translation's complete client-side reference catalog:

```text
translation catalog BPML
OK translation="BPML" books=...
BOOK book=19 name="Salmos" chapters=150
CHAPTER book=19 chapter=23 verses=6
...
END
```

Use it to implement autocomplete without hard-coding a canon. Requires
`translation.get`.

### `TRANSLATION FETCH <slug>`

Installed filename stems must contain 1–128 ASCII characters, start with a
letter or digit, and contain only letters, digits, `_` or `-`. Unsafe catalog or
tooling slugs return `invalid_translation_slug` before downloads or file writes.
Malformed imported book/verse records return `invalid_translation_data`.

For a catalogue entry with a reviewed `import_url`, downloads the exact
recorded archive directly from that source, validates it, installs it locally,
and publishes `translation.provenance` containing the source URL and archive
SHA-256 alongside the installed generation. It never queries a third-party Bible API or discovers new sources.

New imports stage `translation.bt`, `translation.bidx` and
`translation.provenance` together under
`.imports/<lowercase-slug>/<content-hash>/`. One atomic `<lowercase-slug>.current`
manifest replacement publishes the complete generation. Existing flat `.bt` /
`.bidx` installations remain readable; a manifest takes precedence over them.
Tools should resolve the current paths with `bibleit_translation_registry:paths/1`
rather than assuming flat filenames.

Within one Erlang node, each installation and named deletion sharing a
directory is serialized. `DELETE ALL` iterates the editions present in its
initial list; it is not an atomic purge against new concurrent installations. Case variants identify one installed edition. Repeating identical
translation data and provenance reuses its generation; different successful
writes replace the current version in commit order. There is no client request
idempotency key: retrying older data can replace a newer version. A repeated
successful delete returns `translation_not_found`. An atomic deletion marker
keeps the edition unavailable if file cleanup fails; a retry completes cleanup
and returns `translation_not_found`.

Readers already holding a native handle may finish using the previous version;
new registry reads resolve the current manifest even after a registry outage.
Old immutable generations are retained until the translation is deleted, which
also removes provenance and legacy files. Operators must account for that disk
retention when repeatedly replacing editions. Process termination before the
manifest swap leaves the earlier version current; termination afterward leaves
the new version current. Host power-loss durability and shared-directory
multi-node installations are outside this tested contract.

An entry without a reviewed `import_url` returns
`translation_import_required`; import and review an exact local copy before
enabling it. `TRANSLATION FETCH` is an installation mechanism, not a rights
determination: only entries in Bibleit's local public-domain catalogue are
eligible. See [`docs/translation-rights-policy.md`](../translation-rights-policy.md).

### `TRANSLATION DELETE <slug|all>`

Deletes an installed translation and associated indexes. `all` deletes all
installed translations but retains the available-translation catalog cache.
Requires `translation.delete`.

## Live commands

A Live is a persistent presentation session with an owner, translations,
current payload, an optional hashed audience secret, and subscribers. Live IDs
are URL-safe Base62 identifiers, not tokens.

### Lifecycle and discovery

#### `LIVE CREATE [name]`

Creates a running, open Live owned by the current actor. Requires
`live.create`.

```text
live create "Sunday Service"
OK id="7kF3xQ9a" name="Sunday Service" status=running
```

#### `LIVE LIST`

Lists lives owned by the actor or its direct children. Requires `live.list` and
authentication. Each `LIVE` record includes its ID, status, and available
options; privileged callers receive `created_by`.

#### `LIVE <id> INFO`

Returns a permitted Live's configuration and visible state: name, status,
pause state, reference, configured translations, and creation time. Privileged
callers also receive `created_by`. Requires `live.get`.

#### `LIVE <id> STATS`

The `OK` header emits exactly one `connections` field: the number of following
`CONNECTION` records. It is derived from the returned subscriber snapshot, so a
stale cached count cannot override it or introduce a duplicate field.

Returns the owner-only operational view of a Live. The summary includes running
time, revision, stack size, and total, actor-authenticated, and anonymous
connection counts. Each `CONNECTION` record identifies an actor when one is
known; browser viewers and secret-authorized viewers are reported as
`actor="anonymous"`, without exposing the secret.

```text
live 7kF3xQ9a stats
OK id="7kF3xQ9a" connections=2 actor_connections=1 anonymous_connections=1 stack_entries=3 revision=12 running_for_seconds=42
CONNECTION actor="presenter" access=actor connected_at=1790954648 connected_for_seconds=42
CONNECTION actor="anonymous" access=open connected_at=1790954650 connected_for_seconds=40
END
```

It requires `live.get`, but only the Live owner can retrieve it.

#### `LIVE <id> START|STOP`

`STOP` prevents new live reads while retaining the displayed payload. `START`
resumes reads. Requires update authority.

#### `LIVE <id> DELETE` and `LIVE DELETE ALL`

Deletes one managed Live, or all Lives the actor manages. Deletion notifies
subscribers, removes persisted state, and is irreversible. Requires
`live.delete`.

### Live configuration

#### `LIVE <id> SET NAME <name>`

Changes its display name.

#### `LIVE <id> SET REFERENCE <reference>`

Stores a descriptive reference. It does not itself read or display verses.

#### `LIVE <id> SET TRANSLATIONS <translation...>`

Sets default translations used by `LIVE <id> STACK PUSH` when the command omits an explicit
translation. For a multi-translation read, the server resolves the named book
in the first configured translation that recognizes it, then uses its canonical
book ID in each other translation.

All `SET` operations require update authority.

### Presentation payload

#### `LIVE <id> STACK PUSH [translation] <book> [chapter] [verse]`

Reads verses and pushes them onto the Live stack. With an explicit translation
it reads only that translation. Without one, it uses configured translations.
Newly pushed entries appear at the bottom of the audience display.
Book names are case- and accent-insensitive, and the final chapter and verse
may be written together as `chapter:verse`.

```text
live 7kF3xQ9a stack push BPML Salmos 23 1
live 7kF3xQ9a stack push Salmos 23 1
live 7kF3xQ9a stack push Salmos 23:1
```

#### `LIVE <id> STACK POP [count]`

Pops entries from the stack. The default count is `1`; a positive count removes
the most recently pushed entries (the bottom of the audience display). A
negative count removes entries from the oldest side instead.

```text
live 7kF3xQ9a stack pop
live 7kF3xQ9a stack pop 2
live 7kF3xQ9a stack pop -1
```

#### `LIVE <id> STACK INFO`

Shows the current stack in audience-display order, including each entry’s
position, translation, reference, and text. Requires `live.get`.

#### `LIVE <id> STACK CLEAR`

Empties the entire stack. `LIVE <id> CLEAR` remains a shorthand for the same
operation.

#### `LIVE <id> CLEAR`

Destroys the current payload and sends a clear event to subscribers.

#### `LIVE <id> PAUSE` and `LIVE <id> RESUME`

`PAUSE` blanks the audience display without discarding the stack and sends a
“we’ll be right back” state to viewers. `RESUME` rebroadcasts the retained
stack. `RESUME` returns `ERR nothing_to_resume` if the stack is empty. A
stopped Live must be started before it can be resumed.

All presentation-payload operations require update authority. A Live must be
running for stack pushes and pops.

### Live secrets

A Live without a secret is open to subscribers with `live.subscribe`. Creating
a secret restricts audience access to viewers that know it. Keep secrets out of
public URLs and logs.

```text
live 7kF3xQ9a secret create
OK id="7kF3xQ9a" secret="server-generated-secret"
```

The Live owner can create a server-generated secret or rotate an existing one.
These operations require `live.update` and ownership of that Live:

```text
live 7kF3xQ9a secret create
live 7kF3xQ9a secret rotate
live 7kF3xQ9a secret delete
```

`SECRET CREATE` returns `already_protected` if a secret already exists;
`SECRET ROTATE` returns `not_protected` if there is none. Caller-selected
`SECRET SET <value>` is unsupported and returns `bad_command`. Authenticate
using `LIVE <id> SECRET <generated-secret>`.

Creating or rotating a secret immediately disconnects viewers that entered with
the old secret. `SECRET DELETE` also disconnects those viewers and makes the
Live open again. Only a SHA-256 hash of the secret is persisted.

#### `LIVE <id> SUBSCRIBE`

Subscribes the SSH connection to Live events. An open Live requires global
`live.subscribe`. For a secret-protected Live, first authorize the connection
with its secret:

```text
live 7kF3xQ9a secret a-long-private-secret
live 7kF3xQ9a subscribe
```

Successful subscribers may immediately receive the currently showing verse
payload.

The built-in HTTP adapter converts Live events into WebSocket messages.

## Pipelining and live events

A client may send several complete commands without waiting for replies. The
server processes one connection's commands in FIFO order and emits replies in
the same order. Protocol v1 has no request IDs, `PIPELINE`, or `MULTI` command:
associate replies with sent commands by order.

`EVENT` records are asynchronous and can appear between command response
envelopes. They never appear inside a multi-record envelope (`OK`, records,
`END`). Clients with both requests and subscriptions must therefore maintain two
streams of meaning: ordered command replies and unsolicited events.

Live mutations are serialized by their LiveSession actor. A future protocol
revision may add revision-based conditional updates if competing controllers
need compare-and-set semantics.

The HTTP WebSocket listener uses a five-minute idle timeout by default. The
browser automatically reconnects after an interrupted connection, with a
backoff from one to fifteen seconds. Set `websocket_idle_timeout_ms` under the
`http` application configuration to change that server-side timeout:

```erlang
application:set_env(bibleit_server, http, #{port => 8080, websocket_idle_timeout_ms => 300000}).
```

Viewer and management WebSockets and SSH channels also check current authority
while idle, and recheck before delivering protected events. The separate
`stream_auth_interval_ms` application setting defaults to 5000 ms and accepts
100–60000 ms. Logout, browser-session expiry and password reset invalidate
credentialed browser streams; revoked SSH keys invalidate their channels.
An authorization failure or unavailable authority closes the stream safely.
The polling interval is not an absolute closure deadline: outstanding calls
and scheduler delays add latency. Anonymous viewers can continue receiving
public Live events. Normal Live deletion still emits its `closed` event.
Browser stream checks read current session expiry and active account status
directly from PostgreSQL, then verify Live access. They do not cache authority,
load unused global permissions or update session `last_seen_at` on each poll or
payload; ordinary authenticated HTTP requests still update that timestamp.

Viewer and management WebSocket upgrades share `ws_admission_ip`: 120 attempts
per socket-peer IP per minute by default, including anonymous, invalid and
denied requests. The scope also consumes the existing HTTP global budget and
has at most 10000 tracked IP keys. Configure it under `rate_limits`; forwarded
headers do not supply its identity. Exhaustion or an unavailable limiter returns
429 before upgrade with `Retry-After` and `Cache-Control: no-store`.

New upgrades require a responsive authorization service, including anonymous
viewer admission because that service resolves subscription quotas. A missing
or stalled service returns 503; its default call deadline is five seconds.
Validation runs in each request process so stalled upgrades do not block browser
logout behind the session-service queue. Browser credentials are rechecked after
the authority wait and before the viewer's first snapshot; stale browser cookies
are rejected with 401 instead of falling back to an anonymous upgrade.

Established browser streams can continue when their current SQL identity and
Live resource access remain independently verifiable. SSH channels close when
their required authority check fails. Quota failures during an already-upgraded
viewer subscription emit an error and close with 1013; permission denial uses
1008. The source Live survives a quota-authority timeout and can accept a later
retry. A capacity-denied viewer no longer remains connected without a subscription.

Live-secret validation entry points share `live_secret_ip`: 60 attempts per
socket-peer IP per minute, capped at 10000 tracked keys and charged against
`http_global`. This includes `POST /auth/<id>`, existing Live URLs with `secret`
or `code`, and viewer upgrades carrying a per-Live secret cookie. Successful
validation also consumes the budget; changing Live ID, browser session, entry
point or forwarding headers does not reset it. Exhaustion or limiter absence
returns 429 with `Retry-After` and `Cache-Control: no-store` before secret
validation. A secret-cookie upgrade also consumes its normal upgrade budget.

Viewer and management sockets share `ws_frame_ip` for inbound complete messages
and control frames: 6000/minute/IP, capped at 10000 tracked keys, with a separate
`ws_frame_global` ceiling of 60000/minute. These configurable budgets run before
JSON parsing, browser-session authentication or management refresh scheduling;
malformed, unsupported and anonymous commands count too. Reconnecting does not
reset the peer allowance. Exhaustion or limiter unavailability closes with 1008;
viewers first emit `error` with `code: rate_limited`. Server-originated events and
authorization polling do not consume this inbound budget. Ping/pong remain valid
and consume it, including management heartbeat responses. The frame-global gate
is separate from HTTP admission to keep message floods out of that budget.

Cowboy rejects frames declaring more than 4096 bytes and fragmented messages
whose combined payload exceeds 4096 bytes with 1009. The bound applies before
handler dispatch and covers both WebSocket endpoints. Individual non-final
fragments are assembled by Cowboy and do not each trigger the application
message budget. These controls do not provide kernel/network flood protection;
peer-IP policies can group clients behind the same proxy.
