Repository navigation
fix(channels): hiddenChannels privacy test and #257 follow-ups (#276) - #283
Conversation
…e trade-off Relates to #276 - hiddenChannels is on the public GET /api/channels, so a new test pins that it only ever names channels with stored traffic: an unreviewed suggestion is never published there (mutant G1, the EXISTS clause dropped, now fails). - A plain /api/channels request (no includeEncrypted) hands hideRevoked GetChannels' cached slice itself; a new test checks the cache is unchanged afterwards (mutant G2, compacting in place, now fails). - The node harness gains the per-path TTL cache the real api() has, so the invalidateApiCache('/channels') in refreshChannelList is covered on both the revoke and the approval path (mutant F3 now fails). - The "not approved", rather than "was approved before", rule is kept and documented: a pending suggestion can hide a name whose stored messages were decrypted through a config entry since removed. A test pins both that and the config-name exception.
Rapport — CS-Macmini PR#283 #276 — head c0e6508Status: All five points of #276 are addressed; the three mutants the round-2 review reported as surviving (G1, G2, F3) now each fail, and each is caught only by the new test for its point. Draft, not marked ready, not merged. No production behaviour changes. Evidence tags: [T] = test or command run in this session on this head. [A] = analysis/reading of code. [K] = taken from #276 or the #257 review, not re-checked here. Requirements
Points 1–4 are test gaps, so there is no production change to be "red before". Each test is red under its mutant and green on this head; points 1–3 are additionally shown to be red only by the new test, over the full suite. Point 4: the decisionKept the current rule, documented the trade-off. "Hide only names that were ever approved" needs a marker that survives a re-suggestion, and the obvious candidate does not exist: What bounds the trade-off, each pinned by the new test: it needs stored Tests on this head
Rules
CIRun 37398802504 on head
Green first time: no job was re-run, and the known flaky test (#271) did not fire. Remaining
Not verified
|
Review — CS-MacBook PR#283 — head c0e6508Dom: APPROVE Independent, read-only review of a merged tree ( Evidence tags: [T] run this session on the merged tree · [A] code/diff reading · [K] from #276 / the #257 round-2 review, not re-derived. Findings
Requirements (each acceptance criterion: test red-before / green-after)
Points 1–4 are test gaps, so there is no production line to be "red before"; each test is green on this head and red under its mutant, and I confirmed G1/G2/F3 are each caught only by their new test over the full suite. Mutants (all run by me on the merged tree)
All mutants reverted; the merged tree was diffed back to the head archive for My own edge case (not in the PR)
I also checked the consistency the privacy fix depends on: Tests on the merged tree (base 4f1de04)
Scope / invariants
Not verified
I ran this review from an isolated scratch copy; I did not touch the PR, other worktrees, or any remote. |
Relates to #276
What
Follow-ups from the round-2 review of #257. Four of the five points were test
gaps — three mutants the merged suite did not catch — plus a semantics
decision and a docs sentence. No production behaviour changes: the diff is
tests, docs and one
openapi.godescription string.Plan, point by point
1. Privacy:
hiddenChannelsmust never name a proposal without traffic (#276 point 3)hiddenChannelsis served on the publicGET /api/channels, whileproposals themselves are only visible through the authenticated admin route.
The
AND EXISTS (… transmissions …)clause inListNotApprovedChannelsWithTrafficis what keeps a pending suggestion — aname anyone can submit — out of that field. Nothing in
channelschangeswhether the clause is there or not, so only an assertion on
hiddenChannelscatches it.
TestHiddenChannelsNeverNamesAProposalWithoutTrafficputs one real hiddenchannel (revoked, with stored traffic) next to three proposals with no traffic
— pending, rejected and revoked — and asserts
hiddenChannelsis exactly theone with traffic, on the default request and on
?includeEncrypted=true.2. Cache-safety copy on the default path (#276 point 2)
handleChannelscopies the slice only on theincludeEncryptedpath, whichis the one
TestRevokedFilterDoesNotMutateCacheAndKeepsEncryptedexercises.On a plain request
hideRevokedreceivesGetChannels' cached slice itself,so compacting in place corrupts the shared cache for every later request.
TestPlainChannelListRequestDoesNotMutateTheCachedSlicegives three channelsdistinct
first_seenvalues soGetChannels'last_activity DESCorder isdeterministic and the hidden channel sits in the middle (asserted as a
precondition: the test fails loudly if the hidden channel ends up last, where
an in-place compaction would be invisible). It then compares the cached names
before and after three list requests.
3. The client cache TTL hole (#276 point 4)
The node harness in
test-channels-client-state-152.jsstubbedapi()with nocache and
invalidateApiCache()with a no-op, so a page that forgets toinvalidate looked correct. The harness now has an opt-in (
apiTtlCache)per-path TTL cache that mirrors
public/app.js:ttl,bust, and aprefix-wide
invalidateApiCache.Two tests use it, one per entry point into
refreshChannelList(
onRevoked/#251 andonApproved/#232). Both cache two/channelsvariants(all regions and
?region=CPH), apply the admin decision, then switch theregion back without a bust and inside the 15 s TTL. That is the real
browser path:
RegionFilter.onChangecallsloadChannels(true)with no bust,so only
invalidateApiCache('/channels')keeps the pre-decision entry fromanswering. The
bust: truethat #243 added covers just the one requestrefreshChannelListmakes itself.4. Semantics: an anonymous suggestion can hide a channel the admin never acted on (#276 point 1)
Decision: keep the current rule and document the trade-off. Reason: "hide
only names that were ever approved" needs a marker that survives a
re-suggestion, and the obvious candidate does not exist.
submitChannelProposalresurrects a revoked row withreviewed_at = NULL(
cmd/ingestor/channel_proposals.go) — that reset is what makes aresubmission idempotent across a crash-and-retry — so a
reviewed_atrulecannot tell a fresh suggestion from a re-suggestion after a revoke. It would
re-open exactly the hole #257 round 2 closed, where anyone could unhide a
revoked channel by suggesting the name again. A real history column is an
ingestor-side schema change plus a write path, out of scope for a test-gap
follow-up, and
cmd/serverstays read-only.What bounds the trade-off, and what the new test pins:
payload_type = 5traffic for that exact name, so anever-decrypted channel can never be hidden;
whatever its proposal says;
GET /api/channels/{hash}/messages, and/api/analytics/channelsstillcounts it;
lists the channel again.
TestAnonymousPendingSuggestionHidesAHistoricalConfigChannelwalks all four:#oldcfg(traffic, not in the ingestor's names file) and#chat(traffic, inthe names file) are both listed with no proposal; after an unreviewed pending
row for each,
#oldcfgis hidden and named inhiddenChannelswhile#chatstays listed; the history still answers; approving lists
#oldcfgagain.5. Docs (#276 point 5)
docs/api-spec.md,docs/user-guide/channels.mdandcmd/server/openapi.gonow say
hiddenChannelsis global and not filtered byregion, and thatanother open tab keeps the set it last loaded until its list reloads —
including after a re-approval, where live messages do not re-create the row in
that tab until then.
api-spec.mdandchannels.mdalso carry the point-4trade-off.
Tests
hiddenChannelsTestHiddenChannelsNeverNamesAProposalWithoutTrafficAND EXISTS (… transmissions …)droppedTestPlainChannelListRequestDoesNotMutateTheCachedSliceout := resp.Channels(compact in place)invalidateApiCache('/channels')#276tests intest-channels-client-state-152.jsrefreshChannelListTestAnonymousPendingSuggestionHidesAHistoricalConfigChannelp.status = 'revoked'only; S2 built-in exception droppedG1, G2 and F3 are the three mutants the round-2 review reported as surviving;
each now fails, and each is caught only by the new test for its point.
Runs on this head:
cmd/server,cmd/ingestorandinternal/channelregistryvet +go test -count=1all ok;sh test-all.sh220/220;
node test-frontend-helpers.js707/707; 16 channel E2Es against alocal Go server on a CI-prepared
e2e-fixture.db, plustest-channel-proposals-e2e.js26/26 andtest-channels-client-state-152-decrypt-e2e.js18/18.scripts/check-xss-sinks.sh --diff origin/masterexit 0. No newmap[string]interface{}outside tests, no.github/change (fork guardsunchanged: 9 and 1).
Remaining
channel_proposals(ingestor-sideschema + write path) stays unfiled; this PR documents the trade-off instead.
proposal row while messages are stored (documented in fix(channels): hide revoked shared channels from the channel list (#251) #257, needs an
ingestor change).
hiddenChannelshas no cross-tab push; only documented, not changed.🤖 Generated with Claude Code