feat(discord): channel-scoped allowlist for guild messages - #248
Merged
TerrysPOV merged 4 commits intoJul 19, 2026
Merged
Conversation
Adds discord.channelAllowedUserIds config (channelId -> user IDs) that grants access to a specific guild channel without adding the user to the global allowedUserIds list. Channel-scoped access never applies to DMs, even for the same user ID and channel ID as a key — global allowedUserIds remains the only way to reach Sage via DM. Extracted the composed check into isDiscordAuthorized() in allowlist.ts (alongside the existing isAllowed()) so it's unit testable independent of the Discord message-handling flow.
BCDel89
force-pushed
the
bcdel89/discord-channel-allowlist
branch
from
July 5, 2026 16:32
6186a5e to
135a130
Compare
# Conflicts: # .claude-plugin/marketplace.json # .claude-plugin/plugin.json
TerrysPOV
approved these changes
Jul 19, 2026
TerrysPOV
left a comment
Collaborator
There was a problem hiding this comment.
Well-built feature. I ran an adversarial fail-open/bypass audit on the auth logic and it's fail-closed on every path: isDiscordAuthorized checks the global allowlist first, gates the channel-scoped branch on isGuild, and ends with a terminal return false. Verified in particular:
- DM bypass is impossible — channel-scoped entries are
isGuild-gated, so DMs (noguild_id) can only match the global allowlist. Locked by a test. - Empty/missing/malformed-shape config fails closed (
?? []→isAllowed([])→ false). - Wrong-channel access, type confusion, threads, and slash/button interactions are all deny-direction, and #185's front-door hardening is preserved (global-only interaction gate untouched).
Maintainer-committed the version housekeeping in 006a0d8: the branch's 1.0.41 bump conflicted with master (now 1.0.42 after #234/#247), so I merged current master in and re-bumped to 1.0.43. Please rebase on latest master and run the bump scripts yourself for future PRs.
Two non-blocking follow-ups:
- The
config.tsparser's sanitization of malformedchannelAllowedUserIds(non-object, string-instead-of-array, numeric IDs) isn't unit-tested — the "malformed config can't grant access" property is only asserted for theundefinedshape. The code is safe; a couple ofparseSettingstests would pin it. - Two safe-but-undocumented asymmetries worth a doc line: channel-scoped users can send messages but not use slash-commands/buttons (interaction gate is global-only), and their access doesn't extend into threads (distinct channel_id).
Approving.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
discord.channelAllowedUserIdsconfig (channelId -> user IDs[]) — grants a user access to one specific guild channel without adding them to the globalallowedUserIdslistallowedUserIdslist remains the only path to DM accessisDiscordAuthorized()inallowlist.tsso it's unit-testable independent of the Discord message-handling flowWhy
Previously
allowedUserIdswas all-or-nothing: a user was either allowed to reach Sage everywhere (DMs + every guild channel), or blocked everywhere. No way to grant someone scoped access to a single project channel.Testing
tests/allowlist.test.tscovering: global allowlist access in DMs and guild channels, channel-scoped access in the correct channel, explicit denial in other channels, explicit denial via DM (the key leak this feature must avoid), denial for unlisted users, and no-throw on missing configtsc --noEmit: no new errors introduced