Skip to content

feat(discord): channel-scoped allowlist for guild messages - #248

Merged
TerrysPOV merged 4 commits into
moazbuilds:masterfrom
BCDel89:bcdel89/discord-channel-allowlist
Jul 19, 2026
Merged

feat(discord): channel-scoped allowlist for guild messages#248
TerrysPOV merged 4 commits into
moazbuilds:masterfrom
BCDel89:bcdel89/discord-channel-allowlist

Conversation

@BCDel89

@BCDel89 BCDel89 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds discord.channelAllowedUserIds config (channelId -> user IDs[]) — grants a user access to one specific guild channel without adding them to the global allowedUserIds list
  • Channel-scoped access never applies to DMs, even for the same user ID — the global allowedUserIds list remains the only path to DM access
  • Extracted the composed auth check into isDiscordAuthorized() in allowlist.ts so it's unit-testable independent of the Discord message-handling flow

Why

Previously allowedUserIds was 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

  • 7 new unit tests in tests/allowlist.test.ts covering: 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 config
  • Full suite: 117/117 passing, no regressions
  • tsc --noEmit: no new errors introduced
  • Manually verified live against Discord: a channel-scoped test user got real responses in their assigned channel, no response in other guild channels, and an explicit "Unauthorized." on DM — while the existing globally-allowed user was unaffected in all contexts

BCDel89 added 2 commits July 5, 2026 11:30
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
BCDel89 force-pushed the bcdel89/discord-channel-allowlist branch from 6186a5e to 135a130 Compare July 5, 2026 16:32

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (no guild_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:

  1. The config.ts parser's sanitization of malformed channelAllowedUserIds (non-object, string-instead-of-array, numeric IDs) isn't unit-tested — the "malformed config can't grant access" property is only asserted for the undefined shape. The code is safe; a couple of parseSettings tests would pin it.
  2. 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.

@TerrysPOV
TerrysPOV merged commit 2079943 into moazbuilds:master Jul 19, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants