/** * Discord guild permission helpers for dashboard access control. * Includes short-lived cache + 429 retry to avoid Discord rate limits. */ export interface DiscordGuild { id: string; name?: string; icon?: string | null; permissions: string; owner?: boolean; } const MANAGE_GUILD = BigInt(0x20); const ADMINISTRATOR = BigInt(0x8); const CACHE_TTL_MS = 60_000; const MAX_RETRIES = 3; type CacheEntry = { guilds: DiscordGuild[]; expiresAt: number }; const guildCache = new Map(); const inflight = new Map>(); export function canManageGuild(guild: DiscordGuild): boolean { if (guild.owner) return true; try { const perms = BigInt(guild.permissions); return ( (perms & ADMINISTRATOR) === ADMINISTRATOR || (perms & MANAGE_GUILD) === MANAGE_GUILD ); } catch { return false; } } function cacheKey(accessToken: string): string { // Avoid storing raw token as object key longer than needed — short fingerprint is enough for process cache return `tok:${accessToken.slice(0, 12)}:${accessToken.length}:${accessToken.slice(-8)}`; } export function invalidateUserGuildsCache(accessToken?: string) { if (!accessToken) { guildCache.clear(); return; } guildCache.delete(cacheKey(accessToken)); } async function fetchUserGuildsUncached(accessToken: string): Promise { let attempt = 0; while (attempt < MAX_RETRIES) { attempt += 1; const res = await fetch("https://discord.com/api/users/@me/guilds", { headers: { Authorization: `Bearer ${accessToken}` }, cache: "no-store", }); if (res.ok) { return (await res.json()) as DiscordGuild[]; } if (res.status === 429) { let retryAfter = 1; try { const body = await res.json(); retryAfter = Number(body.retry_after ?? 1); } catch { const header = res.headers.get("retry-after"); retryAfter = header ? Number(header) : 1; } const waitMs = Math.min(Math.max(retryAfter * 1000, 300), 5000); console.warn(`Discord guilds rate-limited — retry in ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})`); await new Promise((r) => setTimeout(r, waitMs)); continue; } const body = await res.text().catch(() => ""); console.error("Discord guilds fetch failed:", res.status, body); throw new Error( res.status === 401 ? "Discord session expired — please sign in again." : `Failed to fetch your Discord servers. (${res.status})` ); } throw new Error("Failed to fetch your Discord servers. (rate limited)"); } export async function fetchUserGuilds(accessToken: string): Promise { const key = cacheKey(accessToken); const cached = guildCache.get(key); if (cached && cached.expiresAt > Date.now()) { return cached.guilds; } const existing = inflight.get(key); if (existing) return existing; const promise = fetchUserGuildsUncached(accessToken) .then((guilds) => { guildCache.set(key, { guilds, expiresAt: Date.now() + CACHE_TTL_MS }); return guilds; }) .finally(() => { inflight.delete(key); }); inflight.set(key, promise); return promise; } export async function userCanManageGuild( accessToken: string, guildId: string ): Promise { const guilds = await fetchUserGuilds(accessToken); const guild = guilds.find((g) => String(g.id) === String(guildId)); return guild ? canManageGuild(guild) : false; }