fix(discord): move in-check inside try/catch in channel-access helper

Wrap the `key in channel` probe inside the existing `try/catch` in `readDiscordChannelPropertySafe` so a throwing Proxy `has` trap (or any other reflective error on the presence check) degrades to `undefined` instead of propagating, matching the existing behavior for throwing getters on the value read.

Add a regression test that exposes the interaction channel through a Proxy whose `has` trap throws on `parentId` and asserts the slash-command path still defers and dispatches.

No behavior change for Carbon prototype getters or plain-object channels: the safe accessor still traverses the prototype chain (required for Carbon's `GuildThreadChannel.parentId`), still returns `undefined` for missing or throwing reads, and still preserves null-to-undefined coercion downstream.
This commit is contained in:
Neerav Makwana
2026-04-21 21:30:43 -04:00
committed by Peter Steinberger
parent dbf8fd0db7
commit 349d86c152
2 changed files with 29 additions and 1 deletions

View File

@@ -1,8 +1,11 @@
function readDiscordChannelPropertySafe(channel: unknown, key: string): unknown {
if (!channel || typeof channel !== "object" || !(key in channel)) {
if (!channel || typeof channel !== "object") {
return undefined;
}
try {
if (!(key in channel)) {
return undefined;
}
return (channel as Record<string, unknown>)[key];
} catch {
return undefined;

View File

@@ -215,6 +215,31 @@ describe("Discord native slash commands with commands.allowFrom", () => {
expectNotUnauthorizedReply(interaction);
});
it("tolerates guild thread channels exposed through a Proxy whose has trap throws", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
mutateInteraction: (currentInteraction) => {
const baseChannel = {
type: ChannelType.PublicThread,
id: currentInteraction.channel.id,
};
currentInteraction.channel = new Proxy(baseChannel, {
has(target, key) {
if (key === "parentId") {
throw new Error("has-trap denied");
}
return key in target;
},
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
}) as MockCommandInteraction["channel"];
},
});
expect(interaction.defer).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expectNotUnauthorizedReply(interaction);
});
it("authorizes guild slash commands from an allowlisted channel when commands.allowFrom is not configured", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
mutateConfig: (cfg) => {