/** * @param email * @param allowedDomains */ export function isEmailDomainAllowed(email: string, allowedDomains?: string[] | null): boolean { /** If no domain restrictions are configured, allow all */ if (!allowedDomains || !Array.isArray(allowedDomains) || !allowedDomains.length) { return true; } /** If restrictions exist, validate email format */ if (!email) { return false; } const domain = email.split('@')[1]?.toLowerCase(); if (!domain) { return false; } return allowedDomains.some((allowedDomain) => allowedDomain?.toLowerCase() === domain); } /** * Normalizes a domain string. If the domain is invalid, returns null. * Normalized === lowercase, trimmed, and protocol added if missing. * @param domain */ function normalizeDomain(domain: string): string | null { try { let normalizedDomain = domain.toLowerCase().trim(); // Early return for obviously invalid formats if (normalizedDomain === 'http://' || normalizedDomain === 'https://') { return null; } // If it's not already a URL, make it one if (!normalizedDomain.startsWith('http://') && !normalizedDomain.startsWith('https://')) { normalizedDomain = `https://${normalizedDomain}`; } const url = new URL(normalizedDomain); // Additional validation that hostname isn't just protocol if (!url.hostname || url.hostname === 'http:' || url.hostname === 'https:') { return null; } return url.hostname.replace(/^www\./i, ''); } catch { return null; } } /** * Checks if the given domain is allowed. If no restrictions are set, allows all domains. * @param domain * @param allowedDomains */ export async function isActionDomainAllowed( domain?: string | null, allowedDomains?: string[] | null, ): Promise { if (!domain || typeof domain !== 'string') { return false; } if (!Array.isArray(allowedDomains) || !allowedDomains.length) { return true; } const normalizedInputDomain = normalizeDomain(domain); if (!normalizedInputDomain) { return false; } for (const allowedDomain of allowedDomains) { const normalizedAllowedDomain = normalizeDomain(allowedDomain); if (!normalizedAllowedDomain) { continue; } if (normalizedAllowedDomain.startsWith('*.')) { const baseDomain = normalizedAllowedDomain.slice(2); if ( normalizedInputDomain === baseDomain || normalizedInputDomain.endsWith(`.${baseDomain}`) ) { return true; } } else if (normalizedInputDomain === normalizedAllowedDomain) { return true; } } return false; } /** * Extracts domain from MCP server config URL. * Returns null for stdio transports (no URL) or invalid URLs. * @param config - MCP server configuration (accepts any config with optional url field) */ export function extractMCPServerDomain(config: Record): string | null { const url = config.url; // Stdio transports don't have URLs - always allowed if (!url || typeof url !== 'string') { return null; } try { const parsedUrl = new URL(url); return parsedUrl.hostname.replace(/^www\./i, ''); } catch { return null; } } /** * Validates MCP server domain against allowedDomains. * Reuses isActionDomainAllowed for consistent validation logic. * Stdio transports (no URL) are always allowed. * @param config - MCP server configuration with optional url field * @param allowedDomains - List of allowed domains (with wildcard support) */ export async function isMCPDomainAllowed( config: Record, allowedDomains?: string[] | null, ): Promise { const domain = extractMCPServerDomain(config); // Stdio transports don't have domains - always allowed if (!domain) { return true; } // Reuse existing validation logic (includes wildcard support) return isActionDomainAllowed(domain, allowedDomains); }