The kill switch had no operator surface. streamrevoke.Store.List() existed and
was called from nowhere, so an admin could only terminate one session by id or
revoke a user's streams as a side effect of editing their account — with no way
to see what was revoked, choose a TTL, or undo a mistake. Because expiry is
deliberately monotonic, a wrong 24h kill was irreversible.
- GET/POST/DELETE /api/v1/admin/streams/revocations, admin-only, additive.
- Explicit wire-to-internal kind mapping: the wire accepts "session" (and
"sess"), the store key is "sess". Passing the wire string straight into a Key
would create a revocation IsRevoked never consults.
- Validation: non-empty bounded session ids; canonical positive user ids
(strconv.Itoa round-trip, so "01" is rejected — the cache key is the
canonical form); ttl_seconds bounded to 30d so the duration cannot overflow;
bounded reason and request body. DELETE of an absent key is idempotent.
- Store.Unrevoke, guarded by a bounded in-memory tombstone: the tombstone is
installed and the local entry dropped BEFORE the slow durable/Redis deletes,
so a concurrent poll reconcile cannot re-apply the row it just read. A newer
Revoke on the same key clears the tombstone, so an unrevoke never suppresses
a later legitimate kill. Tombstones age out with the kill they replaced.
- maintain() takes the same operation lock as Revoke/Unrevoke around its
durable block, closing the window where a poll tick could re-Upsert a row
Unrevoke had just deleted — invisible until a restart resurrected the kill.
- Durable self-heal now compares expiry, not mere presence, so a failed Revoke
mirror leaves a stale shorter row that the next tick repairs.
- A failed unrevoke publish fails safe: other processes keep the kill until it
expires. Propagation failures surface as warnings rather than weakening the
local result.
Unrevoking an over-cap victim is legal but the async enforcer will re-revoke it
on its next pass while the user is still over cap; that is documented at the
endpoint.
Part of the stream monitoring & kill-switch epic.
Add the enforcement layer on top of server-observed monitoring: a revocation
kill switch that stops any stream within ~120s and keeps it dead, plus an async
over-cap enforcer that drives kills off the live monitoring picture — entirely
off the per-segment hot path and with no client-protocol change.
- internal/streamrevoke: the central kill list. IsRevoked is a pure in-memory
lookup safe on the request hot path; a Redis pub/sub + poll mirror keeps edge
caches current, and a Postgres durable mirror lets kills survive a server
restart AND a Redis flush so a restart-resilient stream cannot be reconstructed
and re-served after being killed. A user revocation is a cutoff (kills tokens
minted before it, spares post-reauth tokens), not a 24h ban.
- internal/streamenforcer: async over-cap brain — reads the monitoring snapshot
and per-user limits, selects victims, and collapses every reason (exceeded
limit, admin terminate, abuse) to the same action: write a revocation.
- Edge + native + jellycompat enforcement: proxy refuses revoked sessions on
every request and cuts long direct-play/remux pours mid-stream; the transcode
node guards both serve and the reconstruct path so a killed session is never
re-spawned after a node restart; jellycompat serve surfaces close their
kill-switch coverage holes.
- streamtoken.IssuedTime exposes the token iat the user-kill cutoff compares
against; token IssuedTime + revocation guards wire through router, downloads,
and admin terminate-by-id (with admin-list dedupe).
- Restore sendfile zero-copy on direct-play/remux byte counting so the monitor's
served-byte accounting does not cost the sendfile fast path.
- migrations/sql: stream_revocations durable table.
Part of the stream monitoring & kill-switch epic.