fix: resolve the six blocked batch-4 ledger findings (#1393)

* fix(ws): resolve an empty READ audience for a channel whose row is gone

channelReadAudience already failed closed on a GetChannel error; a
deleted channel returns (nil, nil) and fell through to the role scan.
Return nobody for a missing row too — voice teardown callers union the
room's participants and the leaver back in, so their signals still land.

Test locks both halves: the non-participant hears nothing, the leaver
still gets voice_leave. (OC-0090)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ws): re-elect the key holder in CleanupVoiceForChannel

Every other voice-removal path re-elects (finishVoiceLeave, the LiveKit
webhook, registerNow, rollbackVoiceJoin, sweepStaleVoiceStates); the
channel delete/archive path did not, so a torn-down channel's
voiceKeyHolders entry lived for the process lifetime. One updateKeyHolder
call at the end of the teardown deletes it. (OC-0012)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ws): implement BroadcastMemberUnban so unban reaches connected clients

The admin unban path reaches the hub through an optional-capability type
assertion that *ws.Hub never satisfied, so it always missed silently and
clients connected during a ban kept the user missing from their member
store. Implement the mirror of BroadcastMemberBan: fan out the same
member_join a fresh connect sends (clients already map it to addMember),
reporting offline since the unbanned user cannot be connected. A
compile-time assertion in admin pins the wiring so the assertion can
never silently miss again. (OC-0058)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(db): exclude the requester's own flag from the video-cap stream count

EnableCameraIfUnderLimit and EnableScreenshareIfUnderLimit counted every
stream in the channel including the very flag the UPDATE sets, so a user
whose server-side flag was already 1 (client lost track and retried) was
refused at the cap against their own stream, with no path out. Subtract
the outer row's own bit from the correlated count: re-enable becomes
idempotent while the requester's other stream and everyone else's still
count. sqlc layer regenerated. (OC-0081)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(ledger): resolve the six blocked batch-4 findings

Four fixed in this branch (OC-0012, OC-0058, OC-0081, OC-0090), each
with an independent revert-proof pass. Two were already fixed on main by
later sibling fixes and are recorded as such: OC-0086 by the OC-0017
pre-delete re-check (#1374), OC-0101 by the OC-0206 early watermark bump
(#1375). The ledger holds zero open and zero blocked findings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-08-19 18:28:00 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 8cf019c03f
commit c86d803a18
13 changed files with 560 additions and 168 deletions
+11 -2
View File
@@ -76,10 +76,19 @@ type Querier interface {
// OC-0023), and a single user with both flags set must consume two of the N
// slots, not one (OC-0006) -- so both gates sum `vs2.camera + vs2.screenshare`
// across the channel's rows rather than counting rows where either is set.
// The enabling user's own bit is still 0 at gate time, so no self-exclusion
// term is needed.
// The enabling user's own bit for the flag being set CAN already be 1 at
// gate time (a client that lost track of the server-side flag retries the
// enable), so each gate excludes exactly that one bit from the count --
// see the per-query comments below (OC-0081).
// The channel-wide stream count excludes the requester's own camera flag
// (subtracted via the correlated outer-row reference), so re-enabling an
// already-set camera is idempotent at the cap instead of being refused
// against the requester's own stream (OC-0081). Their screenshare, and
// every other user's streams, still count.
EnableCameraIfUnderLimit(ctx context.Context, arg EnableCameraIfUnderLimitParams) (sql.Result, error)
EnablePlugin(ctx context.Context, id int64) error
// Mirror of EnableCameraIfUnderLimit: the count excludes the requester's
// own screenshare flag so re-enable is idempotent at the cap (OC-0081).
EnableScreenshareIfUnderLimit(ctx context.Context, arg EnableScreenshareIfUnderLimitParams) (sql.Result, error)
EvictOldestSessions(ctx context.Context, arg EvictOldestSessionsParams) error
ForceLogoutUser(ctx context.Context, userID int64) error
+13 -4
View File
@@ -102,7 +102,7 @@ const enableCameraIfUnderLimit = `-- name: EnableCameraIfUnderLimit :execresult
UPDATE voice_states SET camera = 1
WHERE voice_states.user_id = ? AND voice_states.channel_id = ?
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) < ?4
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) - voice_states.camera < ?4
`
type EnableCameraIfUnderLimitParams struct {
@@ -118,8 +118,15 @@ type EnableCameraIfUnderLimitParams struct {
// OC-0023), and a single user with both flags set must consume two of the N
// slots, not one (OC-0006) -- so both gates sum `vs2.camera + vs2.screenshare`
// across the channel's rows rather than counting rows where either is set.
// The enabling user's own bit is still 0 at gate time, so no self-exclusion
// term is needed.
// The enabling user's own bit for the flag being set CAN already be 1 at
// gate time (a client that lost track of the server-side flag retries the
// enable), so each gate excludes exactly that one bit from the count --
// see the per-query comments below (OC-0081).
// The channel-wide stream count excludes the requester's own camera flag
// (subtracted via the correlated outer-row reference), so re-enabling an
// already-set camera is idempotent at the cap instead of being refused
// against the requester's own stream (OC-0081). Their screenshare, and
// every other user's streams, still count.
func (q *Queries) EnableCameraIfUnderLimit(ctx context.Context, arg EnableCameraIfUnderLimitParams) (sql.Result, error) {
return q.db.ExecContext(ctx, enableCameraIfUnderLimit,
arg.UserID,
@@ -132,7 +139,7 @@ func (q *Queries) EnableCameraIfUnderLimit(ctx context.Context, arg EnableCamera
const enableScreenshareIfUnderLimit = `-- name: EnableScreenshareIfUnderLimit :execresult
UPDATE voice_states SET screenshare = 1
WHERE voice_states.user_id = ? AND voice_states.channel_id = ?
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) < ?4
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) - voice_states.screenshare < ?4
`
type EnableScreenshareIfUnderLimitParams struct {
@@ -142,6 +149,8 @@ type EnableScreenshareIfUnderLimitParams struct {
MaxVideo int64 `json:"maxVideo"`
}
// Mirror of EnableCameraIfUnderLimit: the count excludes the requester's
// own screenshare flag so re-enable is idempotent at the cap (OC-0081).
func (q *Queries) EnableScreenshareIfUnderLimit(ctx context.Context, arg EnableScreenshareIfUnderLimitParams) (sql.Result, error) {
return q.db.ExecContext(ctx, enableScreenshareIfUnderLimit,
arg.UserID,
@@ -0,0 +1,93 @@
package db_test
import (
"context"
"testing"
)
// OC-0081: EnableCameraIfUnderLimit's guard subquery counted every video
// stream in the channel INCLUDING the requester's own camera row, so a user
// whose server-side camera flag was already 1 could never re-enable at the
// cap: the sole publisher in a max_video=1 room got VIDEO_LIMIT against
// their own stream, with no path out (the client believes the camera is off
// and never sends a disable). Re-enable must be idempotent; a genuinely new
// publisher at the cap must still be refused; the requester's OTHER stream
// (screenshare) must still count against enabling their camera.
func TestVoice_EnableCameraIfUnderLimit_ReEnableIdempotentAtCap(t *testing.T) {
database := newVoiceTestDB(t)
u1 := seedVoiceUser(t, database, "cam-reen-u1")
chanID := seedVoiceChannel(t, database, "cam-reen-ch")
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
ok, err := database.EnableCameraIfUnderLimit(context.Background(), u1, chanID, 1)
if err != nil || !ok {
t.Fatalf("first enable: ok=%v err=%v, want true", ok, err)
}
// Same user, camera row already 1, still the only stream in the room.
ok, err = database.EnableCameraIfUnderLimit(context.Background(), u1, chanID, 1)
if err != nil {
t.Fatalf("re-enable: %v", err)
}
if !ok {
t.Error("re-enable at the cap was refused against the requester's own camera row")
}
}
func TestVoice_EnableCameraIfUnderLimit_OtherUserStillRefusedAtCap(t *testing.T) {
database := newVoiceTestDB(t)
u1 := seedVoiceUser(t, database, "cam-cap-u1")
u2 := seedVoiceUser(t, database, "cam-cap-u2")
chanID := seedVoiceChannel(t, database, "cam-cap-ch")
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
_ = database.JoinVoiceChannel(context.Background(), u2, chanID)
if ok, err := database.EnableCameraIfUnderLimit(context.Background(), u1, chanID, 1); err != nil || !ok {
t.Fatalf("u1 enable: ok=%v err=%v, want true", ok, err)
}
ok, err := database.EnableCameraIfUnderLimit(context.Background(), u2, chanID, 1)
if err != nil {
t.Fatalf("u2 enable: %v", err)
}
if ok {
t.Error("a new publisher was admitted past the video cap")
}
}
func TestVoice_EnableCameraIfUnderLimit_OwnScreenshareStillCounts(t *testing.T) {
database := newVoiceTestDB(t)
u1 := seedVoiceUser(t, database, "cam-ss-u1")
chanID := seedVoiceChannel(t, database, "cam-ss-ch")
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
if ok, err := database.EnableScreenshareIfUnderLimit(context.Background(), u1, chanID, 1); err != nil || !ok {
t.Fatalf("screenshare enable: ok=%v err=%v, want true", ok, err)
}
// The camera would be a SECOND stream from this user; the cap is 1.
ok, err := database.EnableCameraIfUnderLimit(context.Background(), u1, chanID, 1)
if err != nil {
t.Fatalf("camera enable: %v", err)
}
if ok {
t.Error("own screenshare must still count toward the cap when enabling the camera")
}
}
func TestVoice_EnableScreenshareIfUnderLimit_ReEnableIdempotentAtCap(t *testing.T) {
database := newVoiceTestDB(t)
u1 := seedVoiceUser(t, database, "ss-reen-u1")
chanID := seedVoiceChannel(t, database, "ss-reen-ch")
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
if ok, err := database.EnableScreenshareIfUnderLimit(context.Background(), u1, chanID, 1); err != nil || !ok {
t.Fatalf("first enable: ok=%v err=%v, want true", ok, err)
}
ok, err := database.EnableScreenshareIfUnderLimit(context.Background(), u1, chanID, 1)
if err != nil {
t.Fatalf("re-enable: %v", err)
}
if !ok {
t.Error("screenshare re-enable at the cap was refused against the requester's own row")
}
}
+13 -4
View File
@@ -99,18 +99,27 @@ UPDATE voice_states SET server_deafened = 0 WHERE user_id = ? AND channel_id = ?
-- OC-0023), and a single user with both flags set must consume two of the N
-- slots, not one (OC-0006) -- so both gates sum `vs2.camera + vs2.screenshare`
-- across the channel's rows rather than counting rows where either is set.
-- The enabling user's own bit is still 0 at gate time, so no self-exclusion
-- term is needed.
-- The enabling user's own bit for the flag being set CAN already be 1 at
-- gate time (a client that lost track of the server-side flag retries the
-- enable), so each gate excludes exactly that one bit from the count --
-- see the per-query comments below (OC-0081).
-- name: EnableCameraIfUnderLimit :execresult
-- The channel-wide stream count excludes the requester's own camera flag
-- (subtracted via the correlated outer-row reference), so re-enabling an
-- already-set camera is idempotent at the cap instead of being refused
-- against the requester's own stream (OC-0081). Their screenshare, and
-- every other user's streams, still count.
UPDATE voice_states SET camera = 1
WHERE voice_states.user_id = ? AND voice_states.channel_id = ?
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) < sqlc.arg(max_video);
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) - voice_states.camera < sqlc.arg(max_video);
-- name: EnableScreenshareIfUnderLimit :execresult
-- Mirror of EnableCameraIfUnderLimit: the count excludes the requester's
-- own screenshare flag so re-enable is idempotent at the cap (OC-0081).
UPDATE voice_states SET screenshare = 1
WHERE voice_states.user_id = ? AND voice_states.channel_id = ?
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) < sqlc.arg(max_video);
AND (SELECT COALESCE(SUM(vs2.camera), 0) + COALESCE(SUM(vs2.screenshare), 0) FROM voice_states AS vs2 WHERE vs2.channel_id = ?) - voice_states.screenshare < sqlc.arg(max_video);
-- name: ClearVoiceState :exec
DELETE FROM voice_states WHERE user_id = ?;