From 663ccb4bb8e22695769f955c6567e48b71160b61 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Mon, 27 Jul 2026 21:11:14 +0800 Subject: [PATCH] fix(review): accept canary/ptb Discord hosts in loop-escalation webhook validation loop-escalation-wire's ALLOWED_DISCORD_HOSTS listed only discord.com and discordapp.com, while its two sibling validators (alerts.ts, notify-discord.ts) accept the wider, correct four-host set. A valid canary.discord.com or ptb.discord.com DISCORD_WEBHOOK_URL was silently dropped as invalid_global_webhook. Align the constant with the siblings; the host allowlist previously had zero test coverage, now covered directly. --- src/review/loop-escalation-wire.ts | 6 ++++-- test/unit/loop-escalation-wire.test.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/review/loop-escalation-wire.ts b/src/review/loop-escalation-wire.ts index b557012c6d..4747153481 100644 --- a/src/review/loop-escalation-wire.ts +++ b/src/review/loop-escalation-wire.ts @@ -28,7 +28,9 @@ import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; import { resolveLoopOverSelfRepoFullName } from "../config/loopover-repo-focus-manifest"; import { errorMessage } from "../utils/json"; -const ALLOWED_DISCORD_HOSTS = new Set(["discord.com", "discordapp.com"]); +// Byte-faithful with src/review/alerts.ts and src/services/notify-discord.ts (#9288): the two extra +// subdomains (canary/ptb) are valid Discord webhook hosts the sibling validators already accept. +const ALLOWED_DISCORD_HOSTS = new Set(["discord.com", "discordapp.com", "canary.discord.com", "ptb.discord.com"]); const DEFAULT_COOLDOWN_MINUTES = 60; const AUDIT_EVENT_TYPE = "loop_escalation_notification.discord"; const AUDIT_TARGET_KEY = "fleet:loop-escalation"; @@ -88,7 +90,7 @@ function envString(env: Env, name: string): string | undefined { return typeof fromEnv === "string" && fromEnv.trim().length > 0 ? fromEnv.trim() : undefined; } -function isValidDiscordWebhook(url: string): boolean { +export function isValidDiscordWebhook(url: string): boolean { try { const parsed = new URL(url); if (parsed.protocol !== "https:") return false; diff --git a/test/unit/loop-escalation-wire.test.ts b/test/unit/loop-escalation-wire.test.ts index a03a6a70d3..089aefa10b 100644 --- a/test/unit/loop-escalation-wire.test.ts +++ b/test/unit/loop-escalation-wire.test.ts @@ -3,6 +3,7 @@ import * as repositories from "../../src/db/repositories"; import { clearLoopEscalationManifestOverrideCacheForTest, isLoopEscalationSweepEnabled, + isValidDiscordWebhook, loadActiveLoopsFromEnv, parseActiveLoopFacts, resolveLoopEscalationManifestOverride, @@ -13,6 +14,24 @@ import { createTestEnv } from "../helpers/d1"; const SELF_REPO = "JSONbored/loopover"; +describe("isValidDiscordWebhook host allowlist (#9288)", () => { + it("accepts all four Discord webhook hosts its sibling validators accept — including canary/ptb", () => { + for (const host of ["discord.com", "discordapp.com", "canary.discord.com", "ptb.discord.com"]) { + // canary/ptb were rejected before the fix, dropping a valid webhook as invalid_global_webhook. + expect(isValidDiscordWebhook(`https://${host}/api/webhooks/123/abc`)).toBe(true); + expect(isValidDiscordWebhook(`https://${host.toUpperCase()}/api/webhooks/123/abc`)).toBe(true); // host match is case-insensitive + } + }); + + it("still rejects non-Discord hosts, non-https, and non-webhook paths", () => { + expect(isValidDiscordWebhook("https://evil.example.com/api/webhooks/123/abc")).toBe(false); + expect(isValidDiscordWebhook("https://notdiscord.com/api/webhooks/123/abc")).toBe(false); + expect(isValidDiscordWebhook("http://discord.com/api/webhooks/123/abc")).toBe(false); // not https + expect(isValidDiscordWebhook("https://discord.com/not/a/webhook")).toBe(false); // wrong path + expect(isValidDiscordWebhook("not a url")).toBe(false); + }); +}); + describe("isLoopEscalationSweepEnabled (#6349)", () => { it("defaults OFF and accepts the standard truthy env forms", () => { for (const off of [undefined, "", "false", "no", "0", "off"]) {