/api/v1/ws` authority it is spliced into stops
+ // resolving. The connect() tests above cover the three headline cases; this
+ // table pins the edges each half of the guard exists for.
+ it.each([
+ ["2001:db8::1", "[2001:db8::1]"],
+ ["::1", "[::1]"],
+ // Already bracketed (with or without a port) — never double-bracket.
+ ["[2001:db8::1]", "[2001:db8::1]"],
+ ["[2001:db8::1]:8443", "[2001:db8::1]:8443"],
+ // No colon at all: DNS name or IPv4 literal.
+ ["example.com", "example.com"],
+ ["192.168.1.1", "192.168.1.1"],
+ // Exactly one colon is the host:port separator, never an IPv6 literal —
+ // this is why the colon count is `> 1` and not `>= 1`. The IPv4 case also
+ // passes the character-set test, so the colon count is the only thing
+ // keeping it unbracketed.
+ ["example.com:8443", "example.com:8443"],
+ ["192.168.1.1:8443", "192.168.1.1:8443"],
+ // Character-set test is anchored at both ends: a hex-looking prefix with
+ // a non-hex tail (zone id) and a non-hex head with a hex-looking tail are
+ // both rejected — neither is a literal that can sit in a URL authority.
+ ["fe80::1%eth0", "fe80::1%eth0"],
+ ["my-server:1:2", "my-server:1:2"],
+ ])("leaves %s as %s", (input, expected) => {
+ expect(bracketBareIPv6Host(input)).toBe(expected);
+ });
+});
diff --git a/Client/tauri-client/tests/unit/ws-messaging.test.ts b/Client/tauri-client/tests/unit/ws-messaging.test.ts
index f8416458..12f272bc 100644
--- a/Client/tauri-client/tests/unit/ws-messaging.test.ts
+++ b/Client/tauri-client/tests/unit/ws-messaging.test.ts
@@ -37,10 +37,19 @@ describe("message handling edge cases", () => {
emitTauriEvent("ws-state", "open");
const messages: unknown[] = [];
- // pong has no payload listeners, but we verify no crash
client.on("chat_message", (p) => messages.push(p));
- emitTauriEvent("ws-message", JSON.stringify({ type: "pong" }));
+ // "pong" is not part of the ServerMessage union — the transport eats it
+ // before dispatch — so the registry has to be reached through a cast to
+ // prove nothing arrives. The frame carries a payload on purpose: a
+ // payload-less pong is also dropped by the "missing type or payload"
+ // guard further down, which would hide a broken early return.
+ const onPong = vi.fn();
+ (client.on as unknown as (t: string, l: () => void) => () => void)("pong", onPong);
+
+ emitTauriEvent("ws-message", JSON.stringify({ type: "pong", payload: {} }));
+
+ expect(onPong).not.toHaveBeenCalled();
expect(messages).toHaveLength(0);
});
@@ -329,11 +338,13 @@ describe("handleMessage size boundary", () => {
msg.payload.content = "x".repeat(padding);
}
const exactJson = JSON.stringify(msg);
- // Ensure it is exactly at limit (not over)
- expect(exactJson.length).toBeLessThanOrEqual(limit);
+ // Exactly ON the limit, not merely under it — the guard drops only what is
+ // strictly OVER, so a frame one byte short would pass either way and prove
+ // nothing about the boundary.
+ expect(exactJson.length).toBe(limit);
emitTauriEvent("ws-message", exactJson);
- expect(messages.length).toBeGreaterThanOrEqual(0); // should not crash
+ expect(messages).toHaveLength(1);
});
it("drops message one byte over size limit", async () => {
diff --git a/Client/tauri-client/vitest.config.ts b/Client/tauri-client/vitest.config.ts
index 33cc4a22..86cec9e9 100644
--- a/Client/tauri-client/vitest.config.ts
+++ b/Client/tauri-client/vitest.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from "vitest/config";
+import { configDefaults, defineConfig } from "vitest/config";
import { resolve } from "path";
export default defineConfig({
@@ -18,6 +18,10 @@ export default defineConfig({
// Both the `tests/**/*.test.ts` suite and component-local
// `src/**/*.test.ts` files are picked up.
include: ["tests/**/*.test.ts", "src/**/*.test.ts"],
+ // tests/browser/ runs real browser APIs (AudioContext, WASM) under
+ // vitest.config.browser.ts (`npm run test:browser`); it cannot pass in
+ // jsdom and is not part of this suite.
+ exclude: [...configDefaults.exclude, "tests/browser/**"],
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
diff --git a/Server/admin/channels_archive_voice_test.go b/Server/admin/channels_archive_voice_test.go
index c4ac67a7..e8372481 100644
--- a/Server/admin/channels_archive_voice_test.go
+++ b/Server/admin/channels_archive_voice_test.go
@@ -127,3 +127,41 @@ func TestAdminAPI_PatchChannel_ArchiveSurvivesContextCancelAfterCommit(t *testin
t.Errorf("RefreshChannelVisibility calls = %d, want 1", len(hub.visibilityRefreshes))
}
}
+
+// OC-0158, create side: handleCreateChannel commits AdminCreateChannel and
+// only afterwards re-reads the row to broadcast it. A caller cancellation
+// landing in that window (tab close, network blip) failed the re-read and
+// 500ed the request, leaving a durably created channel no connected client
+// was ever told about — the same shape already fixed in the PATCH and DELETE
+// siblings. The hook fires synchronously right after the commit so the window
+// is hit deterministically instead of by wall-clock timing.
+func TestAdminAPI_CreateChannel_SurvivesContextCancelAfterCommit(t *testing.T) {
+ database := openAdminTestDB(t)
+ hub := &mockHub{}
+ handler := admin.NewAdminAPI(database, "1.0.0", hub, nil, nil, nil, nil, newTestModService(database), newTestRoleService(database))
+ token := createAdminUser(t, database)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ restore := admin.SetCreateChannelPostCommitHook(func() {
+ cancel()
+ })
+ defer restore()
+
+ body, _ := json.Marshal(map[string]any{"name": "create-cancel-race", "type": "text"})
+ req := httptest.NewRequest(http.MethodPost, "/channels", bytes.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ req = req.WithContext(ctx)
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ if w.Code != http.StatusCreated {
+ t.Fatalf("status = %d, want 201 (create must survive a caller cancellation that arrives after the row already committed); body: %s", w.Code, w.Body.String())
+ }
+ if len(hub.channelCreates) != 1 {
+ t.Fatalf("BroadcastChannelCreate called %d times, want 1 — the row committed, so connected clients must be told", len(hub.channelCreates))
+ }
+ if hub.channelCreates[0].Name != "create-cancel-race" {
+ t.Errorf("broadcast channel name = %q, want create-cancel-race", hub.channelCreates[0].Name)
+ }
+}
diff --git a/Server/admin/export_test.go b/Server/admin/export_test.go
index 19eddf43..b69f41fd 100644
--- a/Server/admin/export_test.go
+++ b/Server/admin/export_test.go
@@ -59,6 +59,16 @@ func SetPatchChannelPostCommitHook(h func()) (restore func()) {
return func() { patchChannelPostCommitHook = prev }
}
+// SetCreateChannelPostCommitHook installs h to run synchronously right after
+// handleCreateChannel's AdminCreateChannel commit, before the post-commit
+// re-read and hub fan-out — the create-side twin of
+// SetPatchChannelPostCommitHook (OC-0158).
+func SetCreateChannelPostCommitHook(h func()) (restore func()) {
+ prev := createChannelPostCommitHook
+ createChannelPostCommitHook = h
+ return func() { createChannelPostCommitHook = prev }
+}
+
// StubCopyBackup swaps the restore path's file-copy hook so tests can inject
// mid-copy failures that pass the pre-copy integrity gate. CopyBackupForTest
// is the real implementation, for stubs that only want to fail once.
diff --git a/Server/admin/handlers_channels.go b/Server/admin/handlers_channels.go
index abe29f0e..8f03563e 100644
--- a/Server/admin/handlers_channels.go
+++ b/Server/admin/handlers_channels.go
@@ -93,6 +93,13 @@ type createChannelRequest struct {
Position int `json:"position"`
}
+// createChannelPostCommitHook, when non-nil, runs synchronously right after
+// handleCreateChannel's AdminCreateChannel commit, before the post-commit
+// re-read and hub fan-out — the create-side twin of
+// patchChannelPostCommitHook, so tests can land a caller cancellation in that
+// exact window (OC-0158) instead of relying on wall-clock timing.
+var createChannelPostCommitHook func()
+
func handleCreateChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req createChannelRequest
@@ -120,14 +127,28 @@ func handleCreateChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
return
}
- ch, err := database.GetChannel(r.Context(), id)
+ // From here on the row has already committed. If the admin's browser
+ // goes away in this window (tab close, navigation, network blip),
+ // r.Context() cancels, and a GetChannel re-read that still used it
+ // would fail with context.Canceled — 500ing while leaving a durably
+ // created channel unbroadcast, so no connected client learns about it
+ // until it reconnects (OC-0158). Run the rest of the handler on an
+ // uncancellable tail, matching handlePatchChannel and
+ // handleDeleteChannel.
+ tail := context.WithoutCancel(r.Context())
+
+ if createChannelPostCommitHook != nil {
+ createChannelPostCommitHook()
+ }
+
+ ch, err := database.GetChannel(tail, id)
if err != nil || ch == nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to fetch created channel")
return
}
actor := actorFromContext(r)
slog.Info("channel created", "actor_id", actor, "channel", req.Name, "type", req.Type)
- db.WriteAudit(context.WithoutCancel(r.Context()), database, actor, "channel_create", "channel", id,
+ db.WriteAudit(tail, database, actor, "channel_create", "channel", id,
fmt.Sprintf("created #%s (%s)", req.Name, req.Type))
if hub != nil {
hub.BroadcastChannelCreate(ch)
diff --git a/Server/api/clientip_test.go b/Server/api/clientip_test.go
index 9c4d8702..3f9e777a 100644
--- a/Server/api/clientip_test.go
+++ b/Server/api/clientip_test.go
@@ -190,6 +190,39 @@ func TestClientIP_SpoofedXFFFromUntrustedRemoteIgnored(t *testing.T) {
}
}
+// TestClientIP_XForwardedFor_SkipsMalformedEntries pins the skip guard in the
+// right-to-left walk: a garbage or empty XFF entry must be stepped over, never
+// used as a rate-limit/lockout key. The garbage sits to the RIGHT of the real
+// client so the walk actually reaches it. Dropping the guard would return
+// "not-an-ip" / "garbage" as the key.
+func TestClientIP_XForwardedFor_SkipsMalformedEntries(t *testing.T) {
+ trusted := parseCIDRList([]string{"10.0.0.0/8"})
+
+ tests := []struct {
+ name string
+ xff string
+ want string
+ }{
+ {"garbage rightmost", "203.0.113.10, not-an-ip", "203.0.113.10"},
+ {"empty entry", "203.0.113.10, , 10.0.0.1", "203.0.113.10"},
+ {"garbage between hops", "203.0.113.10, ::gg::, 10.0.0.1", "203.0.113.10"},
+ // Nothing parseable at all: fall back to RemoteAddr, never a garbage key.
+ {"all malformed falls back to RemoteAddr", "garbage, , not-an-ip", "10.0.0.1"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req := httptest.NewRequest("GET", "/", nil)
+ req.RemoteAddr = "10.0.0.1:9999" // the trusted proxy
+ req.Header.Set("X-Forwarded-For", tt.xff)
+
+ if ip := clientIPWithProxies(req, trusted); ip != tt.want {
+ t.Errorf("clientIP XFF %q = %q, want %q", tt.xff, ip, tt.want)
+ }
+ })
+ }
+}
+
func TestClientIP_RemoteAddrWithoutPort(t *testing.T) {
// RemoteAddr sometimes has no port (e.g. Unix sockets in tests).
req := httptest.NewRequest("GET", "/", nil)
diff --git a/Server/api/constants_test.go b/Server/api/constants_test.go
index 3bb15ca9..44f98e73 100644
--- a/Server/api/constants_test.go
+++ b/Server/api/constants_test.go
@@ -1,6 +1,10 @@
package api
-import "testing"
+import (
+ "os"
+ "strings"
+ "testing"
+)
// I-7: loginRateLimitPerMinute must be 5 (not 60).
func TestLoginRateLimit_Value(t *testing.T) {
@@ -21,3 +25,83 @@ func TestRateLimiterCleanupHorizon_CoversMaxSlowMode(t *testing.T) {
rateLimiterCleanupMaxWindow, maxSlowMode)
}
}
+
+// setAuthRateScale/scaledAuthLimit gate every per-IP auth limit
+// (auth_handler.go:107-136) and the per-IP login failure threshold that arms
+// the lockout (auth_handler.go:514,537). The multiplier is operator-supplied
+// via security.auth_rate_limit_multiplier and config validates nothing, so
+// this clamp is all that stands between a typo and brute-force protection
+// disappearing.
+func TestSetAuthRateScale_ClampsMultiplier(t *testing.T) {
+ t.Cleanup(func() { setAuthRateScale(1.0) })
+
+ tests := []struct {
+ name string
+ mult float64
+ limit int
+ want int
+ }{
+ {"unset config means 1x", 0, loginRateLimitPerMinute, 5},
+ {"negative means 1x", -3.5, loginRateLimitPerMinute, 5},
+ {"1x leaves the limit alone", 1, registerRateLimitPerMinute, 3},
+ {"above the cap clamps to 100x", 1e9, loginRateLimitPerMinute, 500},
+ {"at the cap is 100x", 100, loginRateLimitPerMinute, 500},
+ {"below the floor clamps to 0.1x", 1e-9, verifyTOTPRateLimitPerMinute, 1},
+ {"at the floor is 0.1x", 0.1, verifyTOTPRateLimitPerMinute, 1},
+ {"in range scales and rounds", 0.5, loginRateLimitPerMinute, 3},
+ {"in range scales the failure threshold", 2, loginFailureThreshold, 18},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setAuthRateScale(tt.mult)
+ if got := scaledAuthLimit(tt.limit); got != tt.want {
+ t.Errorf("setAuthRateScale(%v); scaledAuthLimit(%d) = %d, want %d",
+ tt.mult, tt.limit, got, tt.want)
+ }
+ })
+ }
+}
+
+// A limit of 0 lets nothing through: on the login failure threshold
+// (auth_handler.go:514) that locks every IP out on its first attempt. The
+// smallest allowed multiplier must still leave every scaled limit usable.
+func TestScaledAuthLimit_NeverBelowOne(t *testing.T) {
+ t.Cleanup(func() { setAuthRateScale(1.0) })
+ setAuthRateScale(0.1)
+
+ for _, n := range []int{
+ 1,
+ registerRateLimitPerMinute,
+ loginRateLimitPerMinute,
+ verifyTOTPRateLimitPerMinute,
+ sensitiveEndpointRateLimitPerMinute,
+ loginFailureThreshold,
+ } {
+ if got := scaledAuthLimit(n); got < 1 {
+ t.Errorf("scaledAuthLimit(%d) = %d at the 0.1x floor, want >= 1", n, got)
+ }
+ }
+}
+
+// The multiplier exists for shared-NAT *per-IP* limits. The per-user caps are
+// the only cross-IP brute-force defence, so scaling them would hand a
+// distributed attacker up to 100x the guesses (totp_handler.go:76-80). Those
+// caps are only observable through a limiter key inside the handler, so this
+// pins the call site instead.
+func TestPerUserFailureCapsStayUnscaled(t *testing.T) {
+ for file, constants := range map[string][]string{
+ "totp_handler.go": {"totpFailureRateLimit"},
+ "auth_handler.go": {"loginUserFailureThreshold"},
+ } {
+ src, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatalf("read %s: %v", file, err)
+ }
+ for _, c := range constants {
+ if strings.Contains(string(src), "scaledAuthLimit("+c) {
+ t.Errorf("%s scales %s with the per-IP auth multiplier; per-user caps must stay unscaled",
+ file, c)
+ }
+ }
+ }
+}
diff --git a/Server/api/middleware_test.go b/Server/api/middleware_test.go
index 715bcbb5..11849737 100644
--- a/Server/api/middleware_test.go
+++ b/Server/api/middleware_test.go
@@ -391,6 +391,52 @@ func TestRequirePermission_MultiBitRequiresAllBits(t *testing.T) {
}
}
+// TestRequirePermission_NoRoleInContext pins the fail-closed branch: the
+// server-wide authz chokepoint must deny when the request context carries no
+// usable *db.Role. Every other RequirePermission test composes AuthMiddleware,
+// which always installs a non-nil role, so without this the guard could be
+// rewritten to `if !ok { next.ServeHTTP(w, r); return }` and stay green.
+func TestRequirePermission_NoRoleInContext(t *testing.T) {
+ var nilRole *db.Role
+
+ tests := []struct {
+ name string
+ ctx func(context.Context) context.Context
+ }{
+ {"missing key", func(ctx context.Context) context.Context { return ctx }},
+ {"typed nil role", func(ctx context.Context) context.Context {
+ return context.WithValue(ctx, api.RoleKey, nilRole)
+ }},
+ {"wrong type", func(ctx context.Context) context.Context {
+ return context.WithValue(ctx, api.RoleKey, "administrator")
+ }},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ downstream := false
+ h := api.RequirePermission(permissions.ManageServer)(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ downstream = true
+ ok(w, r)
+ }),
+ )
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ req = req.WithContext(tt.ctx(req.Context()))
+ rr := httptest.NewRecorder()
+
+ h.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusForbidden {
+ t.Errorf("RequirePermission without role status = %d, want 403", rr.Code)
+ }
+ if downstream {
+ t.Error("RequirePermission without role ran the downstream handler")
+ }
+ })
+ }
+}
+
// ─── RateLimitMiddleware tests ────────────────────────────────────────────────
func TestRateLimitMiddleware_UnderLimit(t *testing.T) {
diff --git a/Server/api/waf_test.go b/Server/api/waf_test.go
index 7370a772..4e7ea0d2 100644
--- a/Server/api/waf_test.go
+++ b/Server/api/waf_test.go
@@ -1,6 +1,7 @@
package api
import (
+ "fmt"
"io"
"net/http"
"net/http/httptest"
@@ -89,6 +90,67 @@ func TestWAFMiddleware_BlocksScannerUserAgent(t *testing.T) {
}
}
+// The inline engine's four phase-2 request-body rules must actually block a
+// body-borne attack payload, not just log it: SQLi (942100), XSS (941100),
+// path traversal (930100) and command injection (932100) are all
+// deny,status:403 under an always-on SecRuleEngine. Every other blocking test
+// in the suite fires the phase-1 User-Agent rule on a bodyless GET, so this is
+// what pins wafInspectRequestBody's interruption path.
+//
+// CRS mode off is deliberate: with no CRS engine attached the inline engine is
+// the only thing that can block, so the asserted rule id proves the inline
+// rule fired. Detect mode is included because it is the production default and
+// its CRS engine never interrupts (DetectionOnly), so the block must still
+// come from the inline engine — block mode is left out precisely because there
+// the CRS layer could be the one blocking.
+//
+// Payloads are form-urlencoded: that is the body form coraza parses into
+// ARGS/REQUEST_BODY for this engine (it loads no coraza.conf-recommended, so
+// no JSON body processor is selected — see waf_crs_test.go for the CRS layer,
+// which does inspect JSON bodies).
+func TestWAFMiddleware_BlocksAttackPayloadInRequestBody(t *testing.T) {
+ cases := []struct {
+ name string
+ body string
+ ruleID int
+ }{
+ {"sqli", `q=1%27%20OR%20%271%27%3D%271%20--%20`, 942100},
+ {"xss", `q=%3Cscript%3Ealert%281%29%3C%2Fscript%3E`, 941100},
+ {"path_traversal", `q=..%2F..%2Fetc%2Fpasswd`, 930100},
+ {"command_injection", `q=hello%20%7C%20id`, 932100},
+ }
+
+ for _, mode := range []string{CRSModeOff, CRSModeDetect} {
+ middleware := NewWAFMiddlewareCRS(2, mode)
+ for _, tc := range cases {
+ t.Run(mode+"/"+tc.name, func(t *testing.T) {
+ handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t.Errorf("downstream handler must not be called for a %s request body", tc.name)
+ w.WriteHeader(http.StatusNoContent)
+ }))
+
+ req := httptest.NewRequest(http.MethodPost, "/api/v1/messages", strings.NewReader(tc.body))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("User-Agent", "OwnCordClient/1.0")
+ req.RemoteAddr = "127.0.0.1:9999"
+ rr := httptest.NewRecorder()
+
+ out := captureSlog(t, func() { handler.ServeHTTP(rr, req) })
+
+ if rr.Code != http.StatusForbidden {
+ t.Fatalf("status = %d, want 403; body = %s", rr.Code, rr.Body.String())
+ }
+ if strings.TrimSpace(rr.Body.String()) != `{"error":"request blocked by security rules"}` {
+ t.Fatalf("body = %q, want blocked JSON", rr.Body.String())
+ }
+ if want := fmt.Sprintf("rule_id=%d", tc.ruleID); !strings.Contains(out, want) {
+ t.Fatalf("blocked by the wrong rule: want %s in\n%s", want, out)
+ }
+ })
+ }
+ }
+}
+
// Routes exempted from the app's global 1 MiB body cap (bodyCapExemptPrefixes
// in constants.go) must also be exempted from the inline WAF engine's own
// SecRequestBodyLimit, or coraza's default SecRequestBodyLimitAction (Reject)
diff --git a/Server/auth/totp_encrypt_test.go b/Server/auth/totp_encrypt_test.go
new file mode 100644
index 00000000..faeb154c
--- /dev/null
+++ b/Server/auth/totp_encrypt_test.go
@@ -0,0 +1,252 @@
+package auth_test
+
+import (
+ "bytes"
+ "encoding/hex"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/owncord/server/auth"
+)
+
+// testKey returns a deterministic 32-byte AES-256 key.
+func testKey(fill byte) []byte {
+ key := make([]byte, 32)
+ for i := range key {
+ key[i] = fill
+ }
+ return key
+}
+
+// TestEncryptDecryptTOTPSecret_RoundTrip pins the real AES-GCM path: a secret
+// encrypted with a key must decrypt back to itself byte-for-byte, and the
+// stored form must not be the plaintext.
+func TestEncryptDecryptTOTPSecret_RoundTrip(t *testing.T) {
+ key := testKey(0x2a)
+ secrets := []string{
+ "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", // 32-char base32 TOTP secret
+ "JBSWY3DPEHPK3PXP", // 16-char legacy-length secret
+ "", // empty plaintext still round-trips
+ }
+
+ for _, secret := range secrets {
+ encrypted, err := auth.EncryptTOTPSecret(key, secret)
+ if err != nil {
+ t.Fatalf("EncryptTOTPSecret(%q): %v", secret, err)
+ }
+ if encrypted == secret {
+ t.Fatalf("EncryptTOTPSecret(%q) returned the plaintext", secret)
+ }
+
+ got, err := auth.DecryptTOTPSecret(key, encrypted)
+ if err != nil {
+ t.Fatalf("DecryptTOTPSecret(%q): %v", secret, err)
+ }
+ if got != secret {
+ t.Fatalf("round-trip = %q, want %q", got, secret)
+ }
+ }
+}
+
+// TestEncryptTOTPSecret_NonceIsRandom pins that two encryptions of the same
+// secret differ, so a stored ciphertext cannot be used as a secret fingerprint.
+func TestEncryptTOTPSecret_NonceIsRandom(t *testing.T) {
+ key := testKey(0x11)
+ const secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
+
+ first, err := auth.EncryptTOTPSecret(key, secret)
+ if err != nil {
+ t.Fatalf("EncryptTOTPSecret: %v", err)
+ }
+ second, err := auth.EncryptTOTPSecret(key, secret)
+ if err != nil {
+ t.Fatalf("EncryptTOTPSecret: %v", err)
+ }
+ if first == second {
+ t.Fatal("two encryptions of the same secret produced identical ciphertext (nonce reuse)")
+ }
+}
+
+// TestDecryptTOTPSecret_FailsClosed pins the documented invariant at
+// totp_encrypt.go: a value that has the full encrypted shape (valid hex, long
+// enough for nonce+tag) but fails GCM authentication must return an error and
+// an EMPTY string. Returning the ciphertext would silently mask a wrong
+// TOTP_ENCRYPTION_KEY and hand the caller a bogus "secret".
+func TestDecryptTOTPSecret_FailsClosed(t *testing.T) {
+ key := testKey(0x01)
+ const secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
+
+ encrypted, err := auth.EncryptTOTPSecret(key, secret)
+ if err != nil {
+ t.Fatalf("EncryptTOTPSecret: %v", err)
+ }
+
+ // Flip the last ciphertext byte to simulate tampering/corruption.
+ raw, err := hex.DecodeString(encrypted)
+ if err != nil {
+ t.Fatalf("hex.DecodeString: %v", err)
+ }
+ raw[len(raw)-1] ^= 0xff
+ tampered := hex.EncodeToString(raw)
+
+ tests := []struct {
+ name string
+ key []byte
+ ciphertext string
+ }{
+ {name: "wrong key", key: testKey(0x02), ciphertext: encrypted},
+ {name: "tampered ciphertext", key: key, ciphertext: tampered},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := auth.DecryptTOTPSecret(tc.key, tc.ciphertext)
+ if err == nil {
+ t.Fatalf("DecryptTOTPSecret returned nil error (got %q); must fail closed", got)
+ }
+ if got != "" {
+ t.Fatalf("DecryptTOTPSecret returned %q on auth failure; must return the empty string, never the ciphertext", got)
+ }
+ })
+ }
+}
+
+// TestDecryptTOTPSecret_LegacyPlaintextPassthrough pins the backwards-compat
+// branches: values that cannot be encrypted data are handed back unchanged
+// with no error, which is what makes the fail-closed branch above safe.
+func TestDecryptTOTPSecret_LegacyPlaintextPassthrough(t *testing.T) {
+ key := testKey(0x03)
+ tests := []struct {
+ name string
+ value string
+ }{
+ {name: "short base32 secret", value: "JBSWY3DPEHPK3PXP"},
+ // Long enough for the encrypted format but not valid hex.
+ {name: "long non-hex value", value: "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBV"},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := auth.DecryptTOTPSecret(key, tc.value)
+ if err != nil {
+ t.Fatalf("DecryptTOTPSecret(%q): %v", tc.value, err)
+ }
+ if got != tc.value {
+ t.Fatalf("DecryptTOTPSecret(%q) = %q, want the value unchanged", tc.value, got)
+ }
+ })
+ }
+}
+
+// TestLoadOrGenerateTOTPKey_StableAcrossRestarts pins that the second boot
+// reads totp.key back off disk instead of generating a fresh key. A regression
+// here makes every stored (encrypted) TOTP secret undecryptable after a
+// restart, locking every 2FA account out.
+func TestLoadOrGenerateTOTPKey_StableAcrossRestarts(t *testing.T) {
+ t.Setenv("OWNCORD_TOTP_KEY", "")
+ dataDir := filepath.Join(t.TempDir(), "data")
+
+ first, err := auth.LoadOrGenerateTOTPKey(dataDir)
+ if err != nil {
+ t.Fatalf("first LoadOrGenerateTOTPKey: %v", err)
+ }
+ if len(first) != 32 {
+ t.Fatalf("key length = %d, want 32", len(first))
+ }
+
+ second, err := auth.LoadOrGenerateTOTPKey(dataDir)
+ if err != nil {
+ t.Fatalf("second LoadOrGenerateTOTPKey: %v", err)
+ }
+ if !bytes.Equal(first, second) {
+ t.Fatalf("key changed across restarts: %x then %x", first, second)
+ }
+
+ // The persisted key must be the one returned, so an operator copying
+ // totp.key to another host gets the same decryption key.
+ onDisk, err := os.ReadFile(filepath.Join(dataDir, "totp.key"))
+ if err != nil {
+ t.Fatalf("reading totp.key: %v", err)
+ }
+ if string(onDisk) != hex.EncodeToString(first) {
+ t.Fatalf("totp.key = %q, want %q", onDisk, hex.EncodeToString(first))
+ }
+}
+
+// TestLoadOrGenerateTOTPKey_EnvVar pins that OWNCORD_TOTP_KEY wins over a
+// totp.key on disk (so an operator can rotate without touching the file) and
+// that a wrong-length env key is a hard error rather than a silent fallback to
+// auto-generation.
+func TestLoadOrGenerateTOTPKey_EnvVar(t *testing.T) {
+ t.Run("valid hex wins over the key file", func(t *testing.T) {
+ envKey := testKey(0x7e)
+ t.Setenv("OWNCORD_TOTP_KEY", hex.EncodeToString(envKey))
+ dataDir := t.TempDir()
+ if err := os.WriteFile(filepath.Join(dataDir, "totp.key"),
+ []byte(hex.EncodeToString(testKey(0x01))), 0o600); err != nil {
+ t.Fatalf("writing totp.key: %v", err)
+ }
+
+ got, err := auth.LoadOrGenerateTOTPKey(dataDir)
+ if err != nil {
+ t.Fatalf("LoadOrGenerateTOTPKey: %v", err)
+ }
+ if !bytes.Equal(got, envKey) {
+ t.Fatalf("key = %x, want the env key %x", got, envKey)
+ }
+ })
+
+ t.Run("wrong length is a hard error", func(t *testing.T) {
+ t.Setenv("OWNCORD_TOTP_KEY", hex.EncodeToString(make([]byte, 16)))
+ key, err := auth.LoadOrGenerateTOTPKey(t.TempDir())
+ if err == nil {
+ t.Fatalf("LoadOrGenerateTOTPKey accepted a 16-byte OWNCORD_TOTP_KEY (returned %x)", key)
+ }
+ if key != nil {
+ t.Fatalf("LoadOrGenerateTOTPKey returned key %x alongside an error; want nil", key)
+ }
+ })
+}
+
+// TestLoadOrGenerateTOTPKey_RejectsBadKeyFile pins that a corrupt totp.key is a
+// hard error rather than a silent regeneration (which would orphan every
+// stored secret).
+func TestLoadOrGenerateTOTPKey_RejectsBadKeyFile(t *testing.T) {
+ tests := []struct {
+ name string
+ contents string
+ }{
+ {name: "invalid hex", contents: "not-hex-at-all"},
+ {name: "wrong length", contents: hex.EncodeToString(make([]byte, 16))},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Setenv("OWNCORD_TOTP_KEY", "")
+ dataDir := t.TempDir()
+ keyPath := filepath.Join(dataDir, "totp.key")
+ if err := os.WriteFile(keyPath, []byte(tc.contents), 0o600); err != nil {
+ t.Fatalf("writing totp.key: %v", err)
+ }
+
+ key, err := auth.LoadOrGenerateTOTPKey(dataDir)
+ if err == nil {
+ t.Fatalf("LoadOrGenerateTOTPKey accepted a corrupt totp.key (returned %x)", key)
+ }
+ if key != nil {
+ t.Fatalf("LoadOrGenerateTOTPKey returned key %x alongside an error; want nil", key)
+ }
+
+ // The corrupt file must be left alone, not overwritten with a
+ // freshly generated key.
+ after, readErr := os.ReadFile(keyPath)
+ if readErr != nil {
+ t.Fatalf("reading totp.key after failure: %v", readErr)
+ }
+ if string(after) != tc.contents {
+ t.Fatalf("totp.key was rewritten to %q, want %q", after, tc.contents)
+ }
+ })
+ }
+}
diff --git a/Server/db/migrate_upgrade_test.go b/Server/db/migrate_upgrade_test.go
index d1e93932..c30d72a4 100644
--- a/Server/db/migrate_upgrade_test.go
+++ b/Server/db/migrate_upgrade_test.go
@@ -10,8 +10,9 @@ package db_test
// 2. A database that was created at an older schema point (migrations
// 001..019 only, before any of the phase 2-6 additions) and already has
// data in it can be upgraded by applying the remaining migrations
-// (020..028) without error, and every pre-existing row survives with sane
-// defaults for the newly added columns.
+// (020..head) without error, and every pre-existing row survives with sane
+// defaults for the newly added columns — including the attachments rows
+// that migration 030 copies through a DROP/RENAME table rebuild.
//
// TestMigrate_022SeedsMentionEveryone and TestMigrate_022CreatesMentionSchema
// in migrate_test.go already lock the mention-specific pieces in isolation;
@@ -145,14 +146,19 @@ func TestMigrate_FullChainSchemaIsCoherent(t *testing.T) {
// TestMigrate_UpgradeFromMigration019PreservesData simulates upgrading a
// database that was last migrated at 019_perf_indexes.sql: it builds that
// schema via a filtered view of the real embedded migrations, inserts a row
-// each into users/roles/channels/messages/voice_states/emoji (the tables the
-// 020..028 migrations touch), then applies the full chain and asserts:
+// each into users/roles/channels/messages/voice_states/emoji/attachments (the
+// tables the 020..head migrations touch), then applies the full chain and
+// asserts:
//
// - the upgrade completes without error,
-// - the pre-existing rows are all still present (by primary key), and
+// - the pre-existing rows are all still present (by primary key),
// - the new columns those rows gained have the migration's stated defaults
// (0/NULL), not some other value — i.e. old data is not silently
-// backfilled with something other than the documented default.
+// backfilled with something other than the documented default, and
+// - the attachments row survives migration 030's INSERT…SELECT + DROP +
+// RENAME rebuild with every column value intact. 030 is the only
+// migration that destroys and recreates a table holding user data, so it
+// is the only one whose data copy can silently lose or reorder columns.
func TestMigrate_UpgradeFromMigration019PreservesData(t *testing.T) {
database := openMemory(t)
ctx := context.Background()
@@ -202,6 +208,16 @@ func TestMigrate_UpgradeFromMigration019PreservesData(t *testing.T) {
`INSERT INTO emoji (id, shortcode, filename, uploaded_by) VALUES (1, 'partyparrot', 'stored-uuid', 1)`); err != nil {
t.Fatalf("seed emoji: %v", err)
}
+ // Every attachments column populated (no NULLs, no defaults) so migration
+ // 030's rebuild has something to lose in each of the ten positions it
+ // copies.
+ if _, err := database.ExecContext(ctx,
+ `INSERT INTO attachments (id, message_id, filename, stored_as, mime_type,
+ size, uploaded_at, width, height, uploader_id)
+ VALUES ('att-1', 1, 'cat.png', 'stored-cat-uuid', 'image/png',
+ 4242, '2024-01-02 03:04:05', 640, 480, 1)`); err != nil {
+ t.Fatalf("seed attachment: %v", err)
+ }
// Apply the remaining migrations (020..028) via the real production path.
if err := db.Migrate(database); err != nil {
@@ -220,6 +236,7 @@ func TestMigrate_UpgradeFromMigration019PreservesData(t *testing.T) {
{"SELECT 1 FROM messages WHERE id = ?", []any{1}, "message"},
{"SELECT 1 FROM voice_states WHERE user_id = ?", []any{1}, "voice_states"},
{"SELECT 1 FROM emoji WHERE id = ?", []any{1}, "emoji"},
+ {"SELECT 1 FROM attachments WHERE id = ?", []any{"att-1"}, "attachment"},
} {
var one int
if err := database.QueryRowContext(ctx, tc.query, tc.args...).Scan(&one); err != nil {
@@ -269,6 +286,41 @@ func TestMigrate_UpgradeFromMigration019PreservesData(t *testing.T) {
t.Errorf("emoji.mime_type = %q for pre-existing row, want the migration's documented default %q", mimeType, "image/png")
}
+ // Migration 030 rebuilds attachments (INSERT…SELECT into attachments_v030,
+ // DROP, RENAME) to swap message_id's FK action to ON DELETE SET NULL. The
+ // copy lists ten columns twice, so a dropped, added or reordered column
+ // silently corrupts every pre-existing row — assert all ten came through
+ // unchanged, message_id still bound to the seeded message.
+ var (
+ attMessageID, attSize, attWidth, attHeight, attUploaderID int64
+ attFilename, attStoredAs, attMimeType, attUploadedAt string
+ )
+ if err := database.QueryRowContext(ctx,
+ `SELECT message_id, filename, stored_as, mime_type, size, uploaded_at, width, height, uploader_id
+ FROM attachments WHERE id = 'att-1'`,
+ ).Scan(&attMessageID, &attFilename, &attStoredAs, &attMimeType, &attSize,
+ &attUploadedAt, &attWidth, &attHeight, &attUploaderID); err != nil {
+ t.Fatalf("reading upgraded attachment: %v", err)
+ }
+ for _, tc := range []struct {
+ column string
+ got, want any
+ }{
+ {"message_id", attMessageID, int64(1)},
+ {"filename", attFilename, "cat.png"},
+ {"stored_as", attStoredAs, "stored-cat-uuid"},
+ {"mime_type", attMimeType, "image/png"},
+ {"size", attSize, int64(4242)},
+ {"uploaded_at", attUploadedAt, "2024-01-02 03:04:05"},
+ {"width", attWidth, int64(640)},
+ {"height", attHeight, int64(480)},
+ {"uploader_id", attUploaderID, int64(1)},
+ } {
+ if tc.got != tc.want {
+ t.Errorf("attachments.%s = %v after migration 030's rebuild, want %v", tc.column, tc.got, tc.want)
+ }
+ }
+
var mentionsEveryone int
if err := database.QueryRowContext(ctx,
`SELECT mentions_everyone FROM messages WHERE id = 1`).Scan(&mentionsEveryone); err != nil {
@@ -298,7 +350,7 @@ func TestMigrate_UpgradeFromMigration019PreservesData(t *testing.T) {
// Every migration file, old and new, must be recorded — this is the
// upgrade path's real contract: 001..019 came from the seed/normal path
- // during the first MigrateFS call, 020..028 from the second.
+ // during the first MigrateFS call, 020..head from the second.
all, err := fs.ReadDir(migrations.FS, ".")
if err != nil {
t.Fatalf("reading embedded migrations dir: %v", err)
diff --git a/Server/db/session_expiry_test.go b/Server/db/session_expiry_test.go
index 6a05f0ba..6cba9d66 100644
--- a/Server/db/session_expiry_test.go
+++ b/Server/db/session_expiry_test.go
@@ -79,42 +79,55 @@ func TestDeleteExpiredSessions_SargableFormat(t *testing.T) {
}
}
-// TestMigration031_NormalizesLegacyFormats verifies the one-time UPDATE pass:
-// space-separated and Z-less rows become the RFC3339-Z layout.
+// TestMigration031_NormalizesLegacyFormats drives the real migration file:
+// it builds the pre-031 schema with migrationCutoffFS, seeds legacy
+// space-separated and Z-less expires_at rows on it, then applies the full
+// chain so migration 031's one-time UPDATE pass is what normalizes them.
func TestMigration031_NormalizesLegacyFormats(t *testing.T) {
- database, err := db.Open(":memory:")
- if err != nil {
- t.Fatalf("db.Open: %v", err)
- }
- t.Cleanup(func() { _ = database.Close() })
- if err := db.MigrateFS(database, migrations.FS); err != nil {
- t.Fatalf("MigrateFS: %v", err)
- }
+ database := openMemory(t)
ctx := context.Background()
+
+ if err := db.MigrateFS(database, migrationCutoffFS{underlying: migrations.FS, cutoff: "031_"}); err != nil {
+ t.Fatalf("MigrateFS building pre-031 schema: %v", err)
+ }
+ var idx int
+ if err := database.QueryRowContext(ctx,
+ `SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = 'idx_sessions_expires_at'`).Scan(&idx); err != nil {
+ t.Fatal(err)
+ }
+ if idx != 0 {
+ t.Fatal("idx_sessions_expires_at exists before 031 ran — cutoff FS leaked the migration")
+ }
+
if _, err := database.ExecContext(ctx,
`INSERT INTO users (id, username, password, role_id) VALUES (1, 'u', 'x', 1)`); err != nil {
t.Fatalf("seed user: %v", err)
}
- // Simulate pre-031 rows, then re-run the normalization statements the
- // migration contains (the migration itself already ran on the empty DB).
- if _, err := database.ExecContext(ctx,
- `INSERT INTO sessions (user_id, token, expires_at) VALUES (1, 'legacy', '2030-05-01 10:00:00')`); err != nil {
- t.Fatal(err)
+ cases := []struct{ token, stored, want string }{
+ {"legacy_space", "2030-05-01 10:00:00", "2030-05-01T10:00:00Z"},
+ {"legacy_no_z", "2030-06-02T11:22:33", "2030-06-02T11:22:33Z"},
+ {"already_normalized", "2030-07-03T12:34:56Z", "2030-07-03T12:34:56Z"},
}
- if _, err := database.ExecContext(ctx,
- `UPDATE sessions SET expires_at = replace(expires_at, ' ', 'T') WHERE instr(expires_at, ' ') > 0`); err != nil {
- t.Fatal(err)
+ for _, tc := range cases {
+ if _, err := database.ExecContext(ctx,
+ `INSERT INTO sessions (user_id, token, expires_at) VALUES (1, ?, ?)`, tc.token, tc.stored); err != nil {
+ t.Fatalf("seed session %s: %v", tc.token, err)
+ }
}
- if _, err := database.ExecContext(ctx,
- `UPDATE sessions SET expires_at = expires_at || 'Z' WHERE length(expires_at) = 19`); err != nil {
- t.Fatal(err)
+
+ // Only 031 is left to apply, so any change below is its doing.
+ if err := db.MigrateFS(database, migrations.FS); err != nil {
+ t.Fatalf("MigrateFS applying 031: %v", err)
}
- var got string
- if err := database.QueryRowContext(ctx,
- `SELECT expires_at FROM sessions WHERE token = 'legacy'`).Scan(&got); err != nil {
- t.Fatal(err)
- }
- if got != "2030-05-01T10:00:00Z" {
- t.Fatalf("normalized expires_at = %q, want 2030-05-01T10:00:00Z", got)
+
+ for _, tc := range cases {
+ var got string
+ if err := database.QueryRowContext(ctx,
+ `SELECT expires_at FROM sessions WHERE token = ?`, tc.token).Scan(&got); err != nil {
+ t.Fatalf("read %s: %v", tc.token, err)
+ }
+ if got != tc.want {
+ t.Errorf("%s: expires_at = %q, want %q", tc.token, got, tc.want)
+ }
}
}
diff --git a/Server/updater/updater_test.go b/Server/updater/updater_test.go
index be636984..4ef511bb 100644
--- a/Server/updater/updater_test.go
+++ b/Server/updater/updater_test.go
@@ -9,8 +9,10 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
+ "errors"
"fmt"
"io"
+ "io/fs"
"net/http"
"net/http/httptest"
"os"
@@ -501,6 +503,120 @@ func TestExtractChatserverFromTarGz(t *testing.T) {
}
}
+// tarEntry is one member of a test archive; body is empty for header-only
+// entries such as symlinks.
+type tarEntry struct {
+ hdr tar.Header
+ body []byte
+}
+
+func buildTarGz(t *testing.T, entries ...tarEntry) []byte {
+ t.Helper()
+ var buf bytes.Buffer
+ gw := gzip.NewWriter(&buf)
+ tw := tar.NewWriter(gw)
+ for i := range entries {
+ hdr := entries[i].hdr
+ hdr.Mode = 0o755
+ hdr.Size = int64(len(entries[i].body))
+ if err := tw.WriteHeader(&hdr); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tw.Write(entries[i].body); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := tw.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := gw.Close(); err != nil {
+ t.Fatal(err)
+ }
+ return buf.Bytes()
+}
+
+func TestExtractChatserverFromTarGzEntryFilters(t *testing.T) {
+ want := []byte("#!/bin/real\n")
+ wantSum := sha256.Sum256(want)
+ regular := tarEntry{hdr: tar.Header{Name: "chatserver", Typeflag: tar.TypeReg}, body: want}
+
+ tests := []struct {
+ name string
+ entries []tarEntry
+ wantErr bool
+ }{
+ {
+ // A "chatserver" shipped as a symlink must not be followed.
+ name: "non-regular entry is skipped",
+ entries: []tarEntry{{hdr: tar.Header{Name: "chatserver", Typeflag: tar.TypeSymlink, Linkname: "/etc/passwd"}}, regular},
+ },
+ {
+ name: "path-traversal name is skipped",
+ entries: []tarEntry{{hdr: tar.Header{Name: "../../chatserver", Typeflag: tar.TypeReg}, body: []byte("planted")}, regular},
+ },
+ {
+ name: "other basename is skipped",
+ entries: []tarEntry{{hdr: tar.Header{Name: "chatserver.sig", Typeflag: tar.TypeReg}, body: []byte("sig")}, regular},
+ },
+ {
+ name: "no chatserver member at all",
+ entries: []tarEntry{{hdr: tar.Header{Name: "README", Typeflag: tar.TypeReg}, body: []byte("hi")}, {hdr: tar.Header{Name: "chatserver", Typeflag: tar.TypeSymlink, Linkname: "/bin/sh"}}},
+ wantErr: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ dest := filepath.Join(t.TempDir(), "chatserver")
+ gotHash, err := extractChatserverFromTarGz(bytes.NewReader(buildTarGz(t, tc.entries...)), dest)
+ if tc.wantErr {
+ if err == nil {
+ t.Fatalf("extractChatserverFromTarGz = %q, want error", gotHash)
+ }
+ if _, statErr := os.Stat(dest); !errors.Is(statErr, fs.ErrNotExist) {
+ t.Errorf("destPath exists after a failed extraction (stat err %v)", statErr)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("extractChatserverFromTarGz: %v", err)
+ }
+ if gotHash != hex.EncodeToString(wantSum[:]) {
+ t.Errorf("hash = %q, want hash of the regular chatserver entry", gotHash)
+ }
+ got, readErr := os.ReadFile(dest)
+ if readErr != nil {
+ t.Fatal(readErr)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("extracted %q, want %q", got, want)
+ }
+ })
+ }
+}
+
+// A pre-existing staging path is an attacker-planted file: staging is O_EXCL,
+// so extraction must fail rather than write through it.
+func TestExtractChatserverFromTarGzRefusesExistingDest(t *testing.T) {
+ dest := filepath.Join(t.TempDir(), "chatserver")
+ planted := []byte("planted-by-attacker")
+ if err := os.WriteFile(dest, planted, 0o600); err != nil {
+ t.Fatal(err)
+ }
+
+ archive := buildTarGz(t, tarEntry{hdr: tar.Header{Name: "chatserver", Typeflag: tar.TypeReg}, body: []byte("#!/bin/real\n")})
+ if _, err := extractChatserverFromTarGz(bytes.NewReader(archive), dest); !errors.Is(err, fs.ErrExist) {
+ t.Fatalf("extractChatserverFromTarGz err = %v, want fs.ErrExist", err)
+ }
+ got, err := os.ReadFile(dest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, planted) {
+ t.Errorf("pre-existing file was overwritten: %q", got)
+ }
+}
+
func TestAssetFilenameFromURL(t *testing.T) {
got, err := assetFilenameFromURL("https://github.com/J3vb/OwnCord/releases/download/v1.0.0/chatserver.exe")
if err != nil {
diff --git a/Server/ws/deps.go b/Server/ws/deps.go
index 6a050d54..6aab3c9b 100644
--- a/Server/ws/deps.go
+++ b/Server/ws/deps.go
@@ -84,11 +84,20 @@ type KeyHolderChecker interface {
// it live at dispatch time picks up the late wiring. MessageSvc gates channel
// broadcasts through the same posting policy as a real message send.
type PluginDeps struct {
- Registry func() *plugin.Registry
+ Registry func() CommandDispatcher
MessageSvc *service.MessageService
Limiter *auth.RateLimiter
}
+// CommandDispatcher is the one method the chat_command handler needs from the
+// plugin registry; *plugin.Registry satisfies it. Taking the interface rather
+// than the concrete type is what makes the broadcast path testable: without
+// the wazero build tag a real registry has no runtime and can only ever answer
+// with a Reply, so the CanPost gate would otherwise be unreachable from a test.
+type CommandDispatcher interface {
+ DispatchCommand(ctx context.Context, userID, channelID int64, cmd string, args []string) (*plugin.CommandResult, bool)
+}
+
// VoiceDeps holds dependencies for voice handlers.
type VoiceDeps struct {
DB *db.DB
diff --git a/Server/ws/export_test.go b/Server/ws/export_test.go
index 3ceee54a..e41d8606 100644
--- a/Server/ws/export_test.go
+++ b/Server/ws/export_test.go
@@ -407,14 +407,28 @@ func (h *Hub) HandleWebhookParticipantLeftWithContextForTest(ctx context.Context
// for external tests. identity and roomName are passed raw so a test can feed
// malformed values through the same parse path a hostile webhook would.
func (h *Hub) HandleWebhookParticipantJoinedForTest(identity, roomName string) {
+ h.HandleWebhookParticipantJoinedWithContextForTest(context.Background(), identity, roomName)
+}
+
+// HandleWebhookParticipantJoinedWithContextForTest is
+// HandleWebhookParticipantJoinedForTest with a caller-supplied context, so
+// external tests can simulate the webhook HTTP handler's request context
+// (e.g. already-cancelled, as it would be after the webhook sender hangs up)
+// instead of always running with context.Background(). Mirrors
+// HandleWebhookParticipantLeftWithContextForTest.
+func (h *Hub) HandleWebhookParticipantJoinedWithContextForTest(ctx context.Context, identity, roomName string) {
event := &livekit.WebhookEvent{
Event: "participant_joined",
Participant: &livekit.ParticipantInfo{Identity: identity},
Room: &livekit.Room{Name: roomName},
}
- h.handleWebhookParticipantJoined(context.Background(), event)
+ h.handleWebhookParticipantJoined(ctx, event)
}
+// WebhookMaxBodyBytesForTest exposes the webhook body cap so external tests can
+// build a body that is over it without hardcoding the constant twice.
+const WebhookMaxBodyBytesForTest = webhookMaxBodyBytes
+
// HandleWebhookParticipantJoinedEventForTest exposes
// handleWebhookParticipantJoined with a caller-built event so tests can cover
// the nil-participant and nil-room guards.
diff --git a/Server/ws/handlers_command.go b/Server/ws/handlers_command.go
index ff88587a..674d4e1a 100644
--- a/Server/ws/handlers_command.go
+++ b/Server/ws/handlers_command.go
@@ -17,7 +17,6 @@ import (
"time"
"github.com/owncord/server/auth"
- "github.com/owncord/server/plugin"
"github.com/owncord/server/service"
)
@@ -50,7 +49,7 @@ func handleChatCommandV2(ctx context.Context, cmd Command, _ ClientInfo, deps an
return Result{Error: ClientError{Code: ErrCodeRateLimited, Message: "too many commands"}}
}
- var reg *plugin.Registry
+ var reg CommandDispatcher
if d.Registry != nil {
reg = d.Registry()
}
diff --git a/Server/ws/handlers_command_gate_test.go b/Server/ws/handlers_command_gate_test.go
new file mode 100644
index 00000000..6c9b118c
--- /dev/null
+++ b/Server/ws/handlers_command_gate_test.go
@@ -0,0 +1,192 @@
+package ws
+
+// handlers_command_gate_test.go — success path of the chat_command handler:
+// the ephemeral reply, the MessageService.CanPost broadcast gate, and the
+// plugin_broadcast fan-out. handlers_command_test.go (package ws_test) covers
+// only the refusals, which it can reach through the hub; these need PluginDeps
+// and the unexported command struct, so they live in-package.
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/owncord/server/auth"
+ "github.com/owncord/server/db"
+ "github.com/owncord/server/plugin"
+ "github.com/owncord/server/service"
+)
+
+// stubDispatcher stands in for *plugin.Registry. A real registry is useless
+// here: without the wazero build tag it has no runtime, so DispatchCommand can
+// only ever return the "runtime is not built" Reply — never a Broadcast, and
+// therefore never the CanPost gate below.
+type stubDispatcher struct {
+ result *plugin.CommandResult
+ handled bool
+}
+
+func (s stubDispatcher) DispatchCommand(_ context.Context, _, _ int64, _ string, _ []string) (*plugin.CommandResult, bool) {
+ return s.result, s.handled
+}
+
+// newCommandTestDeps builds PluginDeps whose MessageSvc is the real service
+// (the same CanPost a message send runs) over an in-memory DB, plus a
+// dispatcher stub returning res. Returns the owner (all permissions), a user
+// whose role carries none, and a text channel.
+func newCommandTestDeps(t *testing.T, res *plugin.CommandResult) (deps PluginDeps, ownerID, mutedID, chID int64) {
+ t.Helper()
+ database, err := db.Open(":memory:")
+ if err != nil {
+ t.Fatalf("db.Open: %v", err)
+ }
+ if err := db.Migrate(database); err != nil {
+ t.Fatalf("Migrate: %v", err)
+ }
+ t.Cleanup(func() { _ = database.Close() })
+
+ ctx := context.Background()
+ if ownerID, err = database.CreateUser(ctx, "cmd-owner", "hash", 1); err != nil { // Owner role
+ t.Fatalf("CreateUser owner: %v", err)
+ }
+ role, err := database.CreateRole(ctx, "cmd-muted", nil, 0, 0) // no permission bits
+ if err != nil {
+ t.Fatalf("CreateRole: %v", err)
+ }
+ if mutedID, err = database.CreateUser(ctx, "cmd-muted-user", "hash", int(role.ID)); err != nil {
+ t.Fatalf("CreateUser muted: %v", err)
+ }
+ if chID, err = database.CreateChannel(ctx, "cmd-chan", "text", "", "", 0); err != nil {
+ t.Fatalf("CreateChannel: %v", err)
+ }
+
+ svc := service.New(database, auth.NewRateLimiter())
+ deps = PluginDeps{
+ Registry: func() CommandDispatcher { return stubDispatcher{result: res, handled: true} },
+ MessageSvc: svc.Messages,
+ }
+ return deps, ownerID, mutedID, chID
+}
+
+// A plugin Reply becomes an ephemeral command_reply envelope carrying the
+// request's req_id, and reaches nobody else.
+func TestHandleChatCommandV2_ReplyIsEphemeral(t *testing.T) {
+ deps, ownerID, _, chID := newCommandTestDeps(t, &plugin.CommandResult{Reply: "pong"})
+ cmd := ChatCommandCmd{userID: ownerID, channelID: chID, command: "/ping", reqID: "req-7"}
+
+ result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: ownerID}, deps)
+
+ if result.Error != nil {
+ t.Fatalf("unexpected error: %v", result.Error)
+ }
+ if len(result.Events) != 0 {
+ t.Fatalf("a reply-only command must not broadcast, got %d events", len(result.Events))
+ }
+ var env struct {
+ Type string `json:"type"`
+ ReqID string `json:"req_id"`
+ Payload struct {
+ Text string `json:"text"`
+ } `json:"payload"`
+ }
+ if err := json.Unmarshal(result.Reply, &env); err != nil {
+ t.Fatalf("unmarshal reply %s: %v", result.Reply, err)
+ }
+ if env.Type != MsgTypeCommandReply {
+ t.Errorf("type = %q, want %q", env.Type, MsgTypeCommandReply)
+ }
+ if env.ReqID != "req-7" {
+ t.Errorf("req_id = %q, want req-7", env.ReqID)
+ }
+ if env.Payload.Text != "pong" {
+ t.Errorf("text = %q, want pong", env.Payload.Text)
+ }
+}
+
+// An authorized user's Broadcast fans out as a plugin_broadcast event on the
+// invoking channel.
+func TestHandleChatCommandV2_BroadcastFansOutWhenAllowed(t *testing.T) {
+ deps, ownerID, _, chID := newCommandTestDeps(t, &plugin.CommandResult{Broadcast: "rolled a 6"})
+ cmd := ChatCommandCmd{userID: ownerID, channelID: chID, command: "/roll", reqID: "req-8"}
+
+ result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: ownerID}, deps)
+
+ if result.Error != nil {
+ t.Fatalf("unexpected error: %v", result.Error)
+ }
+ if len(result.Events) != 1 {
+ t.Fatalf("expected 1 broadcast event, got %d", len(result.Events))
+ }
+ ev, ok := result.Events[0].(PluginBroadcastEvent)
+ if !ok {
+ t.Fatalf("expected PluginBroadcastEvent, got %T", result.Events[0])
+ }
+ if ev.ChannelID() != chID {
+ t.Errorf("event channel = %d, want %d", ev.ChannelID(), chID)
+ }
+ var env struct {
+ Type string `json:"type"`
+ Payload struct {
+ ChannelID int64 `json:"channel_id"`
+ UserID int64 `json:"user_id"`
+ Command string `json:"command"`
+ Text string `json:"text"`
+ } `json:"payload"`
+ }
+ if err := json.Unmarshal(ev.Payload(), &env); err != nil {
+ t.Fatalf("unmarshal payload %s: %v", ev.Payload(), err)
+ }
+ if env.Type != MsgTypePluginBroadcast {
+ t.Errorf("type = %q, want %q", env.Type, MsgTypePluginBroadcast)
+ }
+ if env.Payload.ChannelID != chID || env.Payload.UserID != ownerID {
+ t.Errorf("payload ids = (%d,%d), want (%d,%d)", env.Payload.ChannelID, env.Payload.UserID, chID, ownerID)
+ }
+ if env.Payload.Command != "/roll" || env.Payload.Text != "rolled a 6" {
+ t.Errorf("payload = %+v, want command=/roll text=rolled a 6", env.Payload)
+ }
+}
+
+// The CanPost gate: a user whose role cannot post gets FORBIDDEN and nothing
+// reaches the channel — even though the plugin returned a broadcast. The reply
+// is dropped with it (the denial is the security signal; see handlers_command.go).
+func TestHandleChatCommandV2_BroadcastDeniedWithoutPostPermission(t *testing.T) {
+ deps, _, mutedID, chID := newCommandTestDeps(t, &plugin.CommandResult{Reply: "ok", Broadcast: "rolled a 6"})
+ cmd := ChatCommandCmd{userID: mutedID, channelID: chID, command: "/roll", reqID: "req-9"}
+
+ result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: mutedID}, deps)
+
+ ce, ok := result.Error.(ClientError)
+ if !ok {
+ t.Fatalf("expected ClientError, got %T (%v)", result.Error, result.Error)
+ }
+ if ce.Code != ErrCodeForbidden {
+ t.Errorf("code = %q, want %q", ce.Code, ErrCodeForbidden)
+ }
+ if len(result.Events) != 0 {
+ t.Errorf("denied command must not broadcast, got %d events", len(result.Events))
+ }
+ if result.Reply != nil {
+ t.Errorf("denied command must not also reply, got %s", result.Reply)
+ }
+}
+
+// A broadcast aimed at a channel that does not exist is NOT_FOUND, not
+// FORBIDDEN — CanPost's missing-channel branch.
+func TestHandleChatCommandV2_BroadcastUnknownChannel(t *testing.T) {
+ deps, ownerID, _, _ := newCommandTestDeps(t, &plugin.CommandResult{Broadcast: "hi"})
+ cmd := ChatCommandCmd{userID: ownerID, channelID: 424242, command: "/roll"}
+
+ result := handleChatCommandV2(context.Background(), cmd, ClientInfo{UserID: ownerID}, deps)
+
+ ce, ok := result.Error.(ClientError)
+ if !ok {
+ t.Fatalf("expected ClientError, got %T (%v)", result.Error, result.Error)
+ }
+ if ce.Code != ErrCodeNotFound {
+ t.Errorf("code = %q, want %q", ce.Code, ErrCodeNotFound)
+ }
+ if len(result.Events) != 0 {
+ t.Errorf("expected no events, got %d", len(result.Events))
+ }
+}
diff --git a/Server/ws/hub.go b/Server/ws/hub.go
index ab2e9076..fd79179f 100644
--- a/Server/ws/hub.go
+++ b/Server/ws/hub.go
@@ -188,7 +188,15 @@ func NewHub(database *db.DB, limiter *auth.RateLimiter, svc *service.Services) *
// Phase C Step 9 — plugin slash commands. Registry is read live because
// SetPluginRegistry wires it after NewHub; MessageSvc gates broadcasts.
reg.RegisterV2(MsgTypeChatCommand, handleChatCommandV2, PluginDeps{
- Registry: func() *plugin.Registry { return h.pluginRegistry },
+ // A nil registry must yield a nil interface, not a typed-nil
+ // *plugin.Registry — the handler's "no plugins loaded" check is an
+ // interface comparison.
+ Registry: func() CommandDispatcher {
+ if h.pluginRegistry == nil {
+ return nil
+ }
+ return h.pluginRegistry
+ },
MessageSvc: h.messageSvc,
Limiter: h.limiter,
})
diff --git a/Server/ws/hub_broadcast_test.go b/Server/ws/hub_broadcast_test.go
index d0ccaaef..7f720968 100644
--- a/Server/ws/hub_broadcast_test.go
+++ b/Server/ws/hub_broadcast_test.go
@@ -1,6 +1,7 @@
package ws_test
import (
+ "bytes"
"context"
"encoding/json"
"testing"
@@ -15,22 +16,29 @@ import (
// every connected client — an identity key that fails to propagate silently
// breaks E2EE key agreement for everyone already online.
-// awaitMessage reads one message from ch, failing if none arrives.
-func awaitMessage(t *testing.T, ch chan []byte) map[string]any {
+// awaitRawMessage reads one raw frame from ch, failing if none arrives.
+func awaitRawMessage(t *testing.T, ch chan []byte) []byte {
t.Helper()
select {
case raw := <-ch:
- var msg map[string]any
- if err := json.Unmarshal(raw, &msg); err != nil {
- t.Fatalf("unmarshal %q: %v", raw, err)
- }
- return msg
+ return raw
case <-time.After(2 * time.Second):
t.Fatal("no message received")
return nil
}
}
+// awaitMessage reads one message from ch, failing if none arrives.
+func awaitMessage(t *testing.T, ch chan []byte) map[string]any {
+ t.Helper()
+ raw := awaitRawMessage(t, ch)
+ var msg map[string]any
+ if err := json.Unmarshal(raw, &msg); err != nil {
+ t.Fatalf("unmarshal %q: %v", raw, err)
+ }
+ return msg
+}
+
func TestHub_BroadcastUserUpdate(t *testing.T) {
hub, _ := newTestHub(t)
go hub.Run()
@@ -186,14 +194,123 @@ func TestHub_ChannelReadAudience_ExcludesArchivedChannel(t *testing.T) {
assertNotReceived(t, send, "member with base READ_MESSAGES on an archived channel")
}
+// stubDMEvent is a minimal ws.SequencedDMEvent so EmitEvents routes through
+// sendSequencedToUsers — persistEvent's second call site, the one that stamps
+// a non-zero channel_id without going through the broadcast queue.
+type stubDMEvent struct {
+ channelID int64
+ participantIDs []int64
+ payload []byte
+}
+
+func (e stubDMEvent) EventType() string { return "chat_message" }
+func (e stubDMEvent) ChannelID() int64 { return e.channelID }
+func (e stubDMEvent) ParticipantIDs() []int64 { return e.participantIDs }
+func (e stubDMEvent) Payload() []byte { return e.payload }
+
+// TestHub_SetEventPersister pins the invariant persistEvent exists for: the
+// row written to the EventStore carries the same seq (and type, and channel)
+// as the wrapped payload the client received — from both call sites,
+// deliverBroadcast and sendSequencedToUsers. Cold-tier reconnect replay
+// selects rows by row-seq against the payload-seq the client acked, so a
+// mismatch silently replays the wrong window.
func TestHub_SetEventPersister(t *testing.T) {
hub, database := newTestHub(t)
- persister := ws.NewEventPersister(database, 16, 4, 10*time.Millisecond)
+ // The persister needs the real events table; the hub's own test schema has
+ // none, so the store is a separately migrated DB.
+ store := openEventStoreDB(t)
+ persister := ws.NewEventPersister(store, 64, 1, 5*time.Millisecond)
+ persister.Start(context.Background())
// Setting and clearing must both be safe — SetEventPersister is called at
// startup and again on shutdown/reconfiguration.
hub.SetEventPersister(persister)
hub.SetEventPersister(nil)
hub.SetEventPersister(persister)
+
+ go hub.Run()
+ t.Cleanup(func() {
+ hub.Stop()
+ persister.Stop(context.Background())
+ })
+
+ user := seedMemberUser(t, database, "persisted-event-member")
+ chID := seedTestChannel(t, database, "persisted-event-dm")
+
+ send := make(chan []byte, 8)
+ hub.RegisterNowForTest(ws.NewTestClient(hub, user.ID, send))
+
+ // Global broadcast → deliverBroadcast → persistEvent(seq, 0, wrapped).
+ hub.BroadcastToAll([]byte(`{"type":"user_update","payload":{"user_id":7}}`))
+ globalFrame := awaitRawMessage(t, send)
+
+ // Sequenced DM → sendSequencedToUsers → persistEvent(seq, chID, wrapped).
+ hub.EmitEvents(context.Background(), []ws.Event{stubDMEvent{
+ channelID: chID,
+ participantIDs: []int64{user.ID},
+ payload: []byte(`{"type":"chat_message","payload":{"id":1}}`),
+ }})
+ dmFrame := awaitRawMessage(t, send)
+
+ // Stop drains the queue and waits for the flusher to exit, so everything
+ // enqueued above is on disk once it returns. The cleanup's second Stop is
+ // a no-op.
+ persister.Stop(context.Background())
+
+ stored, err := store.GetEventsSince(context.Background(), 0, 100)
+ if err != nil {
+ t.Fatalf("GetEventsSince: %v", err)
+ }
+ bySeq := make(map[int64]db.PersistedEvent, len(stored))
+ seen := make([]int64, 0, len(stored))
+ for _, row := range stored {
+ bySeq[row.Seq] = row
+ seen = append(seen, row.Seq)
+ }
+
+ cases := []struct {
+ label string
+ frame []byte
+ eventType string
+ channelID int64
+ }{
+ {"global broadcast", globalFrame, "user_update", 0},
+ {"sequenced DM", dmFrame, "chat_message", chID},
+ }
+ seqs := make([]int64, 0, len(cases))
+ for _, tc := range cases {
+ var wire struct {
+ Seq int64 `json:"seq"`
+ Type string `json:"type"`
+ }
+ if err := json.Unmarshal(tc.frame, &wire); err != nil {
+ t.Fatalf("%s: unmarshal %q: %v", tc.label, tc.frame, err)
+ }
+ if wire.Seq == 0 {
+ t.Fatalf("%s: delivered frame carries no seq: %s", tc.label, tc.frame)
+ }
+ if wire.Type != tc.eventType {
+ t.Fatalf("%s: frame type = %q, want %q", tc.label, wire.Type, tc.eventType)
+ }
+ seqs = append(seqs, wire.Seq)
+
+ row, ok := bySeq[wire.Seq]
+ if !ok {
+ t.Fatalf("%s: no persisted row at the delivered seq %d (stored seqs %v)",
+ tc.label, wire.Seq, seen)
+ }
+ if row.EventType != tc.eventType {
+ t.Errorf("%s: row event_type = %q, want %q", tc.label, row.EventType, tc.eventType)
+ }
+ if row.ChannelID != tc.channelID {
+ t.Errorf("%s: row channel_id = %d, want %d", tc.label, row.ChannelID, tc.channelID)
+ }
+ if !bytes.Equal(row.Payload, tc.frame) {
+ t.Errorf("%s: row payload = %s, want the delivered frame %s", tc.label, row.Payload, tc.frame)
+ }
+ }
+ if seqs[1] <= seqs[0] {
+ t.Errorf("seqs not monotonic across the two persist call sites: %v", seqs)
+ }
}
diff --git a/Server/ws/livekit_test.go b/Server/ws/livekit_test.go
index d1122a55..f9e86a8a 100644
--- a/Server/ws/livekit_test.go
+++ b/Server/ws/livekit_test.go
@@ -2,7 +2,10 @@ package ws_test
import (
"context"
+ "crypto/sha256"
+ "encoding/base64"
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"os"
@@ -1268,6 +1271,121 @@ func TestWebhookHandler_EmptyBody(t *testing.T) {
}
}
+// signedWebhookRequest builds the request LiveKit itself would send: the body
+// hashed with sha256, that hash carried as the token's sha256 claim, and the
+// token signed with the shared secret. bodyToSign is what the token commits
+// to; bodySent is what actually travels — passing different values simulates a
+// captured token replayed against a forged payload.
+func signedWebhookRequest(t *testing.T, apiKey, apiSecret, bodyToSign, bodySent string) *http.Request {
+ t.Helper()
+
+ sum := sha256.Sum256([]byte(bodyToSign))
+ token, err := auth.NewAccessToken(apiKey, apiSecret).
+ SetValidFor(5 * time.Minute).
+ SetSha256(base64.StdEncoding.EncodeToString(sum[:])).
+ ToJWT()
+ if err != nil {
+ t.Fatalf("minting webhook token: %v", err)
+ }
+
+ req := httptest.NewRequest(http.MethodPost, "/livekit/webhook", strings.NewReader(bodySent))
+ req.Header.Set("Authorization", token)
+ req.Header.Set("Content-Type", "application/webhook+json")
+ return req
+}
+
+// webhookBody renders the protojson payload LiveKit posts for a participant
+// event. pad is an unknown field the parser discards, used only to push the
+// body past the size cap.
+func webhookBody(event string, userID, channelID int64, joinToken, pad string) string {
+ return fmt.Sprintf(
+ `{"event":%q,"room":{"name":%q},"participant":{"identity":%q},"pad":%q}`,
+ event, ws.RoomName(channelID), participantIdentityFor(userID, joinToken), pad)
+}
+
+// TestWebhookHandler_SignedParticipantLeftDispatches is the only webhook test
+// that gets past ReceiveWebhookEvent: it mints a real LiveKit webhook token
+// over the real body and asserts the handler both dispatches the event (the
+// voice_states row is cleared) and answers 200. Without it, the 401 tests above
+// would all still pass if verification were changed to reject everything.
+func TestWebhookHandler_SignedParticipantLeftDispatches(t *testing.T) {
+ t.Parallel()
+
+ const apiKey, apiSecret = "webhook-signed-key", "webhook-signed-secret-0123456789"
+
+ hub, database := newVoiceHub(t)
+ user := seedVoiceOwner(t, database, "webhook-signed-user")
+ chanID := seedVoiceChan(t, database, "webhook-signed-ch")
+
+ if err := database.JoinVoiceChannel(context.Background(), user.ID, chanID); err != nil {
+ t.Fatalf("JoinVoiceChannel: %v", err)
+ }
+ state, err := database.GetVoiceState(context.Background(), user.ID)
+ if err != nil || state == nil {
+ t.Fatalf("GetVoiceState: %v (nil=%v)", err, state == nil)
+ }
+
+ body := webhookBody("participant_left", user.ID, chanID, state.JoinedAt, "")
+ rec := httptest.NewRecorder()
+ hub.NewLiveKitWebhookHandler(apiKey, apiSecret)(rec,
+ signedWebhookRequest(t, apiKey, apiSecret, body, body))
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected 200 for a correctly signed webhook, got %d (%s)",
+ rec.Code, strings.TrimSpace(rec.Body.String()))
+ }
+
+ after, err := database.GetVoiceState(context.Background(), user.ID)
+ if err != nil {
+ t.Fatalf("GetVoiceState after webhook: %v", err)
+ }
+ if after != nil {
+ t.Errorf("participant_left verified but never dispatched: voice state still present (channel %d)",
+ after.ChannelID)
+ }
+}
+
+// TestWebhookHandler_SignedRequestRejections covers the two ways a request
+// carrying a genuinely signed token must still be refused: the token's
+// body-hash claim not matching the body it arrived with (a captured token
+// replayed against a forged payload), and a body past webhookMaxBodyBytes.
+func TestWebhookHandler_SignedRequestRejections(t *testing.T) {
+ t.Parallel()
+
+ const apiKey, apiSecret = "webhook-reject-key", "webhook-reject-secret-0123456789"
+
+ signed := webhookBody("participant_left", 7, 42, "tok", "")
+ // Same token, different body: only the sha256 claim binding catches this.
+ mutated := webhookBody("participant_left", 8, 42, "tok", "")
+ // Correctly signed, but larger than webhookMaxBodyBytes — only the
+ // MaxBytesReader cap catches this one.
+ oversize := webhookBody("participant_left", 7, 42, "tok", strings.Repeat("a", ws.WebhookMaxBodyBytesForTest))
+
+ tests := []struct {
+ name string
+ bodyToSign string
+ bodySent string
+ }{
+ {"token replayed against a mutated body", signed, mutated},
+ {"body over webhookMaxBodyBytes", oversize, oversize},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ hub := ws.NewHubForTest()
+ rec := httptest.NewRecorder()
+ hub.NewLiveKitWebhookHandler(apiKey, apiSecret)(rec,
+ signedWebhookRequest(t, apiKey, apiSecret, tt.bodyToSign, tt.bodySent))
+
+ if rec.Code != http.StatusUnauthorized {
+ t.Errorf("expected 401, got %d", rec.Code)
+ }
+ })
+ }
+}
+
// ---------------------------------------------------------------------------
// livekit_webhook.go – MountWebhookRoute tests
// ---------------------------------------------------------------------------
diff --git a/Server/ws/livekit_webhook_joined_test.go b/Server/ws/livekit_webhook_joined_test.go
index a50f6705..4c70540d 100644
--- a/Server/ws/livekit_webhook_joined_test.go
+++ b/Server/ws/livekit_webhook_joined_test.go
@@ -141,6 +141,43 @@ func TestWebhook_ParticipantJoined_TransientReadErrorDoesNotEvict(t *testing.T)
}
}
+// TestWebhook_ParticipantJoined_SurvivesCancelledRequestContext locks the
+// participant_joined half of OC-0018 (the participant_left half is locked by
+// TestWebhook_ParticipantLeft_SurvivesCancelledRequestContext in
+// livekit_test.go). Without the context.WithoutCancel detach, a webhook sender
+// (LiveKit) that hangs up mid-request cancels r.Context(); GetVoiceState then
+// fails, the handler takes the "transient read failure" branch and skips the
+// rogue-participant check entirely — so a participant presenting a replayed
+// join token is never removed from the SFU.
+func TestWebhook_ParticipantJoined_SurvivesCancelledRequestContext(t *testing.T) {
+ hub, database := newVoiceHub(t)
+ user := seedVoiceOwner(t, database, "joined-ctxcancel-user")
+ chanID := seedVoiceChan(t, database, "joined-ctxcancel-ch")
+
+ // Simulate net/http cancelling the request context because the webhook
+ // sender hung up before the handler finished.
+ cancelledCtx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ logs := captureLogs(t)
+
+ // No voice_states row exists for this user — the join is unauthorized and
+ // must still be flagged and evicted on a dead request context.
+ hub.HandleWebhookParticipantJoinedWithContextForTest(
+ cancelledCtx,
+ participantIdentityFor(user.ID, "replayed-token"),
+ roomNameFor(chanID),
+ )
+
+ out := logs()
+ if strings.Contains(out, "skipping rogue-participant check") {
+ t.Errorf("the cancelled request context was mistaken for a transient DB failure, so the rogue participant was never evicted; log:\n%s", out)
+ }
+ if !strings.Contains(out, "rogue participant_joined") {
+ t.Errorf("no rogue-participant warning logged on a cancelled request context; got:\n%s", out)
+ }
+}
+
func TestWebhook_ParticipantJoined_WrongChannelFlagged(t *testing.T) {
hub, database := newVoiceHub(t)
user := seedVoiceOwner(t, database, "joined-wrongch-user")
diff --git a/Server/ws/voice_audience_test.go b/Server/ws/voice_audience_test.go
index d1a66e46..b2b72420 100644
--- a/Server/ws/voice_audience_test.go
+++ b/Server/ws/voice_audience_test.go
@@ -4,6 +4,8 @@ import (
"context"
"slices"
"testing"
+
+ "github.com/owncord/server/auth"
)
// Voice membership is gated on CONNECT_VOICE alone (voice_join), but the
@@ -108,3 +110,72 @@ func TestFinishVoiceLeave_EvictedUserAlwaysInAudience(t *testing.T) {
t.Fatal("finishVoiceLeave enqueued nothing")
}
}
+
+// Both DB-error branches of the READ-audience resolver must deny. The role
+// scan underneath them treats whatever channel it is handed as a readable
+// non-DM channel, so an unreadable channels row — or an unreadable DM
+// participant list — that fell through would resolve to every connected user
+// holding base READ_MESSAGES, fanning a private room's voice_state /
+// voice_leave out server-wide.
+func TestChannelReadAudience_GetChannelErrorDeniesEveryone(t *testing.T) {
+ ctx := context.Background()
+ database := newHarvestVoiceDB(t)
+ uid := seedHarvestVoiceUser(t, database, "audience-chan-err")
+ chID := mustCreateVoiceChannel(t, database, "audience-room")
+
+ h := NewHub(database, auth.NewRateLimiter(), nil)
+ h.clients[uid] = NewTestClient(h, uid, make(chan []byte, 8))
+
+ // Precondition: the role scan really does grant this user READ on this
+ // channel, so an empty audience after the fault can only be the deny.
+ if got := h.channelReadAudience(ctx, chID); !slices.Contains(got, uid) {
+ t.Fatalf("precondition: user %d must be in the READ audience, got %v", uid, got)
+ }
+
+ // Make exactly GetChannel fail; roles and channel_overrides keep
+ // resolving, so the role scan would still return this user.
+ if _, err := database.ExecContext(ctx, `ALTER TABLE channels RENAME TO channels_offline`); err != nil {
+ t.Fatalf("rename channels: %v", err)
+ }
+
+ if got := h.channelReadAudience(ctx, chID); len(got) != 0 {
+ t.Errorf("channelReadAudience resolved %v for an unreadable channel row — an unresolvable channel must deny, not fall through to the role scan", got)
+ }
+}
+
+// The DM half of the same rule: a DM carries no channel_overrides rows, so its
+// participant list is the only membership evidence there is. When that read
+// fails there is nothing left to filter on and the audience must be empty.
+func TestChannelReadAudience_DMParticipantsErrorDeniesEveryone(t *testing.T) {
+ ctx := context.Background()
+ database := newHarvestVoiceDB(t)
+ alice := seedHarvestVoiceUser(t, database, "audience-dm-alice")
+ bob := seedHarvestVoiceUser(t, database, "audience-dm-bob")
+ mallory := seedHarvestVoiceUser(t, database, "audience-dm-mallory")
+ dm, _, err := database.GetOrCreateDMChannel(ctx, alice, bob)
+ if err != nil {
+ t.Fatalf("GetOrCreateDMChannel: %v", err)
+ }
+
+ h := NewHub(database, auth.NewRateLimiter(), nil)
+ for _, uid := range []int64{alice, bob, mallory} {
+ h.clients[uid] = NewTestClient(h, uid, make(chan []byte, 8))
+ }
+
+ // Precondition: the DM resolves to its participants only — mallory is
+ // connected and holds base READ_MESSAGES, but is not in this DM.
+ if got := h.channelReadAudience(ctx, dm.ID); !slices.Contains(got, alice) ||
+ !slices.Contains(got, bob) || slices.Contains(got, mallory) {
+ t.Fatalf("precondition: DM audience must be exactly participants %d and %d, got %v", alice, bob, got)
+ }
+
+ // Make exactly GetDMParticipantIDs fail; the channels row still resolves
+ // as type "dm", so the resolver reaches the DM branch and nothing else.
+ if _, err := database.ExecContext(ctx, `ALTER TABLE dm_participants RENAME TO dm_participants_offline`); err != nil {
+ t.Fatalf("rename dm_participants: %v", err)
+ }
+
+ if got := h.channelReadAudience(ctx, dm.ID); len(got) != 0 {
+ t.Errorf("channelReadAudience resolved %v for a DM whose participant list could not be read — an unresolvable DM must deny", got)
+ }
+}
diff --git a/Server/ws/voice_handlers_test.go b/Server/ws/voice_handlers_test.go
index 19450e97..3d23406b 100644
--- a/Server/ws/voice_handlers_test.go
+++ b/Server/ws/voice_handlers_test.go
@@ -3,6 +3,7 @@ package ws_test
import (
"context"
"encoding/json"
+ "strings"
"testing"
"testing/fstest"
"time"
@@ -647,27 +648,47 @@ func TestVoice_Camera_DisableAllowedAfterPermissionRevoked(t *testing.T) {
}
}
-// TestVoice_Camera_NoPermission: Member without USE_VIDEO gets FORBIDDEN.
-func TestVoice_Camera_NoPermission(t *testing.T) {
- hub, _ := newVoiceHub(t)
+// assertStreamPermissionRefused fails unless msgs carry a FORBIDDEN error
+// naming perm. A bare "an error arrived" check is not enough here: the stream
+// handlers refuse an un-joined client with VOICE_ERROR "not in a voice channel"
+// long before the permission gate runs, which is exactly how the camera and
+// screenshare permission tests used to pass without ever reaching it.
+func assertStreamPermissionRefused(t *testing.T, msgs [][]byte, perm string) {
+ t.Helper()
+ for _, m := range msgs {
+ if extractCode(t, m) == ws.ErrCodeForbidden &&
+ strings.Contains(extractMessage(t, m), perm) {
+ return
+ }
+ }
+ got := make([]string, 0, len(msgs))
+ for _, m := range msgs {
+ got = append(got, extractType(t, m)+"/"+extractCode(t, m)+"/"+extractMessage(t, m))
+ }
+ t.Errorf("expected FORBIDDEN error naming %s, got %v", perm, got)
+}
- // Client with no user set → hasChannelPerm returns false.
- send := make(chan []byte, 16)
- c := ws.NewTestClient(hub, 7001, send)
- hub.Register(c)
- waitRegistered(t, hub, c)
+// TestVoice_Camera_NoPermission: a member already in voice whose role lacks
+// USE_VIDEO is refused with FORBIDDEN and stays off camera.
+func TestVoice_Camera_NoPermission(t *testing.T) {
+ hub, database := newVoiceHub(t)
+
+ // Role 4 (Member) carries CONNECT_VOICE but neither USE_VIDEO nor
+ // SHARE_SCREEN, so voice_join succeeds and only the toggle is denied.
+ user := seedVoiceUserWithRole(t, database, "cam-noperm", 4)
+ chanID := seedVoiceChan(t, database, "vc-cam-noperm")
+ c, send := joinVoice(t, hub, user, chanID)
hub.HandleMessageForTest(c, voiceCameraMsg(true))
- msgs := drainChanTimeout(send, 30*time.Millisecond)
- found := false
- for _, m := range msgs {
- if extractType(t, m) == "error" {
- found = true
- }
+ assertStreamPermissionRefused(t, drainChanTimeout(send, 30*time.Millisecond), "USE_VIDEO")
+
+ state, err := database.GetVoiceState(context.Background(), user.ID)
+ if err != nil {
+ t.Fatalf("GetVoiceState: %v", err)
}
- if !found {
- t.Error("expected FORBIDDEN error for camera toggle without USE_VIDEO permission")
+ if state == nil || state.Camera {
+ t.Error("camera enabled despite the missing USE_VIDEO permission")
}
}
@@ -834,27 +855,25 @@ func TestVoice_Screenshare_DisableAllowedAfterPermissionRevoked(t *testing.T) {
}
}
-// TestVoice_Screenshare_NoPermission: client without SHARE_SCREEN gets FORBIDDEN.
+// TestVoice_Screenshare_NoPermission: a member already in voice whose role
+// lacks SHARE_SCREEN is refused with FORBIDDEN and publishes nothing.
func TestVoice_Screenshare_NoPermission(t *testing.T) {
- hub, _ := newVoiceHub(t)
+ hub, database := newVoiceHub(t)
- // Client with no user set → hasChannelPerm returns false.
- send := make(chan []byte, 16)
- c := ws.NewTestClient(hub, 7002, send)
- hub.Register(c)
- waitRegistered(t, hub, c)
+ user := seedVoiceUserWithRole(t, database, "ss-noperm", 4) // Member: no SHARE_SCREEN
+ chanID := seedVoiceChan(t, database, "vc-ss-noperm")
+ c, send := joinVoice(t, hub, user, chanID)
hub.HandleMessageForTest(c, voiceScreenshareMsg(true))
- msgs := drainChanTimeout(send, 30*time.Millisecond)
- found := false
- for _, m := range msgs {
- if extractType(t, m) == "error" {
- found = true
- }
+ assertStreamPermissionRefused(t, drainChanTimeout(send, 30*time.Millisecond), "SHARE_SCREEN")
+
+ state, err := database.GetVoiceState(context.Background(), user.ID)
+ if err != nil {
+ t.Fatalf("GetVoiceState: %v", err)
}
- if !found {
- t.Error("expected FORBIDDEN error for screenshare toggle without SHARE_SCREEN permission")
+ if state == nil || state.Screenshare {
+ t.Error("screenshare enabled despite the missing SHARE_SCREEN permission")
}
}
diff --git a/Server/ws/voice_moderation_test.go b/Server/ws/voice_moderation_test.go
index 3d94544a..3bf40ab3 100644
--- a/Server/ws/voice_moderation_test.go
+++ b/Server/ws/voice_moderation_test.go
@@ -11,6 +11,7 @@ import (
"github.com/owncord/server/auth"
"github.com/owncord/server/config"
"github.com/owncord/server/db"
+ "github.com/owncord/server/permissions"
"github.com/owncord/server/ws"
)
@@ -573,6 +574,54 @@ func TestVoiceMod_Move_ArchivedDestination_BadRequest(t *testing.T) {
}
}
+// TestVoiceMod_Move_TargetCannotConnectToDestination_Forbidden locks the
+// destination gate that is evaluated against the TARGET's access, not the
+// moderator's: a move must not become a way to place someone in a channel they
+// could not join themselves. The actor keeps MUTE_MEMBERS and still outranks
+// the target, so only the target's missing CONNECT_VOICE can refuse this.
+func TestVoiceMod_Move_TargetCannotConnectToDestination_Forbidden(t *testing.T) {
+ hub, database := newVoiceModHub(t)
+ fromID := seedVoiceChan(t, database, "vc-move-noconnect-from")
+ toID := seedVoiceChan(t, database, "vc-move-noconnect-to")
+ actor := seedVoiceUserWithRole(t, database, "admin-move-noconnect", 2)
+ target := seedVoiceUserWithRole(t, database, "member-move-noconnect", 4)
+
+ // Destination denies CONNECT_VOICE to the target's role (Member, id 4).
+ if err := database.UpsertChannelOverride(
+ context.Background(), toID, 4, 0, permissions.ConnectVoice,
+ ); err != nil {
+ t.Fatalf("UpsertChannelOverride: %v", err)
+ }
+
+ _, targetSend := joinVoice(t, hub, target, fromID)
+
+ send := make(chan []byte, 16)
+ c := ws.NewTestClientWithUser(hub, actor, fromID, send)
+ hub.Register(c)
+ waitRegistered(t, hub, c)
+
+ hub.HandleMessageForTest(c, voiceModMoveMsg(target.ID, toID))
+
+ if code := receiveErrorCode(send, waitTimeout); code != "FORBIDDEN" {
+ t.Fatalf("error code = %q, want FORBIDDEN", code)
+ }
+ if payload := receiveMsgOfType(targetSend, "voice_moved", 100*time.Millisecond); payload != nil {
+ t.Errorf("target must not receive voice_moved for a refused move, got %v", payload)
+ }
+ state, err := database.GetVoiceState(context.Background(), target.ID)
+ if err != nil {
+ t.Fatalf("GetVoiceState: %v", err)
+ }
+ if state == nil {
+ t.Fatal("a refused move must leave the target in voice")
+ } else if state.ChannelID != fromID {
+ t.Errorf("target channel = %d, want %d (unchanged)", state.ChannelID, fromID)
+ }
+ if slices.Contains(auditActions(t, database), "voice_mod_move") {
+ t.Error("a refused move must not write a voice_mod_move audit entry")
+ }
+}
+
// TestVoiceMod_Kick_EvictionIsScopedToAuthorizedChannel locks the fix for
// v024: voiceModTarget authorizes against a DB snapshot, but the eviction ran
// through the unscoped VoiceModerator.DisconnectFromVoice, which drops the
diff --git a/Server/ws/voice_rate_limits_test.go b/Server/ws/voice_rate_limits_test.go
new file mode 100644
index 00000000..113c0c06
--- /dev/null
+++ b/Server/ws/voice_rate_limits_test.go
@@ -0,0 +1,133 @@
+package ws
+
+// voice_rate_limits_test.go — the refusal branch of the voice limiters that
+// had no coverage: voice_join's precheck (voice_join.go), the shared
+// voice_mute/voice_deafen self-toggle (voice_controls.go) and
+// voice_e2ee_announce (voice_e2ee.go). Their siblings (voice_leave,
+// camera/screenshare, the e2ee offer budgets, plugin_cmd) are all already
+// pinned; these three were the gap.
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/owncord/server/auth"
+)
+
+// decodeErrorFrame extracts the code from a server->client error envelope.
+func decodeErrorFrame(t *testing.T, frame []byte) string {
+ t.Helper()
+ var env struct {
+ Type string `json:"type"`
+ Payload struct {
+ Code string `json:"code"`
+ } `json:"payload"`
+ }
+ if err := json.Unmarshal(frame, &env); err != nil {
+ t.Fatalf("unmarshal frame %s: %v", frame, err)
+ }
+ if env.Type != MsgTypeError {
+ t.Fatalf("expected an error frame, got type %q (%s)", env.Type, frame)
+ }
+ return env.Payload.Code
+}
+
+// voice_join fans a voice_state broadcast out to every connected client, so
+// the limiter is consulted first — before the payload is even parsed. A
+// payload that could never join therefore still burns a token, and once the
+// budget is gone the refusal is RATE_LIMITED rather than the parse error.
+func TestVoiceJoinPrecheck_RateLimited(t *testing.T) {
+ h := &Hub{limiter: auth.NewRateLimiter()}
+ send := make(chan []byte, voiceJoinRateLimit+2)
+ c := &Client{userID: 1, send: send, sendHigh: send, sendLow: send}
+ payload := json.RawMessage(`{"channel_id":"not-an-int"}`)
+
+ for i := range voiceJoinRateLimit + 1 {
+ if _, _, ok := h.voiceJoinPrecheck(context.Background(), c, payload); ok {
+ t.Fatalf("call %d: precheck passed on a malformed payload", i)
+ }
+ }
+
+ if got := len(send); got != voiceJoinRateLimit+1 {
+ t.Fatalf("queued %d error frames, want %d", got, voiceJoinRateLimit+1)
+ }
+ for i := range voiceJoinRateLimit {
+ if code := decodeErrorFrame(t, <-send); code != ErrCodeBadRequest {
+ t.Errorf("call %d: code = %q, want %q (limit not yet reached)", i, code, ErrCodeBadRequest)
+ }
+ }
+ if code := decodeErrorFrame(t, <-send); code != ErrCodeRateLimited {
+ t.Errorf("call past the budget: code = %q, want %q", code, ErrCodeRateLimited)
+ }
+}
+
+// The V2 voice handlers whose rate-limit refusal was unexercised. Each case
+// runs with VoiceChannelID=0 so the calls that pass the limiter stop at the
+// next gate (VOICE_ERROR) instead of reaching the DB — which also pins the
+// ordering: the limiter runs before the in-voice check.
+func TestVoiceHandlersV2_RateLimited(t *testing.T) {
+ ctx := context.Background()
+ info := ClientInfo{UserID: 1, Username: "alice", VoiceChannelID: 0}
+
+ tests := []struct {
+ name string
+ limit int
+ wantMsg string
+ call func(VoiceDeps) Result
+ }{
+ {
+ name: "voice_mute",
+ limit: voiceMuteRateLimit,
+ wantMsg: "too many mute toggles",
+ call: func(d VoiceDeps) Result {
+ return handleVoiceMuteV2(ctx, VoiceMuteCmd{userID: 1, muted: true}, info, d)
+ },
+ },
+ {
+ name: "voice_deafen",
+ limit: voiceDeafenRateLimit,
+ wantMsg: "too many deafen toggles",
+ call: func(d VoiceDeps) Result {
+ return handleVoiceDeafenV2(ctx, VoiceDeafenCmd{userID: 1, deafened: true}, info, d)
+ },
+ },
+ {
+ name: "voice_e2ee_announce",
+ limit: voiceE2EERateLimit,
+ wantMsg: "too many e2ee announcements",
+ call: func(d VoiceDeps) Result {
+ return handleVoiceE2EEAnnounceV2(ctx, VoiceE2EEAnnounceCmd{userID: 1, publicKey: validB64Key}, info, d)
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ deps := VoiceDeps{Limiter: auth.NewRateLimiter()}
+
+ for i := range tt.limit {
+ res := tt.call(deps)
+ ce, ok := res.Error.(ClientError)
+ if !ok {
+ t.Fatalf("call %d: expected ClientError, got %v", i, res.Error)
+ }
+ if ce.Code != ErrCodeVoiceError {
+ t.Fatalf("call %d: code = %q, want %q (limit not yet reached)", i, ce.Code, ErrCodeVoiceError)
+ }
+ }
+
+ res := tt.call(deps)
+ ce, ok := res.Error.(ClientError)
+ if !ok {
+ t.Fatalf("expected ClientError past the budget, got %v", res.Error)
+ }
+ if ce.Code != ErrCodeRateLimited {
+ t.Errorf("code = %q, want %q", ce.Code, ErrCodeRateLimited)
+ }
+ if ce.Message != tt.wantMsg {
+ t.Errorf("message = %q, want %q", ce.Message, tt.wantMsg)
+ }
+ })
+ }
+}
diff --git a/docs/audit-test-coverage-2026-08-19.md b/docs/audit-test-coverage-2026-08-19.md
new file mode 100644
index 00000000..7a58fe5c
--- /dev/null
+++ b/docs/audit-test-coverage-2026-08-19.md
@@ -0,0 +1,163 @@
+# OwnCord — Test Audit
+
+**Date:** 2026-08-19
+**Branch:** fix/test-audit-2026-08-19 (baseline `4ff199e1`, 43 commits of churn since the 08-04 audit)
+**Scope:** are the existing tests still correct, and what is missing — all three surfaces. Hybrid depth: mechanical sweep everywhere, deep read in auth / permissions / voice-E2EE / ws hub / rate limits / migrations / Rust proxies+TOFU. Every candidate finding was adversarially verified (refute-by-default) before it was fixed; 40 candidates → 35 confirmed (refuted ones listed in §5 so they are not re-raised), plus 2 manual timezone findings and 12 Stryker-survivor findings.
+**Relationship to prior audits:** [audit-test-coverage-2026-07-25.md](audit-test-coverage-2026-07-25.md) (coverage; backlog items 1,2,3,4 closed since) and [audit-2026-08-04-docs-and-coverage.md](audit-2026-08-04-docs-and-coverage.md) (UI flow matrix). Nothing closed there is restated.
+
+## 1. Method
+
+- **Fact collection:** `go test -coverpkg=./... ./...` cross-package coverage + zero-coverage function list; `vitest run --coverage`; a changed-since-baseline table (187 changed source files since `4ff199e1`, 82 with no test change) that scoped the finders.
+- **Find + verify workflow:** 7 opus finder agents (one per surface slice: server core, server ws/voice, server auth/permissions, client lib, client stores/UI, Rust, e2e/config), each returning structured findings; every candidate then went to an opus refuter with a refute-by-default prompt. 40 candidates → 35 confirmed, 5 refuted (§5). Two timezone-dependent tests found manually during Stryker dry runs (T-24, T-25).
+- **Stryker (bonus signal):** mutation testing over 13 risky client modules — score 67.04%, 595 survived / 108 no-coverage. Each module's survivors became one round-2 finding (T-38..T-49), fixed by a second 12-agent wave (opus on dispatcher/e2eeCrypto/ws/identity/livekitE2EE, sonnet otherwise).
+- **Proving tests can fail:** every `missing`-kind fix carried an agent-run RED proof (break the source, watch the new test fail, restore byte-identical, watch green). Three re-proved by hand afterwards, one per surface: `TestSetAuthRateScale_ClampsMultiplier` (clamp ×2 → `scaledAuthLimit(9) = 36, want 18`), `validate_server_url_rejects_unsafe_urls` (https guard → `if false` → panic at update_commands.rs:240), `tests/unit/host-validation.test.ts` (IPv6 `&&` → `||` → 4 failed).
+- **Gates:** full ci-check — 4 Go build-tag variants, vet, `-race` (all packages), `-tags deadlock ./ws/`, golangci-lint, sqlc + protocol drift verify, vitest, tsc, eslint, prettier, `cargo test`, clippy `-D warnings` — all green at the end of the branch.
+
+## 2. Findings
+
+49 findings, all **RESOLVED** (0 declined, 0 open). 14 high / 32 medium / 3 low; 19 stale / 30 missing. T-01..23 + T-26..37 from the verified sweep, T-24/25 manual (timezone), T-38..49 Stryker survivors per module.
+
+| ID (T-2026-08-19-…) | Sev | Kind | Finding | Status |
+|----|-----|------|---------|--------|
+| T-01 | HIGH | stale | `Client/tauri-client/src-tauri/src/commands.rs:315` — The two `fingerprint_validation_*` tests never call `store_cert_fingerprint` (or any production function) — they re-implement the validation loop inside the test body and assert… | **RESOLVED** |
+| T-02 | HIGH | missing | `Client/tauri-client/src-tauri/src/secret_store.rs:139` — `set_with`'s two read-back-failure arms — the keyring returning a *different* secret (which must purge the foreign entry) and the keyring returning *no* entry (the mock-store bu… | **RESOLVED** |
+| T-03 | HIGH | missing | `Client/tauri-client/src-tauri/src/update_commands.rs:120` — `validate_server_url` — the only guard on the updater's server URL, on a path that downloads and executes an installer — has zero tests; the file's test module covers only endpo… | **RESOLVED** |
+| T-04 | HIGH | missing | `Client/tauri-client/src/components/ChannelSidebar.ts:128` — The sidebar-layer TOCTOU guard on the E2EE identity re-pin — pin the key captured BEFORE the async fingerprint compute, never a fresh membersStore re-read — is stated as an inva… | **RESOLVED** |
+| T-05 | HIGH | missing | `Client/tauri-client/src/lib/ws.ts:126` — The TS mirror of the Rust cert-TOFU store key, normalizeHostForCertCompare, was never updated (and has no test) for the bracketed-IPv6 collapse that the same bughunt commit adde… | **RESOLVED** **+ source bug fixed** |
+| T-06 | HIGH | missing | `Server/api/constants.go:29` — setAuthRateScale / scaledAuthLimit — added since the baseline and the multiplier for every per-IP auth rate limit and the login failure/lockout threshold — have no test at all, … | **RESOLVED** |
+| T-07 | HIGH | missing | `Server/auth/totp_encrypt.go:140` — DecryptTOTPSecret's real decryption path — hex decode through AES-GCM Open, including the explicitly documented "Fail CLOSED" branch on authentication failure — is never execute… | **RESOLVED** |
+| T-08 | HIGH | stale | `Server/db/session_expiry_test.go:84` — TestMigration031_NormalizesLegacyFormats claims to verify migration 031's one-time normalization pass but executes a hand-copied duplicate of the migration's SQL instead of runn… | **RESOLVED** |
+| T-09 | HIGH | missing | `Server/migrations/030_attachments_unlink_on_message_delete.sql:36` — Migration 030 is the only migration in the chain that destroys and recreates a table holding user data, and no test ever runs it with rows present, so its data-copy fidelity is … | **RESOLVED** |
+| T-10 | HIGH | missing | `Server/ws/handlers_command.go:83` — The entire success path of the plugin chat_command handler — ephemeral reply, the MessageService.CanPost broadcast gate, and the plugin_broadcast fan-out — has zero coverage, so… | **RESOLVED** |
+| T-11 | HIGH | missing | `Server/ws/livekit_webhook.go:50` — No test ever feeds the LiveKit webhook endpoint a validly-signed request, so neither the SDK's signature/body-hash verification nor the participant_joined/participant_left dispa… | **RESOLVED** |
+| T-12 | HIGH | missing | `Server/ws/livekit_webhook.go:111` — The OC-0018 "detach from the request context" fix landed on both webhook handlers but only the participant_left sibling got a regression test; the participant_joined side is unt… | **RESOLVED** |
+| T-13 | HIGH | missing | `Server/ws/voice_controls.go:148` — The permission gate that blocks enabling a camera without USE_VIDEO or a screenshare without SHARE_SCREEN has no test — its refusal branch never executes in the suite. | **RESOLVED** |
+| T-14 | HIGH | missing | `Server/ws/voice_moderation.go:385` — No test asserts that voice_mod_move refuses when the TARGET lacks CONNECT_VOICE on the destination channel — the guard that stops a moderator move from placing someone into a ch… | **RESOLVED** |
+| T-15 | MEDIUM | missing | `Client/tauri-client/src-tauri/src/credentials.rs:88` — `with_credential_lock`'s stated poison-recovery invariant — a panic inside one credential command must not permanently wedge every later credential operation — has no test, thou… | **RESOLVED** |
+| T-16 | MEDIUM | missing | `Client/tauri-client/src-tauri/src/tofu.rs:87` — `CaptureVerifier` — the seam that records the leaf certificate fingerprint for both the ws and http proxies' TOFU decision — has no test, while its sibling `PinnedVerifier`/`Hos… | **RESOLVED** |
+| T-17 | MEDIUM | missing | `Client/tauri-client/src/lib/hostValidation.ts:24` — hostValidation.ts — extracted in the last bughunt commit as the single gate for every user-supplied server address — has no test file of its own, and nothing asserts the two gua… | **RESOLVED** |
+| T-18 | MEDIUM | missing | `Client/tauri-client/src/lib/rate-limiter.ts:163` — No test ties the client's pre-configured voice limiter to the server budget its own header comment claims to mirror; createVoiceLimiter allows 20 sends/second while the server c… | **RESOLVED** **+ source bug fixed** |
+| T-19 | MEDIUM | missing | `Client/tauri-client/src/pages/connect-page/LoginForm.ts:643` — The login form's anti-phishing cap on server-controlled error text — truncate any auth error over 200 characters — is a stated security guard that no test asserts. | **RESOLVED** |
+| T-20 | MEDIUM | missing | `Client/tauri-client/src/pages/main-page/SidebarArea.ts:475` — The OC-0174 fix (announcement channels count as text-like for every automatic channel fallback) was applied to five call sites, but only the three in dispatcher.ts got regressio… | **RESOLVED** |
+| T-21 | MEDIUM | missing | `Client/tauri-client/src/stores/voice.store.ts:412` — The module-level `pttPollingLive` capability flag and its stated invariant that `resetVoiceStore()` must not clear it are asserted by no test — both accessor bodies are never ex… | **RESOLVED** |
+| T-22 | MEDIUM | stale | `Client/tauri-client/tests/browser/smoke.test.ts:3` — The entire tests/browser suite is this one file, which imports no application module and asserts only that the browser environment exists, so it cannot fail on any product chang… | **RESOLVED** |
+| T-23 | MEDIUM | stale | `Client/tauri-client/tests/unit/media.test.ts:1173` — The lightbox test named "cleans up document-level listeners on close" contains no assertion whatsoever — it dispatches three events after closing and asserts nothing, so it pass… | **RESOLVED** |
+| T-24 | MEDIUM | stale | `Client/tauri-client/tests/unit/renderers.test.ts:551` — `formats full date correctly for bare SQLite timestamp` formats UTC midnight ("2026-03-19 00:00:00" is treated as UTC) in the machine's local zone and asserts the day is 19 — it… | **RESOLVED** |
+| T-25 | MEDIUM | stale | `Client/tauri-client/tests/unit/renderers.test.ts:1228` — The formatMessageTimestamp DST tests set process.env.TZ inside beforeEach, which Node only honours in the main thread / a forked process; under a worker-thread pool (Stryker's v… | **RESOLVED** |
+| T-26 | MEDIUM | missing | `Server/admin/handlers_channels.go:123` — OC-0158's post-commit-cancellation fix and test were applied to the PATCH and DELETE channel handlers but not to the sibling CREATE handler, which still re-reads on the cancelab… | **RESOLVED** **+ source bug fixed** |
+| T-27 | MEDIUM | missing | `Server/api/middleware.go:185` — RequirePermission's fail-closed branch — the 403 taken when the request context carries no *db.Role — has zero coverage, so nothing pins that the server-wide authz chokepoint de… | **RESOLVED** |
+| T-28 | MEDIUM | missing | `Server/api/middleware.go:301` — The skip-invalid-entry guard in the X-Forwarded-For right-to-left walk (the BUG-112 anti-spoofing logic that derives every rate-limit and lockout key) is never exercised: no tes… | **RESOLVED** |
+| T-29 | MEDIUM | missing | `Server/api/waf.go:468` — The inline WAF engine's four phase-2 request-body attack rules (SQLi 942100, XSS 941100, path traversal 930100, command injection 932100 — all `deny,status:403` under an always-… | **RESOLVED** |
+| T-30 | MEDIUM | missing | `Server/auth/totp_encrypt.go:45` — LoadOrGenerateTOTPKey's "read the existing totp.key from disk" branch has zero coverage, so nothing asserts the TOTP encryption key is stable across restarts. | **RESOLVED** |
+| T-31 | MEDIUM | missing | `Server/updater/download.go:227` — The updater's tar extraction has a documented O_EXCL anti-TOCTOU guard and three tar-entry filters that no test exercises — the only test hits the happy path into a path that ne… | **RESOLVED** |
+| T-32 | MEDIUM | missing | `Server/ws/hub_broadcast.go:230` — Both "fail closed" DB-error branches of the broadcast audience resolver are the only uncovered blocks in channelReadAudience — nothing asserts that an unreadable channel row or … | **RESOLVED** |
+| T-33 | MEDIUM | missing | `Server/ws/hub_events.go:106` — No test ever broadcasts through a hub that has an EventPersister attached, so the invariant that the persisted row's seq matches the seq embedded in the wrapped payload — the ba… | **RESOLVED** |
+| T-34 | MEDIUM | missing | `Server/ws/voice_join.go:91` — The rate-limit refusal branch is never executed for voice_join, for the shared voice_mute/voice_deafen self-toggle, or for voice_e2ee_announce, even though the sibling handlers'… | **RESOLVED** |
+| T-35 | LOW | missing | `Client/tauri-client/src/components/UserProfilePopup.ts:125` — `position()`'s viewport flip-and-clamp — written specifically to fix a card that ran off the bottom of the window — has no test; every one of its four branches is unexercised. | **RESOLVED** |
+| T-36 | LOW | missing | `Client/tauri-client/src/lib/e2eeCrypto.ts:293` — The epoch-range reject path added by the OC-0001 room-key epoch fix is the one guard in unwrapRoomKey's v1 header parse that no test reaches, even though its sibling guards (edi… | **RESOLVED** |
+| T-37 | LOW | stale | `Client/tauri-client/tests/unit/log-persistence.test.ts:478` — A test for clearPendingPersistedLogs's no-op path contains no assertion at all, so it passes for any implementation that does not hang. | **RESOLVED** |
+| T-38 | MEDIUM | stale | `Client/tauri-client/src/lib/credentials.ts:9` — Stryker (2026-08-19, pre-round-1 tree): 13 of 33 mutants in Client/tauri-client/src/lib/credentials.ts survived (score 60.6%) — the unit tests covering this module do not pin th… | **RESOLVED** |
+| T-39 | MEDIUM | stale | `Client/tauri-client/src/lib/dispatcher.ts:87` — Stryker (2026-08-19, pre-round-1 tree): 125 of 453 mutants in Client/tauri-client/src/lib/dispatcher.ts survived (score 72.4%) — the unit tests covering this module do not pin t… | **RESOLVED** |
+| T-40 | MEDIUM | stale | `Client/tauri-client/src/lib/permissions.ts:107` — Stryker (2026-08-19, pre-round-1 tree): 3 of 54 mutants in Client/tauri-client/src/lib/permissions.ts survived (score 94.4%) — the unit tests covering this module do not pin the… | **RESOLVED** |
+| T-41 | MEDIUM | stale | `Client/tauri-client/src/lib/rate-limiter.ts:41` — Stryker (2026-08-19, pre-round-1 tree): 5 of 42 mutants in Client/tauri-client/src/lib/rate-limiter.ts survived (score 88.1%) — the unit tests covering this module do not pin th… | **RESOLVED** |
+| T-42 | MEDIUM | stale | `Client/tauri-client/src/lib/hostValidation.ts:25` — Stryker (2026-08-19, pre-round-1 tree): 11 of 35 mutants in Client/tauri-client/src/lib/hostValidation.ts survived (score 68.6%) — the unit tests covering this module do not pin… | **RESOLVED** |
+| T-43 | MEDIUM | stale | `Client/tauri-client/src/stores/messages.store.ts:197` — Stryker (2026-08-19, pre-round-1 tree): 52 of 338 mutants in Client/tauri-client/src/stores/messages.store.ts survived (score 84.6%) — the unit tests covering this module do not… | **RESOLVED** |
+| T-44 | MEDIUM | stale | `Client/tauri-client/src/lib/e2eeCrypto.ts:19` — Stryker (2026-08-19, pre-round-1 tree): 30 of 99 mutants in Client/tauri-client/src/lib/e2eeCrypto.ts survived (score 69.7%) — the unit tests covering this module do not pin the… | **RESOLVED** |
+| T-45 | MEDIUM | stale | `Client/tauri-client/src/lib/ws.ts:8` — Stryker (2026-08-19, pre-round-1 tree): 150 of 352 mutants in Client/tauri-client/src/lib/ws.ts survived (score 57.4%) — the unit tests covering this module do not pin these bra… | **RESOLVED** |
+| T-46 | MEDIUM | stale | `Client/tauri-client/src/lib/identity.ts:24` — Stryker (2026-08-19, pre-round-1 tree): 25 of 75 mutants in Client/tauri-client/src/lib/identity.ts survived (score 66.7%) — the unit tests covering this module do not pin these… | **RESOLVED** |
+| T-47 | MEDIUM | stale | `Client/tauri-client/src/lib/livekitE2EE.ts:33` — Stryker (2026-08-19, pre-round-1 tree): 234 of 484 mutants in Client/tauri-client/src/lib/livekitE2EE.ts survived (score 51.7%) — the unit tests covering this module do not pin … | **RESOLVED** |
+| T-48 | MEDIUM | stale | `Client/tauri-client/src/stores/auth.store.ts:16` — Stryker (2026-08-19, pre-round-1 tree): 6 of 21 mutants in Client/tauri-client/src/stores/auth.store.ts survived (score 71.4%) — the unit tests covering this module do not pin t… | **RESOLVED** |
+| T-49 | MEDIUM | stale | `Client/tauri-client/src/stores/voice.store.ts:124` — Stryker (2026-08-19, pre-round-1 tree): 49 of 134 mutants in Client/tauri-client/src/stores/voice.store.ts survived (score 63.4%) — the unit tests covering this module do not pi… | **RESOLVED** |
+
+## 3. Measured baselines (diff against these next time)
+
+### Go — cross-package (`go test -coverpkg=./... ./...`)
+
+| | Before | After |
+|---|---|---|
+| Total statements | 80.1% | **80.8%** |
+| Zero-coverage functions (excl. dbgen/scripts/main) | 62 | **54** |
+
+Per-package before→after (mean per-function statement coverage):
+
+| Package | Before | After |
+|---|---|---|
+| admin | 86.6% | 86.6% |
+| api | 84.4% | 84.8% |
+| auth | 91.5% | 93.5% |
+| config | 80.6% | 80.6% |
+| db | 84.1% | 84.2% |
+| diskutil | 87.5% | 87.5% |
+| invariants | 81.8% | 81.8% |
+| logctx | 96.0% | 96.0% |
+| permissions | 100.0% | 100.0% |
+| plugin | 77.0% | 77.3% |
+| service | 91.5% | 91.5% |
+| stackutil | 94.1% | 94.1% |
+| storage | 89.6% | 89.6% |
+| syncutil | n/a | n/a |
+| telemetry | 74.4% | 74.4% |
+| updater | 87.3% | 87.8% |
+| ws | 88.2% | 89.5% |
+
+Zero-coverage note: both counts come from the identical filter over the before/after profiles (an earlier scratch note said 51 — that used a narrower scope). The −8 is nine functions gaining coverage (`handleGetAuditLog`, ws `ChannelID`/`Payload`, `buildCommandReply`/`buildCommandBroadcast`, hub `Register`/`Unregister`, `maxColdReplayLimit`, `rejectIfRunning`) while `EventPersisterStats` merely moved lines. The remaining 54 are dominated by the root-package CLI (`token_cli.go`, `restart.go Mode`) and similar wiring.
+
+### Client (`vitest run --coverage`)
+
+| | Before | After |
+|---|---|---|
+| Test files | 185 | 185 |
+| Tests | 5045 | 5164 |
+| Statements | 96.17% | **96.41%** |
+| Branches | 92.52% | **92.93%** |
+| Functions | 94.70% | **95.02%** |
+
+### Rust (`cargo test --lib`)
+
+108 → 114 tests.
+
+### Stryker (risky client modules, before round 2)
+
+| Module | Killed | Survived | Score |
+|---|---|---|---|
+| `src/lib/credentials.ts` | 20 | 13 | 60.6% |
+| `src/lib/dispatcher.ts` | 328 | 125 | 72.4% |
+| `src/lib/permissions.ts` | 51 | 3 | 94.4% |
+| `src/lib/rate-limiter.ts` | 37 | 5 | 88.1% |
+| `src/lib/cert-reconnect.ts` | 13 | 0 | 100% |
+| `src/lib/hostValidation.ts` | 24 | 11 | 68.6% |
+| `src/stores/messages.store.ts` | 286 | 52 | 84.6% |
+| `src/lib/e2eeCrypto.ts` | 69 | 30 | 69.7% |
+| `src/lib/ws.ts` | 202 | 150 | 57.4% |
+| `src/lib/identity.ts` | 50 | 25 | 66.7% |
+| `src/lib/livekitE2EE.ts` | 250 | 234 | 51.7% |
+| `src/stores/auth.store.ts` | 15 | 6 | 71.4% |
+| `src/stores/voice.store.ts` | 85 | 49 | 63.4% |
+
+Overall: **67.04%**, 595 survived / 108 no-coverage before round 2.
+
+Round 2 (T-38..49) then killed the actionable survivors per module with strengthened assertions; mutants proven genuinely equivalent (unobservable behaviour) were documented in the fix notes and left. Stryker was not re-run after round 2 (≈40 min a pass); the next audit's run diffs against the table above.
+
+## 4. Bugs surfaced by the tests
+
+All non-security; each fixed test-first inside its finding's commit:
+
+- `Client/tauri-client/src/lib/ws.ts:127` — `normalizeHostForCertCompare` never gained the portless bracketed-IPv6 unwrap its Rust twin `cert_store_key` (`src-tauri/src/tofu.rs:302`) got in the same OC-series fix — cert-pin comparison could mismatch on bracketed IPv6 hosts.
+- `Client/tauri-client/src/lib/rate-limiter.ts` — `createVoiceLimiter()` allowed 20 ops/s where the server budget is 2/s; client now matches `Server/ws/voice_broadcast.go`.
+- `Server/admin/handlers_channels.go` — `handleCreateChannel` re-read the committed row with the request context, so a caller cancellation landing after commit 500ed the request after the channel was created (OC-0158 sibling).
+
+Also: `ws.CommandDispatcher` gained a small test seam (`deps.go`/`hub.go`) so command dispatch is unit-testable without a live hub.
+
+## 5. Refuted candidates (do not re-raise)
+
+- `Client/tauri-client/src/lib/api.ts:300` — The OC-0161 invariant — a 401 that is a per-call verdict rather than a session verdict must not fire the global onUnauthorized sink — was fixed and tested fo… — refuted: The verifyTotp 401 path is already covered by a test that pins deliberate behavior (tests/unit/api.test.ts:541), and the OC-0161 harm cannot occur there: the credential-deleting/logout logic hangs off authStore.subscribe
+- `Client/tauri-client/src/pages/main-page/VoiceCallbacks.ts:146` — `createVoiceModerationCallbacks` — the client's four moderator voice commands (voice_mod_mute / voice_mod_deafen / voice_mod_move / voice_mod_kick) and their… — refuted: createVoiceModerationCallbacks is not untested: the mocked E2E spec drives the real factory through the voice context menu and asserts the exact wire type and payload for voice_mod_mute ({channel_id:10,user_id:2,muted:tr
+- `Client/tauri-client/src/pages/main-page/SidebarDmHelpers.ts:186` — `handleCreateGroupDm` and the `dmChannelFromPayload` mapper it depends on are the only two exported functions in SidebarDmHelpers.ts with no unit test at all… — refuted: handleCreateGroupDm and dmChannelFromPayload are covered indirectly by the mocked E2E social-parity spec, which clicks through the member picker, asserts the POST /api/v1/dms/group body, and then asserts the app switched
+- `Client/tauri-client/src/stores/voice.store.ts:508` — The four E2EE peer-verification writers in voice.store.ts (setPeerVerification, clearPeerVerification, setLocalSessionFingerprint, clearPeerVerifications) ar… — refuted: The claim "never executed by any test" is false: tests/e2e/voice-e2ee-verify.spec.ts drives the real app (real livekitE2EE + real voice.store, CI job client-e2e runs `npx playwright test --config=playwright.config.ts`),
+- `Client/tauri-client/src-tauri/src/tofu.rs:371` — `mismatch_message`'s doc states the frontend parses `Stored:` out of it and the shape must stay stable, but no Rust test asserts that shape and the TypeScrip… — refuted: Both emitters send `storedFingerprint` as an explicit JSON field (ws_proxy.rs:212, http_proxy.rs:434) and ws.ts prefers it (`raw.storedFingerprint ?? parseStoredFingerprint(raw.message)`), so a change to `mismatch_messag
+
+## 6. Backlog
+
+| # | Item | Finding | Sev |
+|---|------|---------|-----|
+| 1 | Re-run Stryker on the 13 risky modules to measure the round-2 kill rate; chase any remaining non-equivalent survivors (worst pre-round-2: `livekitE2EE.ts` 51.7%, `ws.ts` 57.4%) | T-38..49 follow-up | low |
diff --git a/graphify-out/.graphify_labels.json b/graphify-out/.graphify_labels.json
index 76e01d39..d421b316 100644
--- a/graphify-out/.graphify_labels.json
+++ b/graphify-out/.graphify_labels.json
@@ -3,11 +3,11 @@
"1": "createElement",
"2": "testing.T",
"3": "livekitSession.ts",
- "4": "dispatcher.ts",
+ "4": "messages.store.ts",
"5": "openMigratedMemory",
"6": "context.Context",
"7": "buildChannelRouter",
- "8": "MessageInput.ts",
+ "8": "content-parser.ts",
"9": "attachments.ts",
"10": "telemetry.go",
"11": "waitRegistered",
@@ -15,8 +15,8 @@
"13": "NewAdminAPI",
"14": "Fixed",
"15": "messages_test.go",
- "16": "main.ts",
- "17": "net/http.HandlerFunc",
+ "16": "ConnectPage.ts",
+ "17": "writeErr",
"18": "NewTestClient",
"19": "newHandlerHub",
"20": "livekitE2EE.ts",
@@ -25,21 +25,21 @@
"23": "tofu.rs",
"24": "newAuthTestDB",
"25": "newMigratedTestDB",
- "26": "time.Time",
+ "26": "EventPersister",
"27": "Config",
"28": "secret_store.rs",
- "29": "User",
+ "29": "Hub",
"30": "database/sql.Result",
- "31": "newUploadTestDB",
- "32": "MainPage.ts",
- "33": "writeJSON",
+ "31": "net/http.Handler",
+ "32": "channels.store.ts",
+ "33": "net/http.HandlerFunc",
"34": "newAdminTestDB",
"35": "HashToken",
- "36": "content-parser.ts",
+ "36": "MessageList.ts",
"37": "ChannelSidebar.ts",
"38": "plugin/registry_test.go",
- "39": "DB",
- "40": "Instance",
+ "39": "newDeafenRaceDB",
+ "40": "Registry",
"41": "livekit_test.go",
"42": "NewChecker",
"43": "middleware_test.go",
@@ -50,12 +50,12 @@
"48": "livekit_proxy.rs",
"49": "native/helpers.ts",
"50": "NewRouter",
- "51": "net/http.Handler",
- "52": "totp_test.go",
+ "51": "profileCreateToken",
+ "52": "RateLimiter",
"53": "newTestDB",
- "54": "createLogger",
+ "54": "reaction-tooltip.ts",
"55": "newServeHub",
- "56": "OwnCord — Comprehensive Project Audit",
+ "56": "3. Security",
"57": "permissions_test.go",
"58": "seedMemberUser",
"59": "postJSONWithToken",
@@ -66,66 +66,66 @@
"64": "ProfileManager",
"65": "README.md",
"66": "devDependencies",
- "67": "itoa",
+ "67": "doRequest",
"68": "helpers_test.go",
- "69": "ChannelService",
+ "69": "profiles.ts",
"70": "Security Policy",
"71": "Role",
"72": "channels.sql.go",
"73": "Deployment Guide",
"74": "livekit_proxy_test.go",
- "75": "newEmojiService",
+ "75": "Emoji",
"76": "ws_proxy.rs",
"77": "bughunt.js",
"78": "openAdminTestDB",
- "79": "db/db.go",
+ "79": "DB",
"80": "Tables",
"81": "newMentionFixture",
"82": "Channel",
- "83": "NewWAFMiddlewareCRS",
- "84": "E2EEManager",
+ "83": "newWAFMiddleware",
+ "84": "LiveKitSession",
"85": "storage_test.go",
"86": "Migrate",
"87": "Hub",
"88": "chdirTemp",
- "89": "AppearanceTab.ts",
+ "89": "themes.ts",
"90": "updater_test.go",
"91": "newTestMessageService",
- "92": "textAssetServer",
+ "92": "newTestUpdater",
"93": "Hub",
"94": "compilerOptions",
- "95": "NewEventRingBuffer",
- "96": "HandlerRegistry",
+ "95": "screenShare.ts",
+ "96": "deps.go",
"97": "emoji_handler_test.go",
- "98": "handleCreateEmoji",
- "99": "doRequest",
- "100": "audioPipeline.ts",
- "101": "LoadOrGenerate",
+ "98": "buildErrorMsg",
+ "99": "DB",
+ "100": "noise-suppression.ts",
+ "101": "AdminActions.ts",
"102": "Auth Endpoints",
- "103": "MigrateFS",
- "104": "newOverrideFixture",
+ "103": "openMemory",
+ "104": "PermissionService",
"105": "Save",
"106": "REST API Reference",
- "107": "NewRegistry",
- "108": "voice_moderation_test.go",
+ "107": "media-visibility.ts",
+ "108": "joinVoice",
"109": "ptt.rs",
"110": "OwnCord Audit — Documentation Accuracy & UI/UX Test Coverage (2026-08-04)",
"111": "scripts",
- "112": "ResolveTokenHash",
+ "112": "checkSourceWith",
"113": "Load",
- "114": "handleVoiceE2EEOfferV2",
- "115": "commands.rs",
+ "114": "handleVoiceE2EEAnnounceV2",
+ "115": "NewHandler",
"116": "newEmitTestHub",
"117": "Plan: Remediate security-hardening review regressions",
"118": "verify.go",
"119": "newRoleCRUDService",
- "120": "Checker",
+ "120": "VoiceDeps",
"121": "EnsureLiveKitBinary",
"122": "clientip_test.go",
- "123": "AudioElements",
- "124": "buildErrorMsg",
+ "123": "connectionStats.ts",
+ "124": "buildVoiceLeave",
"125": "gif_handler_test.go",
- "126": "navigateToMainPage",
+ "126": "e2e/helpers.ts",
"127": "messages.sql.go",
"128": "Channel Endpoints",
"129": "newSignedTestUpdater",
@@ -133,8 +133,8 @@
"131": "Topic",
"132": "DB",
"133": "users",
- "134": "Client",
- "135": "identity.ts",
+ "134": "User",
+ "135": "update_commands.rs",
"136": "Queries",
"137": "Queries",
"138": "Queries",
@@ -148,17 +148,17 @@
"146": "handleSetup",
"147": "log/slog.Value",
"148": "Updater",
- "149": "Credential storage",
+ "149": "syntax-highlight.ts",
"150": "dependencies",
"151": "newHarvestVoiceDB",
"152": "Config Key Reference",
- "153": "newTestPermService",
+ "153": "seedChannel",
"154": "markdown.ts",
- "155": "VideoGrid.ts",
- "156": "Manifest",
+ "155": "deep-link.ts",
+ "156": ".attemptAutoReconnect",
"157": "rate-limiter.ts",
- "158": "e2e/helpers.ts",
- "159": "main.test.ts",
+ "158": "Error",
+ "159": "video-grid.test.ts",
"160": "testing.M",
"161": "newMockDB",
"162": "OwnCord",
@@ -166,7 +166,7 @@
"164": "buildTauriMockScript",
"165": "OwnCord Introspection MCP Server",
"166": "Bug-detection improvements — design",
- "167": "ptt.ts",
+ "167": "AuditWriter",
"168": "eslint-rules.js",
"169": "DB",
"170": "OwnCord — Security Review",
@@ -174,8 +174,8 @@
"172": "net/http.Request",
"173": "ChannelTopic",
"174": "UserService",
- "175": "Blocked — fix attempted, revert-proof failed",
- "176": "handlers_backup.go",
+ "175": "OwnCord Findings Ledger",
+ "176": "handleRestoreBackup",
"177": "logger.ts",
"178": "fallback_crypto.rs",
"179": "Direct Messages",
@@ -183,7 +183,7 @@
"181": "command.go",
"182": "NewRingBuffer",
"183": "ws-load.js",
- "184": "NewMessageService",
+ "184": "newDMFixture",
"185": "handler",
"186": "tauri-client/package.json",
"187": "screen-share-tracks.test.ts",
@@ -193,28 +193,28 @@
"191": "Role Management",
"192": "User Profile & Sessions",
"193": "F3 — Voice E2EE identity keys + TOFU (the remaining work)",
- "194": "run",
- "195": "newWazeroTestRegistry",
+ "194": "Server/main.go",
+ "195": "totp_encrypt_test.go",
"196": "newUserSvc",
"197": "plugins_handler_test.go",
"198": "badDirFile",
"199": "Contributing",
- "200": ".handleFreshConnect",
+ "200": "github.com/coder/websocket.Conn",
"201": "mcp-introspect/package.json",
"202": "v1.2.0-alpha.1 — Discord feature parity",
"203": "Task Observer — Continuous Skill Discovery & Improvement",
- "204": "scanPluginDirectory",
- "205": "loadPref",
+ "204": "handleVoiceE2EEOfferV2",
+ "205": "AudioPipeline",
"206": "1. Channel sidebar",
- "207": "Voice, Video & E2EE — target UX",
- "208": "Updater",
+ "207": "Messaging — target UX",
+ "208": "time.Time",
"209": "LiveKitClient",
"210": "migrate.go",
"211": "Queries",
- "212": "Messaging — target UX",
- "213": "Registry",
+ "212": "setupRouter",
+ "213": ".verify_server_cert",
"214": "handleChannelFocusV2",
- "215": "handlers_channel_perms_test.go",
+ "215": "itoa",
"216": "knip.json",
"217": "RNNoiseProcessor",
"218": "DeviceManager",
@@ -223,7 +223,7 @@
"221": "Connection & Authentication — target UX",
"222": "Settings & Admin — target UX",
"223": "buildClientUpdateRouter",
- "224": "logPersistence.ts",
+ "224": "Voice, Video & E2EE — target UX",
"225": "newTestRoleService",
"226": "event.go",
"227": "NewTopicRateLimiter",
@@ -237,21 +237,21 @@
"235": "LiveKit Setup Guide",
"236": "Voice Signaling",
"237": "Quick Start Guide",
- "238": "readPump",
- "239": "mockTauriFullSessionWithVoice",
+ "238": "OwnCord — Test Audit",
+ "239": "newBackupFileDB",
"240": "index.mjs",
- "241": "countingReadStateStore",
+ "241": "TestAdminAPI_PatchRole_MemberLookupFailureBlanketInvalidates",
"242": "bughunt.harness.mjs",
"243": "voice-audio-tab.test.ts",
"244": "reactions.sql.go",
"245": "Channel Permission Overrides",
"246": "Server Stats & User Administration",
- "247": "deep-link.ts",
- "248": "EventSink",
+ "247": ".DeleteAccount",
+ "248": "scaledAuthLimit",
"249": "handleChatCommandV2",
"250": "slashFS",
"251": "Running the bughunt pipeline",
- "252": "OverlayManagers.ts",
+ "252": "MainPage.ts",
"253": "reconnectAfterCertAccept",
"254": "VoiceTopic",
"255": "emoji-voicemod.parity.spec.ts",
@@ -262,7 +262,7 @@
"260": "OwnCord — Test-Coverage Audit",
"261": "OriginAcceptOptions",
"262": "EventRingBuffer",
- "263": "IsUniqueConstraintError",
+ "263": ".UpdateUserProfile",
"264": "newTokenTestDB",
"265": "scripts",
"266": "bughunt-fix.harness.mjs",
@@ -273,7 +273,7 @@
"271": "TestMigrate_UpgradeFromMigration019PreservesData",
"272": "Queries",
"273": "TestChannelVisibility_RESTWSAgreement",
- "274": "seed.go",
+ "274": "OwnCord — Comprehensive Project Audit",
"275": "Finish the V2 Dispatch Migration (backlog item 11) — Design",
"276": "Port Forwarding Guide",
"277": "Chat Messages",
@@ -291,12 +291,12 @@
"289": "Member Updates",
"290": "genprotocol/main.go",
"291": "Hub",
- "292": ".finishVoiceLeave",
+ "292": "FenwickTree",
"293": "ChatSendCmd",
"294": "Environments, Activation Setup, and Handoff-Doc Mode",
- "295": "handlePingV2",
+ "295": "newBlockService",
"296": "capabilities-scope.test.ts",
- "297": "savePref",
+ "297": "handleDiagnosticsConnectivity",
"298": "GET /admin/api/updates",
"299": "Channel-Visibility Unification (backlog item 3) — Design",
"300": "sqlc Adoption (D2) — Progress & Plan",
@@ -308,7 +308,7 @@
"306": "openFileDB",
"307": "hello plugin",
"308": "TestAdminAPI_PatchChannel_ArchiveCleansVoice",
- "309": "window-state.ts",
+ "309": "default_verify_schemes",
"310": "Tauri HTTP Capability Narrowing — Design",
"311": "protocol_contract_test.go",
"312": "ChatCommandCmd",
@@ -317,31 +317,31 @@
"315": "VoiceWidgetOptions",
"316": "LiveKitProcess",
"317": "cert-tofu.spec.ts",
- "318": "updater.spec.ts",
- "319": "message_reactions_test.go",
+ "318": "navigateToMainPageReady",
+ "319": "4. Dependencies & Supply Chain",
"320": "tsconfig.build.json",
"321": "User Blocks",
"322": "PATCH /admin/api/settings",
"323": "GET /api/v1/gif/search",
"324": "First-Run Setup",
"325": "LiveKit Endpoints",
- "326": "types.go",
+ "326": "5. Test Coverage & Quality",
"327": "Tailscale Guide (Zero-Config Remote Access)",
"328": "LiveKitProcess",
- "329": "DB",
+ "329": "perm_grid_test.go",
"330": "ChatEditCmd",
"331": "VoiceE2EEOfferCmd",
"332": "VoiceModDeafenCmd",
"333": "VoiceModMuteCmd",
- "334": "TestHandlePresenceUpdate_BareStatusReadFailureAbortsBeforeCommit",
- "335": "New",
+ "334": "admin-static-channel-perms.test.ts",
+ "335": "NewDMService",
"336": "GET /api/v1/client-update/{target}/{current_version}",
"337": "OwnCord Architecture Blueprints",
"338": "Voice End-to-End Encryption",
"339": "feature_request.md",
"340": "volume-menu.test.ts",
- "341": "buildMetricsRouter",
- "342": "RunningInContainer",
+ "341": "isAddrInUse",
+ "342": "MetricsSources",
"343": "ChatDeleteCmd",
"344": "Permission-Middleware Consolidation (audit finding A-2026-07-16) — Design",
"345": "MessageDeletedDMEvent",
@@ -390,7 +390,7 @@
"388": "TypingDMEvent",
"389": "VoiceStateEvent",
"390": "TestDeleteExpiredSessions_SargableFormat",
- "391": ".EmitEvents",
+ "391": "jsdom.d.ts",
"392": "stubChannelEvent",
"393": "stubUserTargetedEvent",
"394": "TestPresenceEvents_InvisibleBlanksCustomStatusForOthers",
@@ -413,9 +413,16 @@
"411": "pre-commit",
"412": "pre-push",
"413": "stubBroadcastAllEvent",
+ "414": "updater.test.ts",
+ "415": "1. Architecture",
"416": "015_plugins.sql",
"417": "tryLoadPluginTOML",
"418": "tryLoadPluginTOML",
+ "419": "6. CI/CD & DevEx",
+ "420": "7. Observability",
+ "421": "syscall.SysProcAttr",
+ "422": "Security Policy",
+ "423": "erroringMembersStore",
"424": "protocol-change/SKILL.md",
"425": "strip-appimage-bundled-libs.sh",
"426": "build.rs",
@@ -441,14 +448,14 @@
"446": "voice-test.sh",
"447": "proc_spawner_nix.go",
"448": "proc_spawner_win.go",
- "449": "RateLimiter",
+ "449": "Capture",
"450": "playwright.config.ts",
"451": "playwright.config.admin.ts",
"452": "playwright.config.native.ts",
"453": "playwright.config.prod.ts",
"454": "constants.rs",
"455": "vite-env.d.ts",
- "456": "smoke.test.ts",
+ "456": "audio-pipeline-vad-worklet.test.ts",
"457": ".addEventListener",
"458": "tauri-conf-webview2-args.test.ts",
"459": "video-grid-track-muted-css.test.ts",
@@ -527,5 +534,19 @@
"532": "TestRegisterNow_ReplacementNeverLosesAConcurrentGlobalBroadcast",
"533": "prettier",
"534": "@vitest/browser",
- "535": "@vitest/coverage-v8"
+ "535": "@vitest/coverage-v8",
+ "536": "MockAudioContext",
+ "537": "D7 — Module map",
+ "538": "WebSocket / Real-time Engine",
+ "539": "Audit 2026-07-19 — Maintainer Decisions",
+ "540": "RunningUnderSupervisor",
+ "541": "owncord-introspect (MCP dev tool)",
+ "542": "addrinuse_unix.go",
+ "543": "addrinuse_windows.go",
+ "544": "msg-actions-bar-focus-css.test.ts",
+ "545": "livekit-client",
+ "546": "hub_wiring_test.go",
+ "547": "@stryker-mutator/api/core",
+ "548": "@tauri-apps/api/core",
+ "549": "@tauri-apps/api/event"
}
diff --git a/graphify-out/.graphify_labels.json.sig b/graphify-out/.graphify_labels.json.sig
index 2a6c771f..3f3af21f 100644
--- a/graphify-out/.graphify_labels.json.sig
+++ b/graphify-out/.graphify_labels.json.sig
@@ -1 +1 @@
-{"0": "55d4333c099463e8", "1": "3fcb494d84468adc", "2": "5bf559833ed34e3a", "3": "92f68cd273a9b874", "4": "19ca1a25db61d663", "5": "61cf987cf7452c9a", "6": "9738762c13b334a0", "7": "f81d81a13ecadfa8", "8": "13588d006d722cc8", "9": "e93f3dcdaef84531", "10": "c4c406d63c108656", "11": "10201f3ea23ad708", "12": "60cb9cb7fb659b6d", "13": "cbb68f028cfbb21d", "14": "e464617647577a97", "15": "817836e980a3357a", "16": "c61f0361ce12efaa", "17": "44bd031808470d52", "18": "5926dc5a8e1f25f3", "19": "6da31498f5760e90", "20": "231e5a17a2859d6f", "21": "f0f247c2c7337c0e", "22": "6b8f58222c3833a8", "23": "63059476d68e4a0e", "24": "abae34a4daecd786", "25": "4672dca4307707cd", "26": "6fba5a61f5d8b349", "27": "7643bc33c4ad1c2f", "28": "3803c2250ce08426", "29": "275af00e980ff405", "30": "a13577618cf9d5af", "31": "d4bb1a6ccff4a470", "32": "76a783eb5c0d6dd9", "33": "d8f3269550204eec", "34": "69c90c519b4f37df", "35": "e5ec377919bd61cb", "36": "5b39a764583cf0ce", "37": "e70d3918d399ae67", "38": "c497b6f8f332d4ce", "39": "9b837b3d513202cf", "40": "69b6c3d79932b24e", "41": "4362ce47f6b57aa8", "42": "a224a236f84b9b24", "43": "3004faaaa783a4f8", "44": "1e5a7d01130b9eb7", "45": "0b91a56597dff67e", "46": "d2df6de74e0b8c9c", "47": "30acdeea63d4a257", "48": "2a883b5f7e85a353", "49": "6bdf4d070b3aae61", "50": "18a2f089fa4b8676", "51": "f0ba95e21aa0dc11", "52": "6fbd89d3329f8269", "53": "518d8c7abd745ce0", "54": "7d71ecc736a5b320", "55": "8043f13f8493b340", "56": "1d3e3be8c1c5ecfc", "57": "d3f2d7efc055060d", "58": "60822ba74d8e3524", "59": "a6bb6347cb638ae8", "60": "5b96dd71c07d1ec9", "61": "3fd16b351ea067c7", "62": "6b362d223956d0d6", "63": "f2b85adb7d2d07b1", "64": "67b7afb27c1f614d", "65": "2edd8adbe4a3739f", "66": "1fbf914ecae8473b", "67": "8a0c3bfbb1c35dcb", "68": "811aba9b7e859132", "69": "d833fa28ee6fbc92", "70": "4a2f9e11fd8ad084", "71": "190a5d4d7930ba0b", "72": "545bbbd39a248e29", "73": "7e4a40b5c5c1d071", "74": "8b0559bf907640fe", "75": "a2011e254f787940", "76": "19496e7a6ed18688", "77": "2fcf0c737b65d3fb", "78": "319819f61a111e60", "79": "9605b2be347fd6e3", "80": "91d3aac154fbba39", "81": "595b598a5d0c859c", "82": "6643375618a9857b", "83": "057121a264996488", "84": "134e3f6f3827609e", "85": "cc27d34a82baf0cb", "86": "b98b1cf70a6c7386", "87": "f68168880d92608e", "88": "ee9d9ecc0ef77585", "89": "09a7f21251a86b28", "90": "feb99704515dfe79", "91": "0b1fce1d9aef71a6", "92": "5df08bc640341c29", "93": "c07543955830df37", "94": "21de920d64fa7cf5", "95": "69959e9d2e8fd013", "96": "0eec0028096bad6c", "97": "efd51105f845d501", "98": "bc3725a16bd3a726", "99": "bc068222442d552e", "100": "4b7c06611d017e8b", "101": "01d5c2ac668ea3e6", "102": "0ce01a8d103c1ce5", "103": "fc74ebf917288d7c", "104": "24d6e60e30f1ecca", "105": "8fc345f8960bd6ec", "106": "5500263c62849987", "107": "12ee317892b3141d", "108": "4b59811e8fccd96f", "109": "faf160496c49a4af", "110": "7fa5e86fb5b859f7", "111": "5cf7ebaa1ab0f37c", "112": "c9628305b663055c", "113": "60f658c8e3cb0292", "114": "0d215edb303c512e", "115": "2bbbecf4edb799b2", "116": "d20fa1f6db7f9d56", "117": "e28dca951d7ca0a3", "118": "ce7667c6140515eb", "119": "ac77d0122ef1555b", "120": "d403824ed0d83917", "121": "58b4fa416f80b293", "122": "57ec126dcfa926e7", "123": "1dbbd9d83021a4c0", "124": "347659248b05b186", "125": "b4c1a3ea46e3acda", "126": "901d521414ce4d54", "127": "c799ccdc983e40b4", "128": "385b7cd4bd246b4c", "129": "21f72c3b967e4ff5", "130": "c6264f7746fc3977", "131": "e507c1e1a5c0a902", "132": "0c1d9e131250f3a0", "133": "189a87afcf0f3b02", "134": "decd5f6f129f1d9a", "135": "7565980acb241648", "136": "6e4ea9f6236016a6", "137": "c62385c22afafc1f", "138": "bba80df6c941b043", "139": "5bc2ed234ee250fc", "140": "5d1e96037e649140", "141": "92b8387e905bd90a", "142": "94cfcbe868f0c1f8", "143": "0ef764c5f251be42", "144": "996f94557438583f", "145": "6bcb5b6388ea20dd", "146": "6c51e6e013679d1a", "147": "d7ca194d611b730c", "148": "0c1bcff18cd6454e", "149": "801f765642d5458b", "150": "202079945a56ce0a", "151": "355fbfbe7c02a5b1", "152": "7dcd65f1f266a81d", "153": "d3855463a821b69a", "154": "a4f35b8aaeac94b3", "155": "2d98f4b3ba32e1d1", "156": "4bcb08f10ee16efb", "157": "997d402f1879ea0d", "158": "ab0436af885c2d09", "159": "63250123ef2f900a", "160": "0047f55b4c80ff3b", "161": "c79b342c3c88739d", "162": "5349dc2e2ead6b40", "163": "42b1082d08c39b50", "164": "a64556f048a3fa3a", "165": "29cf43922e27fcc7", "166": "17cd6b23530afb97", "167": "a0bed1324d3989ab", "168": "425cb5871b3739fc", "169": "4de1fbcf63bab0b2", "170": "9583c984e370d0d7", "171": "70a44128ddd2a585", "172": "326e64383bc229e7", "173": "c1351003d2eb31ea", "174": "10acddec2c053124", "175": "967bbb1cad5550ae", "176": "a881d34e40ac1a91", "177": "fa42e96ab5f434a4", "178": "8d3980fb2d44c170", "179": "740ee3439e73891d", "180": "86dd0abd68cce7bd", "181": "ab27823e12e1283b", "182": "cfe6225489122f9f", "183": "695cd7213266a959", "184": "3364318ae6fb1865", "185": "a9871babeec7452b", "186": "49d33e1357a9c00e", "187": "b3fb262962922e5d", "188": "375479e75c342a49", "189": "2dbaefd48b31fbc1", "190": "46a836d2f70462de", "191": "c6b01ea3e539e92d", "192": "d1b05590fa813dda", "193": "6a2bad7ce6457339", "194": "7fd14c5da47ac9c2", "195": "c9bdd61260d7d29d", "196": "eac5a8cdd274c1cf", "197": "59a29a27fe2aeb9b", "198": "ee9af2e31bb4d69e", "199": "45b5cdd1c0a3ed42", "200": "94b4265f6ff47772", "201": "7f371359f8cbcae2", "202": "bde48ea01a78a31d", "203": "0e16b67c17622c6f", "204": "671b442d9999c649", "205": "a028ba73f60a203e", "206": "5be335bd678dc0e1", "207": "01fc0d960461d67a", "208": "79618ed6048c329f", "209": "6cf8ae3a4377e8b3", "210": "993a2cc7f78ff5ee", "211": "17ed6623f148ec33", "212": "2688a8e6bd6d4a8b", "213": "965551988bbb62da", "214": "814ba695ab3fe9d4", "215": "d44501def814f35f", "216": "c9502ff1fbec6e44", "217": "efcccc9129fb764a", "218": "0b5b4ceef8f6d1cd", "219": "2cd77561eef54d01", "220": "e75e725824ea9795", "221": "8011beca70a26ddf", "222": "e36690c4c6ea70dc", "223": "cbc3054c23c3e7c5", "224": "5231382f9e3a1b10", "225": "fe91cbfe2aa8d0a3", "226": "cd43b54a96b9b626", "227": "073ebfc0d82ece5d", "228": "3d31ed6ba0035a9b", "229": "34b2126166cd1733", "230": "65b8b45a87700755", "231": "77dc6aacce128d60", "232": "69e3b0ad6e14fa56", "233": "dfcf1f53c2773eb6", "234": "d7151cc443a0d3ae", "235": "2d05c9dc165201e2", "236": "d47bda53fbf25407", "237": "b416fd92acbee855", "238": "9fd98b1ced3799e3", "239": "a607edf903085a68", "240": "0f16e037eb9da7b4", "241": "1143027a021f88b0", "242": "36a425c8aef4d209", "243": "10281c2ef334ecf4", "244": "7b77c2e3209f5c22", "245": "37abd99ab905def1", "246": "5271b4f27e7b6403", "247": "f6a4863320b8c1ac", "248": "8b65988aa3fd8b1c", "249": "999c75ec1d65804f", "250": "c1df22372e374115", "251": "4947e519c0aa830e", "252": "f044b6c0d34ae8f7", "253": "81fd07e37694bb91", "254": "af4768cb283824b6", "255": "49c7a214980abb46", "256": "7294472aa6f83fef", "257": "f298841a49015d5c", "258": "5b30b4eda20a98ee", "259": "4713e1339750ca17", "260": "f291869ba39f0a1c", "261": "1491b8a5363601cd", "262": "90d13fab64f252bc", "263": "cc1e7b533d4da379", "264": "ee9e8568ffd4078c", "265": "d76c76e1145c5388", "266": "00a64e391b99cf8b", "267": "085196be307a22ff", "268": "ad708fbbe273ea95", "269": "5176ddbb64f53f47", "270": "fc4d27cde7ab8041", "271": "a340dfcd2eef6466", "272": "b6ab67e936bbd2e8", "273": "2da1d6a29e2fa105", "274": "a80167531e7353f8", "275": "61c4dcf3f1ab0602", "276": "787ddc45a8e0ec99", "277": "bea1ab731f0016f2", "278": "a09acd94fcd91d09", "279": "ab844ddf2be9507a", "280": "05c52b1b5478604d", "281": "18c2e73219a8171e", "282": "1e74126cfe8be8f6", "283": "f79a7ca288131b40", "284": "987acf92654061b4", "285": "6d758949f980ac59", "286": "ea17edcb394b6a26", "287": "b522c0397de29a55", "288": "55212395aebde232", "289": "61d341e3d6044e8e", "290": "810021694c13c86a", "291": "686d734cb58c0f0d", "292": "3ba63cdc21d55afb", "293": "dcd2c0a709ab925f", "294": "7a86aaf4c3bb953c", "295": "54e16886397a0f05", "296": "8f62c91f05330539", "297": "f8358e8e213b85b6", "298": "9718befeaf5e5906", "299": "3c2ae2c4e7a1949b", "300": "8c5971b6925882d9", "301": "a067f6922115aaf0", "302": "a4603b1fbd5baddb", "303": "01585298c38b36a8", "304": "475f5428cc83ef87", "305": "136daf70e87e02a1", "306": "af2df6004f1e66a0", "307": "056986f3ca294dd3", "308": "a2dcf3915447346e", "309": "762206e727553d79", "310": "f62d0ff4241734e7", "311": "25e9a083b16b3359", "312": "a6e061b71798841a", "313": "584cf83aec10d424", "314": "acb3d709fc667892", "315": "ff6b62985215b71d", "316": "28b9538b6974aef9", "317": "aabbe1539b1ea302", "318": "88da36f056092097", "319": "abc019b2420c28a5", "320": "d43c8e41ad02647b", "321": "2988d6d8f485a46c", "322": "2ea6e398df06e887", "323": "d5e5145b976fe497", "324": "ff359fa92047ec71", "325": "fa0ccab3bbbcb772", "326": "56b04324d7677ccd", "327": "ae70af7c50b68cc1", "328": "6945d99925be4760", "329": "4b7b7ffa54d68fd9", "330": "f5a77f29a2d6d382", "331": "4a291a3cef78d580", "332": "853cf2126f3d3ada", "333": "1b5f723dea319f63", "334": "03d27e3e8323ffca", "335": "bf76b20d585a1edf", "336": "dd4bf71037bdf5e9", "337": "bdf0e713b9cb48eb", "338": "8be778cfe8685fb6", "339": "4543c1aaff9332d7", "340": "455559150f1f41ac", "341": "82ddf091c9b63fbc", "342": "9dd604b2237f7ea5", "343": "21be76d4e02295dc", "344": "97aa1c8e01d7a9c0", "345": "f15493f543e0c1b1", "346": "e7fea49f1afe32dd", "347": "bcb3eb55e616c950", "348": "0965d9688e11641b", "349": "34bdc9bb28b363f4", "350": "89dccdf78f8c8f4f", "351": "9352b4af27abc878", "352": "2c252003e531c9bb", "353": "69e0f04ac9f09daf", "354": "b5bd1df08506feb0", "355": "c2b0575b173483c1", "356": "d26d35a80d122be4", "357": "0ecd5fdfa040950b", "358": "90747e9042f6671d", "359": "14d1ae3b1cff934b", "360": "30b502e616e3e01f", "361": "036ef64c84d005dc", "362": "a17154091b064a55", "363": "9c064fb7bd3825f9", "364": "929f7966f607c2d6", "365": "accfebd3c599e7d0", "366": "434949c322bbb405", "367": "03766f79be04df0e", "368": "f52d7b87fa0e13b7", "369": "87bc1a0079f4f956", "370": "eb6489b237602a3d", "371": "a0585796f9a4646b", "372": "fa8b6c3807ddf216", "373": "6788416604fad624", "374": "5a983f36c68ca3fc", "375": "f24eef4943ea4f25", "376": "efd98c8abf12ad1d", "377": "9a35a514b76efbd1", "378": "9a0d84f44fe4ffc1", "379": "8277fe2d31b3a3f4", "380": "d1c5c1a32b6c4110", "381": "b39493ed39b45bf3", "382": "74af31ee46d6ef60", "383": "d24cfd824810865e", "384": "b357e2723c6a435a", "385": "767ac74dca429121", "386": "8ac01c348ddfdfe5", "387": "a9e4077d96048018", "388": "1fefb64451ff7eaa", "389": "381b13c3a3ba2e23", "390": "d0cdc78e9b7af43f", "391": "ed31860cbb4e96c7", "392": "1532d0b3c0551e59", "393": "06fb128ecf74138b", "394": "c3446b19fa9dba31", "395": "7c07f28b71bd0b3e", "396": "708dd0b18a0a4798", "397": "3241630562395df9", "398": "6a32693f62f4d088", "399": "76864a54ed005737", "400": "b696475a2e95f25f", "401": "5bd0d859725efccb", "402": "a8129cea84bdcfe6", "403": "470ea7aa81f5394b", "404": "e3c70389ad967171", "405": "6db915995bc0b271", "406": "0a3aca6ad397c44b", "407": "3f6f84c96be50d9d", "408": "cc9ee9a966b5e226", "409": "5cae267c36b80958", "410": "6b09b53b2124e9a9", "411": "16c63dcd9e5ed158", "412": "fb8672f03334a2a4", "413": "93fd39edfeaa955b", "416": "98b98cadd83b05c3", "417": "3765a56555254119", "418": "84cc8611b5a317a5", "424": "ec847b053e0fc5cc", "425": "99edf7dd8356014f", "426": "1528a41acca8dc22", "427": "e6b0c5ae9144bfae", "428": "0bf8dbaa131f820a", "429": "b713ce65d240607c", "430": "bb8a02b6078be30b", "431": "b98e2c63125b8b99", "432": "512542ea60d90da9", "433": "b88f507bb73b8651", "434": "92c9d40a819f368c", "435": "2076e1d05932fb62", "436": "d6ec261087f6bc77", "437": "34981a383ae71409", "438": "9d98be4fc7b9da16", "439": "649a9984065fda98", "440": "9ed13a6b1fdd29aa", "441": "4ca798e5f087260d", "442": "0c612ec956a1e9a3", "443": "3a3e18d9ac643f35", "444": "77ec63a8255989aa", "445": "47ce12ec866ee546", "446": "ee2953da63e41f03", "447": "15ae692d06dbdc79", "448": "00e62a56bdf3e98e", "449": "6a0e7adcff0907d7", "450": "d08e08c1bef8ce42", "451": "2388d3b1bcf4afd0", "452": "3eaf0f83b4b5d217", "453": "0602067ecbc34d32", "454": "855a53cf44d537f7", "455": "c076f675fe74d98b", "456": "2f301d70eea30507", "457": "96083d75134d170d", "458": "6b646ba69c312c9e", "459": "c8b3ee00dc822e1f", "460": "5cc6567aacbdccba", "461": "e4685a8dbc331296", "462": "c8cdb11a4360d582", "463": "1bdd1e69dc924512", "464": "b96ad4e2e72ee4e0", "465": "e30952305e11ce35", "466": "b0904ef0f0aaf7d4", "467": "4423f00984596bb5", "468": "b9e2755adb66c626", "469": "5748723725fac7bc", "470": "1aaf664c6ffe5a62", "471": "96a9c1c3b06d45b6", "472": "61e9b8c4d917cbbf", "473": "3dd7b2223b12a178", "474": "4ecc297368533e1d", "475": "2ed3d7513c9e9432", "476": "2feb06e3b9fe8085", "477": "8fcea9acee447ea8", "478": "2018180f7ade5b0e", "479": "b5c49b147ba220ed", "480": "c8ab14ccfb2ceb29", "481": "8a2b334f3892ec42", "482": "d5fe45ae7ec07826", "483": "3ada99807193fa75", "484": "c32d2544c6f24a5d", "485": "4f37fd92e0afd609", "486": "82870a72bd6f6775", "487": "922a3676522aa1db", "488": "0c5f4349a24e692e", "489": "4346fd7b60bbee63", "490": "e651d1e0a6a9d2f7", "491": "d97c392f2988607d", "492": "1cc0949deb48d80b", "493": "f73cc7f508a09a27", "494": "328cc6def8813c2b", "495": "ac2f1b72332c19e0", "496": "b9bc48fd0c5bd742", "497": "7b531173c84842e3", "498": "bac56080d614f4e5", "499": "ef4b31ee0b8691a7", "500": "04a02bc8cec46614", "501": "d26007b612e0d28f", "502": "399136a1201e626a", "503": "34da019da67be529", "504": "6f34b9f536c17cca", "505": "cf10ee822d8aa3b5", "506": "634f6e7943a22ae0", "507": "4930f7207553cdd2", "508": "f84d9c6dd0ae72e4", "509": "d786feb243f024f3", "510": "b08fe51e110c766a", "511": "2d6f4b8ec71816fb", "512": "20baa70a65a36421", "513": "ed98e9a9d974754d", "514": "3374a018515d73e0", "515": "75e25c552169d0b1", "516": "31c658375ca655fb", "517": "6faffa92a422d0e1", "518": "ac654ab9bd4ae870", "519": "b8d1ed53bae3e3dd", "520": "25ba94c1e0b355b4", "521": "9747497c2bde5eba", "522": "7eef6136f6be4db7", "523": "a20171ea17b536bb", "524": "945939db6a08497b", "525": "57a40d480d1a07de", "526": "f5c67e18959e1a96", "527": "8782ecf8e7770bcc", "528": "cbcfc4ab756bcc7e", "529": "5c2a9932d803ae8d", "530": "589eb257be817a5b", "531": "1a46e3c34fd4dfbb", "532": "93a1d044672ad228", "533": "fdb6d627efd3ea42", "534": "49fcd2d6813add0a", "535": "72d68125d7c45efa"}
\ No newline at end of file
+{"0": "10d02ac7f58a09aa", "1": "4390a12dc49bbd3c", "2": "08ce47eb9ac30323", "3": "40dec5085a1ca982", "4": "fabfcffd1cc1a99e", "5": "de84cedd97d56181", "6": "84b038b29c92422f", "7": "16731578e5f91f5a", "8": "ba8ecb89a45f61aa", "9": "3700d55fb5a6424c", "10": "27ea693b2435810b", "11": "cd03725dadd62284", "12": "62b5847dcacd948f", "13": "4fa68210b84027c3", "14": "3a99e55c1a8c5e99", "15": "0788d9d8e33dc3d7", "16": "acdbfc0b6e742a4a", "17": "fa29dc04abf6ad18", "18": "449f6059e00c34a8", "19": "ee1e7b50ca91f5dc", "20": "98f9c889deca44b8", "21": "20268135d3844d10", "22": "6b8f58222c3833a8", "23": "8380ce92e2f943ee", "24": "32205e04159fc03f", "25": "cb74bcf9458e2c66", "26": "a18ed1205ea22c35", "27": "fdb44d355ff70fab", "28": "fd3ba1d91df8b616", "29": "f7b5c27ea2ccff2e", "30": "59bb9a2533fe4194", "31": "230c619b788aafd1", "32": "7125f1abcf24af8f", "33": "9a63feb93e4aac98", "34": "115b1416b6c06927", "35": "e477e1f3f923ff73", "36": "d5cba17dd0ae8bc6", "37": "95aa4be7a1733e31", "38": "46e742f553598f37", "39": "a99387f9796707fc", "40": "2a30dd68f6194ad8", "41": "d3b9cf95bddc68a1", "42": "7d531a2af6c09b7f", "43": "3e12fb11a5ed9d46", "44": "7d741d30235c8657", "45": "a5d8430e4664ee13", "46": "ef5f72af7001b310", "47": "14b859671e5f45d5", "48": "2a883b5f7e85a353", "49": "6bdf4d070b3aae61", "50": "edadf917344e719d", "51": "72305af506fad397", "52": "acc11463239bd3e6", "53": "518d8c7abd745ce0", "54": "0cbab45d2459ff76", "55": "52c03ae4f4404445", "56": "d08592eb98a96332", "57": "064123b81d806408", "58": "60822ba74d8e3524", "59": "d4015e68559d194b", "60": "5b96dd71c07d1ec9", "61": "8da4d567c24081ec", "62": "83305e37bf7e2ffb", "63": "f2b85adb7d2d07b1", "64": "67b7afb27c1f614d", "65": "23ce778f57af1ea6", "66": "1fbf914ecae8473b", "67": "bda86bc0ab18f670", "68": "811aba9b7e859132", "69": "f13abb7e18d7ce48", "70": "4a2f9e11fd8ad084", "71": "87f486d958a939a2", "72": "545bbbd39a248e29", "73": "7e4a40b5c5c1d071", "74": "8b0559bf907640fe", "75": "636fac55b3cfa103", "76": "48b763021519f2a6", "77": "2fcf0c737b65d3fb", "78": "74200049349848a0", "79": "9f9178cb5f09df7a", "80": "91d3aac154fbba39", "81": "2fa09cafe372ee48", "82": "62a10bb594e1a21c", "83": "d3123d9bb0787158", "84": "fb2a07021d7297fc", "85": "cc27d34a82baf0cb", "86": "88cc864f8a44299d", "87": "88e35d7382a187e8", "88": "08c435bca3bc1af6", "89": "4fbd97280674f17c", "90": "165b591ba4c9511e", "91": "195519720e50c3c3", "92": "3e369913d3232f88", "93": "7f0bf7100c000533", "94": "21de920d64fa7cf5", "95": "886a5ed55308f326", "96": "757eeac4955bdd43", "97": "efd51105f845d501", "98": "1a97ffc45638efac", "99": "653c2741af003aa1", "100": "d02abac0abcc2c33", "101": "ac9fa6c62d449a83", "102": "0ce01a8d103c1ce5", "103": "08b7e55fe2faf069", "104": "7be5bfb2ebec405d", "105": "8fc345f8960bd6ec", "106": "5500263c62849987", "107": "5728bbfc930a4de7", "108": "cc82f5e3263f8b52", "109": "faf160496c49a4af", "110": "7fa5e86fb5b859f7", "111": "5cf7ebaa1ab0f37c", "112": "2bc803d90aa52552", "113": "7ab858a59463838e", "114": "1b3668e066f5eca2", "115": "3d94c956f08e9926", "116": "bcba7b715cfd8d7f", "117": "e28dca951d7ca0a3", "118": "ce7667c6140515eb", "119": "ac77d0122ef1555b", "120": "9d7d78c14ca494f7", "121": "071c3814840ec72b", "122": "56211f0abbbe1337", "123": "64c93c60bc4bb48f", "124": "00a7b2f47a70765d", "125": "b4c1a3ea46e3acda", "126": "d81320ddfc819f0b", "127": "c799ccdc983e40b4", "128": "385b7cd4bd246b4c", "129": "0bf148cef1dbd79f", "130": "b9f9504a399eb7bd", "131": "ea68ec76dd8619f8", "132": "2329e8c5a30a94ca", "133": "189a87afcf0f3b02", "134": "f620406b61f176ed", "135": "8d0b846a5849b267", "136": "6e4ea9f6236016a6", "137": "c62385c22afafc1f", "138": "bba80df6c941b043", "139": "0c106bef901e7836", "140": "603fc7ac275a955b", "141": "92b8387e905bd90a", "142": "2b304474febf47a8", "143": "0ef764c5f251be42", "144": "996f94557438583f", "145": "6bcb5b6388ea20dd", "146": "9d851d313ff05596", "147": "d7ca194d611b730c", "148": "b709ac21fbb8fff8", "149": "683e83e60c3505b6", "150": "202079945a56ce0a", "151": "3cc5a0fe50bd8645", "152": "7dcd65f1f266a81d", "153": "835c692da9348e03", "154": "a4f35b8aaeac94b3", "155": "151decdc3f5702d8", "156": "1c2365293f54002e", "157": "997d402f1879ea0d", "158": "c2c7a8be71ddb82c", "159": "5fc41909360529ec", "160": "0047f55b4c80ff3b", "161": "c79b342c3c88739d", "162": "5349dc2e2ead6b40", "163": "42b1082d08c39b50", "164": "cd2c511a507fee5c", "165": "c6019ae39467b559", "166": "17cd6b23530afb97", "167": "9e2feed38d818a16", "168": "425cb5871b3739fc", "169": "4de1fbcf63bab0b2", "170": "9583c984e370d0d7", "171": "70a44128ddd2a585", "172": "7e90da6e0e149564", "173": "5af7c123b002b2ee", "174": "5399b236d0428259", "175": "432adda824954169", "176": "75981b5d65839ce6", "177": "ec91dd8a177174fb", "178": "8d3980fb2d44c170", "179": "740ee3439e73891d", "180": "86dd0abd68cce7bd", "181": "ab27823e12e1283b", "182": "cfe6225489122f9f", "183": "695cd7213266a959", "184": "0562c3509a5a2873", "185": "a9871babeec7452b", "186": "49d33e1357a9c00e", "187": "b3fb262962922e5d", "188": "375479e75c342a49", "189": "2dbaefd48b31fbc1", "190": "46a836d2f70462de", "191": "c6b01ea3e539e92d", "192": "d1b05590fa813dda", "193": "6a2bad7ce6457339", "194": "3f3177401d40a5d5", "195": "80338e7eabe1b94c", "196": "347db61f12983c8b", "197": "59a29a27fe2aeb9b", "198": "ee9af2e31bb4d69e", "199": "45b5cdd1c0a3ed42", "200": "b19234452753a963", "201": "7f371359f8cbcae2", "202": "c5aa85f5c27ba192", "203": "0e16b67c17622c6f", "204": "50b0db243e43934e", "205": "bd27a688225b6a33", "206": "5be335bd678dc0e1", "207": "fe462dcf8baab556", "208": "65035abb2546cfba", "209": "6cf8ae3a4377e8b3", "210": "993a2cc7f78ff5ee", "211": "17ed6623f148ec33", "212": "fbb4344484a7fbb6", "213": "08ecd11fee448983", "214": "814ba695ab3fe9d4", "215": "67450fb4fccf2edc", "216": "c9502ff1fbec6e44", "217": "efcccc9129fb764a", "218": "31f8ddd06ddf3f6b", "219": "2cd77561eef54d01", "220": "e75e725824ea9795", "221": "8011beca70a26ddf", "222": "e36690c4c6ea70dc", "223": "cbc3054c23c3e7c5", "224": "44b65edd735f755b", "225": "1ca0e4f9e70ba0d8", "226": "be63a5fb74ce6591", "227": "073ebfc0d82ece5d", "228": "3d31ed6ba0035a9b", "229": "34b2126166cd1733", "230": "4d5ee29d62784bce", "231": "2ec66a29728379d7", "232": "69e3b0ad6e14fa56", "233": "dfcf1f53c2773eb6", "234": "d7151cc443a0d3ae", "235": "2d05c9dc165201e2", "236": "d47bda53fbf25407", "237": "b416fd92acbee855", "238": "45e2860c8c0d0d86", "239": "0e5086758666bd0c", "240": "0f16e037eb9da7b4", "241": "bfa1db7e1aad8600", "242": "36a425c8aef4d209", "243": "10281c2ef334ecf4", "244": "7b77c2e3209f5c22", "245": "37abd99ab905def1", "246": "5271b4f27e7b6403", "247": "52abca3569d568bb", "248": "18259707b6496a5d", "249": "5b1a296b990bdd06", "250": "c1df22372e374115", "251": "4947e519c0aa830e", "252": "4616fa2cb3ac06e1", "253": "81fd07e37694bb91", "254": "8959843bbefbe780", "255": "92b3baf8159e81d2", "256": "1bb48df81eb9177a", "257": "f298841a49015d5c", "258": "5b30b4eda20a98ee", "259": "4713e1339750ca17", "260": "ba0167872c22b9d6", "261": "1491b8a5363601cd", "262": "b787ba3da01c1b90", "263": "67d7668d4d0e26cb", "264": "ee9e8568ffd4078c", "265": "d76c76e1145c5388", "266": "00a64e391b99cf8b", "267": "085196be307a22ff", "268": "ad708fbbe273ea95", "269": "918a9a562f01d4b3", "270": "fc4d27cde7ab8041", "271": "a340dfcd2eef6466", "272": "b6ab67e936bbd2e8", "273": "2da1d6a29e2fa105", "274": "02ea03bd94b8d70e", "275": "61c4dcf3f1ab0602", "276": "787ddc45a8e0ec99", "277": "bea1ab731f0016f2", "278": "a09acd94fcd91d09", "279": "08b86ed2f6248fbe", "280": "05c52b1b5478604d", "281": "18c2e73219a8171e", "282": "1e74126cfe8be8f6", "283": "f79a7ca288131b40", "284": "987acf92654061b4", "285": "6d758949f980ac59", "286": "ea17edcb394b6a26", "287": "b522c0397de29a55", "288": "55212395aebde232", "289": "61d341e3d6044e8e", "290": "810021694c13c86a", "291": "686d734cb58c0f0d", "292": "4d1dfde6c1945b9d", "293": "dcd2c0a709ab925f", "294": "7a86aaf4c3bb953c", "295": "ee5414973eed2a25", "296": "8f62c91f05330539", "297": "23392c213d354e7f", "298": "9718befeaf5e5906", "299": "3c2ae2c4e7a1949b", "300": "8c5971b6925882d9", "301": "a067f6922115aaf0", "302": "a4603b1fbd5baddb", "303": "01585298c38b36a8", "304": "475f5428cc83ef87", "305": "136daf70e87e02a1", "306": "af2df6004f1e66a0", "307": "056986f3ca294dd3", "308": "6818f385c306eaa4", "309": "ad1afd0a1335a7b6", "310": "f62d0ff4241734e7", "311": "25e9a083b16b3359", "312": "a6e061b71798841a", "313": "584cf83aec10d424", "314": "acb3d709fc667892", "315": "ff6b62985215b71d", "316": "28b9538b6974aef9", "317": "aabbe1539b1ea302", "318": "3cb4433f63bb8819", "319": "6eb94279bdfa4c8e", "320": "d43c8e41ad02647b", "321": "2988d6d8f485a46c", "322": "2ea6e398df06e887", "323": "d5e5145b976fe497", "324": "ff359fa92047ec71", "325": "fa0ccab3bbbcb772", "326": "4698af944673671b", "327": "b24dc65160a65c31", "328": "6945d99925be4760", "329": "1957e05ff32f608d", "330": "f5a77f29a2d6d382", "331": "4a291a3cef78d580", "332": "853cf2126f3d3ada", "333": "1b5f723dea319f63", "334": "f0746bdf38c1afea", "335": "35c5b06c09ddcf7b", "336": "dd4bf71037bdf5e9", "337": "bdf0e713b9cb48eb", "338": "8be778cfe8685fb6", "339": "4543c1aaff9332d7", "340": "455559150f1f41ac", "341": "ff74a0c0b8496fb3", "342": "6c813dc2c3dec42b", "343": "21be76d4e02295dc", "344": "97aa1c8e01d7a9c0", "345": "f15493f543e0c1b1", "346": "e7fea49f1afe32dd", "347": "bcb3eb55e616c950", "348": "0965d9688e11641b", "349": "34bdc9bb28b363f4", "350": "89dccdf78f8c8f4f", "351": "9352b4af27abc878", "352": "2c252003e531c9bb", "353": "69e0f04ac9f09daf", "354": "b5bd1df08506feb0", "355": "c2b0575b173483c1", "356": "d26d35a80d122be4", "357": "0ecd5fdfa040950b", "358": "90747e9042f6671d", "359": "14d1ae3b1cff934b", "360": "30b502e616e3e01f", "361": "036ef64c84d005dc", "362": "a17154091b064a55", "363": "9c064fb7bd3825f9", "364": "929f7966f607c2d6", "365": "accfebd3c599e7d0", "366": "434949c322bbb405", "367": "03766f79be04df0e", "368": "f52d7b87fa0e13b7", "369": "87bc1a0079f4f956", "370": "eb6489b237602a3d", "371": "a0585796f9a4646b", "372": "fa8b6c3807ddf216", "373": "6788416604fad624", "374": "5a983f36c68ca3fc", "375": "f24eef4943ea4f25", "376": "efd98c8abf12ad1d", "377": "9a35a514b76efbd1", "378": "9a0d84f44fe4ffc1", "379": "8277fe2d31b3a3f4", "380": "d1c5c1a32b6c4110", "381": "b39493ed39b45bf3", "382": "74af31ee46d6ef60", "383": "d24cfd824810865e", "384": "b357e2723c6a435a", "385": "767ac74dca429121", "386": "8ac01c348ddfdfe5", "387": "a9e4077d96048018", "388": "1fefb64451ff7eaa", "389": "381b13c3a3ba2e23", "390": "d0cdc78e9b7af43f", "391": "1558277daf85e870", "392": "1532d0b3c0551e59", "393": "06fb128ecf74138b", "394": "c3446b19fa9dba31", "395": "7c07f28b71bd0b3e", "396": "708dd0b18a0a4798", "397": "3241630562395df9", "398": "6a32693f62f4d088", "399": "76864a54ed005737", "400": "b696475a2e95f25f", "401": "5bd0d859725efccb", "402": "892f87b2e7d2e7be", "403": "470ea7aa81f5394b", "404": "e3c70389ad967171", "405": "6db915995bc0b271", "406": "0a3aca6ad397c44b", "407": "3f6f84c96be50d9d", "408": "cc9ee9a966b5e226", "409": "5cae267c36b80958", "410": "6b09b53b2124e9a9", "411": "16c63dcd9e5ed158", "412": "fb8672f03334a2a4", "413": "93fd39edfeaa955b", "414": "dc99b776b2d3bde9", "415": "155f139761bfda01", "416": "98b98cadd83b05c3", "417": "3765a56555254119", "418": "84cc8611b5a317a5", "419": "a824e2955ce6ec76", "420": "d6992de6b08f4dc2", "421": "f8e524ce16d0fad4", "422": "74f36a13253765e1", "423": "13bb81b4ea9530f6", "424": "ec847b053e0fc5cc", "425": "99edf7dd8356014f", "426": "1528a41acca8dc22", "427": "e6b0c5ae9144bfae", "428": "0bf8dbaa131f820a", "429": "b713ce65d240607c", "430": "bb8a02b6078be30b", "431": "b98e2c63125b8b99", "432": "512542ea60d90da9", "433": "b88f507bb73b8651", "434": "92c9d40a819f368c", "435": "2076e1d05932fb62", "436": "d6ec261087f6bc77", "437": "34981a383ae71409", "438": "9d98be4fc7b9da16", "439": "649a9984065fda98", "440": "9ed13a6b1fdd29aa", "441": "4ca798e5f087260d", "442": "0c612ec956a1e9a3", "443": "3a3e18d9ac643f35", "444": "77ec63a8255989aa", "445": "47ce12ec866ee546", "446": "ee2953da63e41f03", "447": "15ae692d06dbdc79", "448": "00e62a56bdf3e98e", "449": "4c0a8ecc52436e89", "450": "d08e08c1bef8ce42", "451": "2388d3b1bcf4afd0", "452": "3eaf0f83b4b5d217", "453": "0602067ecbc34d32", "454": "855a53cf44d537f7", "455": "c076f675fe74d98b", "456": "c43182f74537ef96", "457": "96083d75134d170d", "458": "6b646ba69c312c9e", "459": "c8b3ee00dc822e1f", "460": "5cc6567aacbdccba", "461": "e4685a8dbc331296", "462": "c8cdb11a4360d582", "463": "1bdd1e69dc924512", "464": "b96ad4e2e72ee4e0", "465": "e30952305e11ce35", "466": "b0904ef0f0aaf7d4", "467": "4423f00984596bb5", "468": "b9e2755adb66c626", "469": "5748723725fac7bc", "470": "1aaf664c6ffe5a62", "471": "96a9c1c3b06d45b6", "472": "61e9b8c4d917cbbf", "473": "3dd7b2223b12a178", "474": "4ecc297368533e1d", "475": "2ed3d7513c9e9432", "476": "2feb06e3b9fe8085", "477": "8fcea9acee447ea8", "478": "2018180f7ade5b0e", "479": "b5c49b147ba220ed", "480": "c8ab14ccfb2ceb29", "481": "8a2b334f3892ec42", "482": "d5fe45ae7ec07826", "483": "3ada99807193fa75", "484": "c32d2544c6f24a5d", "485": "4f37fd92e0afd609", "486": "82870a72bd6f6775", "487": "922a3676522aa1db", "488": "0c5f4349a24e692e", "489": "4346fd7b60bbee63", "490": "e651d1e0a6a9d2f7", "491": "d97c392f2988607d", "492": "1cc0949deb48d80b", "493": "f73cc7f508a09a27", "494": "328cc6def8813c2b", "495": "ac2f1b72332c19e0", "496": "b9bc48fd0c5bd742", "497": "7b531173c84842e3", "498": "bac56080d614f4e5", "499": "ef4b31ee0b8691a7", "500": "04a02bc8cec46614", "501": "d26007b612e0d28f", "502": "399136a1201e626a", "503": "34da019da67be529", "504": "6f34b9f536c17cca", "505": "cf10ee822d8aa3b5", "506": "634f6e7943a22ae0", "507": "4930f7207553cdd2", "508": "f84d9c6dd0ae72e4", "509": "d786feb243f024f3", "510": "b08fe51e110c766a", "511": "2d6f4b8ec71816fb", "512": "20baa70a65a36421", "513": "ed98e9a9d974754d", "514": "3374a018515d73e0", "515": "75e25c552169d0b1", "516": "31c658375ca655fb", "517": "6faffa92a422d0e1", "518": "ac654ab9bd4ae870", "519": "b8d1ed53bae3e3dd", "520": "25ba94c1e0b355b4", "521": "9747497c2bde5eba", "522": "7eef6136f6be4db7", "523": "a20171ea17b536bb", "524": "945939db6a08497b", "525": "57a40d480d1a07de", "526": "f5c67e18959e1a96", "527": "8782ecf8e7770bcc", "528": "cbcfc4ab756bcc7e", "529": "5c2a9932d803ae8d", "530": "589eb257be817a5b", "531": "1a46e3c34fd4dfbb", "532": "93a1d044672ad228", "533": "fdb6d627efd3ea42", "534": "49fcd2d6813add0a", "535": "72d68125d7c45efa", "536": "dddb44bb19569eb2", "537": "c61e20863630c32d", "538": "80f28d09566e7863", "539": "848d72d6be632b3f", "540": "75b620b9607e6d07", "541": "200534e51312a03e", "542": "c60c6111389210c3", "543": "7afbc10971e64d7b", "544": "c81dfbf19c9e877f", "545": "6880eadd7d37de1c", "546": "e4ae735963728e8d", "547": "db4057e429756870", "548": "0a4016518602de5a", "549": "61602d52d6b9c79b"}
\ No newline at end of file
diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md
index 506502d7..92b112dd 100644
--- a/graphify-out/GRAPH_REPORT.md
+++ b/graphify-out/GRAPH_REPORT.md
@@ -1,16 +1,16 @@
-# Graph Report - OwnCord (2026-08-16)
+# Graph Report - OwnCord (2026-08-20)
## Corpus Check
-- 1081 files · ~1,387,726 words
+- 1124 files · ~1,482,121 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
-- 11331 nodes · 32666 edges · 529 communities (431 shown, 98 thin omitted)
-- Extraction: 86% EXTRACTED · 14% INFERRED · 0% AMBIGUOUS · INFERRED: 4683 edges (avg confidence: 0.8)
+- 11770 nodes · 34088 edges · 550 communities (450 shown, 100 thin omitted)
+- Extraction: 86% EXTRACTED · 14% INFERRED · 0% AMBIGUOUS · INFERRED: 4900 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
-- Built from commit: `33b7d471`
+- Built from commit: `c28341ca`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -19,11 +19,11 @@
- createElement
- testing.T
- livekitSession.ts
-- dispatcher.ts
+- messages.store.ts
- openMigratedMemory
- context.Context
- buildChannelRouter
-- MessageInput.ts
+- content-parser.ts
- attachments.ts
- telemetry.go
- waitRegistered
@@ -31,8 +31,8 @@
- NewAdminAPI
- Fixed
- messages_test.go
-- main.ts
-- net/http.HandlerFunc
+- ConnectPage.ts
+- writeErr
- NewTestClient
- newHandlerHub
- livekitE2EE.ts
@@ -41,21 +41,21 @@
- tofu.rs
- newAuthTestDB
- newMigratedTestDB
-- time.Time
+- EventPersister
- Config
- secret_store.rs
-- User
+- Hub
- database/sql.Result
-- newUploadTestDB
-- MainPage.ts
-- writeJSON
+- net/http.Handler
+- channels.store.ts
+- net/http.HandlerFunc
- newAdminTestDB
- HashToken
-- content-parser.ts
+- MessageList.ts
- ChannelSidebar.ts
- plugin/registry_test.go
-- DB
-- Instance
+- newDeafenRaceDB
+- Registry
- livekit_test.go
- NewChecker
- middleware_test.go
@@ -66,12 +66,12 @@
- livekit_proxy.rs
- native/helpers.ts
- NewRouter
-- net/http.Handler
-- totp_test.go
+- profileCreateToken
+- RateLimiter
- newTestDB
-- createLogger
+- reaction-tooltip.ts
- newServeHub
-- OwnCord — Comprehensive Project Audit
+- 3. Security
- permissions_test.go
- seedMemberUser
- postJSONWithToken
@@ -82,66 +82,66 @@
- ProfileManager
- README.md
- devDependencies
-- itoa
+- doRequest
- helpers_test.go
-- ChannelService
+- profiles.ts
- Security Policy
- Role
- channels.sql.go
- Deployment Guide
- livekit_proxy_test.go
-- newEmojiService
+- Emoji
- ws_proxy.rs
- bughunt.js
- openAdminTestDB
-- db/db.go
+- DB
- Tables
- newMentionFixture
- Channel
-- NewWAFMiddlewareCRS
-- E2EEManager
+- newWAFMiddleware
+- LiveKitSession
- storage_test.go
- Migrate
- Hub
- chdirTemp
-- AppearanceTab.ts
+- themes.ts
- updater_test.go
- newTestMessageService
-- textAssetServer
+- newTestUpdater
- Hub
- compilerOptions
-- NewEventRingBuffer
-- HandlerRegistry
+- screenShare.ts
+- deps.go
- emoji_handler_test.go
-- handleCreateEmoji
-- doRequest
-- audioPipeline.ts
-- LoadOrGenerate
+- buildErrorMsg
+- DB
+- noise-suppression.ts
+- AdminActions.ts
- Auth Endpoints
-- MigrateFS
-- newOverrideFixture
+- openMemory
+- PermissionService
- Save
- REST API Reference
-- NewRegistry
-- voice_moderation_test.go
+- media-visibility.ts
+- joinVoice
- ptt.rs
- OwnCord Audit — Documentation Accuracy & UI/UX Test Coverage (2026-08-04)
- scripts
-- ResolveTokenHash
+- checkSourceWith
- Load
-- handleVoiceE2EEOfferV2
-- commands.rs
+- handleVoiceE2EEAnnounceV2
+- NewHandler
- newEmitTestHub
- Plan: Remediate security-hardening review regressions
- verify.go
- newRoleCRUDService
-- Checker
+- VoiceDeps
- EnsureLiveKitBinary
- clientip_test.go
-- AudioElements
-- buildErrorMsg
+- connectionStats.ts
+- buildVoiceLeave
- gif_handler_test.go
-- navigateToMainPage
+- e2e/helpers.ts
- messages.sql.go
- Channel Endpoints
- newSignedTestUpdater
@@ -149,8 +149,8 @@
- Topic
- DB
- users
-- Client
-- identity.ts
+- User
+- update_commands.rs
- Queries
- Queries
- Queries
@@ -164,17 +164,17 @@
- handleSetup
- log/slog.Value
- Updater
-- Credential storage
+- syntax-highlight.ts
- dependencies
- newHarvestVoiceDB
- Config Key Reference
-- newTestPermService
+- seedChannel
- markdown.ts
-- VideoGrid.ts
-- Manifest
+- deep-link.ts
+- .attemptAutoReconnect
- rate-limiter.ts
-- e2e/helpers.ts
-- main.test.ts
+- Error
+- video-grid.test.ts
- testing.M
- newMockDB
- OwnCord
@@ -182,7 +182,7 @@
- buildTauriMockScript
- OwnCord Introspection MCP Server
- Bug-detection improvements — design
-- ptt.ts
+- AuditWriter
- eslint-rules.js
- DB
- OwnCord — Security Review
@@ -190,8 +190,8 @@
- net/http.Request
- ChannelTopic
- UserService
-- Blocked — fix attempted, revert-proof failed
-- handlers_backup.go
+- OwnCord Findings Ledger
+- handleRestoreBackup
- logger.ts
- fallback_crypto.rs
- Direct Messages
@@ -199,7 +199,7 @@
- command.go
- NewRingBuffer
- ws-load.js
-- NewMessageService
+- newDMFixture
- handler
- tauri-client/package.json
- screen-share-tracks.test.ts
@@ -209,28 +209,28 @@
- Role Management
- User Profile & Sessions
- F3 — Voice E2EE identity keys + TOFU (the remaining work)
-- run
-- newWazeroTestRegistry
+- Server/main.go
+- totp_encrypt_test.go
- newUserSvc
- plugins_handler_test.go
- badDirFile
- Contributing
-- .handleFreshConnect
+- github.com/coder/websocket.Conn
- mcp-introspect/package.json
- v1.2.0-alpha.1 — Discord feature parity
- Task Observer — Continuous Skill Discovery & Improvement
-- scanPluginDirectory
-- loadPref
+- handleVoiceE2EEOfferV2
+- AudioPipeline
- 1. Channel sidebar
-- Voice, Video & E2EE — target UX
-- Updater
+- Messaging — target UX
+- time.Time
- LiveKitClient
- migrate.go
- Queries
-- Messaging — target UX
-- Registry
+- setupRouter
+- .verify_server_cert
- handleChannelFocusV2
-- handlers_channel_perms_test.go
+- itoa
- knip.json
- RNNoiseProcessor
- DeviceManager
@@ -239,7 +239,7 @@
- Connection & Authentication — target UX
- Settings & Admin — target UX
- buildClientUpdateRouter
-- logPersistence.ts
+- Voice, Video & E2EE — target UX
- newTestRoleService
- event.go
- NewTopicRateLimiter
@@ -253,21 +253,21 @@
- LiveKit Setup Guide
- Voice Signaling
- Quick Start Guide
-- readPump
-- mockTauriFullSessionWithVoice
+- OwnCord — Test Audit
+- newBackupFileDB
- index.mjs
-- countingReadStateStore
+- TestAdminAPI_PatchRole_MemberLookupFailureBlanketInvalidates
- bughunt.harness.mjs
- voice-audio-tab.test.ts
- reactions.sql.go
- Channel Permission Overrides
- Server Stats & User Administration
-- deep-link.ts
-- EventSink
+- .DeleteAccount
+- scaledAuthLimit
- handleChatCommandV2
- slashFS
- Running the bughunt pipeline
-- OverlayManagers.ts
+- MainPage.ts
- reconnectAfterCertAccept
- VoiceTopic
- emoji-voicemod.parity.spec.ts
@@ -278,7 +278,7 @@
- OwnCord — Test-Coverage Audit
- OriginAcceptOptions
- EventRingBuffer
-- IsUniqueConstraintError
+- .UpdateUserProfile
- newTokenTestDB
- scripts
- bughunt-fix.harness.mjs
@@ -289,7 +289,7 @@
- TestMigrate_UpgradeFromMigration019PreservesData
- Queries
- TestChannelVisibility_RESTWSAgreement
-- seed.go
+- OwnCord — Comprehensive Project Audit
- Finish the V2 Dispatch Migration (backlog item 11) — Design
- Port Forwarding Guide
- Chat Messages
@@ -307,12 +307,12 @@
- Member Updates
- genprotocol/main.go
- Hub
-- .finishVoiceLeave
+- FenwickTree
- ChatSendCmd
- Environments, Activation Setup, and Handoff-Doc Mode
-- handlePingV2
+- newBlockService
- capabilities-scope.test.ts
-- savePref
+- handleDiagnosticsConnectivity
- GET /admin/api/updates
- Channel-Visibility Unification (backlog item 3) — Design
- sqlc Adoption (D2) — Progress & Plan
@@ -324,7 +324,7 @@
- openFileDB
- hello plugin
- TestAdminAPI_PatchChannel_ArchiveCleansVoice
-- window-state.ts
+- default_verify_schemes
- Tauri HTTP Capability Narrowing — Design
- protocol_contract_test.go
- ChatCommandCmd
@@ -333,31 +333,31 @@
- VoiceWidgetOptions
- LiveKitProcess
- cert-tofu.spec.ts
-- updater.spec.ts
-- message_reactions_test.go
+- navigateToMainPageReady
+- 4. Dependencies & Supply Chain
- tsconfig.build.json
- User Blocks
- PATCH /admin/api/settings
- GET /api/v1/gif/search
- First-Run Setup
- LiveKit Endpoints
-- types.go
+- 5. Test Coverage & Quality
- Tailscale Guide (Zero-Config Remote Access)
- LiveKitProcess
-- DB
+- perm_grid_test.go
- ChatEditCmd
- VoiceE2EEOfferCmd
- VoiceModDeafenCmd
- VoiceModMuteCmd
-- TestHandlePresenceUpdate_BareStatusReadFailureAbortsBeforeCommit
-- New
+- admin-static-channel-perms.test.ts
+- NewDMService
- GET /api/v1/client-update/{target}/{current_version}
- OwnCord Architecture Blueprints
- Voice End-to-End Encryption
- feature_request.md
- volume-menu.test.ts
-- buildMetricsRouter
-- RunningInContainer
+- isAddrInUse
+- MetricsSources
- ChatDeleteCmd
- Permission-Middleware Consolidation (audit finding A-2026-07-16) — Design
- MessageDeletedDMEvent
@@ -406,7 +406,7 @@
- TypingDMEvent
- VoiceStateEvent
- TestDeleteExpiredSessions_SargableFormat
-- .EmitEvents
+- jsdom.d.ts
- stubChannelEvent
- stubUserTargetedEvent
- TestPresenceEvents_InvisibleBlanksCustomStatusForOthers
@@ -429,9 +429,16 @@
- pre-commit
- pre-push
- stubBroadcastAllEvent
+- updater.test.ts
+- 1. Architecture
- 015_plugins.sql
- tryLoadPluginTOML
- tryLoadPluginTOML
+- 6. CI/CD & DevEx
+- 7. Observability
+- syscall.SysProcAttr
+- Security Policy
+- erroringMembersStore
- protocol-change/SKILL.md
- strip-appimage-bundled-libs.sh
- jitsi-rnnoise.d.ts
@@ -443,7 +450,8 @@
- 014_events_table.sql
- chaos-test.sh
- voice-test.sh
-- RateLimiter
+- Capture
+- audio-pipeline-vad-worklet.test.ts
- github.com/owncord/server
- owncord-client
- attachments
@@ -455,230 +463,232 @@
- prettier
- @vitest/browser
- @vitest/coverage-v8
+- MockAudioContext
+- D7 — Module map
+- WebSocket / Real-time Engine
+- Audit 2026-07-19 — Maintainer Decisions
+- RunningUnderSupervisor
+- owncord-introspect (MCP dev tool)
## God Nodes (most connected - your core abstractions)
-1. `waitRegistered()` - 287 edges
-2. `DB` - 279 edges
-3. `NewTestClientWithUser()` - 268 edges
-4. `NewAdminAPI()` - 232 edges
-5. `openAdminTestDB()` - 215 edges
-6. `doRequest()` - 213 edges
+1. `DB` - 310 edges
+2. `waitRegistered()` - 290 edges
+3. `NewTestClientWithUser()` - 273 edges
+4. `NewAdminAPI()` - 239 edges
+5. `openAdminTestDB()` - 225 edges
+6. `doRequest()` - 219 edges
7. `createElement()` - 208 edges
-8. `newTestModService()` - 194 edges
-9. `newTestRoleService()` - 192 edges
-10. `openMigratedMemory()` - 180 edges
+8. `newTestModService()` - 201 edges
+9. `newTestRoleService()` - 199 edges
+10. `Fixed` - 189 edges
## Surprising Connections (you probably didn't know these)
- `buildTotpSection()` --indirect_call--> `render()` [INFERRED]
Client/tauri-client/src/components/settings/AccountTab.ts → .superpowers/render-ledger.mjs
- `renderMessage()` --indirect_call--> `att()` [INFERRED]
Client/tauri-client/src/components/message-list/renderers.ts → Client/tauri-client/tests/unit/attachments-media.test.ts
-- `SidebarAreaResult` --references--> `MountableComponent` [EXTRACTED]
- Client/tauri-client/src/pages/main-page/SidebarArea.ts → Client/tauri-client/src/lib/safe-render.ts
-- `SidebarMemberSectionResult` --references--> `MountableComponent` [EXTRACTED]
- Client/tauri-client/src/pages/main-page/SidebarMemberSection.ts → Client/tauri-client/src/lib/safe-render.ts
-- `E2EEDeps` --references--> `WsClient` [EXTRACTED]
- Client/tauri-client/src/lib/livekitE2EE.ts → Client/tauri-client/src/lib/ws.ts
+- `createSidebarDmSection()` --indirect_call--> `dm()` [INFERRED]
+ Client/tauri-client/src/pages/main-page/SidebarDmSection.ts → Client/tauri-client/tests/unit/read-state.test.ts
+- `setupRestartAfterResponse()` --calls--> `tryDirectRestartPending()` [INFERRED]
+ Server/admin/setup_handler.go → Server/admin/restart.go
+- `extract_host_for_cert_store()` --calls--> `cert_store_key()` [INFERRED]
+ Client/tauri-client/src-tauri/src/update_commands.rs → Client/tauri-client/src-tauri/src/tofu.rs
## Import Cycles
+- 3-file cycle: `Client/tauri-client/src/lib/audioElements.ts -> Client/tauri-client/src/lib/livekitSession.ts -> Client/tauri-client/src/lib/roomEventHandlers.ts -> Client/tauri-client/src/lib/audioElements.ts`
- 3-file cycle: `Client/tauri-client/src/components/message-list/attachments.ts -> Client/tauri-client/src/components/message-list/media.ts -> Client/tauri-client/src/components/message-list/content-parser.ts -> Client/tauri-client/src/components/message-list/attachments.ts`
- 3-file cycle: `Client/tauri-client/src/components/message-list/attachments.ts -> Client/tauri-client/src/components/message-list/media.ts -> Client/tauri-client/src/components/message-list/embeds.ts -> Client/tauri-client/src/components/message-list/attachments.ts`
-- 3-file cycle: `Client/tauri-client/src/lib/audioElements.ts -> Client/tauri-client/src/lib/livekitSession.ts -> Client/tauri-client/src/lib/roomEventHandlers.ts -> Client/tauri-client/src/lib/audioElements.ts`
- 4-file cycle: `Client/tauri-client/src/components/message-list/attachments.ts -> Client/tauri-client/src/components/message-list/media.ts -> Client/tauri-client/src/components/message-list/content-parser.ts -> Client/tauri-client/src/components/message-list/custom-emoji.ts -> Client/tauri-client/src/components/message-list/attachments.ts`
-## Communities (529 total, 98 thin omitted)
+## Communities (550 total, 100 thin omitted)
### Community 0 - "members.store.ts"
Cohesion: 0.03
-Nodes (72): appendGroup(), assignableRoleNames(), closeActiveMenu(), closeActivePopup(), createMemberItem(), createMemberList(), destroy(), FALLBACK_ASSIGNABLE_ROLES (+64 more)
+Nodes (77): appendGroup(), assignableRoleNames(), closeActiveMenu(), closeActivePopup(), createMemberItem(), createMemberList(), destroy(), FALLBACK_ASSIGNABLE_ROLES (+69 more)
### Community 1 - "createElement"
Cohesion: 0.02
-Nodes (165): appendBanFlow(), BAN_DURATIONS, ChannelContextMenuOptions, ContextMenuResult, createChannelContextMenu(), createMemberContextMenu(), createMenuItem(), createSeparator() (+157 more)
+Nodes (185): appendBanFlow(), buildRow(), CertFirstUseModalOptions, CertMismatchModalOptions, createCertFirstUseModal(), createCertMismatchModal(), createIdentityMismatchModal(), IdentityMismatchModalOptions (+177 more)
### Community 2 - "testing.T"
Cohesion: 0.02
-Nodes (166): testing.T, adminPanelSource(), TestAdminPanelEmojiSectionIsWired(), TestAdminPanelEmojiUsesTheMemberAPI(), TestLoginRateLimit_Value(), TestRateLimiterCleanupHorizon_CoversMaxSlowMode(), TestLiveKitHealth_Degraded_NilError(), TestLiveKitHealth_Degraded_WithError() (+158 more)
+Nodes (181): testing.T, adminPanelSource(), TestAdminPanelEmojiSectionIsWired(), TestAdminPanelEmojiUsesTheMemberAPI(), TestSlashFS_GlobNormalizes(), TestSlashFS_ResolvesBackslashPath(), setupDiagnosticsRouter(), TestDiagnosticsConnectivity_MemberForbidden() (+173 more)
### Community 3 - "livekitSession.ts"
-Cohesion: 0.01
-Nodes (154): VoiceModerationCallbacks, buildVoiceAudioTabInner(), CameraInvalidationRegistrar, CameraRegistrar, createVoiceAudioTab(), MicRegistrar, VoiceAudioTabHandle, createVoiceWidget() (+146 more)
+Cohesion: 0.02
+Nodes (144): invalidateReactionUsers(), AudioElements, log, userVolumeKey(), log, SCREENSHARE_TILE_ID_OFFSET, log, DispatcherCleanup (+136 more)
-### Community 4 - "dispatcher.ts"
-Cohesion: 0.03
-Nodes (150): DispatcherCleanup, enforceModeratorAudioState(), livekitSession(), log, mapDmPayload(), mapDmUser(), wireConnectionStatus(), wireDispatcher() (+142 more)
+### Community 4 - "messages.store.ts"
+Cohesion: 0.04
+Nodes (88): MessageListComponent, createNsfwGate(), NsfwGateOptions, findChannelById(), acknowledgeNsfw(), clearNsfwAcknowledgements(), isNsfwAcknowledged(), nsfwGateRequired() (+80 more)
### Community 5 - "openMigratedMemory"
Cohesion: 0.03
-Nodes (186): setRole(), TestDeleteAccount_AdminAllowedWhenOwnerExists(), TestDeleteAccount_AllowedWhenOtherAdminExists(), TestDeleteAccount_AnonymisesUsername(), TestDeleteAccount_ClearsAvatarAndTOTP(), TestDeleteAccount_ClearsPassword(), TestDeleteAccount_ClearsProfileFields(), TestDeleteAccount_DeletesSessions() (+178 more)
+Nodes (188): setRole(), TestDeleteAccount_AdminAllowedWhenOwnerExists(), TestDeleteAccount_AllowedWhenOtherAdminExists(), TestDeleteAccount_AnonymisesUsername(), TestDeleteAccount_ClearsAvatarAndTOTP(), TestDeleteAccount_ClearsPassword(), TestDeleteAccount_ClearsProfileFields(), TestDeleteAccount_DeletesSessions() (+180 more)
### Community 6 - "context.Context"
Cohesion: 0.02
-Nodes (59): channelPermissionsResponse, Attachment, AttachmentAccess, channelFields, ChannelUpdate, fakeAuditStore, SessionWithBanStatus, context.Context (+51 more)
+Nodes (53): APITokenListItem, Attachment, ChannelUpdate, fakeAuditor, fakeAuditStore, ServerStats, SessionWithBanStatus, context.Context (+45 more)
### Community 7 - "buildChannelRouter"
Cohesion: 0.06
-Nodes (138): aroundResponse, offlineBroadcaster, purgeBroadcast, purgeResponseBody, reactionUsersResponse, recordingPurgeBroadcaster, aroundPath(), decodeAround() (+130 more)
+Nodes (147): aroundResponse, offlineBroadcaster, purgeBroadcast, purgeResponseBody, reactionUsersResponse, recordingPurgeBroadcaster, net/http/httptest.ResponseRecorder, aroundPath() (+139 more)
-### Community 8 - "MessageInput.ts"
+### Community 8 - "content-parser.ts"
Cohesion: 0.03
-Nodes (82): buildPreview(), byLabel(), createEmojiAutocomplete(), EmojiAutocompleteComponent, EmojiAutocompleteOptions, EmojiSuggestion, filterEmojiSuggestions(), MAX_EMOJI_SUGGESTIONS (+74 more)
+Nodes (125): byLabel(), createEmojiAutocomplete(), EmojiAutocompleteComponent, EmojiAutocompleteOptions, EmojiSuggestion, filterEmojiSuggestions(), MAX_EMOJI_SUGGESTIONS, MIN_EMOJI_QUERY (+117 more)
### Community 9 - "attachments.ts"
Cohesion: 0.03
-Nodes (107): animateGifsPref, buildDownloadButton(), buildFileMeta(), clearAttachmentCaches(), closeDbAfterTransaction(), createObjectUrl(), downloadFile(), fetchMediaAsObjectUrl() (+99 more)
+Nodes (108): createDmProfileSidebar(), DmProfileData, DmProfileSidebarComponent, DmProfileSidebarOptions, legacyNoteKey(), loadNote(), saveNote(), scopedNoteKey() (+100 more)
### Community 10 - "telemetry.go"
Cohesion: 0.03
-Nodes (81): go.opentelemetry.io/otel/attribute.KeyValue, go.opentelemetry.io/otel/metric.Float64Gauge, go.opentelemetry.io/otel/metric.Float64Histogram, go.opentelemetry.io/otel/metric.Int64Counter, go.opentelemetry.io/otel/metric.Meter, go.opentelemetry.io/otel/sdk/metric.MeterProvider, go.opentelemetry.io/otel/sdk/trace.TracerProvider, go.opentelemetry.io/otel/trace.Span (+73 more)
+Nodes (77): go.opentelemetry.io/otel/attribute.KeyValue, go.opentelemetry.io/otel/metric.Float64Gauge, go.opentelemetry.io/otel/metric.Float64Histogram, go.opentelemetry.io/otel/metric.Int64Counter, go.opentelemetry.io/otel/metric.Meter, go.opentelemetry.io/otel/trace.Span, go.opentelemetry.io/otel/trace.Tracer, Invite (+69 more)
### Community 11 - "waitRegistered"
-Cohesion: 0.07
-Nodes (124): TestBuildReady_IncludesCanSend(), TestBuildReady_CarriesChannelFeatureFlags(), TestHandleVoiceCamera_BadPayload(), TestHandleVoiceCamera_NotInVoice2(), TestHandleVoiceDeafen_BadPayload(), TestHandleVoiceDeafen_NotInVoice2(), TestHandleVoiceMute_BadPayload(), TestHandleVoiceMute_NotInVoice2() (+116 more)
+Cohesion: 0.05
+Nodes (158): TestBuildReady_IncludesCanSend(), TestBuildReady_CarriesChannelFeatureFlags(), TestBuildDMChannelOpen_NilAvatar(), TestBuildDMChannelOpen_NilRecipient(), TestBuildDMChannelOpen_ValidRecipient(), TestHandleVoiceCamera_BadPayload(), TestHandleVoiceCamera_NotInVoice2(), TestHandleVoiceDeafen_BadPayload() (+150 more)
### Community 12 - "types.ts"
Cohesion: 0.02
-Nodes (101): SearchOverlayOptions, ApiClientConfig, createApiClient(), log, OnUnauthorized, SessionInfo, SessionsListResponse, ensureHttpProxy() (+93 more)
+Nodes (116): createGifPicker(), GIF_UNAVAILABLE_MESSAGE, GifPickerOptions, MessageInputOptions, SearchOverlayOptions, enableRovingNavigation(), setRovingTabindex(), ApiClientConfig (+108 more)
### Community 13 - "NewAdminAPI"
-Cohesion: 0.08
-Nodes (108): TestAdminAPI_AuditLog_InvalidLimitParam(), TestAdminAPI_AuditLog_Pagination(), TestAdminAPI_CheckUpdate_NilUpdater(), TestAdminAPI_CreateChannel_DefaultsTypeToText(), TestAdminAPI_CreateChannel_InvalidBody(), TestAdminAPI_DeleteChannel_InvalidID(), TestAdminAPI_ForceLogout_InvalidID(), TestAdminAPI_ListUsers_CapLargeLimit() (+100 more)
+Cohesion: 0.09
+Nodes (99): TestAdminAPI_AuditLog_InvalidLimitParam(), TestAdminAPI_AuditLog_Pagination(), TestAdminAPI_CheckUpdate_NilUpdater(), TestAdminAPI_CreateChannel_DefaultsTypeToText(), TestAdminAPI_CreateChannel_InvalidBody(), TestAdminAPI_DeleteChannel_InvalidID(), TestAdminAPI_ForceLogout_InvalidID(), TestAdminAPI_ListUsers_CapLargeLimit() (+91 more)
### Community 14 - "Fixed"
Cohesion: 0.01
-Nodes (140): Fixed, OC-0002 — high — A dead E2EE worker is invisible; the Secured badge cannot detect it, OC-0004 — medium — Key-holder promotion silently no-ops when the client's own voice_state has not arrived, OC-0005 — medium — Rotation offers exceed the server rate limit in large channels, permanently starving the same peers, OC-0006 — medium — Both rotation paths call keyProvider.setKey with no session-generation guard, OC-0007 — medium — Reconnect reaches the Secured state without confirming the room key is current, OC-0008 — medium — restoreLocalVoiceState has no internal supersession guard, OC-0009 — low — attemptAutoReconnect's tail has no supersession checkpoints after connected (+132 more)
+Nodes (188): Fixed, OC-0001 — high — Wrapped room keys have no freshness binding, so old offers replay forever, OC-0002 — high — A dead E2EE worker is invisible; the Secured badge cannot detect it, OC-0003 — high — Unverified peers get no safety number, removing TOFU's only out-of-band escape hatch, OC-0004 — medium — Key-holder promotion silently no-ops when the client's own voice_state has not arrived, OC-0005 — medium — Rotation offers exceed the server rate limit in large channels, permanently starving the same peers, OC-0006 — medium — Both rotation paths call keyProvider.setKey with no session-generation guard, OC-0007 — medium — Reconnect reaches the Secured state without confirming the room key is current (+180 more)
### Community 15 - "messages_test.go"
-Cohesion: 0.05
-Nodes (67): buildAuthError(), buildChannelCreate(), buildChannelDelete(), buildChannelUpdate(), buildChatBulkDeleted(), buildChatDeleted(), buildChatEdited(), buildChatSendOK() (+59 more)
+Cohesion: 0.06
+Nodes (53): buildAuthError(), buildChannelCreate(), buildChannelUpdate(), buildChatBulkDeleted(), buildChatDeleted(), buildChatEdited(), buildMemberBan(), buildMemberJoin() (+45 more)
-### Community 16 - "main.ts"
-Cohesion: 0.03
-Nodes (68): createUserUpdateCredentialSaver(), deleteCredential(), getInvoke(), loadCredential(), log, saveCredential(), SavedCredential, jumpToMessage() (+60 more)
+### Community 16 - "ConnectPage.ts"
+Cohesion: 0.07
+Nodes (27): createUserUpdateCredentialSaver(), deleteCredential(), getInvoke(), loadCredential(), log, saveCredential(), SavedCredential, renderPage() (+19 more)
-### Community 17 - "net/http.HandlerFunc"
+### Community 17 - "writeErr"
Cohesion: 0.09
-Nodes (64): createChannelRequest, createTokenRequest, createTokenResponse, errorResponse, HubBroadcaster, memberUnbanBroadcaster, patchUserRequest, PermissionInvalidator (+56 more)
+Nodes (62): createChannelRequest, createTokenRequest, createTokenResponse, errorResponse, HubBroadcaster, PermissionInvalidator, putChannelPermissionRequest, reorderRolesRequest (+54 more)
### Community 18 - "NewTestClient"
Cohesion: 0.06
-Nodes (82): NewTestClient(), SetClientLastActivityForTest(), TestChatCommand_MalformedPayload_ReturnsBadRequest(), TestChatCommand_NoRegistry_ReturnsError(), TestChatCommand_RateLimited_ReturnsError(), TestChatCommand_UnknownCommand_ReturnsError(), TestEventSink_Emit_NilSink_NoOp(), awaitMessage() (+74 more)
+Nodes (84): NewTestClient(), TestChatCommand_MalformedPayload_ReturnsBadRequest(), TestChatCommand_NoRegistry_ReturnsError(), TestChatCommand_RateLimited_ReturnsError(), TestChatCommand_UnknownCommand_ReturnsError(), TestEventSink_Emit_NilSink_NoOp(), TestHub_SetPluginEventSink_NoOp(), awaitMessage() (+76 more)
### Community 19 - "newHandlerHub"
Cohesion: 0.08
-Nodes (90): channelFocusMsg(), denyReadOnChannel(), TestChannelFocus_AdminBypassesDeny(), TestChannelFocus_AllowedByDefault(), TestChannelFocus_DeniedByOverride(), TestChatSend_DeniedWithoutSendMessages(), ClientChannelIDForTest(), NewTestClientWithTokenHash() (+82 more)
+Nodes (91): channelFocusMsg(), denyReadOnChannel(), TestChannelFocus_AdminBypassesDeny(), TestChannelFocus_AllowedByDefault(), TestChannelFocus_DeniedByOverride(), TestChatSend_DeniedWithoutSendMessages(), ClientChannelIDForTest(), NewTestClientWithTokenHash() (+83 more)
### Community 20 - "livekitE2EE.ts"
-Cohesion: 0.11
-Nodes (28): closeIdentityModal(), openIdentityMismatchModal(), ANNOUNCE_DOMAIN, base64ToUint8(), buildAnnounceMessage(), computeKeyFingerprint(), deriveWrappingKey(), generateECDHKeyPair() (+20 more)
+Cohesion: 0.06
+Nodes (55): ANNOUNCE_DOMAIN, base64ToUint8(), buildAnnounceMessage(), computeKeyFingerprint(), computeRawKeyFingerprint(), deriveWrappingKey(), encodeOfferEpoch(), exportIdentityKeyPair() (+47 more)
### Community 21 - "drainChanTimeout"
-Cohesion: 0.10
-Nodes (87): drainChanTimeout(), TestWebhook_ParticipantLeft_SurvivesCancelledRequestContext(), captureLogs(), participantIdentityFor(), roomNameFor(), TestWebhook_ParticipantJoined_MalformedInput(), TestWebhook_ParticipantJoined_NilFieldsIgnored(), TestWebhook_ParticipantJoined_RogueParticipantFlagged() (+79 more)
+Cohesion: 0.09
+Nodes (93): drainChanTimeout(), TestWebhook_ParticipantLeft_SurvivesCancelledRequestContext(), TestWebhookHandler_SignedParticipantLeftDispatches(), captureLogs(), participantIdentityFor(), roomNameFor(), TestWebhook_ParticipantJoined_MalformedInput(), TestWebhook_ParticipantJoined_NilFieldsIgnored() (+85 more)
### Community 22 - "buildDMRouter"
Cohesion: 0.08
Nodes (74): cancelAfterArm, cancelOnLookupStore, evictCall, mockBroadcaster, mockBroadcastMsg, watermarkVoiceBroadcaster, decodeBlockedIDs(), dmPut() (+66 more)
### Community 23 - "tofu.rs"
-Cohesion: 0.06
-Nodes (54): CapturedFingerprint, CertificateDer, CaptureVerifier, cert_store_key(), decide(), default_verify_schemes(), evaluate(), extract_host() (+46 more)
+Cohesion: 0.11
+Nodes (25): CapturedFingerprint, CaptureVerifier, cert_store_key(), decide(), evaluate(), extract_host(), host_scoped(), host_scoped_default_rejection_propagates() (+17 more)
### Community 24 - "newAuthTestDB"
-Cohesion: 0.08
-Nodes (82): recordingAuthBroadcaster, TestDeleteAccount_BroadcastsMemberBan(), TestDeleteAccount_NoBroadcasterOmitted(), buildAuthRouter(), buildAuthRouterWithProxies(), contains(), containsStr(), deleteJSONWithToken() (+74 more)
+Cohesion: 0.09
+Nodes (80): buildAuthRouter(), buildAuthRouterWithProxies(), contains(), containsStr(), deleteJSONWithToken(), expiredInviteDB(), newAuthTestDB(), postJSON() (+72 more)
### Community 25 - "newMigratedTestDB"
Cohesion: 0.06
-Nodes (71): TestBlockUser_And_IsBlocked(), TestBlockUser_Idempotent(), TestBlockUser_SelfBlockIsSilentlyDropped(), TestIsEitherBlocked(), TestListBlockedUsers(), TestUnblockUser(), TestUnblockUser_NotBlockedIsNoOp(), seedEmojiUploader() (+63 more)
+Nodes (72): TestBlockUser_And_IsBlocked(), TestBlockUser_Idempotent(), TestBlockUser_SelfBlockIsSilentlyDropped(), TestIsEitherBlocked(), TestListBlockedUsers(), TestUnblockUser(), TestUnblockUser_NotBlockedIsNoOp(), seedEmojiUploader() (+64 more)
-### Community 26 - "time.Time"
+### Community 26 - "EventPersister"
Cohesion: 0.04
-Nodes (26): touchThrottle, failingLockoutStore, PluginRow, rowScanner, rowsScanner, Event, GetEventsSinceParams, GetEventsSinceRow (+18 more)
+Nodes (28): PluginRow, rowScanner, rowsScanner, sync/atomic.Bool, DB, parseSQLiteTime(), scanEventRows(), PersistedEvent (+20 more)
### Community 27 - "Config"
-Cohesion: 0.11
-Nodes (22): BackupConfig, DatabaseConfig, EventPersistenceConfig, LoggingConfig, PluginsConfig, SecurityConfig, ServerConfig, UploadConfig (+14 more)
+Cohesion: 0.09
+Nodes (27): BackupConfig, DatabaseConfig, EventPersistenceConfig, LoggingConfig, PluginsConfig, SecurityConfig, ServerConfig, UploadConfig (+19 more)
### Community 28 - "secret_store.rs"
Cohesion: 0.07
-Nodes (61): credential_lock_serializes_overlapping_commands(), CredentialData, CredentialStoreProbe, delete_credential(), delete_identity_key(), identity_account(), load_credential(), load_identity_key() (+53 more)
-
-### Community 29 - "User"
-Cohesion: 0.04
-Nodes (36): User, TestBuildDMChannelOpen_NilAvatar(), TestBuildDMChannelOpen_NilRecipient(), TestBuildDMChannelOpen_ValidRecipient(), TestQualityBitrate_EmptyFallsBackToMedium(), TestQualityBitrate_KnownPresets(), TestQualityBitrate_UnknownFallsBackToMedium(), TestBuildChatSendOK_ValidJSON() (+28 more)
+Nodes (65): credential_lock_serializes_overlapping_commands(), CredentialData, CredentialStoreProbe, delete_credential(), delete_identity_key(), identity_account(), load_credential(), load_identity_key() (+57 more)
### Community 30 - "database/sql.Result"
Cohesion: 0.04
-Nodes (34): ApplyVoiceServerDeafenParams, ApplyVoiceServerMuteParams, ClearVoiceServerDeafenParams, ClearVoiceServerMuteParams, CreateAPITokenParams, DeleteOtherSessionsParams, DeleteSessionByIDParams, EnableCameraIfUnderLimitParams (+26 more)
+Nodes (30): ApplyVoiceServerDeafenParams, ApplyVoiceServerMuteParams, ClearVoiceServerDeafenParams, ClearVoiceServerMuteParams, CreateAPITokenParams, DeleteOtherSessionsParams, DeleteSessionByIDParams, EnableCameraIfUnderLimitParams (+22 more)
-### Community 31 - "newUploadTestDB"
-Cohesion: 0.13
-Nodes (64): io.Closer, io.Seeker, buildAvatarRouter(), doAvatarUpload(), TestUploadAvatar_IsReadableByOtherUsersWhileInUse(), TestUploadAvatar_NotMountedWithoutStorage(), TestUploadAvatar_RejectsNonImageAndOversizedDimensions(), TestUploadAvatar_RequiresAuthAndAFile() (+56 more)
+### Community 31 - "net/http.Handler"
+Cohesion: 0.07
+Nodes (94): TLSResult, crypto/tls.Config, go.opentelemetry.io/otel/sdk/metric.MeterProvider, go.opentelemetry.io/otel/sdk/trace.TracerProvider, io.Closer, io.Seeker, net/http.Handler, buildAvatarRouter() (+86 more)
-### Community 32 - "MainPage.ts"
+### Community 32 - "channels.store.ts"
Cohesion: 0.02
-Nodes (134): closeActiveLightbox(), QuickSwitcherOptions, QuickSwitchProfile, applyConnectionStatus(), createServerBanner(), ServerBannerControl, ToastContainer, ToastType (+126 more)
+Nodes (150): QuickSwitcherOptions, QuickSwitchProfile, createVoiceWidget(), formatElapsed(), QUALITY_BARS, QUALITY_COLORS, STATUS_LABELS, navigateToChannel() (+142 more)
-### Community 33 - "writeJSON"
-Cohesion: 0.09
-Nodes (65): changePasswordRequest, createDMRequest, createGroupDMRequest, createInviteRequest, dmVisibilityMarker, dmVoiceEvictor, inviteResponse, ProfileBroadcaster (+57 more)
+### Community 33 - "net/http.HandlerFunc"
+Cohesion: 0.07
+Nodes (83): changePasswordRequest, createDMRequest, createGroupDMRequest, createInviteRequest, dmVisibilityMarker, dmVoiceEvictor, EmojiBroadcaster, emojiResponse (+75 more)
### Community 34 - "newAdminTestDB"
-Cohesion: 0.05
-Nodes (61): fakeAuditor, slowAuditStore, newAdminTestDB(), TestAdminCreateChannel(), TestAdminCreateChannel_DefaultsNotNSFW(), TestAdminCreateChannel_EmptyOptionals(), TestAdminDeleteChannel(), TestAdminDeleteChannel_NonExistent() (+53 more)
+Cohesion: 0.06
+Nodes (60): slowAuditStore, newAdminTestDB(), TestAdminCreateChannel(), TestAdminCreateChannel_DefaultsNotNSFW(), TestAdminCreateChannel_EmptyOptionals(), TestAdminDeleteChannel(), TestAdminDeleteChannel_NonExistent(), TestAdminUpdateChannel() (+52 more)
### Community 35 - "HashToken"
-Cohesion: 0.11
-Nodes (48): TestNewRouter_LiveKitProcessStartFailure_VoiceJoinFailsClosed(), voiceJoinWSMsg(), GenerateToken(), HashToken(), TestGenerateToken_HexCharacters(), TestGenerateToken_Length(), TestGenerateToken_MultiDeviceUniqueness(), TestGenerateToken_Uniqueness() (+40 more)
+Cohesion: 0.10
+Nodes (52): recordingAuthBroadcaster, TestDeleteAccount_BroadcastsMemberBan(), TestDeleteAccount_NoBroadcasterOmitted(), TestNewRouter_LiveKitProcessStartFailure_VoiceJoinFailsClosed(), voiceJoinWSMsg(), GenerateToken(), HashToken(), TestGenerateToken_HexCharacters() (+44 more)
-### Community 36 - "content-parser.ts"
-Cohesion: 0.02
-Nodes (128): baseMime(), isAudioMime(), isVideoMime(), setServerHost(), appendBlocks(), appendInline(), buildChannelNode(), buildList() (+120 more)
+### Community 36 - "MessageList.ts"
+Cohesion: 0.04
+Nodes (52): renderMentions(), CLOCK_TIME_FORMAT, formatFullDate(), formatMessageTimestamp(), formatTime(), FULL_DATE_FORMAT, getUserRole(), GROUP_THRESHOLD_MS (+44 more)
### Community 37 - "ChannelSidebar.ts"
-Cohesion: 0.04
-Nodes (89): attachChannelContextMenu(), CHANNEL_MUTE_CHANGED, attachDragHandlers(), DragState, ensureGlobalDragListeners(), listenerOwners, releaseOwner(), retargetDetachedDrag() (+81 more)
+Cohesion: 0.03
+Nodes (107): attachChannelContextMenu(), CHANNEL_MUTE_CHANGED, attachDragHandlers(), DragState, ensureGlobalDragListeners(), listenerOwners, releaseOwner(), retargetDetachedDrag() (+99 more)
### Community 38 - "plugin/registry_test.go"
-Cohesion: 0.19
-Nodes (27): buildZip(), Registry, newRegistryWithDir(), simpleManifest(), TestRegistry_Activate_AfterClose(), TestRegistry_Activate_WithoutRuntime(), TestRegistry_DisablePlugin_ClearsFlagAndCommands(), TestRegistry_DisablePlugin_UnknownIDIsNoOp() (+19 more)
+Cohesion: 0.05
+Nodes (74): Config, foundPlugin, TestEventDeliveryHasNoGuestPath(), TestManifestCommandsValidation(), TestRegisterCommandRequiresManifestDeclaration(), TestStorageKeysIsolatedPerPlugin(), TestStorageRejectsOversizedKeyAndValue(), openPluginTestDB() (+66 more)
-### Community 39 - "DB"
-Cohesion: 0.06
-Nodes (63): backupFile, roleDeletingInvalidator, AuthBroadcaster, authSuccessResponse, deleteAccountRequest, loginRequest, passwordConfirmationRequest, registerRequest (+55 more)
+### Community 39 - "newDeafenRaceDB"
+Cohesion: 0.73
+Nodes (5): mustCreateDeafenRaceChannel(), newDeafenRaceDB(), seedDeafenRaceUser(), TestVoiceModDeafen_RollbackFollowsTargetChannelMove(), TestVoiceModDeafen_UndeafenRollbackDoesNotApplyOnUnauthorizedChannel()
-### Community 40 - "Instance"
-Cohesion: 0.09
-Nodes (14): github.com/tetratelabs/wazero/api.Memory, github.com/tetratelabs/wazero/api.Module, CommandResult, Instance, Registry, Registry, Registry, Manifest (+6 more)
+### Community 40 - "Registry"
+Cohesion: 0.05
+Nodes (31): matchRecorder, archive/zip.File, archive/zip.Reader, github.com/tetratelabs/wazero/api.Memory, github.com/tetratelabs/wazero/api.Module, sync.Mutex, sync.RWMutex, Broadcaster (+23 more)
### Community 41 - "livekit_test.go"
Cohesion: 0.06
-Nodes (52): google.golang.org/protobuf/proto.Message, TestWebhookParseIdentity_Invalid(), TestWebhookParseIdentity_Valid(), TestWebhookParseRoomChannelID_Invalid(), TestWebhookParseRoomChannelID_Valid(), ParseIdentityForTest(), ParseParticipantIdentityForTest(), ParseRoomChannelIDForTest() (+44 more)
+Nodes (55): google.golang.org/protobuf/proto.Message, TestWebhookParseIdentity_Invalid(), TestWebhookParseIdentity_Valid(), TestWebhookParseRoomChannelID_Invalid(), TestWebhookParseRoomChannelID_Valid(), ParseIdentityForTest(), ParseParticipantIdentityForTest(), ParseRoomChannelIDForTest() (+47 more)
### Community 42 - "NewChecker"
-Cohesion: 0.20
-Nodes (47): NewChecker(), TestHandleChannelFocus_SkipsNoOpReadStateWrite(), NewChannelService(), TestHandlePresenceUpdate_CustomStatusWriteFailureSwallowedAfterStatusCommit(), TestHandleChannelFocus_DMExemptFromArchiveGate(), TestHandleChannelFocus_RefusedInArchivedChannel(), TestHandleTyping_BlockedInDMEmitsNothing(), TestHandleTyping_NoRateLimitKeyForNonexistentChannel() (+39 more)
+Cohesion: 0.13
+Nodes (56): NewChecker(), TestHandleChannelFocus_SkipsNoOpReadStateWrite(), NewChannelService(), Store, TestHandlePresenceUpdate_BareStatusReadFailureAbortsBeforeCommit(), TestHandlePresenceUpdate_CustomStatusWriteFailureKeepsStoredText(), TestHandlePresenceUpdate_CustomStatusWriteFailureSwallowedAfterStatusCommit(), TestHandleChannelFocus_DMExemptFromArchiveGate() (+48 more)
### Community 43 - "middleware_test.go"
-Cohesion: 0.09
-Nodes (50): SecurityHeaders(), AdminIPRestrict(), AuthMiddleware(), MaxBodySize(), RateLimitMiddleware(), RequirePermission(), SecurityHeadersWithTLS(), newAPITestDB() (+42 more)
+Cohesion: 0.08
+Nodes (57): contextKey, errorResponse, SecurityHeaders(), TestRateLimitMiddlewareWithPrefix_SeparatesClientUpdateBucket(), TestRateLimitMiddlewareWithPrefix_SeparatesLiveKitBucket(), AdminIPRestrict(), AuthMiddleware(), MaxBodySize() (+49 more)
### Community 44 - "authStore"
-Cohesion: 0.03
-Nodes (86): createDmProfileSidebar(), DmProfileData, DmProfileSidebarComponent, DmProfileSidebarOptions, legacyNoteKey(), loadNote(), saveNote(), scopedNoteKey() (+78 more)
+Cohesion: 0.04
+Nodes (78): StatusOption, colorForStatus(), createStatusPicker(), STATUS_DEFS, StatusDef, StatusPickerComponent, StatusPickerOptions, createUserBar() (+70 more)
### Community 45 - "DB"
-Cohesion: 0.05
-Nodes (27): APITokenListItem, ChannelUnread, MessageWithUser, ReactionCount, ReactionInfo, ServerStats, UserPublic, context.CancelFunc (+19 more)
+Cohesion: 0.07
+Nodes (20): MessageWithUser, ReactionCount, ReactionInfo, UserPublic, ReactionUser, Message, DB, Message (+12 more)
### Community 46 - "testing.F"
-Cohesion: 0.08
-Nodes (20): fuzzTestHelper, github.com/livekit/protocol/livekit.WebhookEvent, testing.F, FuzzValidateAvatarURL(), FuzzValidateDisplayName(), FuzzSanitizeUploadFilename(), FuzzValidateUsername(), fuzzOpenMigratedMemory() (+12 more)
+Cohesion: 0.06
+Nodes (33): fuzzTestHelper, testing.F, Capability, CommandSpec, Resources, UISpec, UITab, fuzzGIFBytes() (+25 more)
### Community 47 - "Result"
-Cohesion: 0.11
-Nodes (46): VoiceState, requirePerm(), Event, BuildCallSignalForTest(), handleCallDeclineV2(), handleCallRingV2(), dmEventOrFallback(), TestDMEventOrFallback() (+38 more)
+Cohesion: 0.12
+Nodes (38): Key(), requirePerm(), Event, BuildCallSignalForTest(), TestPingV2_HappyPath_ReturnsPongReply(), TestPingV2_NoEvents(), TestPingV2_RateLimited_ReturnsEmpty(), handleCallDeclineV2() (+30 more)
### Community 48 - "livekit_proxy.rs"
Cohesion: 0.06
@@ -689,36 +699,36 @@ Cohesion: 0.09
Nodes (35): CDP_PORT, cleanupUserDataDir(), createUserDataDir(), __dirname, __filename, NativeFixtures, acquirePersistentPage(), CDP_PORT (+27 more)
### Community 50 - "NewRouter"
-Cohesion: 0.06
-Nodes (44): clientDiag, diagnosticsResponse, EventPersisterMetrics, healthDeps, healthResponse, infoResponse, livekitHealthResponse, MetricsSources (+36 more)
-
-### Community 51 - "net/http.Handler"
-Cohesion: 0.11
-Nodes (50): identityKeyFailStore, net/http.Handler, net/http/httptest.ResponseRecorder, doRequestRaw(), getWithToken(), TestUpdateProfile_BroadcastCarriesEveryProfileField(), TestUpdateProfile_RejectsBadDisplayName(), TestUpdateProfile_SetsDisplayNameAndAbout() (+42 more)
-
-### Community 52 - "totp_test.go"
Cohesion: 0.08
-Nodes (37): PartialAuthChallenge, pendingTOTPEnrollment, BuildTOTPURI(), generateOpaqueToken(), PartialAuthStore, PendingTOTPStore, UsedTOTPCodeStore, NewPartialAuthStore() (+29 more)
+Nodes (40): healthDeps, healthResponse, infoResponse, livekitHealthResponse, TestBodyCapExemptions_RouteEnvelopesReachable(), TestHandleHealth_CanceledRequestDoesNotPoisonCache(), TestHandleHealth_ChecksAreCached(), TestHandleHealth_DegradedReturns503WithReason() (+32 more)
+
+### Community 51 - "profileCreateToken"
+Cohesion: 0.11
+Nodes (49): identityKeyFailStore, getWithToken(), TestUpdateProfile_BroadcastCarriesEveryProfileField(), TestUpdateProfile_RejectsBadDisplayName(), TestUpdateProfile_SetsDisplayNameAndAbout(), TestChangePassword_MalformedBody(), TestChangePassword_MissingNewPassword(), TestChangePassword_MissingOldPassword() (+41 more)
+
+### Community 52 - "RateLimiter"
+Cohesion: 0.04
+Nodes (84): AuthBroadcaster, authSuccessResponse, deleteAccountRequest, loginRequest, passwordConfirmationRequest, registerRequest, totpConfirmationRequest, totpEnableResponse (+76 more)
### Community 53 - "newTestDB"
Cohesion: 0.04
Nodes (79): newTestDB(), TestBanUser_Permanent(), TestBanUser_Temporary(), TestCreateInvite_Success(), TestCreateInvite_UnlimitedUses(), TestCreateSession_Success(), TestCreateUser_CaseInsensitiveDuplicate(), TestCreateUser_DuplicateUsername() (+71 more)
-### Community 54 - "createLogger"
-Cohesion: 0.07
-Nodes (40): attachReactionTooltip(), buildReactionTooltip(), cache, cacheKey(), chipSetFor(), clearReactionUsersCache(), formatReactorNames(), getCachedReactionUsers() (+32 more)
+### Community 54 - "reaction-tooltip.ts"
+Cohesion: 0.13
+Nodes (20): attachReactionTooltip(), cache, cacheKey(), chipSetFor(), formatReactorNames(), getCachedReactionUsers(), hide(), hoveringChips (+12 more)
### Community 55 - "newServeHub"
Cohesion: 0.09
-Nodes (45): ParseChannelIDForTest(), TestBuildReady_IncludesDMVoiceStates(), TestBuildReady_PropagatesDMChannelsError(), TestBuildReady_PropagatesListMembersError(), TestBuildReady_PropagatesUnreadCountsError(), TestBuildReady_IncludesOwnVoiceStateAfterDMClosed(), newServeHub(), ownerRole() (+37 more)
+Nodes (47): ParseChannelIDForTest(), dmChannelStatusFor(), TestBuildReady_DMChannelsHidesDisconnectedRecipientStatus(), TestBuildReady_IncludesDMVoiceStates(), TestBuildReady_PropagatesDMChannelsError(), TestBuildReady_PropagatesListMembersError(), TestBuildReady_PropagatesUnreadCountsError(), TestBuildReady_IncludesOwnVoiceStateAfterDMClosed() (+39 more)
-### Community 56 - "OwnCord — Comprehensive Project Audit"
-Cohesion: 0.04
-Nodes (48): 1. Architecture, 3. Security, 4. Dependencies & Supply Chain, 5. Test Coverage & Quality, 6. CI/CD & DevEx, 7. Observability, 8. Plugin System Governance, 9. Prioritized Top-10 Action List (+40 more)
+### Community 56 - "3. Security"
+Cohesion: 0.20
+Nodes (10): 3. Security, Authentication & Authorization, Input Validation, Observations (not blocking), Overall Posture: **GOOD** (no critical issues in core app security), Rate Limiting, Secrets & Configuration, SQL Injection (+2 more)
### Community 57 - "permissions_test.go"
-Cohesion: 0.06
-Nodes (54): overrideMatrixBits(), permGridBits(), TestAdminPanelOverrideMatrixCoversChannelScopedBits(), TestAdminPanelOverrideMatrixHasSingleDefinedBits(), TestAdminPanelPermGridCoversEveryPermissionBit(), TestAdminPanelPermGridHasNoDuplicateOrCompositeBits(), EffectiveChannelPerms(), EffectivePerms() (+46 more)
+Cohesion: 0.09
+Nodes (39): EffectivePerms(), HasAdmin(), HasAnyPerm(), HasPerm(), HasServerPerm(), TestAdminPerimeter_Membership(), TestEffectivePerms_AllowAddsPermission(), TestEffectivePerms_AllowAndDenyTogether() (+31 more)
### Community 58 - "seedMemberUser"
Cohesion: 0.17
@@ -726,51 +736,51 @@ Nodes (47): callMsg(), seedGroupDM(), TestCallDecline_ForwardsToOtherParticipant
### Community 59 - "postJSONWithToken"
Cohesion: 0.12
-Nodes (46): postJSONWithToken(), buildCombinedRouter(), TestCombinedRouter_ProfileAndInvites(), TestCreateInvite_MalformedJSON(), TestCreateInvite_WithExpiration(), TestEnableTOTP_AlreadyEnabled(), TestListInvites_MemberForbidden(), TestRevokeInvite_AlreadyRevoked() (+38 more)
+Nodes (47): postJSONWithToken(), buildCombinedRouter(), TestCombinedRouter_ProfileAndInvites(), TestCreateInvite_MalformedJSON(), TestCreateInvite_WithExpiration(), TestEnableTOTP_AlreadyEnabled(), TestListInvites_MemberForbidden(), TestRevokeInvite_AlreadyRevoked() (+39 more)
### Community 60 - "http_proxy.rs"
Cohesion: 0.08
Nodes (39): A, B, copy_with_deadline(), copy_with_deadline_reclaims_a_stalled_connection(), handle_connection(), HttpProxyState, ProxyEntry, remove_if_port_matches_removes_only_matching_entry() (+31 more)
### Community 61 - "newVoiceTestDB"
-Cohesion: 0.15
-Nodes (44): TestVoice_CountActiveCameras_SomeCameras(), TestVoice_CountActiveCameras_Zero(), TestVoice_EnableCameraIfUnderLimit_AtLimit(), TestVoice_EnableCameraIfUnderLimit_Success(), TestVoice_GetAllVoiceStates_MultipleChannels(), TestVoice_JoinVoiceChannelIfCapacity_AtLimit(), TestVoice_JoinVoiceChannelIfCapacity_ReplacesOwnState(), TestVoice_JoinVoiceChannelIfCapacity_UnderLimit() (+36 more)
+Cohesion: 0.13
+Nodes (48): TestVoice_CountActiveCameras_SomeCameras(), TestVoice_CountActiveCameras_Zero(), TestVoice_EnableCameraIfUnderLimit_AtLimit(), TestVoice_EnableCameraIfUnderLimit_Success(), TestVoice_GetAllVoiceStates_MultipleChannels(), TestVoice_JoinVoiceChannelIfCapacity_AtLimit(), TestVoice_JoinVoiceChannelIfCapacity_ReplacesOwnState(), TestVoice_JoinVoiceChannelIfCapacity_UnderLimit() (+40 more)
### Community 62 - "messages.go"
-Cohesion: 0.05
-Nodes (49): userUpdateSpy, encoding/json.RawMessage, parseCallChannelID(), BuildDMChannelOpenInfoForTest(), buildChatMessage(), buildDMChannelOpen(), buildDMChannelOpenFor(), buildUserUpdate() (+41 more)
+Cohesion: 0.04
+Nodes (62): userUpdateSpy, BuildDMChannelOpenInfoForTest(), buildChannelCreateFor(), buildChannelDelete(), buildChatMessage(), buildChatSendOK(), buildDMChannelOpen(), buildDMChannelOpenFor() (+54 more)
### Community 63 - "dbgen/models.go"
Cohesion: 0.05
Nodes (27): Attachment, AuditLog, Channel, ChannelOverride, ChannelUserOverride, DmOpenState, DmParticipant, Emoji (+19 more)
### Community 65 - "README.md"
-Cohesion: 0.09
-Nodes (17): Client Architecture (Tauri), D7 — Module map, Key mechanisms, Quality tooling, D2 — Package map, D3 — REST request lifecycle, Server Architecture, D1 — System context and trust boundaries (+9 more)
+Cohesion: 0.11
+Nodes (9): D2 — Package map, D3 — REST request lifecycle, Server Architecture, D1 — System context and trust boundaries, D8 — Deployment topology, System Overview, D6 — Voice join + E2EE key exchange, Voice and End-to-End Encryption (+1 more)
### Community 66 - "devDependencies"
Cohesion: 0.06
Nodes (35): devDependencies, eslint, @eslint/js, fast-check, jsdom, knip, oxlint, @playwright/test (+27 more)
-### Community 67 - "itoa"
-Cohesion: 0.11
-Nodes (37): itoa(), TestDeleteChannelPermission_RefusesEqualOrHigherRole(), TestPutChannelPermission_ModeratorCannotEscalate(), TestPutChannelPermission_RefusesEqualOrHigherRole(), seedOverrideTarget(), TestChannelUserPermission_NonAdminForbidden(), TestDeleteChannelUserPermission_ClearsOverride(), TestPutChannelUserPermission_AdministratorCanGrantAnyBit() (+29 more)
+### Community 67 - "doRequest"
+Cohesion: 0.10
+Nodes (38): doRequest(), TestPutChannelUserPermission_CannotTargetHigherRankedUser(), TestAdminAPI_PatchUser_RefusedRoleChangeDoesNotLeaveBanCommitted(), TestAdminAPI_LogStreamTicketFlow_APIToken(), createRoleUser(), newModeratorHandler(), TestAuditAndSettings_BitHoldersAllowed(), TestAuditAndSettings_ModeratorForbidden() (+30 more)
### Community 68 - "helpers_test.go"
Cohesion: 0.09
Nodes (39): ExtractBearerToken(), IsEffectivelyBanned(), IsSessionExpired(), TestExtractBearerToken_BearerCaseInsensitive(), TestExtractBearerToken_BearerWithNoToken(), TestExtractBearerToken_EmptyHeaderValue(), TestExtractBearerToken_MissingHeader(), TestExtractBearerToken_MultipleSpaces() (+31 more)
-### Community 69 - "ChannelService"
-Cohesion: 0.10
-Nodes (12): ReactionUser, channelRefs(), ChannelService, Store, permOverrides(), MessageService, Store, requireChannelWritable() (+4 more)
+### Community 69 - "profiles.ts"
+Cohesion: 0.06
+Nodes (30): ensureHttpProxy(), log, pending, stopHttpProxy(), CreateProfileData, createProfileManager(), createTauriBackend(), FetchFn (+22 more)
### Community 70 - "Security Policy"
Cohesion: 0.14
Nodes (14): Account Deletion, Audit Logging, Client Security Hardening, Credential Storage, Input Validation, Known Limitations, Reporting Vulnerabilities, Search and Rate Limiting (+6 more)
### Community 71 - "Role"
-Cohesion: 0.08
-Nodes (21): sync.RWMutex, Role, Store, ModerationService, Store, NewModerationService(), PermissionService, Store (+13 more)
+Cohesion: 0.11
+Nodes (17): sync/atomic.Int64, Role, Name(), TestMentionEveryone_BitIsFreeAndNamed(), TestName_KnownAndUnknownBits(), ModerationService, RoleInput, RoleService (+9 more)
### Community 72 - "channels.sql.go"
Cohesion: 0.08
@@ -784,45 +794,41 @@ Nodes (39): Admin Backup Endpoint, Auto-Update, Background Maintenance, Backup S
Cohesion: 0.08
Nodes (45): net/url.URL, LiveKitHealthHandlerForTest(), copyWS(), isOriginAllowed(), isWebSocketUpgrade(), NewLiveKitProxy(), proxyWebSocket(), TestIsOriginAllowed_CaseInsensitive() (+37 more)
-### Community 75 - "newEmojiService"
-Cohesion: 0.10
-Nodes (28): recordingEmojiBroadcaster, Emoji, EmojiImageURL(), Store, NewEmojiService(), NormalizeShortcode(), newEmojiService(), TestEmojiCreate_DuplicateShortcodeIsConflict() (+20 more)
+### Community 75 - "Emoji"
+Cohesion: 0.19
+Nodes (8): recordingEmojiBroadcaster, Emoji, FuzzValidateShortcode(), NormalizeShortcode(), TestValidateShortcode_Accepts(), TestValidateShortcode_Rejects(), ValidateShortcode(), EmojiService
### Community 76 - "ws_proxy.rs"
-Cohesion: 0.12
-Nodes (25): AtomicU64, accept_cert_fingerprint(), clear_sender_if_current(), disconnect_closes_the_outbound_channel(), disconnect_invalidates_an_in_flight_connect_attempt(), emit_cert_tofu(), emit_ws_state(), is_valid_cert_fingerprint() (+17 more)
+Cohesion: 0.06
+Nodes (41): AtomicU64, get_cert_fingerprint(), get_identity_pin(), get_settings(), identity_pin_key(), is_settings_key_allowed(), log_cmd_err(), open_devtools() (+33 more)
### Community 77 - "bughunt.js"
Cohesion: 0.06
Nodes (31): ARGS, BUGCLASS_LENSES, buildAdaptiveLenses(), churnFiles, cleanStreak, clusterOf(), confirmedAll, confirmedSorted (+23 more)
### Community 78 - "openAdminTestDB"
-Cohesion: 0.11
-Nodes (34): TestNewHandler_APIRoutesMounted(), TestNewHandler_AuthProtectedRoute(), TestNewHandler_ReturnsNonNilHandler(), TestNewHandler_ServesStaticRoot(), TestNewHandler_SetsCSPOnRoot(), TestNewHandler_WithUpdater(), TestOwnerOnlyMiddleware_AdminDenied(), TestOwnerOnlyMiddleware_MemberDenied() (+26 more)
+Cohesion: 0.10
+Nodes (43): openAdminTestDB(), TestAdminAPI_PatchRole_NoRenameNoMemberUpdate(), TestAdminAPI_PatchRole_RenameBroadcastsMemberUpdate(), createUserWithRole(), decodeRole(), doRequestRaw(), newRolesHandler(), TestAdminAPI_CreateRole_DuplicateNameIsBadRequest() (+35 more)
-### Community 79 - "db/db.go"
-Cohesion: 0.07
-Nodes (26): dbtx, DBTX, database/sql.DB, database/sql.Row, database/sql.Rows, database/sql.Stmt, database/sql.Tx, database/sql.TxOptions (+18 more)
+### Community 79 - "DB"
+Cohesion: 0.06
+Nodes (46): roleDeletingInvalidator, dbtx, DBTX, database/sql.DB, database/sql.Row, database/sql.Rows, database/sql.Stmt, Queries (+38 more)
### Community 80 - "Tables"
Cohesion: 0.05
Nodes (37): Admin perimeter, api_tokens, attachments, audit_log, Bit Map, channel_overrides, channel_user_overrides, channels (+29 more)
### Community 81 - "newMentionFixture"
-Cohesion: 0.15
-Nodes (34): parseMentionTokens(), MessageService, mentionCount(), newMentionFixture(), sendAs(), TestChannelFocus_ClearsMentionCount(), TestEditMessage_EveryoneGateApplies(), TestEditMessage_ReplacesMentionsWithoutRecounting() (+26 more)
+Cohesion: 0.13
+Nodes (36): FuzzParseMentionTokens(), parseMentionTokens(), MessageService, mentionCount(), newMentionFixture(), sendAs(), TestChannelFocus_ClearsMentionCount(), TestEditMessage_EveryoneGateApplies() (+28 more)
### Community 82 - "Channel"
-Cohesion: 0.07
-Nodes (10): memberUpdateCall, mockHub, mockHubWB, restartCall, unbanMockHub, Channel, buildChannelCreateFor(), channelPayloadFrom() (+2 more)
+Cohesion: 0.04
+Nodes (31): memberUpdateCall, mockHub, mockHubWB, restartCall, context.CancelFunc, DB, Channel, ChannelOverride (+23 more)
-### Community 83 - "NewWAFMiddlewareCRS"
-Cohesion: 0.09
-Nodes (33): matchRecorder, coraza.WAF, github.com/corazawaf/coraza/v3/types.Interruption, github.com/corazawaf/coraza/v3/types.MatchedRule, github.com/corazawaf/coraza/v3/types.Transaction, captureSlog(), TestNewCRSWAF_LoadsCoreRuleSet(), TestNormalizeCRSMode() (+25 more)
-
-### Community 84 - "E2EEManager"
-Cohesion: 0.13
-Nodes (4): generateRoomKey(), E2EEManager, clearPeerVerification(), clearPeerVerifications()
+### Community 83 - "newWAFMiddleware"
+Cohesion: 0.10
+Nodes (39): coraza.WAF, github.com/corazawaf/coraza/v3/types.Interruption, github.com/corazawaf/coraza/v3/types.MatchedRule, github.com/corazawaf/coraza/v3/types.Transaction, captureSlog(), TestNewCRSWAF_LoadsCoreRuleSet(), TestNormalizeCRSMode(), TestWAFMiddleware_CRSBlockMode_AllowsBenignJSONRequest() (+31 more)
### Community 85 - "storage_test.go"
Cohesion: 0.07
@@ -830,79 +836,79 @@ Nodes (46): TestSave_FilesystemFailureIsErrIO(), New(), newTestStorage(), TestDe
### Community 86 - "Migrate"
Cohesion: 0.13
-Nodes (31): failReadFS, TestNewRouterRefusesToStartWithMalformedTOTPKey(), Migrate(), openMemory(), TestBegin(), TestCloseIdempotent(), TestExec(), TestForeignKeysEnabled() (+23 more)
+Nodes (24): failReadFS, Migrate(), TestBegin(), TestCloseIdempotent(), TestExec(), TestForeignKeysEnabled(), TestMigrateCreatesAllTables(), TestMigrateCreatesFTSTable() (+16 more)
### Community 87 - "Hub"
-Cohesion: 0.05
-Nodes (25): AuditStore, pendingAudit, sync/atomic.Bool, sync/atomic.Pointer, sync/atomic.Uint64, sync.Once, AuditWriter, DB (+17 more)
+Cohesion: 0.09
+Nodes (8): sync/atomic.Pointer, Client, LiveKitProcess, Hub, TopicRateLimiter, broadcastMsg, clientEvent, pendingPresence
### Community 88 - "chdirTemp"
-Cohesion: 0.11
-Nodes (31): TestOwnerOnlyMiddleware_OwnerAllowed(), backdate(), listBackupFiles(), mustSetSetting(), TestMaintainBackups_RetentionNeverDeletesNewest(), TestMaintainBackups_ScheduleAndRetention(), CaptureSetupLimiter(), SetBackupBaseDir() (+23 more)
+Cohesion: 0.10
+Nodes (38): CaptureSetupLimiter(), CurrentRestartState(), ForceRestartState(), ResetRestartState(), SetApplyRestartDelay(), SetSetupLimiterReapTiming(), StubCloseError(), StubCopyBackup() (+30 more)
-### Community 89 - "AppearanceTab.ts"
-Cohesion: 0.17
-Nodes (21): buildAppearanceTab(), getDefaultAccent(), hexToRgb(), applyTheme(), applyStoredAppearance(), syncOsMotionListener(), applyThemeByName(), BUILT_IN_THEMES (+13 more)
+### Community 89 - "themes.ts"
+Cohesion: 0.18
+Nodes (18): applyTheme(), ThemeName, THEMES, applyStoredAppearance(), syncOsMotionListener(), applyThemeByName(), BUILT_IN_THEMES, deleteCustomTheme() (+10 more)
### Community 90 - "updater_test.go"
-Cohesion: 0.07
-Nodes (52): TestCheckForUpdate_ErrorCaching(), TestCheckForUpdate_IncludesAssetsList(), TestDownloadFile_NoTokenToExternalHost(), TestDownloadFile_SendsTokenToGitHub(), TestFetchTextAsset_Error(), TestFetchTextAsset_Success(), TestFindClientAssets_ByTarget(), TestFindClientAssets_NilCache() (+44 more)
+Cohesion: 0.09
+Nodes (38): archive/tar.Header, TestDownloadFile_NoTokenToExternalHost(), TestDownloadFile_SendsTokenToGitHub(), TestFetchTextAsset_Error(), TestFetchTextAsset_Success(), TestFindClientAssets_ByTarget(), TestFindClientAssets_NilCache(), TestFindClientAssets_NoMatchingAssets() (+30 more)
### Community 91 - "newTestMessageService"
Cohesion: 0.12
-Nodes (31): seedAroundHistory(), TestGetMessagesAround_ClampsLimit(), TestGetMessagesAround_DeletedCentreIsNotFound(), TestGetMessagesAround_EdgesReportNoMore(), TestGetMessagesAround_ExactFitReportsNoMore(), TestGetMessagesAround_MessageFromAnotherChannelIsNotFound(), TestGetMessagesAround_RejectsBadIDs(), TestGetMessagesAround_SplitsTheLimitAroundTheCentre() (+23 more)
+Nodes (30): seedAroundHistory(), TestGetMessagesAround_ClampsLimit(), TestGetMessagesAround_DeletedCentreIsNotFound(), TestGetMessagesAround_DMNonParticipantIsNotFound(), TestGetMessagesAround_EdgesReportNoMore(), TestGetMessagesAround_ExactFitReportsNoMore(), TestGetMessagesAround_MessageFromAnotherChannelIsNotFound(), TestGetMessagesAround_RejectsBadIDs() (+22 more)
-### Community 92 - "textAssetServer"
-Cohesion: 0.22
-Nodes (10): net/http/httptest.Server, dialAndAuthWS(), TestNewRouter_DeleteAccount_BroadcastsMemberBanOverWS(), TestCheckForUpdateCancelledCallerDoesNotPoisonCache(), TestFetchTextAssetCachedCancelledCallerDoesNotPoisonCache(), TestFetchTextAssetCachedCachesFailures(), TestFetchTextAssetCachedCoalescesConcurrentMisses(), TestFetchTextAssetCachedEvictsExpiredKeys() (+2 more)
+### Community 92 - "newTestUpdater"
+Cohesion: 0.08
+Nodes (32): net/http/httptest.Server, dialAndAuthWS(), TestNewRouter_DeleteAccount_BroadcastsMemberBanOverWS(), TestCheckForUpdateCancelledCallerDoesNotPoisonCache(), TestFetchTextAssetCachedCancelledCallerDoesNotPoisonCache(), TestCheckForUpdate_ErrorCaching(), TestCheckForUpdate_IncludesAssetsList(), serverDownloadAssetName() (+24 more)
### Community 93 - "Hub"
-Cohesion: 0.08
-Nodes (10): TestExtractEventType(), TestExtractEventTypeLengthCap(), Hub, extractEventType(), wrapWithSeq(), buildPresenceMsg(), buildRolesUpdate(), buildServerRestartMsg() (+2 more)
+Cohesion: 0.07
+Nodes (6): TestExtractEventType(), TestExtractEventTypeLengthCap(), Hub, extractEventType(), Hub, wrapWithSeq()
### Community 94 - "compilerOptions"
Cohesion: 0.06
Nodes (32): compilerOptions, esModuleInterop, forceConsistentCasingInFileNames, isolatedModules, lib, module, moduleResolution, noEmit (+24 more)
-### Community 95 - "NewEventRingBuffer"
-Cohesion: 0.20
-Nodes (18): NewEventRingBuffer(), TestConcurrent_PushAndEventsSince(), TestEventsSince_AfterSeqEqualsOldest_ReturnsNil(), TestEventsSince_AfterSeqZero_ReturnsBehavior(), TestEventsSince_AfterSpecificSeq(), TestEventsSince_AheadOfNewestSeq(), TestEventsSince_AtLatestSeq(), TestEventsSince_CapacityBoundaries() (+10 more)
+### Community 95 - "screenShare.ts"
+Cohesion: 0.11
+Nodes (33): attachDiagnosticListeners(), bumpGeneration(), CAMERA_PRESETS, CAMERA_PUBLISH_BITRATES, CameraTrackState, disableCamera(), disableScreenshare(), enableCamera() (+25 more)
-### Community 96 - "HandlerRegistry"
-Cohesion: 0.13
-Nodes (20): registerCallHandlers(), registerPingHandler(), registerPresenceHandlers(), reactionV2Handler(), registerReactionHandlers(), NewHandlerRegistry(), fullV2Registry(), TestAllV2Types_SmokeDispatch() (+12 more)
+### Community 96 - "deps.go"
+Cohesion: 0.09
+Nodes (30): MessageService, Store, hasPerm(), registerCallHandlers(), registerChatHandlers(), registerPingHandler(), registerPresenceHandlers(), reactionV2Handler() (+22 more)
### Community 97 - "emoji_handler_test.go"
Cohesion: 0.17
Nodes (32): emojiHarness, emojiSeedUser(), gifBytes(), jpegBytes(), newEmojiHarness(), pngBytes(), TestBroadcastEmojiSet_SurvivesCanceledRequestContext(), TestEmojiDelete_BadIDIs400() (+24 more)
-### Community 98 - "handleCreateEmoji"
-Cohesion: 0.10
-Nodes (34): EmojiBroadcaster, emojiResponse, FileStore, uploadResponse, net/http.Client, broadcastEmojiSet(), chi.Router, handleCreateEmoji() (+26 more)
-
-### Community 99 - "doRequest"
-Cohesion: 0.17
-Nodes (25): doRequest(), TestAdminAPI_PatchRole_NoRenameNoMemberUpdate(), TestAdminAPI_PatchRole_RenameBroadcastsMemberUpdate(), createUserWithRole(), decodeRole(), newRolesHandler(), TestAdminAPI_CreateRole_DuplicateNameIsBadRequest(), TestAdminAPI_CreateRole_OK() (+17 more)
-
-### Community 100 - "audioPipeline.ts"
-Cohesion: 0.10
-Nodes (11): log, createRNNoiseProcessor(), createScriptProcessorPipeline(), loadRNNoise(), log, ProcessingPipeline, RNNoiseModule, { mockLoadPref, mockSavePref } (+3 more)
-
-### Community 101 - "LoadOrGenerate"
+### Community 98 - "buildErrorMsg"
Cohesion: 0.16
-Nodes (25): TLSResult, crypto/tls.Config, fileExists(), GenerateSelfSigned(), loadACME(), loadCertPair(), LoadOrGenerate(), loadOrGenerateSelfSigned() (+17 more)
+Nodes (12): encoding/json.RawMessage, VoiceState, parseCallChannelID(), Client, Hub, buildErrorMsg(), buildVoiceState(), parseChannelID() (+4 more)
+
+### Community 99 - "DB"
+Cohesion: 0.11
+Nodes (9): channelPermissionsResponse, channelFields, channelFromFields(), Channel, ChannelOverride, ChannelRoleOverride, ChannelUserOverride, DB (+1 more)
+
+### Community 100 - "noise-suppression.ts"
+Cohesion: 0.18
+Nodes (7): createRNNoiseProcessor(), createScriptProcessorPipeline(), loadRNNoise(), log, ProcessingPipeline, RNNoiseModule, { mockLoadPref, mockSavePref }
+
+### Community 101 - "AdminActions.ts"
+Cohesion: 0.13
+Nodes (16): BAN_DURATIONS, ChannelContextMenuOptions, ContextMenuResult, createChannelContextMenu(), createMemberContextMenu(), createMenuItem(), createSeparator(), MemberContextMenuOptions (+8 more)
### Community 102 - "Auth Endpoints"
Cohesion: 0.07
Nodes (30): Auth Endpoints, DELETE /api/v1/auth/account, DELETE /api/v1/users/me/totp, Errors, Errors, Errors, Errors, GET /api/v1/auth/me (+22 more)
-### Community 103 - "MigrateFS"
-Cohesion: 0.20
-Nodes (29): testing/fstest.MapFS, MigrateFS(), countVersions(), hasVersion(), simpleFS(), tableExists(), TestMigrate_AllMigrationsRecorded(), TestMigrate_AppliedAtIsISO8601() (+21 more)
+### Community 103 - "openMemory"
+Cohesion: 0.18
+Nodes (36): testing/fstest.MapFS, openMemory(), TestMigrateFSInvalidSQL(), TestMigrateFSReadFileError(), TestMigrateFSSkipsNonSQL(), MigrateFS(), countVersions(), hasVersion() (+28 more)
-### Community 104 - "newOverrideFixture"
-Cohesion: 0.62
-Nodes (6): newOverrideFixture(), seedChannelUserOverride(), TestListVisibleChannels_PerUserOverrideSplitsRoleMates(), TestPermissionService_AppliesUserOverrideLayer(), TestPermissionService_InvalidateUserPicksUpNewOverride(), visibleIDs()
+### Community 104 - "PermissionService"
+Cohesion: 0.16
+Nodes (12): newOverrideFixture(), seedChannelUserOverride(), TestListVisibleChannels_PerUserOverrideSplitsRoleMates(), TestPermissionService_AppliesUserOverrideLayer(), TestPermissionService_InvalidateUserPicksUpNewOverride(), visibleIDs(), Store, NewEmojiService() (+4 more)
### Community 105 - "Save"
Cohesion: 0.18
@@ -912,13 +918,13 @@ Nodes (20): bytesProvider, go.yaml.in/yaml/v3.Node, validateYAML(), applyPatch()
Cohesion: 0.07
Nodes (29): Admin API Authorization, Audit Log, Authentication, Channel Management (admin), Diagnostics, Error Codes, GET /admin/api/audit-log, GET /admin/api/me (+21 more)
-### Community 107 - "NewRegistry"
-Cohesion: 0.18
-Nodes (16): TestEmptyAllowlistDeniesEveryHost(), TestManifestCommandsValidation(), TestRegisterCommandRequiresManifestDeclaration(), TestStorageKeysIsolatedPerPlugin(), TestStorageRejectsOversizedKeyAndValue(), openPluginTestDB(), TestDispatchCommandRuntimePlatformRace(), ParseManifest() (+8 more)
+### Community 107 - "media-visibility.ts"
+Cohesion: 0.13
+Nodes (19): allTracked, captureStaticFrame(), createPlayPauseButton(), destroyObserver(), ensureVisibilityListener(), freezeImage(), getObserver(), MediaEntry (+11 more)
-### Community 108 - "voice_moderation_test.go"
-Cohesion: 0.27
-Nodes (28): voiceMuteMsg(), auditActions(), joinVoice(), newVoiceModHub(), seedVoiceUserWithRole(), TestVoiceDeafen_SelfUndeafenWhileServerDeafened_Refused(), TestVoiceMod_Deafen_ClearingRestoresSelfUnmute(), TestVoiceMod_Deafen_SetsServerDeafenedAndMutes() (+20 more)
+### Community 108 - "joinVoice"
+Cohesion: 0.26
+Nodes (29): voiceMuteMsg(), auditActions(), joinVoice(), newVoiceModHub(), seedVoiceUserWithRole(), TestVoiceDeafen_SelfUndeafenWhileServerDeafened_Refused(), TestVoiceMod_Deafen_ClearingRestoresSelfUnmute(), TestVoiceMod_Deafen_SetsServerDeafenedAndMutes() (+21 more)
### Community 109 - "ptt.rs"
Cohesion: 0.11
@@ -932,25 +938,25 @@ Nodes (28): 10. Appendix — session command log index, 11. Remediation addendum
Cohesion: 0.07
Nodes (27): scripts, build, dev, format, format:check, knip, lint, lint:fix (+19 more)
-### Community 112 - "ResolveTokenHash"
-Cohesion: 0.17
-Nodes (9): fakeStore, tokenStore, ResolveTokenHash(), future(), past(), TestResolveTokenHash(), apiTokenFromGen(), APIToken (+1 more)
+### Community 112 - "checkSourceWith"
+Cohesion: 0.16
+Nodes (18): go/ast.File, go/ast.ImportSpec, go/token.FileSet, Rule, Violation, allowIndex(), CheckSource(), checkSourceWith() (+10 more)
### Community 113 - "Load"
Cohesion: 0.16
-Nodes (22): IsDefaultVoiceCredentials(), Load(), TestIsDefaultVoiceCredentials(), TestLoadDefaults(), TestLoadEnvironmentVariableOverrides(), TestLoadEnvOverride_EventPersistence(), TestLoadEnvOverridesPrecedenceOverYAML(), TestLoadEnvVarNoUnderscore() (+14 more)
+Nodes (23): IsDefaultVoiceCredentials(), Load(), TestIsDefaultVoiceCredentials(), TestLoadDefaults(), TestLoadEnvironmentVariableOverrides(), TestLoadEnvOverride_EventPersistence(), TestLoadEnvOverridesPrecedenceOverYAML(), TestLoadEnvVarNoUnderscore() (+15 more)
-### Community 114 - "handleVoiceE2EEOfferV2"
-Cohesion: 0.16
-Nodes (24): offerDeps(), TestVoiceE2EEOfferV2_EmptyFields(), TestVoiceE2EEOfferV2_HappyPath(), TestVoiceE2EEOfferV2_InvalidBase64(), TestVoiceE2EEOfferV2_NilKeyHolder_ReturnsInternal(), TestVoiceE2EEOfferV2_NoReply(), TestVoiceE2EEOfferV2_NotInVoiceChannel(), TestVoiceE2EEOfferV2_NotKeyHolder() (+16 more)
+### Community 114 - "handleVoiceE2EEAnnounceV2"
+Cohesion: 0.25
+Nodes (12): TestVoiceE2EEAnnounceV2_EmptyPublicKey(), TestVoiceE2EEAnnounceV2_HappyPath(), TestVoiceE2EEAnnounceV2_InvalidBase64(), TestVoiceE2EEAnnounceV2_NoReply(), TestVoiceE2EEAnnounceV2_NoSignature_LegacyAccepted(), TestVoiceE2EEAnnounceV2_NotInVoiceChannel(), TestVoiceE2EEAnnounceV2_PublicKeyTooLarge(), TestVoiceE2EEAnnounceV2_SignatureInvalidBase64() (+4 more)
-### Community 115 - "commands.rs"
+### Community 115 - "NewHandler"
Cohesion: 0.14
-Nodes (15): get_cert_fingerprint(), get_identity_pin(), get_settings(), identity_pin_key(), is_settings_key_allowed(), log_cmd_err(), open_devtools(), AppHandle (+7 more)
+Nodes (19): TestNewHandler_APIRoutesMounted(), TestNewHandler_AuthProtectedRoute(), TestNewHandler_ReturnsNonNilHandler(), TestNewHandler_ServesStaticRoot(), TestNewHandler_SetsCSPOnRoot(), TestNewHandler_WithUpdater(), TestOwnerOnlyMiddleware_AdminDenied(), TestOwnerOnlyMiddleware_MemberDenied() (+11 more)
### Community 116 - "newEmitTestHub"
-Cohesion: 0.21
-Nodes (20): TestEmitEvents_PresenceEvent_UsesNormalPriorityQueue(), drainChan(), Hub, newEmitTestHub(), registerEmitTestClient(), registerEmitTestVoiceClient(), TestEmitEvents_BroadcastAllEvent(), TestEmitEvents_ChannelEvent_CallsBroadcastToChannel() (+12 more)
+Cohesion: 0.18
+Nodes (21): TestEmitEvents_PresenceOthersEvent_UsesNormalPriorityQueue(), TestEmitEvents_PresenceEvent_UsesNormalPriorityQueue(), TestEmitEvents_PresenceSelfEvent_UsesNormalPriorityQueue(), drainChan(), Hub, newEmitTestHub(), registerEmitTestClient(), registerEmitTestVoiceClient() (+13 more)
### Community 117 - "Plan: Remediate security-hardening review regressions"
Cohesion: 0.08
@@ -964,33 +970,33 @@ Nodes (15): aead.dev/minisign.PublicKey, os.File, TestEnsureVPrefix(), ensureVPr
Cohesion: 0.17
Nodes (24): assertAudit(), newRoleCRUDService(), TestAffectedUserIDs(), TestCreateRole_CannotGrantUnheldBit(), TestCreateRole_CannotPlaceAtOrAboveOwnRank(), TestCreateRole_DefaultPlacementAvoidsCollision(), TestCreateRole_DefaultsToJustBelowActor(), TestCreateRole_HappyPath() (+16 more)
-### Community 120 - "Checker"
-Cohesion: 0.11
-Nodes (18): DB, ChannelOverride, ChannelRef, Checker, MessageService, Store, hasChannelAccess(), hasChannelAccessLive() (+10 more)
+### Community 120 - "VoiceDeps"
+Cohesion: 0.22
+Nodes (18): registerVoiceControlsV2(), disconnectFromVoiceIn(), Hub, handleVoiceModDeafenV2(), handleVoiceModKickV2(), handleVoiceModMoveV2(), handleVoiceModMuteV2(), onOff() (+10 more)
### Community 121 - "EnsureLiveKitBinary"
-Cohesion: 0.15
-Nodes (24): io.Reader, io.ReaderAt, sync/atomic.Int32, extractChatserverFromTarGz(), TestExtractChatserverFromTarGz(), cleanupOldLiveKitBinaries(), downloadTo(), EnsureLiveKitBinary() (+16 more)
+Cohesion: 0.17
+Nodes (23): io.Reader, io.ReaderAt, sync/atomic.Int32, cleanupOldLiveKitBinaries(), downloadTo(), EnsureLiveKitBinary(), ensureLiveKitStageBinary(), extractLiveKitFromTarGz() (+15 more)
### Community 122 - "clientip_test.go"
-Cohesion: 0.12
-Nodes (30): contextKey, errorResponse, net.IPNet, TestBodyCapExemptions_RouteEnvelopesReachable(), inCIDRs(), TestClientIP_BroadTrustedCIDRKeepsClientsDistinct(), TestClientIP_NoTrustedProxies_UsesRemoteAddr(), TestClientIP_RemoteAddrWithoutPort() (+22 more)
+Cohesion: 0.18
+Nodes (24): net.IPNet, inCIDRs(), TestClientIP_BroadTrustedCIDRKeepsClientsDistinct(), TestClientIP_NoTrustedProxies_UsesRemoteAddr(), TestClientIP_RemoteAddrWithoutPort(), TestClientIP_SpoofedXFFFromUntrustedRemoteIgnored(), TestClientIP_TrustedProxy_NoXRealIP_FallsBackToRemoteAddr(), TestClientIP_TrustedProxy_UsesXRealIP() (+16 more)
-### Community 123 - "AudioElements"
+### Community 123 - "connectionStats.ts"
+Cohesion: 0.18
+Nodes (12): collectAllStats(), ConnectionStats, ConnectionStatsPoller, createConnectionStatsPoller(), EMPTY_STATS, extractMetrics(), formatBytes(), formatRate() (+4 more)
+
+### Community 124 - "buildVoiceLeave"
Cohesion: 0.11
-Nodes (5): AudioElements, getSavedUserVolume(), userVolumeKey(), { mockLoadPref, mockSavePref }, mockVoiceStoreState
-
-### Community 124 - "buildErrorMsg"
-Cohesion: 0.13
-Nodes (9): TestBuildErrorMsgWithID(), TestChannelCanSend(), Client, Hub, Client, Hub, buildErrorMsg(), buildErrorMsgWithID() (+1 more)
+Nodes (13): github.com/livekit/protocol/livekit.WebhookEvent, Client, Hub, Client, Hub, MountWebhookRoute(), parseParticipantIdentity(), parseRoomChannelID() (+5 more)
### Community 125 - "gif_handler_test.go"
Cohesion: 0.30
Nodes (19): lastRequest, buildGIFRouter(), decodeGIFError(), decodeGIFResults(), gifGET(), stubKlipy(), TestGIFAuthCheckedBeforeDisabledCheck(), TestGIFDisabledMakesNoUpstreamCall() (+11 more)
-### Community 126 - "navigateToMainPage"
-Cohesion: 0.19
-Nodes (6): emitWsEvent(), emitWsMessage(), mockTauriFullSession(), mockTauriFullSessionWithMessages(), navigateToMainPage(), simulateReconnect()
+### Community 126 - "e2e/helpers.ts"
+Cohesion: 0.14
+Nodes (12): MOCK_INVITES, MOCK_MESSAGES_RICH, MOCK_READY_PAYLOAD, mockTauriFullSession(), mockTauriFullSessionWithAutoConnect(), mockTauriFullSessionWithEcho(), mockTauriFullSessionWithFailingMessages(), mockTauriFullSessionWithMessages() (+4 more)
### Community 127 - "messages.sql.go"
Cohesion: 0.13
@@ -1001,32 +1007,32 @@ Cohesion: 0.09
Nodes (23): Channel Endpoints, DELETE /api/v1/channels/{id}/pins/{messageId}, Errors, GET /api/v1/channels, GET /api/v1/channels/{id}/messages, GET /api/v1/channels/{id}/messages/around/{messageId}, GET /api/v1/channels/{id}/messages/{messageId}/reactions/{emoji}/users, GET /api/v1/channels/{id}/pins (+15 more)
### Community 129 - "newSignedTestUpdater"
-Cohesion: 0.19
-Nodes (23): aead.dev/minisign.PrivateKey, multiAssetManifest(), testHash(), TestVerifyReleaseManifest_LegacySingleAssetStillVerifies(), TestVerifyReleaseManifest_MultiAssetBadChecksumFails(), TestVerifyReleaseManifest_MultiAssetSelectsLinuxEntry(), TestVerifyReleaseManifest_MultiAssetSelectsWindowsEntry(), TestVerifyReleaseManifest_MultiAssetUnknownAssetFails() (+15 more)
+Cohesion: 0.20
+Nodes (22): aead.dev/minisign.PrivateKey, multiAssetManifest(), testHash(), TestVerifyReleaseManifest_LegacySingleAssetStillVerifies(), TestVerifyReleaseManifest_MultiAssetBadChecksumFails(), TestVerifyReleaseManifest_MultiAssetSelectsLinuxEntry(), TestVerifyReleaseManifest_MultiAssetSelectsWindowsEntry(), TestVerifyReleaseManifest_MultiAssetUnknownAssetFails() (+14 more)
### Community 130 - "host_http_test.go"
-Cohesion: 0.14
-Nodes (18): net.Conn, net.IP, HTTPRequest, HTTPResponse, Registry, GuardedDialContext(), ipAllowed(), Registry (+10 more)
+Cohesion: 0.11
+Nodes (21): net.Conn, net.IP, net.Listener, HTTPRequest, HTTPResponse, TestEmptyAllowlistDeniesEveryHost(), Registry, GuardedDialContext() (+13 more)
### Community 131 - "Topic"
-Cohesion: 0.19
-Nodes (8): TestEmitEvents_DirectPresenceDropsQueuedEntry(), Client, NewPubSub(), TestUserTopic(), topicFor(), UserTopic(), PubSub, Topic
+Cohesion: 0.32
+Nodes (3): Client, PubSub, Topic
### Community 132 - "DB"
-Cohesion: 0.09
-Nodes (13): mentionExecer, ChannelOverride, DB, MentionTarget, Message, insertMentionRows(), LowerASCII(), BroadcastStatus() (+5 more)
+Cohesion: 0.13
+Nodes (9): mentionExecer, mentionTargetColumn, ChannelOverride, DB, MentionTarget, Message, insertMentionRows(), LowerASCII() (+1 more)
### Community 133 - "users"
Cohesion: 0.14
Nodes (15): channel_overrides, channels, messages, messages_fts, roles, sessions, users, voice_states (+7 more)
-### Community 134 - "Client"
-Cohesion: 0.12
-Nodes (4): Hub, Client, newClient(), wsConn
+### Community 134 - "User"
+Cohesion: 0.05
+Nodes (24): adminContextKey, adminMeResponse, adminUserResponse, fakeStore, tokenStore, adminAuthMiddleware(), RequireAdminAuth(), requirePerm() (+16 more)
-### Community 135 - "identity.ts"
-Cohesion: 0.19
-Nodes (22): exportIdentityKeyPair(), exportPublicKey(), generateIdentityKeyPair(), importIdentityKeyPair(), deleteIdentityKey(), ensureIdentityKeyPublished(), getIdentityPin(), getInvoke() (+14 more)
+### Community 135 - "update_commands.rs"
+Cohesion: 0.22
+Nodes (14): build_tls_config(), build_update_endpoint(), build_updater(), check_client_update(), download_and_install_update(), DownloadProgress, extract_host_for_cert_store(), AppHandle (+6 more)
### Community 136 - "Queries"
Cohesion: 0.12
@@ -1041,20 +1047,20 @@ Cohesion: 0.12
Nodes (9): GetAuditLogParams, GetAuditLogRow, ListAllUsersParams, ListAllUsersRow, LogAuditParams, SetSettingParams, UpdateUserRoleParams, Queries (+1 more)
### Community 139 - "middleware_and_spawn_test.go"
-Cohesion: 0.16
-Nodes (20): adminAuthMiddleware(), isolateSpawnedTestBinary(), openWhiteboxTestDB(), TestAdminAuthMiddleware_RoleNotFound(), TestHandleGetAuditLog_DBError(), TestHandleGetSettings_DBError(), TestHandleGetStats_DBError(), TestHandleListChannels_DBError() (+12 more)
+Cohesion: 0.15
+Nodes (21): handleListUsers(), TestQueryInt_LimitStillCapped(), TestQueryInt_OffsetNotClampedByLimitCap(), queryInt(), isolateSpawnedTestBinary(), openWhiteboxTestDB(), TestAdminAuthMiddleware_RoleNotFound(), TestHandleGetAuditLog_DBError() (+13 more)
### Community 140 - "password_test.go"
-Cohesion: 0.14
-Nodes (19): CheckPassword(), FuzzValidatePasswordStrength(), getDummyHash(), TestCheckPassword_CorrectPassword(), TestCheckPassword_EmptyHash(), TestCheckPassword_EmptyHashTimingResistance(), TestCheckPassword_EmptyPassword(), TestCheckPassword_MalformedHash() (+11 more)
+Cohesion: 0.16
+Nodes (18): CheckPassword(), getDummyHash(), TestCheckPassword_CorrectPassword(), TestCheckPassword_EmptyHash(), TestCheckPassword_EmptyHashTimingResistance(), TestCheckPassword_EmptyPassword(), TestCheckPassword_MalformedHash(), TestCheckPassword_WrongPassword() (+10 more)
### Community 141 - "newPurgeService"
Cohesion: 0.19
Nodes (20): TestAddReaction_RefusedInArchivedChannel(), TestEditMessage_RefusedInArchivedChannel(), TestPurgeMessages_RefusedInArchivedChannel(), TestSendMessage_AllowedAfterUnarchive(), TestSendMessage_RefusedInArchivedChannel(), TestSetMessagePinned_RefusedInArchivedChannel(), MessageService, newPurgeService() (+12 more)
### Community 142 - "handleVoiceTokenRefreshV2"
-Cohesion: 0.18
-Nodes (16): seedTokenRefreshUser(), seedVoiceOnlyRole(), TestVoiceTokenRefreshV2_GenerateTokenError(), TestVoiceTokenRefreshV2_HappyPath(), TestVoiceTokenRefreshV2_IsKeyHolderReflectedInReply(), TestVoiceTokenRefreshV2_NoEvents(), TestVoiceTokenRefreshV2_NotInVoice(), TestVoiceTokenRefreshV2_PermissionsPassedToTokenGen() (+8 more)
+Cohesion: 0.16
+Nodes (17): seedTokenRefreshUser(), seedVoiceOnlyRole(), TestVoiceTokenRefreshV2_GenerateTokenError(), TestVoiceTokenRefreshV2_HappyPath(), TestVoiceTokenRefreshV2_IsKeyHolderReflectedInReply(), TestVoiceTokenRefreshV2_NoEvents(), TestVoiceTokenRefreshV2_NotInVoice(), TestVoiceTokenRefreshV2_PermissionsPassedToTokenGen() (+9 more)
### Community 143 - "pubsub_test.go"
Cohesion: 0.26
@@ -1069,60 +1075,56 @@ Cohesion: 0.13
Nodes (13): gapProbeSSEWriter, revokingSSEWriter, lockedBuffer, bytes.Buffer, net/http.Header, TestHandleLogStream_EntryWrittenDuringBackfillIsDelivered(), TestRingBuffer_SnapshotAndSubscribe_NoGap(), TestRingBuffer_SnapshotAndSubscribe_SnapshotExcludedFromChannel() (+5 more)
### Community 146 - "handleSetup"
-Cohesion: 0.18
-Nodes (18): setupDefaults, SetupOptions, setupRequest, setupResponse, setupStatusResponse, setupWizardRequest, handleSetup(), isSameOrigin() (+10 more)
+Cohesion: 0.15
+Nodes (26): setupDefaults, SetupOptions, setupRequest, setupResponse, setupStatusResponse, setupWizardRequest, handleSetup(), handleSetupStatus() (+18 more)
### Community 147 - "log/slog.Value"
Cohesion: 0.14
Nodes (9): log/slog.Value, logAttrValue(), Config, GIFConfig, GitHubConfig, VoiceConfig, redactSecret(), Session (+1 more)
### Community 148 - "Updater"
-Cohesion: 0.16
-Nodes (14): tauriPlatformResponse, tauriUpdateResponse, golang.org/x/sync/singleflight.Group, ensureV(), chi.Router, handleClientUpdate(), MountClientUpdateRoute(), Updater (+6 more)
+Cohesion: 0.14
+Nodes (16): tauriPlatformResponse, tauriUpdateResponse, golang.org/x/sync/singleflight.Group, net/http.Client, ensureV(), chi.Router, handleClientUpdate(), MountClientUpdateRoute() (+8 more)
-### Community 149 - "Credential storage"
-Cohesion: 0.25
-Nodes (8): Credential storage, Environment causes that remain possible, From the client, From Windows directly, Root cause of the 2026-07 identity-key regression, The fix, Verifying the credential store on a machine, Write verification and the fallback store
+### Community 149 - "syntax-highlight.ts"
+Cohesion: 0.12
+Nodes (14): ALIASES, BACKTICK_STRING, C_BLOCK_COMMENT, CodeToken, DQ_STRING, HASH_COMMENT, JS_LIKE, LANGS (+6 more)
### Community 150 - "dependencies"
Cohesion: 0.09
Nodes (23): dependencies, @jitsi/rnnoise-wasm, livekit-client, @tauri-apps/api, @tauri-apps/plugin-autostart, @tauri-apps/plugin-deep-link, @tauri-apps/plugin-dialog, @tauri-apps/plugin-fs (+15 more)
### Community 151 - "newHarvestVoiceDB"
-Cohesion: 0.18
-Nodes (18): mustCreateVoiceChannel(), newHarvestVoiceDB(), seedHarvestVoiceUser(), TestHandleVoiceCameraV2_ChannelLookupErrorFailsClosed(), TestHandleVoiceJoin_AbortedSwitchDoesNotResurrectVoiceTopicSubscription(), TestSweepStaleVoiceStates_EvictionIsScopedToCheckedChannel(), TestRefreshChannelVisibility_ReconnectDuringFanOutActsOnLiveClient(), TestCleanupVoiceForChannel_NotifiesReadAudienceOfArchivedChannel() (+10 more)
+Cohesion: 0.14
+Nodes (22): mustCreateVoiceChannel(), newHarvestVoiceDB(), seedHarvestVoiceUser(), TestHandleVoiceCameraV2_ChannelLookupErrorFailsClosed(), TestHandleVoiceJoin_AbortedSwitchDoesNotResurrectVoiceTopicSubscription(), TestSweepStaleVoiceStates_EvictionIsScopedToCheckedChannel(), TestRefreshChannelVisibility_ReconnectDuringFanOutActsOnLiveClient(), TestCleanupVoiceForChannel_NotifiesReadAudienceOfArchivedChannel() (+14 more)
### Community 152 - "Config Key Reference"
Cohesion: 0.10
Nodes (21): Backups (`backup`), Config Key Reference, Database (`database`), Environment Variable Overrides, Event Persistence (`event_persistence`), Example config.yaml, First-run setup wizard, GIF Picker (`gif`) (+13 more)
-### Community 153 - "newTestPermService"
-Cohesion: 0.21
-Nodes (15): TestCacheStats_HitsAndMisses(), newTestPermService(), TestHasChannelPerm_Allowed(), TestHasChannelPerm_Denied(), TestHasChannelPerm_OverrideAllow(), TestHasChannelPerm_OverrideDeny(), TestHasChannelPerm_UnknownUserReturnsFalse(), TestInvalidateAll_ClearsEntireCache() (+7 more)
+### Community 153 - "seedChannel"
+Cohesion: 0.22
+Nodes (20): TestCacheStats_HitsAndMisses(), newTestPermService(), TestHasChannelPerm_AdminBypass(), TestHasChannelPerm_Allowed(), TestHasChannelPerm_Denied(), TestHasChannelPerm_OverrideAllow(), TestHasChannelPerm_OverrideDeny(), TestHasChannelPerm_OverrideFetchErrorDenies() (+12 more)
### Community 154 - "markdown.ts"
Cohesion: 0.18
Nodes (19): BlockNode, buildMatches(), codeSpanEnd(), DELIMS, DelimSpec, EMPTY_MATCHES, InlineNode, InlineStyle (+11 more)
-### Community 155 - "VideoGrid.ts"
-Cohesion: 0.05
-Nodes (33): appendModerationSection(), showUserVolumeMenu(), VoiceModMenuOptions, computeGridLayout(), createVideoGrid(), GridLayout, setButtonIcon(), TileConfig (+25 more)
-
-### Community 156 - "Manifest"
-Cohesion: 0.22
-Nodes (9): Capability, CommandSpec, Resources, UISpec, UITab, Manifest, FuzzValidateRelativePath(), TestValidateRelativePath() (+1 more)
+### Community 155 - "deep-link.ts"
+Cohesion: 0.09
+Nodes (16): initDeepLinks(), InviteLink, linkSegments(), log, MessageLink, parseIdSegment(), parseInviteLink(), parseMessageLink() (+8 more)
### Community 157 - "rate-limiter.ts"
Cohesion: 0.22
Nodes (12): createChatLimiter(), createPresenceLimiter(), createRateLimiter(), createRateLimiterSet(), createReactionLimiter(), createTypingLimiter(), createVideoCameraLimiter(), createVoiceLimiter() (+4 more)
-### Community 158 - "e2e/helpers.ts"
-Cohesion: 0.18
-Nodes (11): MOCK_INVITES, MOCK_MESSAGES_RICH, MOCK_READY_PAYLOAD, mockTauriConnect(), mockTauriConnectWith2FA(), mockTauriFullSessionWithEcho(), mockTauriFullSessionWithMessagesAndEcho(), mockTauriLoginError() (+3 more)
+### Community 158 - "Error"
+Cohesion: 0.48
+Nodes (7): CertificateDer, Error, StubVerifier, verify_tls12(), verify_tls13(), DigitallySignedStruct, HandshakeSignatureValid
-### Community 159 - "main.test.ts"
-Cohesion: 0.23
-Nodes (13): createWsClient(), ensureTauriApis(), normalizeHostForCertCompare(), parseStoredFingerprint(), uuid(), emitTauriEvent(), eventHandlers, mockInvoke (+5 more)
+### Community 159 - "video-grid.test.ts"
+Cohesion: 0.14
+Nodes (7): TileConfig, mockGetScreenshareAudioMuted, mockGetScreenshareAudioVolume, mockGetUserVolume, mockMuteScreenshareAudio, mockSetScreenshareAudioVolume, mockSetUserVolume
### Community 160 - "testing.M"
Cohesion: 0.11
@@ -1141,20 +1143,20 @@ Cohesion: 0.11
Nodes (15): allResults, ARGS, byFile, clusters, commits, excluded, FIX_RESULTS, fixed (+7 more)
### Community 164 - "buildTauriMockScript"
-Cohesion: 0.13
-Nodes (13): buildReadyPayload(), buildTauriMockScript(), chatEchoHandlers(), MOCK_LOGIN_2FA_RESPONSE, MOCK_LOGIN_RESPONSE, MOCK_TOKEN, mockTauriFullSessionWithAutoConnect(), mockTauriFullSessionWithFailingMessages() (+5 more)
+Cohesion: 0.12
+Nodes (14): buildReadyPayload(), buildTauriMockScript(), chatEchoHandlers(), MOCK_LOGIN_2FA_RESPONSE, MOCK_LOGIN_RESPONSE, MOCK_TOKEN, mockTauriConnect(), mockTauriConnectWith2FA() (+6 more)
### Community 165 - "OwnCord Introspection MCP Server"
-Cohesion: 0.08
-Nodes (22): 1. Install dependencies, 2. Mint an API token, 3. Put the token in your environment, 4. Enable in Claude Code, `api_request`, API tokens (server side), Authentication, `client_logs` (+14 more)
+Cohesion: 0.11
+Nodes (19): 1. Install dependencies, 2. Mint an API token, 3. Put the token in your environment, 4. Enable in Claude Code, `api_request`, API tokens (server side), Authentication, `client_logs` (+11 more)
### Community 166 - "Bug-detection improvements — design"
Cohesion: 0.11
Nodes (18): 1a. `make fuzz`, 1b. Scoped Stryker runs, 1c. Browser-mode vitest, 1d. Prerequisite, 3a. Client model-based tests, 3b. Server hub simulation, 3c. Fault-injected transport, Bug-detection improvements — design (+10 more)
-### Community 167 - "ptt.ts"
-Cohesion: 0.16
-Nodes (19): buildKeybindsTab(), captureKeyPress(), initPtt(), log, stopPtt(), ungateMic(), updatePttKey(), VK_NAMES (+11 more)
+### Community 167 - "AuditWriter"
+Cohesion: 0.20
+Nodes (6): AuditStore, pendingAudit, sync/atomic.Uint64, sync.Once, AuditWriter, DB
### Community 168 - "eslint-rules.js"
Cohesion: 0.12
@@ -1173,28 +1175,28 @@ Cohesion: 0.11
Nodes (17): Built-in commands, Code surface, Concurrency & lifecycle, Failure modes & UX, Files-to-touch checklist (for the implementing agent), Manifest changes, Non-goals, Open questions (+9 more)
### Community 172 - "net/http.Request"
-Cohesion: 0.25
-Nodes (11): PluginAdminHandler, net/http.Request, net/http.ResponseWriter, okHandler(), ok(), hasZipMagic(), isZipContentType(), parsePluginID() (+3 more)
+Cohesion: 0.12
+Nodes (31): memberUnbanBroadcaster, patchUserRequest, PluginAdminHandler, uploadResponse, net/http.Request, net/http.ResponseWriter, handlePatchUser(), patchUserApplyBan() (+23 more)
### Community 173 - "ChannelTopic"
-Cohesion: 0.16
-Nodes (15): TestEmitSequencedDM_UsesNormalQueueToPreserveSeqOrder(), TestEmitUserTargeted_KeepsHighPriorityFastLane(), NewTestClientWithChannel(), TestComputeAllowedChannels_DMLookupErrorIsFatal(), TestDeliverBroadcast_ShedFrameLeavesNoSeqGap(), TestEmitEvents_DMChannelOpenForcesFullResyncForOlderClients(), TestFailedHandshake_MarksUserOfflineWhenNoReplacementRemains(), TestKickClient_SubscribeRaceCannotLeaveDeadSubscriber() (+7 more)
+Cohesion: 0.13
+Nodes (16): TestEmitSequencedDM_UsesNormalQueueToPreserveSeqOrder(), TestEmitUserTargeted_KeepsHighPriorityFastLane(), NewTestClientWithChannel(), TestComputeAllowedChannels_DMLookupErrorIsFatal(), TestDeliverBroadcast_ShedFrameLeavesNoSeqGap(), TestEmitEvents_DMChannelOpenForcesFullResyncForOlderClients(), TestFailedHandshake_MarksUserOfflineWhenNoReplacementRemains(), TestKickClient_SubscribeRaceCannotLeaveDeadSubscriber() (+8 more)
### Community 174 - "UserService"
-Cohesion: 0.19
-Nodes (9): github.com/owncord/server/syncutil.Mutex, cleanText(), Store, nullable(), resolveOptional(), ChangePasswordResult, keyedMutex, ProfilePatch (+1 more)
-
-### Community 175 - "Blocked — fix attempted, revert-proof failed"
-Cohesion: 0.14
-Nodes (13): Blocked — fix attempted, revert-proof failed, Declined, OC-0001 — high — Wrapped room keys have no freshness binding, so old offers replay forever, OC-0003 — high — Unverified peers get no safety number, removing TOFU's only out-of-band escape hatch, OC-0012 — low — CleanupVoiceForChannel never clears voiceKeyHolders, OC-0039 — medium — DeleteMessage treats a GetChannel read error as "not a DM", letting a moderator hard-delete another user's private DM message, OC-0058 — low — Unban emits no WS event — *ws.Hub does not implement memberUnbanBroadcaster, OC-0081 — low — voice_max_video cap counts the requester's own camera row, so a user whose server-side camera flag is already 1 can never re-enable (+5 more)
-
-### Community 176 - "handlers_backup.go"
Cohesion: 0.25
-Nodes (8): backupEntry, absOrRaw(), closeDatabase(), handleRestoreBackup(), init(), requestRestart(), SetBackupDir(), CheckBackupIntegrity()
+Nodes (6): cleanText(), nullable(), resolveOptional(), ChangePasswordResult, ProfilePatch, UserService
+
+### Community 175 - "OwnCord Findings Ledger"
+Cohesion: 0.29
+Nodes (6): Declined, Duplicate, OC-0039 — medium — DeleteMessage treats a GetChannel read error as "not a DM", letting a moderator hard-delete another user's private DM message, OC-0092 — low — Plugin slash-command broadcast can post live content into an archived (read-only) channel, OC-0159 — medium — Handshake/replay writes have no write deadline, so a client that stops reading pins a server goroutine + socket forever, OwnCord Findings Ledger
+
+### Community 176 - "handleRestoreBackup"
+Cohesion: 0.10
+Nodes (25): backupEntry, backupFile, MaintainBackups(), pruneExpiredBackups(), runScheduledBackup(), scanBackups(), absOrRaw(), closeDatabase() (+17 more)
### Community 177 - "logger.ts"
-Cohesion: 0.11
-Nodes (24): createLogsTab(), formatLogEntry(), LOG_FILTER_LEVELS, LOG_LEVEL_COLORS, LOG_MIN_LEVELS, LogsTabHandle, TabName, getSessionDebugInfo (+16 more)
+Cohesion: 0.06
+Nodes (48): createLogsTab(), formatLogEntry(), LOG_FILTER_LEVELS, LOG_LEVEL_COLORS, LOG_MIN_LEVELS, LogsTabHandle, TabName, createUpdateNotifier() (+40 more)
### Community 178 - "fallback_crypto.rs"
Cohesion: 0.25
@@ -1220,9 +1222,9 @@ Nodes (24): log/slog.Leveler, TestRingBuffer_WriteDoesNotAllocate(), NewMultiHan
Cohesion: 0.12
Nodes (14): authTime, broadcastLatency, CHANNEL_ID, handleSummary(), options, textSummary(), wsAcks, wsAuthed (+6 more)
-### Community 184 - "NewMessageService"
+### Community 184 - "newDMFixture"
Cohesion: 0.23
-Nodes (15): Store, newDMFixture(), TestDeleteMessage_DMFanoutSurvivesDeleterDisconnectAfterCommit(), TestDeleteMessage_FailsClosedWhenChannelLookupErrors(), TestDeleteMessage_RefusedInArchivedChannel(), TestEditMessage_DMFanoutSurvivesEditorDisconnectAfterCommit(), TestEditMessage_FailsClosedWhenChannelLookupErrors(), TestSendMessage_AttachmentsSurviveSenderDisconnectAfterLink() (+7 more)
+Nodes (11): Message, newDMFixture(), TestDeleteMessage_DMFanoutSurvivesDeleterDisconnectAfterCommit(), TestDeleteMessage_FailsClosedWhenChannelLookupErrors(), TestDeleteMessage_RefusedInArchivedChannel(), TestEditMessage_DMFanoutSurvivesEditorDisconnectAfterCommit(), TestEditMessage_FailsClosedWhenChannelLookupErrors(), TestSendMessage_DMFanoutSurvivesSenderDisconnectAfterCommit() (+3 more)
### Community 185 - "handler"
Cohesion: 0.09
@@ -1256,17 +1258,17 @@ Nodes (16): DELETE /api/v1/users/me/sessions/{id}, Errors, Errors, GET /api/v1/u
Cohesion: 0.12
Nodes (15): Client session (`livekitSession.ts`), Compatibility posture (transition), F3 status 2026-07-23 (branch `feat/e2ee-identity-tofu`), F3 — Voice E2EE identity keys + TOFU (the remaining work), F6 detail (done, committed `e6a0d87`), Infrastructure (mirror existing patterns), Notes carried from the build, Resume checklist (do these first) (+7 more)
-### Community 194 - "run"
-Cohesion: 0.16
-Nodes (18): log/slog.LevelVar, log/slog.Logger, SetDatabasePath(), ParseLevel(), TestLoggingLevelFromEnv(), TestParseLevel(), getOutboundIP(), healthcheckTLSConfig() (+10 more)
+### Community 194 - "Server/main.go"
+Cohesion: 0.08
+Nodes (45): log/slog.LevelVar, log/slog.Logger, net/http.Server, time.Timer, SetDatabasePath(), serveWithBindRetry(), getOutboundIP(), healthcheckTLSConfig() (+37 more)
-### Community 195 - "newWazeroTestRegistry"
-Cohesion: 0.35
-Nodes (13): Registry, newWazeroTestRegistry(), TestWazeroActivateCompilesModule(), TestWazeroCloseTearsDownRuntime(), TestWazeroConcurrentDispatchRace(), TestWazeroCPUBudgetOverrunDoesNotBrickPlugin(), TestWazeroDeactivateClosesCompiledModule(), TestWazeroDisablePluginFreesModule() (+5 more)
+### Community 195 - "totp_encrypt_test.go"
+Cohesion: 0.32
+Nodes (11): DecryptTOTPSecret(), EncryptTOTPSecret(), LoadOrGenerateTOTPKey(), TestDecryptTOTPSecret_FailsClosed(), TestDecryptTOTPSecret_LegacyPlaintextPassthrough(), TestEncryptDecryptTOTPSecret_RoundTrip(), TestEncryptTOTPSecret_NonceIsRandom(), testKey() (+3 more)
### Community 196 - "newUserSvc"
-Cohesion: 0.23
-Nodes (13): Store, newUserSvc(), TestClearCustomStatus(), TestSetCustomStatus_RoundTripClearAndBound(), TestUpdateProfile_AvatarOnlyPatchDoesNotOverwriteUsername(), TestUpdateProfile_ConcurrentUpdatesSerializePerUser(), TestUpdateProfile_RejectsOverlongFields(), TestUpdateProfile_SanitizesAndTrims() (+5 more)
+Cohesion: 0.21
+Nodes (14): Store, newUserSvc(), TestClearCustomStatus(), TestSetCustomStatus_RoundTripClearAndBound(), TestUpdateProfile_AvatarOnlyPatchDoesNotOverwriteUsername(), TestUpdateProfile_ConcurrentUpdatesSerializePerUser(), TestUpdateProfile_RejectsOverlongFields(), TestUpdateProfile_SanitizesAndTrims() (+6 more)
### Community 197 - "plugins_handler_test.go"
Cohesion: 0.34
@@ -1280,37 +1282,41 @@ Nodes (5): badDirFile, fakeDir, fakeDirEntry, io/fs.DirEntry, io/fs.FileInfo
Cohesion: 0.13
Nodes (15): Active Branches, Available Commands, Branch Naming, Client (Tauri v2), Code Style, Commit Format, Contributing, Dependency Policy (+7 more)
-### Community 200 - ".handleFreshConnect"
-Cohesion: 0.30
-Nodes (6): github.com/coder/websocket.Conn, applyConnectStatus(), authenticateConn(), Client, Hub, resumeHint
+### Community 200 - "github.com/coder/websocket.Conn"
+Cohesion: 0.18
+Nodes (14): github.com/coder/websocket.Conn, applyConnectStatus(), Client, Hub, handshakeWrite(), Client, Hub, TestWritePump_DrainsAllNormalBeforeAnyLow() (+6 more)
### Community 201 - "mcp-introspect/package.json"
Cohesion: 0.13
Nodes (14): @modelcontextprotocol/sdk, dependencies, @modelcontextprotocol/sdk, zod, description, engines, node, name (+6 more)
### Community 202 - "v1.2.0-alpha.1 — Discord feature parity"
-Cohesion: 0.08
-Nodes (24): Behavioural changes operators must know about, Changelog, Deferred work, Messaging & mentions, Phase B — Acceleration, Phase C — Differentiation, Roles, permissions & moderation, Security (+16 more)
+Cohesion: 0.06
+Nodes (32): Behavioural changes operators must know about, Changelog, Deferred work, Messaging & mentions, Phase B — Acceleration, Phase C — Differentiation, Roles, permissions & moderation, Security (+24 more)
### Community 203 - "Task Observer — Continuous Skill Discovery & Improvement"
Cohesion: 0.14
Nodes (13): Acting on Observations, Archival on Write, How to Log, Log Structure, Quick Reference, Reference files — load on demand, not up front, Referencing Observations, Session Start Protocol (+5 more)
-### Community 204 - "scanPluginDirectory"
-Cohesion: 0.24
-Nodes (10): foundPlugin, Manifest, rejectSymlinksUnder(), scanPluginDirectory(), TestRejectSymlinksUnderClean(), TestRejectSymlinksUnderFindsNestedSymlink(), TestRejectSymlinksUnderFindsSymlink(), TestScanPluginDirectoryRejectsSymlinkEntrypoint() (+2 more)
+### Community 204 - "handleVoiceE2EEOfferV2"
+Cohesion: 0.38
+Nodes (12): offerDeps(), TestVoiceE2EEOfferV2_EmptyFields(), TestVoiceE2EEOfferV2_HappyPath(), TestVoiceE2EEOfferV2_InvalidBase64(), TestVoiceE2EEOfferV2_NilKeyHolder_ReturnsInternal(), TestVoiceE2EEOfferV2_NoReply(), TestVoiceE2EEOfferV2_NotInVoiceChannel(), TestVoiceE2EEOfferV2_NotKeyHolder() (+4 more)
+
+### Community 205 - "AudioPipeline"
+Cohesion: 0.14
+Nodes (3): AudioPipeline, { mockLoadPref, mockSavePref }, { mockLoadPref, mockSavePref }
### Community 206 - "1. Channel sidebar"
Cohesion: 0.14
Nodes (14): 1.1 Channel type affordances, 1.1a Per-channel notification mutes, 1.2 Channel switching, 1.3 Reorder & CRUD (admin), 1. Channel sidebar, 2.1 Typing indicator, 2.2 Member actions (context menu), 2. Member list (+6 more)
-### Community 207 - "Voice, Video & E2EE — target UX"
-Cohesion: 0.09
-Nodes (20): 1. View-state vocabulary, 2. Feedback primitives, 3. Connection status is a first-class, observable state, 4. Global event → reaction map, 5. Error & permission reaction matrix, 6. Cross-cutting principles, Documents, Maintenance rule (+12 more)
+### Community 207 - "Messaging — target UX"
+Cohesion: 0.08
+Nodes (23): 1. Message list — states, 2. Composer — permission & connection gating, 3. Sending — optimistic lifecycle, 4. Edit / delete, 5. Reactions, 6. Attachments, 7. Replies, pins, search, read/unread, 7a. Jumping to a message (+15 more)
-### Community 208 - "Updater"
-Cohesion: 0.15
-Nodes (8): assetFilenameFromURL(), Updater, isGitHubHost(), TestIsGitHubHost(), Updater, TestAssetFilenameFromURL(), ClientAssets, textAssetCacheEntry
+### Community 208 - "time.Time"
+Cohesion: 0.07
+Nodes (17): touchThrottle, failingLockoutStore, Event, GetEventsSinceParams, GetEventsSinceRow, PersistEventParams, time.Time, Queries (+9 more)
### Community 209 - "LiveKitClient"
Cohesion: 0.13
@@ -1324,29 +1330,29 @@ Nodes (13): io/fs.FS, applyMigration(), ensureSchemaVersions(), DB, isApplied(),
Cohesion: 0.15
Nodes (7): CountRoleMembersRow, CreateRoleParams, GetUserWithRoleRow, SetRolePositionParams, UpdateRoleParams, Queries, Role
-### Community 212 - "Messaging — target UX"
-Cohesion: 0.14
-Nodes (14): 1. Message list — states, 2. Composer — permission & connection gating, 3. Sending — optimistic lifecycle, 4. Edit / delete, 5. Reactions, 6. Attachments, 7. Replies, pins, search, read/unread, 7a. Jumping to a message (+6 more)
+### Community 212 - "setupRouter"
+Cohesion: 0.32
+Nodes (11): setupRouter(), TestAPIV1InfoEndpoint(), TestAPIV1InfoOmitsVersion(), TestAPIV1InfoReturnsServerName(), TestHealthEndpointOmitsVersion(), TestHealthEndpointReturns200(), TestHealthEndpointReturnsJSON(), TestHealthEndpointStatusOK() (+3 more)
-### Community 213 - "Registry"
-Cohesion: 0.18
-Nodes (6): bytesReaderAt, Config, UITabBinding, PluginStore, Registry, TestBytesReaderAt()
+### Community 213 - ".verify_server_cert"
+Cohesion: 0.36
+Nodes (6): capture_verifier_records_leaf_not_intermediate(), fingerprint_hex(), ServerName, verify(), ServerCertVerified, UnixTime
### Community 214 - "handleChannelFocusV2"
Cohesion: 0.40
Nodes (10): newFocusTestDeps(), TestChannelFocusV2_ChannelNotFound_SilentDrop(), TestChannelFocusV2_HappyPath_SetsChannelID(), TestChannelFocusV2_InvalidChannelID_SilentDrop(), TestChannelFocusV2_NoEvents(), TestChannelFocusV2_NoPermission_ReturnsForbidden(), TestChannelFocusV2_RateLimited_SilentDrop(), TestMarkReadV2_RateLimited_SkipsReadStateWrite() (+2 more)
-### Community 215 - "handlers_channel_perms_test.go"
-Cohesion: 0.15
-Nodes (10): mockPermInvalidator, TestDeleteChannelPermission_ClearsOverride(), TestDeleteChannelPermission_UnknownRole(), TestGetChannelPermissions_DMRejected(), TestGetChannelPermissions_NotFound(), TestGetChannelPermissions_ReturnsAllRoles(), TestPutChannelPermission_AdministratorCanGrantAnyBit(), TestPutChannelPermission_MasksUnknownBits() (+2 more)
+### Community 215 - "itoa"
+Cohesion: 0.11
+Nodes (27): mockPermInvalidator, unbanMockHub, itoa(), TestDeleteChannelPermission_ClearsOverride(), TestDeleteChannelPermission_RefusesEqualOrHigherRole(), TestDeleteChannelPermission_UnknownRole(), TestGetChannelPermissions_DMRejected(), TestPutChannelPermission_AdministratorCanGrantAnyBit() (+19 more)
### Community 216 - "knip.json"
Cohesion: 0.15
Nodes (12): entry, ignore, ignoreDependencies, ignoreExportsUsedInFile, public/**, project, $schema, src/lib/protocolTypes.ts (+4 more)
### Community 218 - "DeviceManager"
-Cohesion: 0.16
-Nodes (5): DeviceManager, log, mockGetLocalDevices, { mockLoadPref, mockSavePref }, mockVoiceState
+Cohesion: 0.13
+Nodes (6): DeviceManager, isMicPolicyGated(), setLocalMuted(), mockGetLocalDevices, { mockLoadPref, mockSavePref }, mockVoiceState
### Community 219 - "finish"
Cohesion: 0.38
@@ -1368,17 +1374,17 @@ Nodes (13): 1. Settings overlay, 2.1 Profile edit, 2.2 Change password (with ses
Cohesion: 0.41
Nodes (12): buildClientUpdateRouter(), fakeGitHubRelease(), platformEntry(), TestClientUpdate_AlreadyLatest(), TestClientUpdate_DebTargetNoContent(), TestClientUpdate_FutureVersion(), TestClientUpdate_GitHubError(), TestClientUpdate_LinuxArm64TargetGetsAarch64AppImage() (+4 more)
-### Community 224 - "logPersistence.ts"
-Cohesion: 0.19
-Nodes (13): addLogListener(), buffer, flushBuffer(), flushLogs(), initLogPersistence(), log, logFilePath(), onLogEntry() (+5 more)
+### Community 224 - "Voice, Video & E2EE — target UX"
+Cohesion: 0.18
+Nodes (11): 1. Two state machines, one status, 2. Join / leave, 3. Local controls, 4. Push-to-talk, 5. Voice roster (per channel), 6. Token refresh & reconnect (invisible), 7. E2EE identity verification surface, 8. Media processing & devices (+3 more)
### Community 225 - "newTestRoleService"
-Cohesion: 0.31
-Nodes (12): newTestModerationService(), newTestRoleService(), roleIDOf(), TestBanUser_AuthorizedSucceeds(), TestBanUser_HierarchyEnforced(), TestBanUser_RequiresBanPermission(), TestChangeUserRole_AuditWritten(), TestChangeUserRole_CannotAssignAtOrAboveOwnRank() (+4 more)
+Cohesion: 0.23
+Nodes (14): Store, NewModerationService(), newTestModerationService(), newTestRoleService(), roleIDOf(), TestBanUser_AuthorizedSucceeds(), TestBanUser_HierarchyEnforced(), TestBanUser_RequiresBanPermission() (+6 more)
### Community 226 - "event.go"
-Cohesion: 0.27
-Nodes (11): presenceEvents(), BroadcastAllEvent, ChannelEvent, ClientError, Event, ExcludeSenderEvent, SequencedDMEvent, UserTargetedEvent (+3 more)
+Cohesion: 0.22
+Nodes (12): presenceEvents(), TestFlushPresenceQueue_ConcurrentDirectPresenceOrdersLast(), BroadcastAllEvent, ChannelEvent, ClientError, Event, ExcludeSenderEvent, SequencedDMEvent (+4 more)
### Community 227 - "NewTopicRateLimiter"
Cohesion: 0.24
@@ -1393,12 +1399,12 @@ Cohesion: 0.17
Nodes (11): categories, correctness, perf, suspicious, ignorePatterns, public, rules, no-map-spread (+3 more)
### Community 230 - "message.go"
-Cohesion: 0.18
-Nodes (12): MessageService, sanitizeContent(), sanitizePass(), SanitizeText(), sanitizeToFixpoint(), FuzzSanitizeContent(), TestSanitizeContent_EntitySmugglingBlocked(), TestSanitizeContent_PlainTextRoundTrip() (+4 more)
+Cohesion: 0.09
+Nodes (21): AttachmentInfo, ReactionUser, MessageService, Store, requireChannelWritable(), RequireDMNotBlocked(), MessageService, MessageService (+13 more)
### Community 231 - "e2e/dm-system.spec.ts"
-Cohesion: 0.23
-Nodes (10): MOCK_DM_CHANNELS, MOCK_READY_WITH_DMS, mockTauriSessionWithDms(), navigateToMainPageWithDms(), MOCK_AUTH_OK, MOCK_CHANNELS, MOCK_ROLES, submitLogin() (+2 more)
+Cohesion: 0.20
+Nodes (13): MOCK_DM_CHANNELS, MOCK_READY_WITH_DMS, mockTauriSessionWithDms(), navigateToMainPageWithDms(), emitWsEvent(), emitWsMessage(), MOCK_AUTH_OK, MOCK_CHANNELS (+5 more)
### Community 232 - "Queries"
Cohesion: 0.21
@@ -1424,21 +1430,21 @@ Nodes (12): voice_camera (Client -> Server), voice_config (Server -> Client, dir
Cohesion: 0.17
Nodes (12): Choose Your Setup Path, Client Connection Notes, If Remote Users Cannot Connect, Next Steps, Option A: Prebuilt binaries (recommended), Option B: Docker (Linux server), Option C: Build from source, Optional: enable the GIF picker (+4 more)
-### Community 238 - "readPump"
-Cohesion: 0.22
-Nodes (7): TestWritePump_DrainsQueuedFramesAfterCloseSend(), Client, Hub, TestWritePump_DrainsAllNormalBeforeAnyLow(), readPump(), writePump(), TestHandleReconnect_HandshakeWriteFailure_TearsDownOnlyOnce()
+### Community 238 - "OwnCord — Test Audit"
+Cohesion: 0.18
+Nodes (11): 1. Method, 2. Findings, 3. Measured baselines (diff against these next time), 4. Bugs surfaced by the tests, 5. Refuted candidates (do not re-raise), 6. Backlog, Client (`vitest run --coverage`), Go — cross-package (`go test -coverpkg=./... ./...`) (+3 more)
-### Community 239 - "mockTauriFullSessionWithVoice"
-Cohesion: 0.25
-Nodes (6): joinVoiceChannelByName(), mockTauriFullSessionWithVoice(), mockTauriFullSessionWithVoiceFailure(), voiceJoinFailureHandler(), NOTE: These tests do NOT exercise real LiveKit/WebRTC connections., VOICE_STATE_EVENT
+### Community 239 - "newBackupFileDB"
+Cohesion: 0.35
+Nodes (10): newBackupFileDB(), TestBackupToSafe_ErrorKeepsPreexistingFile(), TestBackupToSafe_RejectsDoubleQuote(), TestBackupToSafe_RejectsNullByte(), TestBackupToSafe_RejectsPathOutsideRoot(), TestBackupToSafe_RejectsSemicolon(), TestBackupToSafe_RejectsSingleQuote(), TestBackupToSafe_RejectsSQLComment() (+2 more)
### Community 240 - "index.mjs"
Cohesion: 0.23
Nodes (6): collectLogs(), httpsAgent(), REPO_ROOT, request(), safeParse(), server
-### Community 241 - "countingReadStateStore"
-Cohesion: 0.18
-Nodes (5): failingMembersStore, sync/atomic.Int64, Store, countingReadStateStore, countingStore
+### Community 241 - "TestAdminAPI_PatchRole_MemberLookupFailureBlanketInvalidates"
+Cohesion: 0.33
+Nodes (3): failingMembersStore, TestAdminAPI_PatchRole_MemberLookupFailureBlanketInvalidates(), Store
### Community 242 - "bughunt.harness.mjs"
Cohesion: 0.18
@@ -1460,17 +1466,17 @@ Nodes (11): Audit, Cache and fan-out, Channel Permission Overrides, DELETE /admi
Cohesion: 0.18
Nodes (11): DELETE /admin/api/users/{id}/sessions, Errors, GET /admin/api/stats, GET /admin/api/users, PATCH /admin/api/users/{id}, Request, Response 200 OK, Response 200 OK (+3 more)
-### Community 247 - "deep-link.ts"
-Cohesion: 0.22
-Nodes (11): initDeepLinks(), InviteLink, linkSegments(), log, MessageLink, parseIdSegment(), parseInviteLink(), parseMessageLink() (+3 more)
+### Community 247 - ".DeleteAccount"
+Cohesion: 0.33
+Nodes (7): database/sql.Tx, database/sql.TxOptions, anonymiseUser(), deleteAccountAdminGuard(), deleteAccountCloseDMChannels(), deleteAccountDMChannels(), DB
-### Community 248 - "EventSink"
-Cohesion: 0.18
-Nodes (7): Broadcaster, TestEventDeliveryHasNoGuestPath(), EventSink, NewEventSink(), TestEventSink_Emit_DeliversToBroadcaster(), TestEventSink_Emit_NilBroadcaster_NoOp(), TestHub_SetPluginEventSink_NoOp()
+### Community 248 - "scaledAuthLimit"
+Cohesion: 0.27
+Nodes (7): scaledAuthLimit(), setAuthRateScale(), TestLoginRateLimit_Value(), TestPerUserFailureCapsStayUnscaled(), TestRateLimiterCleanupHorizon_CoversMaxSlowMode(), TestScaledAuthLimit_NeverBelowOne(), TestSetAuthRateScale_ClampsMultiplier()
### Community 249 - "handleChatCommandV2"
-Cohesion: 0.18
-Nodes (13): getCommandConstructor(), TestCanPluginBroadcast_NilServiceFailsClosed(), TestChatCommandConstructor_Errors(), TestHandleChatCommandV2_NoRegistry(), TestHandleVoiceJoinV2_SignalsJoin(), TestHandleVoiceLeaveV2_RateLimited(), TestHandleVoiceLeaveV2_SignalsLeave(), TestVoiceJoinConstructor_Errors() (+5 more)
+Cohesion: 0.15
+Nodes (18): getCommandConstructor(), TestCanPluginBroadcast_NilServiceFailsClosed(), TestChatCommandConstructor_Errors(), TestHandleChatCommandV2_NoRegistry(), TestHandleVoiceJoinV2_SignalsJoin(), TestHandleVoiceLeaveV2_RateLimited(), TestHandleVoiceLeaveV2_SignalsLeave(), TestVoiceJoinConstructor_Errors() (+10 more)
### Community 250 - "slashFS"
Cohesion: 0.29
@@ -1480,25 +1486,25 @@ Nodes (4): slashFS, failReadDirFS, io/fs.File, toSlashPath()
Cohesion: 0.20
Nodes (9): 1. Hunt, 2. Gate (human), 3. Fix, 4. Verify the fixes independently — REQUIRED, Composing the batch, Running the bughunt pipeline, Security findings, Testing the workflows themselves (+1 more)
-### Community 252 - "OverlayManagers.ts"
-Cohesion: 0.07
-Nodes (34): MessageListComponent, findChannelById(), hasMessageJumpHandler(), log, MessageJumpHandler, setMessageJumpHandler(), showToast(), ChannelController (+26 more)
+### Community 252 - "MainPage.ts"
+Cohesion: 0.03
+Nodes (82): appendModerationSection(), showUserVolumeMenu(), VoiceModMenuOptions, IncomingCallBannerComponent, closeActiveLightbox(), clearReactionUsersCache(), setReactionUsersFetcher(), applyConnectionStatus() (+74 more)
### Community 253 - "reconnectAfterCertAccept"
Cohesion: 0.31
Nodes (3): CertReconnectRouter, CertReconnectWs, reconnectAfterCertAccept()
### Community 254 - "VoiceTopic"
-Cohesion: 0.23
-Nodes (11): TestVoiceTopic(), VoiceTopic(), Client, Hub, setupVoiceRoom(), TestRegisterNow_KeepsKeyHolderWhenVoiceStateTransfers(), TestRegisterNow_ReelectsKeyHolderWhenReplacedClientLeavesVoice(), TestRegisterNow_ResumeRestoresVoiceTopicAndE2EEKey() (+3 more)
+Cohesion: 0.14
+Nodes (16): TestEmitEvents_DirectPresenceDropsQueuedEntry(), NewPubSub(), TestUserTopic(), TestVoiceTopic(), topicFor(), UserTopic(), VoiceTopic(), Client (+8 more)
### Community 255 - "emoji-voicemod.parity.spec.ts"
-Cohesion: 0.24
-Nodes (8): CapturedCall, getCapturedCalls(), mockSessionWithCustomEmoji(), mockVoiceSessionWithoutModPermission(), SEEDED_CUSTOM_EMOJI, waitForCapturedCall(), emitWsMessageAndWait(), voiceWsHandlers()
+Cohesion: 0.19
+Nodes (10): CapturedCall, getCapturedCalls(), mockSessionWithCustomEmoji(), mockVoiceSessionWithoutModPermission(), SEEDED_CUSTOM_EMOJI, waitForCapturedCall(), emitWsMessageAndWait(), mockTauriFullSessionWithVoice() (+2 more)
### Community 256 - "voice-e2ee-verify.spec.ts"
-Cohesion: 0.22
-Nodes (6): MOCK_CHANNELS_WITH_CATEGORIES, MOCK_VOICE_STATE, emitPeerAnnounce(), mockE2EEVoiceSession(), PeerCrypto, voiceJoinWithTokenHandler()
+Cohesion: 0.18
+Nodes (7): joinVoiceChannelByName(), MOCK_CHANNELS_WITH_CATEGORIES, MOCK_VOICE_STATE, emitPeerAnnounce(), mockE2EEVoiceSession(), PeerCrypto, voiceJoinWithTokenHandler()
### Community 257 - "include"
Cohesion: 0.20
@@ -1513,7 +1519,7 @@ Cohesion: 0.20
Nodes (10): Custom Emoji, DELETE /api/v1/emoji/{id}, Errors, Errors, GET /api/v1/emoji, GET /api/v1/emoji/{id}/image, POST /api/v1/emoji, Response 200 OK (+2 more)
### Community 260 - "OwnCord — Test-Coverage Audit"
-Cohesion: 0.18
+Cohesion: 0.20
Nodes (10): 1. How coverage was measured (and why the CI number is wrong), 2. Finding closure status, 3. Measured baselines (diff against these next time), 4. Two bugs surfaced by writing the tests, 5. CI gates after this pass, 6. Backlog, Client (`npx vitest run --coverage`), Go — cross-package (`make cover-all`) (+2 more)
### Community 261 - "OriginAcceptOptions"
@@ -1521,12 +1527,12 @@ Cohesion: 0.31
Nodes (8): github.com/coder/websocket.AcceptOptions, OriginAcceptOptions(), TestOriginAcceptOptions_EmptyList(), TestOriginAcceptOptions_ExplicitOrigins(), TestOriginAcceptOptions_MixedWithWildcard(), TestOriginAcceptOptions_NilList(), TestOriginAcceptOptions_ReturnsAcceptOptions(), TestOriginAcceptOptions_WildcardEnablesInsecureSkipVerify()
### Community 262 - "EventRingBuffer"
-Cohesion: 0.13
-Nodes (4): github.com/owncord/server/syncutil.RWMutex, Hub, eventEntry, EventRingBuffer
+Cohesion: 0.31
+Nodes (3): github.com/owncord/server/syncutil.RWMutex, eventEntry, EventRingBuffer
-### Community 263 - "IsUniqueConstraintError"
-Cohesion: 0.21
-Nodes (11): IsUniqueConstraintError(), TestIsUniqueConstraintError_CaseSensitive(), TestIsUniqueConstraintError_MatchesSQLiteMessage(), TestIsUniqueConstraintError_NilError(), TestIsUniqueConstraintError_UnrelatedError(), TestIsUniqueConstraintError_WrappedSQLiteError(), TestSentinelErrors_AreDistinct(), TestSentinelErrors_DoubleWrapped() (+3 more)
+### Community 263 - ".UpdateUserProfile"
+Cohesion: 0.28
+Nodes (4): UpdateUserCustomStatusParams, UpdateUserPasswordParams, UpdateUserProfileParams, Queries
### Community 264 - "newTokenTestDB"
Cohesion: 0.51
@@ -1549,7 +1555,7 @@ Cohesion: 0.22
Nodes (8): CI wiring (`.github/workflows/ci.yml`), Current status: 291 web tests, 291 passed (100%), E2E Test Status — 2026-08-05, Environment notes for local runs, History (dispositions of the old contents of this file), Known issues (open), Resolved (2026-08-04 remediation), Suite inventory
### Community 269 - "render-ledger.mjs"
-Cohesion: 0.43
+Cohesion: 0.27
Nodes (6): main(), render(), selftest(), SEV_RANK, VALID_STATUS, validate()
### Community 270 - "MountGIFRoutes"
@@ -1568,9 +1574,9 @@ Nodes (4): CreateAttachmentParams, GetAttachmentByIDRow, GetAttachmentWithChanne
Cohesion: 0.67
Nodes (6): equalSets(), idSet(), seedVisibilityUser(), sortedKeys(), TestChannelVisibility_RESTWSAgreement(), TestChannelVisibility_UserOverrideAgreement()
-### Community 274 - "seed.go"
-Cohesion: 0.27
-Nodes (10): seedChannel, seedMessage, seedUser, createChannels(), createDMConversation(), createMessages(), createUsers(), messageExists() (+2 more)
+### Community 274 - "OwnCord — Comprehensive Project Audit"
+Cohesion: 0.22
+Nodes (9): 8. Plugin System Governance, 9. Prioritized Top-10 Action List, Bonus (quick wins), CRITICAL Issues, Finding closure status (maintained; last updated 2026-07-20), OwnCord — Comprehensive Project Audit, Plugin Architecture, Strengths (+1 more)
### Community 275 - "Finish the V2 Dispatch Migration (backlog item 11) — Design"
Cohesion: 0.22
@@ -1585,8 +1591,8 @@ Cohesion: 0.22
Nodes (9): chat_bulk_deleted (Server -> Client, broadcast), chat_delete (Client -> Server), chat_deleted (Server -> Client, broadcast), chat_edit (Client -> Server), chat_edited (Server -> Client, broadcast), chat_message (Server -> Client, broadcast), Chat Messages, chat_send (Client -> Server) (+1 more)
### Community 279 - "DMChannelInfo"
-Cohesion: 0.29
-Nodes (7): createDMResponse, listDMsResponse, TestNewDMChannelInfo_EmptyRecipientsIsNotNil(), TestNewDMChannelInfo_ExcludesViewerAndPicksRecipient(), DMChannelInfo, DMUser, NewDMChannelInfo()
+Cohesion: 0.11
+Nodes (15): createDMResponse, listDMsResponse, MemberSummary, TestNewDMChannelInfo_EmptyRecipientsIsNotNil(), TestNewDMChannelInfo_ExcludesViewerAndPicksRecipient(), DMChannelInfo, DMUser, NewDMChannelInfo() (+7 more)
### Community 280 - "RingBuffer"
Cohesion: 0.32
@@ -1632,25 +1638,21 @@ Nodes (8): emoji_update (Server -> Client, broadcast), member_ban (Server -> Cli
Cohesion: 0.57
Nodes (7): message, schema, header(), main(), renderGo(), renderTS(), validate()
-### Community 292 - ".finishVoiceLeave"
-Cohesion: 0.50
-Nodes (3): Client, Hub, leaveVoiceChannelWithRetry()
-
### Community 294 - "Environments, Activation Setup, and Handoff-Doc Mode"
Cohesion: 0.29
Nodes (6): Compaction behaviour, Environments, Activation Setup, and Handoff-Doc Mode, Handoff-doc analysis (when one arrives), Handoff-doc mode (no persistent storage), Recommended activation setup, User-facing documentation
-### Community 295 - "handlePingV2"
-Cohesion: 0.60
-Nodes (4): TestPingV2_HappyPath_ReturnsPongReply(), TestPingV2_NoEvents(), TestPingV2_RateLimited_ReturnsEmpty(), handlePingV2()
+### Community 295 - "newBlockService"
+Cohesion: 0.46
+Nodes (7): newBlockService(), TestBlockService_BlockUser(), TestBlockService_BlockUser_Idempotent(), TestBlockService_BlockUser_Rejections(), TestBlockService_ListBlocked(), TestBlockService_UnblockUser(), TestBlockService_UnblockUser_Rejections()
### Community 296 - "capabilities-scope.test.ts"
Cohesion: 0.29
Nodes (4): Permission, permissions, ScopedPermission, ScopeEntry
-### Community 297 - "savePref"
-Cohesion: 0.38
-Nodes (5): THEME_KEYS, ThemeName, THEMES, savePref(), STORAGE_PREFIX
+### Community 297 - "handleDiagnosticsConnectivity"
+Cohesion: 0.48
+Nodes (6): clientDiag, diagnosticsResponse, serverDiag, voiceDiag, handleDiagnosticsConnectivity(), isPrivateIP()
### Community 298 - "GET /admin/api/updates"
Cohesion: 0.29
@@ -1692,9 +1694,13 @@ Nodes (6): openFileDB(), seedChannelAndUser(), TestFilePool_ConcurrentReadsAndWr
Cohesion: 0.29
Nodes (6): Build command, Building the WASM, hello plugin, Manifest, Prerequisites, Tests
-### Community 309 - "window-state.ts"
-Cohesion: 0.22
-Nodes (7): initWindowState(), isRectOnScreen(), log, MonitorRect, WindowRect, h, PRIMARY
+### Community 308 - "TestAdminAPI_PatchChannel_ArchiveCleansVoice"
+Cohesion: 0.29
+Nodes (6): TestAdminAPI_CreateChannel_SurvivesContextCancelAfterCommit(), TestAdminAPI_PatchChannel_ArchiveCleansVoice(), TestAdminAPI_PatchChannel_ArchiveSurvivesContextCancelAfterCommit(), TestAdminAPI_PatchChannel_UnarchiveDoesNotCleanVoice(), SetCreateChannelPostCommitHook(), SetPatchChannelPostCommitHook()
+
+### Community 309 - "default_verify_schemes"
+Cohesion: 0.67
+Nodes (3): default_verify_schemes(), Vec, SignatureScheme
### Community 310 - "Tauri HTTP Capability Narrowing — Design"
Cohesion: 0.22
@@ -1716,6 +1722,14 @@ Nodes (5): Approval policy, Comprehensive Review (scheduled or fallback), Constr
Cohesion: 0.33
Nodes (4): CertTofuPayload, FIRST_USE, MISMATCH, MISMATCH_LIVE_HOST
+### Community 318 - "navigateToMainPageReady"
+Cohesion: 0.18
+Nodes (5): mockTauriFullSessionWithVoiceFailure(), navigateToMainPageReady(), voiceJoinFailureHandler(), mockUpdaterSession(), NOTE: These tests do NOT exercise real LiveKit/WebRTC connections.
+
+### Community 319 - "4. Dependencies & Supply Chain"
+Cohesion: 0.29
+Nodes (7): 4. Dependencies & Supply Chain, Go Modules — 30 direct deps, ALL exact-pinned ✅, Known Vulnerabilities, License Compliance, Lockfile Status, npm — 13 production deps, ALL floating (^) ⚠️, Overall Posture: **MODERATE RISK** (Go excellent, npm floating)
+
### Community 320 - "tsconfig.build.json"
Cohesion: 0.33
Nodes (5): exclude, extends, include, src, ./tsconfig.json
@@ -1740,21 +1754,25 @@ Nodes (6): First-Run Setup, GET /admin/api/setup/status, POST /admin/api/setup,
Cohesion: 0.33
Nodes (6): GET /api/v1/livekit/health, LiveKit Endpoints, /livekit/* (Reverse Proxy), POST /api/v1/livekit/webhook, Response 200 OK, Response 503 Service Unavailable
-### Community 326 - "types.go"
-Cohesion: 0.38
-Nodes (6): adminContextKey, adminMeResponse, adminUserResponse, toAdminUserResponse(), toAdminUserResponseFromUser(), UserWithRole
+### Community 326 - "5. Test Coverage & Quality"
+Cohesion: 0.29
+Nodes (7): 5. Test Coverage & Quality, Go — Critical Coverage Gaps, Go — Package Coverage, Go — Test Quality: GOOD, TypeScript — E2E Coverage: EXCELLENT, TypeScript — Test Files, TypeScript — Unit Coverage: MINIMAL (<10%)
### Community 327 - "Tailscale Guide (Zero-Config Remote Access)"
-Cohesion: 0.11
-Nodes (14): D4a — Connect, authenticate, replay, D4b — Broadcast fanout and backpressure, D4c — Typed command dispatch, WebSocket / Real-time Engine, Benefits, Setup, Tailscale Guide (Zero-Config Remote Access), TLS Recommendation (+6 more)
+Cohesion: 0.33
+Nodes (6): Benefits, Setup, Tailscale Guide (Zero-Config Remote Access), TLS Recommendation, Voice/Video with Tailscale, Why Tailscale
-### Community 334 - "TestHandlePresenceUpdate_BareStatusReadFailureAbortsBeforeCommit"
+### Community 329 - "perm_grid_test.go"
Cohesion: 0.48
-Nodes (4): Store, TestHandlePresenceUpdate_BareStatusReadFailureAbortsBeforeCommit(), TestHandlePresenceUpdate_CustomStatusWriteFailureKeepsStoredText(), faultyStore
+Nodes (6): overrideMatrixBits(), permGridBits(), TestAdminPanelOverrideMatrixCoversChannelScopedBits(), TestAdminPanelOverrideMatrixHasSingleDefinedBits(), TestAdminPanelPermGridCoversEveryPermissionBit(), TestAdminPanelPermGridHasNoDuplicateOrCompositeBits()
-### Community 335 - "New"
-Cohesion: 0.38
-Nodes (6): NewDMService(), TestDMService_CreateDM_AllowsLapsedTemporaryBan(), TestDMService_CreateDM_RefusesBannedRecipient(), TestDMService_CreateGroupDM_RefusesBannedRecipient(), Store, New()
+### Community 334 - "admin-static-channel-perms.test.ts"
+Cohesion: 0.33
+Nodes (4): ADMIN_HTML, ADMIN_HTML_PATH, ADMIN_HTML_SOURCE, FetchCall
+
+### Community 335 - "NewDMService"
+Cohesion: 0.53
+Nodes (5): NewDMService(), TestDMService_CreateDM_AllowsLapsedTemporaryBan(), TestDMService_CreateDM_RefusesBannedRecipient(), TestDMService_CreateGroupDM_RefusesBannedRecipient(), TestDMService_CreateGroupDM_SurvivesCancelledPostCommitRead()
### Community 336 - "GET /api/v1/client-update/{target}/{current_version}"
Cohesion: 0.40
@@ -1772,13 +1790,13 @@ Nodes (5): voice_e2ee_announce (Client -> Server), voice_e2ee_announce (Server -
Cohesion: 0.40
Nodes (4): Additional Context, Alternatives Considered, Problem, Proposed Solution
-### Community 341 - "buildMetricsRouter"
-Cohesion: 0.53
-Nodes (5): buildMetricsRouter(), TestHandleMetrics_AdminIPRestrict_AllowsAdmin(), TestHandleMetrics_AdminIPRestrict_BlocksNonAdmin(), TestHandleMetrics_ReturnsExpectedFields(), TestHandleMetrics_WithoutLiveKitHealthCheck()
+### Community 341 - "isAddrInUse"
+Cohesion: 0.40
+Nodes (4): isAddrInUse(), TestIsAddrInUse_RealBindConflict(), TestIsAddrInUse_Table(), TestServeWithBindRetry()
-### Community 342 - "RunningInContainer"
+### Community 342 - "MetricsSources"
Cohesion: 0.50
-Nodes (3): RunningInContainer(), TestRunningInContainer_BareMetalDefault(), TestRunningInContainer_EnvSemantics()
+Nodes (4): EventPersisterMetrics, MetricsSources, ServerMetrics, database/sql.DBStats
### Community 344 - "Permission-Middleware Consolidation (audit finding A-2026-07-16) — Design"
Cohesion: 0.33
@@ -1852,29 +1870,73 @@ Nodes (3): Presence, presence (Server -> Client, broadcast), presence_update (Cl
Cohesion: 0.67
Nodes (3): Transport Layer, Transport Limits, WebSocket Endpoint
-### Community 449 - "RateLimiter"
-Cohesion: 0.21
-Nodes (9): entry, lockoutEntry, LockoutPersister, rateLimiterShard, time.Duration, RateLimiter, NewPersistentRateLimiter(), newRateLimiter() (+1 more)
+### Community 414 - "updater.test.ts"
+Cohesion: 0.40
+Nodes (4): invoke, listen, relaunch, unlisten
+
+### Community 415 - "1. Architecture"
+Cohesion: 0.40
+Nodes (5): 1. Architecture, Anti-patterns, Communication Patterns, Dependency Direction, Layer Map
+
+### Community 419 - "6. CI/CD & DevEx"
+Cohesion: 0.40
+Nodes (5): 6. CI/CD & DevEx, Build Reproducibility, Gaps, Linting Enforcement, Pipeline Gates
+
+### Community 420 - "7. Observability"
+Cohesion: 0.40
+Nodes (5): 7. Observability, Client-Side: LIMITED ⚠️, Error Surfacing: GOOD ✅, Logging: STRONG ✅, Metrics & Tracing: PRESENT (build-tag gated)
+
+### Community 421 - "syscall.SysProcAttr"
+Cohesion: 0.40
+Nodes (3): syscall.SysProcAttr, liveKitSysProcAttr(), liveKitSysProcAttr()
+
+### Community 422 - "Security Policy"
+Cohesion: 0.40
+Nodes (4): Hardening documentation, Reporting a vulnerability, Security Policy, Supported versions
+
+### Community 423 - "erroringMembersStore"
+Cohesion: 0.40
+Nodes (3): Store, erroringMembersStore, rendezvousListStore
+
+### Community 449 - "Capture"
+Cohesion: 0.50
+Nodes (3): Capture(), panicWithSecretArgs(), TestCaptureOmitsArguments()
+
+### Community 537 - "D7 — Module map"
+Cohesion: 0.50
+Nodes (4): Client Architecture (Tauri), D7 — Module map, Key mechanisms, Quality tooling
+
+### Community 538 - "WebSocket / Real-time Engine"
+Cohesion: 0.50
+Nodes (4): D4a — Connect, authenticate, replay, D4b — Broadcast fanout and backpressure, D4c — Typed command dispatch, WebSocket / Real-time Engine
+
+### Community 539 - "Audit 2026-07-19 — Maintainer Decisions"
+Cohesion: 0.50
+Nodes (4): Audit 2026-07-19 — Maintainer Decisions, Decisions, Explicitly not decided here, Suggested sequencing
+
+### Community 541 - "owncord-introspect (MCP dev tool)"
+Cohesion: 0.50
+Nodes (3): Full documentation, owncord-introspect (MCP dev tool), Quickstart
## Knowledge Gaps
-- **1822 isolated node(s):** `DispatcherCleanup`, `ClientMessageTypeValue`, `MessageTypeValue`, `ServerMessageTypeValue`, `RemoteVideoCallback` (+1817 more)
+- **1881 isolated node(s):** `here`, `scenarios`, `FOUR_FILES`, `PROVE_FAIL`, `meta` (+1876 more)
These have ≤1 connection - possible missing edges or undocumented components.
-- **98 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
+- **100 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
-- **Why does `DB` connect `DB` to `testing.T`, `openMigratedMemory`, `context.Context`, `buildChannelRouter`, `newTokenTestDB`, `middleware_and_spawn_test.go`, `waitRegistered`, `NewAdminAPI`, `MountGIFRoutes`, `TestMigrate_UpgradeFromMigration019PreservesData`, `newChannelTestAPI`, `net/http.HandlerFunc`, `handleLogStream`, `handleSetup`, `seed.go`, `newPurgeService`, `buildDMRouter`, `newHandlerHub`, `newAuthTestDB`, `newMigratedTestDB`, `newTestPermService`, `newHarvestVoiceDB`, `drainChanTimeout`, `User`, `database/sql.Result`, `newUploadTestDB`, `writeJSON`, `newAdminTestDB`, `HashToken`, `NewChecker`, `middleware_test.go`, `Result`, `handlers_backup.go`, `NewRouter`, `net/http.Handler`, `openFileDB`, `newTestDB`, `newServeHub`, `NewMessageService`, `seedMemberUser`, `postJSONWithToken`, `newVoiceTestDB`, `message_reactions_test.go`, `run`, `itoa`, `newUserSvc`, `plugins_handler_test.go`, `types.go`, `Role`, `.handleFreshConnect`, `newEmojiService`, `handleVoiceTokenRefreshV2`, `messages_test.go`, `openAdminTestDB`, `db/db.go`, `newMentionFixture`, `Channel`, `Migrate`, `Hub`, `newTestMessageService`, `NewTestClient`, `TestChannelVisibility_RESTWSAgreement`, `emoji_handler_test.go`, `handleCreateEmoji`, `doRequest`, `newTestRoleService`, `MigrateFS`, `newOverrideFixture`, `errDMChannelIDsStore`, `NewRegistry`, `voice_moderation_test.go`, `TestHandleVoiceCameraV2_RefusedWhenScreenshareSlotFull`, `countingReadStateStore`, `newRoleCRUDService`, `Checker`, `gif_handler_test.go`?**
- _High betweenness centrality (0.037) - this node is a cross-community bridge._
+- **Why does `DB` connect `DB` to `testing.T`, `openMigratedMemory`, `context.Context`, `buildChannelRouter`, `waitRegistered`, `NewAdminAPI`, `writeErr`, `NewTestClient`, `newHandlerHub`, `drainChanTimeout`, `buildDMRouter`, `newAuthTestDB`, `newMigratedTestDB`, `EventPersister`, `Hub`, `database/sql.Result`, `net/http.Handler`, `net/http.HandlerFunc`, `newAdminTestDB`, `HashToken`, `plugin/registry_test.go`, `newDeafenRaceDB`, `NewChecker`, `middleware_test.go`, `Result`, `NewRouter`, `profileCreateToken`, `RateLimiter`, `newTestDB`, `newServeHub`, `seedMemberUser`, `postJSONWithToken`, `newVoiceTestDB`, `doRequest`, `Role`, `openAdminTestDB`, `newMentionFixture`, `Channel`, `Migrate`, `Hub`, `newTestMessageService`, `deps.go`, `emoji_handler_test.go`, `DB`, `openMemory`, `PermissionService`, `joinVoice`, `NewHandler`, `newRoleCRUDService`, `VoiceDeps`, `gif_handler_test.go`, `User`, `middleware_and_spawn_test.go`, `newPurgeService`, `handleVoiceTokenRefreshV2`, `newChannelTestAPI`, `handleLogStream`, `handleSetup`, `newHarvestVoiceDB`, `seedChannel`, `AuditWriter`, `net/http.Request`, `handleRestoreBackup`, `newDMFixture`, `Server/main.go`, `newUserSvc`, `plugins_handler_test.go`, `github.com/coder/websocket.Conn`, `itoa`, `newTestRoleService`, `newBackupFileDB`, `.DeleteAccount`, `newTokenTestDB`, `MountGIFRoutes`, `TestMigrate_UpgradeFromMigration019PreservesData`, `TestChannelVisibility_RESTWSAgreement`, `DMChannelInfo`, `newBlockService`, `openFileDB`, `TestHandleVoiceCameraV2_RefusedWhenScreenshareSlotFull`, `errDMChannelIDsStore`?**
+ _High betweenness centrality (0.019) - this node is a cross-community bridge._
- **Why does `installTestPlugin()` connect `newMigratedTestDB` to `testing.T`, `context.Context`?**
_High betweenness centrality (0.016) - this node is a cross-community bridge._
-- **Why does `mustSetSetting()` connect `chdirTemp` to `testing.T`, `context.Context`?**
+- **Why does `mustSetSetting()` connect `NewHandler` to `testing.T`, `context.Context`?**
_High betweenness centrality (0.016) - this node is a cross-community bridge._
-- **Are the 281 inferred relationships involving `waitRegistered()` (e.g. with `TestChannelFocus_AdminBypassesDeny()` and `TestChannelFocus_AllowedByDefault()`) actually correct?**
- _`waitRegistered()` has 281 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 284 inferred relationships involving `waitRegistered()` (e.g. with `TestChannelFocus_AdminBypassesDeny()` and `TestChannelFocus_AllowedByDefault()`) actually correct?**
+ _`waitRegistered()` has 284 INFERRED edges - model-reasoned connections that need verification._
- **Are the 2 inferred relationships involving `NewTestClientWithUser()` (e.g. with `TestRefreshChannelVisibility_ReconnectDuringFanOutActsOnLiveClient()` and `TestHandleReconnect_VisibilityChangeDuringHandshake_ForcesFullReady()`) actually correct?**
_`NewTestClientWithUser()` has 2 INFERRED edges - model-reasoned connections that need verification._
-- **What connects `DispatcherCleanup`, `ClientMessageTypeValue`, `MessageTypeValue` to the rest of the system?**
- _1822 weakly-connected nodes found - possible documentation gaps or missing edges._
+- **What connects `here`, `scenarios`, `FOUR_FILES` to the rest of the system?**
+ _1881 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `members.store.ts` be split into smaller, more focused modules?**
- _Cohesion score 0.03450134770889488 - nodes in this community are weakly interconnected._
\ No newline at end of file
+ _Cohesion score 0.030991735537190084 - nodes in this community are weakly interconnected._
\ No newline at end of file
diff --git a/graphify-out/graph.html b/graphify-out/graph.html
index 16a8a20e..b2e96c81 100644
--- a/graphify-out/graph.html
+++ b/graphify-out/graph.html
@@ -63,12 +63,12 @@
- 529 nodes · 1449 edges · 529 communities
+ 550 nodes · 1469 edges · 550 communities