Repository navigation
fix(channels): hide revoked shared channels from the channel list (#251) - #257
Conversation
Relates to #251 GET /api/channels leaves out names whose proposal is revoked (history stays stored and readable; re-approval lists the channel again; names decrypted via the built-in/config list are never hidden, fail-open without the names file). The revoked set rides on the existing 10 s approved snapshot, so there is no per-request query. Admin list marks case near-duplicates (nearDuplicateOf); case stays significant because the hashtag key is sha256 of the exact name. Admin revoke now refreshes the channel list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rapport — CS-MacBook PR#257 #251 — head 02b99c4Status: Implemented and locally verified; draft PR open, CI pending (not polled); not merged, not marked ready, issue not closed. Evidence tags: [T] = test or command run in this session, [A] = analysis/reading of code or source, [K] = taken from existing knowledge or the repo's own docs, not re-checked here. Case handling — decided from source
Acceptance criteria
All four mutants were applied one at a time and reverted; the working tree was clean afterwards [T]. PerformanceNo per-request DB scan. The revoked names are read with one extra bounded query (capped at 1024) inside the existing 10 s approved-names snapshot refresh, and the hidden set is built once per refresh. Per request the filter is O(channels) map lookups on a copy; with nothing revoked it returns the cached slice untouched. Follow-up commit 02b99c4The first commit (0e0ff5e) added two mentions of Verification run (base 9989b97)
CIPending, not polled. Known flaky: #244 and #250. Remaining / known limits
|
dborup
left a comment
There was a problem hiding this comment.
Reviewed commit 02b99c45e9be6a63fc5d8292b8353dad006c24e0. I found three reproducible P2 issues that should be addressed before merging.
1. [P2] Apply the admin status filter before limiting the result
cmd/server/channel_proposals.go:495–508
The new all-status query applies the default limit of 484 before filtering for the requested status. With one older approved proposal and 484 newer rejected proposals, GET /api/admin/channel-proposals?status=approved returns an empty list although the approved channel is still active. The administrator therefore loses access to its Remove action. The same problem can hide older pending proposals.
I reproduced this with the real handler and existing test fixture. The previous status-filtered SQL query returns the approved row; the new endpoint returns zero rows. Preserve the filtered row query and gather cross-status case-duplicate metadata separately, so that the hint cannot remove actionable proposals from the selected view.
2. [P2] Keep revoked channels hidden when old decoded transmissions arrive over WebSocket
public/channels.js:2017–2019, with the row creation at 1785–1797.
After a revoke, the new REST refresh correctly removes the channel and closes its conversation. However, if a previously decoded transmission is subsequently received through a new observer or path, the ingestor retains its stored decoded_json, and the server broadcasts that old CHAN payload for the new observation. processWSBatch then creates the channel row again even though /api/channels continues to exclude it. No locally stored channel key is needed.
Reproduced using the production frontend from this exact commit and the server's actual type: "packet" envelope:
local revoke: channelStillListed=false, selectedHash=null
after old CHAN WebSocket observation: channelStillListed=true
Apply the revoked-channel visibility policy to live channel-list updates as well. This can be done without deleting historical messages or suppressing the underlying packet observation globally.
3. [P2] Do not let the revoked-name snapshot cap restore still-revoked channels
internal/channelregistry/sql.go:59–62
ListRevokedNames returns only the 1,024 most recently revoked names. Rejected/revoked rows have time-based retention, but there is no corresponding 1,024-row cap on the table. After 1,024 later revocations, an older revoked name falls out of hidden, and its stored messages make it appear in /api/channels again while its proposal remains revoked and is still within retention.
A deterministic handler test confirms that the old channel is initially hidden and then reappears after adding 1,024 newer revoked rows and expiring the snapshot. This differs from the explicitly documented pending-resubmission and retention behavior. Use a bounded approach that preserves the decision for every channel being listed, rather than silently treating an omitted revoked row as visible.
Documentation note
The user guide promises that other open conversations close within about 15 seconds. The Channels page currently has no periodic channel-list reload or cross-tab revoke notification: its interval only updates relative timestamps. Running 60 interval callbacks produced zero new /channels requests, and another tab's selected channel remained open. Either implement the propagation or describe the actual next-refresh behavior.
Verification
- Existing targeted revoked-channel Go tests passed.
test-channel-proposals.js: 39 passed, 0 failed.test-channels-client-state-152.js: 66 passed, 0 failed.- The three additional review probes reproduced the issues above.
- Pending re-suggestion and retention restoring visibility are explicitly documented policy choices and are not included as findings here.
Review — CS-pve-agent3 PR#257 hide-revoked — head 02b99c4Dom: REQUEST CHANGES Independent, read-only review. Evidence tags: [T] = test, probe or command run in this review; [A] = analysis/reading of code or source; [K] = taken from the PR, the author's report or existing knowledge, not re-checked here. The core filter is well built. It hides the right names, never hides built-in or config names, fails open, rides on the snapshot, and copies instead of mutating the cache. Every mutant I ran was caught. Two problems block approval: a regression in the admin list that this PR introduces, and a re-suggest/reject path that brings a revoked channel back permanently. Findings
Suggested direction for 1: keep the status-filtered Suggested direction for 2 (author's call): hide every non-approved proposal name that is not built-in, i.e. 1. Hide on revoke
2. Built-in channels are never hidden
3. Re-approval and the author's leftovers
4. Letter case
5. Frontend
6. Documentation
7. Rules
Tests (merged tree =
|
| Mutant | Result |
|---|---|
M1 filter removed (hideRevoked returns early) |
caught: 6 server tests fail |
| M2 built-in channel hidden | caught: TestBuiltinChannelIsNeverHiddenByRevokedProposal |
| M3 re-approval ignored (approved names added to the hidden set) | caught: 4 tests incl. TestReapprovedChannelIsListedAgain |
M3b defensive keep subtraction removed on its own |
survives. Expected: the UNIQUE name makes it unreachable, as the author notes |
| M4a hiding made case-insensitive | caught: TestRevokedChannelHidesOnlyTheExactName |
| M4b near-duplicate fold made case-sensitive | caught: TestAdminListFlagsCaseNearDuplicates |
M4c NormalizeName folds case |
caught: 1 channelregistry, 7 ingestor, 1 server test |
F1 onRevoked call removed / F2 unwired in channels.js / F3 near-dup escape removed |
caught by test-channel-proposals.js / test-channels-client-state-152.js / test-channel-proposals.js |
CI (run 37316732571 on head): Go Build & Test ✅. Playwright E2E ❌ in test-issue-180-packets-url-modal-e2e.js: "Clear Filters on #/packets/?… reload shows 3 packets, Clear showed 5". This is neither #250 nor #256. It does not touch channel code, and it passed 2/2 locally against the merged build. It looks like a time-window flake, but it is not on the known list. Because the step is fail-fast, the E2Es after it did not run in CI, including test-channel-proposals-e2e.js. They are covered by my local run above [T].
Not verified
- Behaviour on the new master
0572e7f9(only the merge was checked). - Whether finding 4 (the "0 messages" header) also happens on master for other unlisted hashes.
- The "Known channels (catalogue)" section of the channel list with a revoked name.
- Android/Ripple lowercasing claims from the meshcore-cli help text [K]; meshcore-open citations [K].
- The author's benchmark numbers; I did not run
BenchmarkVisibleChannels. - Browser validation on a staging or production instance (out of scope).
…e-revoked-channels
Relates to #251 - admin list: keep the status filter in SQL before the row limit; the case near-duplicate hint reads a separate name-only list - hide channels whose proposal is not approved (revoked, re-suggested and pending, or rejected), decided per channel with stored traffic instead of from a capped list of revoked rows - name the hidden channels in /api/channels (hiddenChannels) so a live WebSocket packet does not re-create their list row - docs: next-refresh behaviour, reject path, analytics counts Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review feedback addressed (commit
|
| Finding | Mutant | Result |
|---|---|---|
| 1 | status filter moved out of SQL (ListProposals(…, "", limit)) |
caught: TestAdminStatusFilterIsAppliedBeforeTheLimit, TestAdminChannelProposalListAndDecisions, TestAdminListFlagsCaseNearDuplicates [T] |
| 2 | only revoked hidden (p.status = 'revoked') |
caught: TestPendingAndRejectedProposalsKeepAChannelHidden [T] |
| 3 | processWSBatch guard removed |
caught: the red run of the new test before the change (66 passed, 1 failed) [T] |
| 4 | ORDER BY … LIMIT 1024 reintroduced |
first survived, because my test's newer rows had no stored traffic and so never reached the cap; I added traffic to each newer row, after which it is caught by TestRevokedChannelStaysHiddenBehindManyNewerRevokedRows [T] |
| round 1, re-run on the new code | built-in channel hidden | caught: TestBuiltinChannelIsNeverHiddenByRevokedProposal, TestPendingAndRejectedProposalsKeepAChannelHidden [T] |
| round 1, re-run on the new code | re-approval ignored (approved rows also hidden) | caught: 6 tests incl. TestReapprovedChannelIsListedAgain [T] |
The round-1 "filter removed" and "case folded" mutants were not re-run on this code; the filter and NormalizeName are unchanged apart from hideRevoked also setting hiddenChannels [A].
CI
Run 37324492678 on head 9c80787: Go Build & Test pending at the time of writing; not polled. The previous head's Playwright step failed in test-issue-180-packets-url-modal-e2e.js (reported in the review as a time-window flake, not on the known list); I did not investigate it and it does not touch channel code [K].
Remaining
- A hidden channel returns when retention prunes its non-approved proposal row (
retentionDaysafter the review) while messages are still stored. Fixing that needs an ingestor-side change (do not prune while messages exist); a follow-up. builtin-channels.jsonis capped at 4096 names, so an operator with more config names than that could see one of them hidden if it also has a non-approved proposal. Unrealistic today (the rainbow table has about 320 names); not changed./api/analytics/channelsstill counts revoked channels (documented).- A second open tab keeps a revoked channel and its conversation until its list reloads (documented; no cross-tab push).
Review — CS-pve-agent3 PR#257 runde 2 — head 9c80787Dom: APPROVE med nits This is an independent, read-only re-review of round 2. Evidence tags: [T] means a test, probe or command I ran in this review. [A] means analysis or reading of code. [K] means taken from the PR, the author's follow-up or an earlier review, and not re-checked here. Every finding from my round-1 review and from the formal review (three P2s and the docs note) is fixed, and a test locks each fix. My two round-1 probes now pass on head. The new semantics, Findings
1. Earlier findings: fixed and locked
Other mutants: G3 ( 2. New semantics
|
| Mutant | Result |
|---|---|
| G1 EXISTS clause dropped | survives PR suite (finding 3); killed by my probes |
G2 hideRevoked compacts the cached slice in place |
survives PR suite (finding 2); killed by my probe |
G3 hiddenChannels never filled |
caught |
| G4 unreadable names file treated as known-empty (fail-closed) | caught |
| G5 near-duplicate index from returned rows only | caught |
| G6 hidden set not rebuilt on refresh | caught (4 tests) |
| F1 client hidden set accumulates | caught |
| F2 client hidden set from the wrong field | caught |
F3 refreshChannelList without cache invalidation |
survives unit tests (finding 4) |
Probes (scratch file, not part of the PR), results on head / on origin/master:
- Round-1 probes: pass / approved-tab passes, resuggest fails (master has no hiding).
- Never-decrypted, built-in in any status, fail-open,
hiddenChannelssubset, plain-request cache: pass / n/a. - Anonymous pending: hidden / listed (finding 1).
Not verified
- A browser check of the multi-tab behaviour after re-approval (finding 6). This is code reading only.
- Finding 1 end to end through the real submit endpoint and ingestor. I simulated it with the row the ingestor inserts.
- The author's
BenchmarkHideRevokednumbers; I did not run it. - Staging or production behaviour (out of scope).
Relates to #251
What
An administrator who revokes a shared hashtag channel expects it to leave the channel list. Until now it stayed, because already-decoded messages keep the channel in the list query.
GET /api/channelsnow leaves out channels with stored messages whose proposal is not approved (revoked, re-suggested and pending, or rejected), and names them inhiddenChannelsso live WebSocket packets do not re-create their rows. Nothing is deleted:GET /api/channels/{hash}/messagesstill returns the history, and the channel comes back (with its history) when the proposal is approved again.cmd/serverstays read-only; it only filters.hashChannels,channelKeys) is never hidden. If the ingestor'sbuiltin-channels.jsonis missing or unreadable, nothing is hidden (fail-open).sha256("#name")[:16]of the exact bytes (MeshCoredocs/companion_protocol.md"Hashtag Channels"; meshcore-openderivePskFromHashtagdoes not change case), so#HelloWorldand#helloworldare different channels with different keys. They stay separate proposals; the admin list now marks case near-duplicates (nearDuplicateOf, also vs. built-in names) and the review dialog shows "Same name in different case".Performance
No per-request DB scan. The revoked set is read with one extra bounded query (
status = 'revoked', capped at 1024) in the existing 10 s approved-names snapshot refresh, and the hidden set (revoked − approved − built-in) is built once per refresh. Per request the filter is O(channels) map lookups on a copy; with nothing revoked it returns the cached slice untouched.BenchmarkVisibleChannels(5000 channels, 3 revoked): ~80 µs/op, 1 alloc (one copy of the list, only when something is revoked).TestRevokedSetIsServedFromTheSnapshotdrops the table and checks the list is still answered from the snapshot.Tests
includeEncryptedunaffected, no-op without revoked rows, missing table,ListRevokedNames, near-duplicate marking incl. status filter, benchmark.#test); the two case variants are two proposals with two keys.onRevokedwiring, a revoked row is not carried over by the client merge and closes an open conversation.test-channel-proposals-e2e.js): a stored message on the shared channel; after the revoke the channel is gone from/api/channels, history readable, still gone after a restart.Docs
cmd/server/openapi.go,docs/api-spec.md,docs/user-guide/channels.md,docs/user-guide/configuration.md.Known limits
retentionDays; a hidden channel with surviving messages then reappears./api/analytics/channelsis unchanged and still counts these channels (documented).🤖 Generated with Claude Code