Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
fetch-helper.ts12.4 kB
/** * @file src/utils/fetch-helper.ts * @description Utility for SSL-aware fetch requests with automatic certificate handling */ import https from 'https'; /** * Create fetch options with SSL certificate handling * * Automatically configures SSL/TLS settings based on the * `NODE_TLS_REJECT_UNAUTHORIZED` environment variable. * * When `NODE_TLS_REJECT_UNAUTHORIZED=0`, disables certificate validation * for HTTPS requests. Useful for development with self-signed certificates. * * **Security Warning:** Never set `NODE_TLS_REJECT_UNAUTHORIZED=0` in production! * * @param url - The URL to fetch * @param options - Base fetch options to extend * @returns Fetch options with SSL agent configured if needed * * @example * ```ts * // Development with self-signed cert * process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; * const options = createFetchOptions('https://localhost:8443/api'); * const response = await fetch('https://localhost:8443/api', options); * * // Production (validates certificates) * delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; * const options = createFetchOptions('https://api.example.com'); * const response = await fetch('https://api.example.com', options); * ``` */ export function createFetchOptions(url: string, options: RequestInit = {}): RequestInit { const fetchOptions = { ...options }; // Handle NODE_TLS_REJECT_UNAUTHORIZED for HTTPS requests if (url.startsWith('https://') && process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') { // For Node.js fetch (undici), we need to pass an agent (fetchOptions as any).agent = new https.Agent({ rejectUnauthorized: false }); } return fetchOptions; } /** * Add Authorization Bearer header to fetch options * * Reads API key from environment variable and adds it as a Bearer token. * If the environment variable is not set, returns options unchanged. * * @param options - Fetch options to extend * @param apiKeyEnvVar - Environment variable name for API key (default: MIMIR_LLM_API_KEY) * @returns Fetch options with Authorization header if API key exists * * @example * ```ts * process.env.MIMIR_LLM_API_KEY = 'sk-abc123'; * * let options = {}; * options = addAuthHeader(options); * // options.headers['Authorization'] = 'Bearer sk-abc123' * * // Custom API key variable * process.env.MY_API_KEY = 'custom-key'; * options = addAuthHeader({}, 'MY_API_KEY'); * // options.headers['Authorization'] = 'Bearer custom-key' * ``` */ export function addAuthHeader(options: RequestInit, apiKeyEnvVar: string = 'MIMIR_LLM_API_KEY'): RequestInit { const apiKey = process.env[apiKeyEnvVar]; if (apiKey) { const headers = new Headers(options.headers); headers.set('Authorization', `Bearer ${apiKey}`); return { ...options, headers }; } return options; } /** * Create an AbortSignal with timeout for fetch requests * * Prevents hanging requests by automatically aborting after the specified timeout. * * @param timeoutMs - Timeout in milliseconds (default: 10000ms = 10 seconds) * @returns AbortSignal that will abort after timeout * * @example * ```ts * // 10 second timeout (default) * const signal = createTimeoutSignal(); * try { * const response = await fetch('https://api.example.com', { signal }); * } catch (error) { * if (error.name === 'AbortError') { * console.log('Request timed out after 10 seconds'); * } * } * * // Custom 30 second timeout * const signal = createTimeoutSignal(30000); * const response = await fetch('https://slow-api.example.com', { signal }); * ``` */ export function createTimeoutSignal(timeoutMs: number = 10000): AbortSignal { const controller = new AbortController(); setTimeout(() => controller.abort(), timeoutMs); return controller.signal; } /** * Validate OAuth bearer token format to prevent security attacks * * Performs comprehensive validation to prevent: * - **SSRF attacks**: Ensures token doesn't contain URLs or protocols * - **Injection attacks**: Blocks newlines, control characters, HTML/JS * - **DoS attacks**: Enforces maximum token length (8KB) * * Valid tokens contain only base64url characters, dots, hyphens, underscores, * and URL-safe characters commonly found in JWT and OAuth tokens. * * @param token - The token to validate * @returns true if token format is valid * @throws Error if token format is invalid with specific reason * * @example * ```ts * // Valid JWT token * const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123'; * validateOAuthTokenFormat(token); // Returns true * * // Invalid: contains newline (injection attempt) * try { * validateOAuthTokenFormat('token\nmalicious-header: value'); * } catch (error) { * console.log(error.message); // 'Token contains suspicious patterns' * } * * // Invalid: too long (DoS attempt) * try { * validateOAuthTokenFormat('a'.repeat(10000)); * } catch (error) { * console.log(error.message); // 'Token exceeds maximum length' * } * ``` */ export function validateOAuthTokenFormat(token: string): boolean { if (!token || typeof token !== 'string') { throw new Error('Token must be a non-empty string'); } // Token should not be excessively long (max 8KB for OAuth tokens) if (token.length > 8192) { throw new Error('Token exceeds maximum length'); } // Token should only contain valid base64url characters, dots, hyphens, and underscores // This prevents injection of newlines, control characters, or other malicious content const validTokenPattern = /^[A-Za-z0-9\-_\.~\+\/=]+$/; if (!validTokenPattern.test(token)) { throw new Error('Token contains invalid characters'); } // Token should not contain suspicious patterns that could indicate injection attempts const suspiciousPatterns = [ /[\r\n]/, // Newlines (HTTP header injection) /[<>]/, // HTML/XML tags /javascript:/i, // JavaScript protocol /data:/i, // Data protocol /file:/i, // File protocol ]; for (const pattern of suspiciousPatterns) { if (pattern.test(token)) { throw new Error('Token contains suspicious patterns'); } } return true; } /** * Validate OAuth userinfo URL to prevent SSRF (Server-Side Request Forgery) attacks * * Ensures the URL is safe to fetch by blocking: * - **Private IP ranges**: 10.x.x.x, 192.168.x.x, 172.16-31.x.x * - **Localhost**: 127.0.0.1, ::1, localhost * - **Link-local addresses**: 169.254.x.x, fe80:: * - **Non-HTTP(S) protocols**: file://, javascript://, data:// * * In production, only HTTPS is allowed (unless `MIMIR_OAUTH_ALLOW_HTTP=true`). * In development, localhost is permitted for local OAuth testing. * * @param url - The URL to validate * @returns true if URL is safe to fetch * @throws Error if URL is unsafe with specific reason * * @example * ```ts * // Valid production URL * validateOAuthUserinfoUrl('https://oauth.example.com/userinfo'); * // Returns true * * // Invalid: private IP (SSRF attempt) * try { * validateOAuthUserinfoUrl('https://192.168.1.1/admin'); * } catch (error) { * console.log(error.message); // 'Private IP addresses are not allowed' * } * * // Development: localhost allowed * process.env.NODE_ENV = 'development'; * validateOAuthUserinfoUrl('http://localhost:3000/userinfo'); * // Returns true * * // Production: localhost blocked * process.env.NODE_ENV = 'production'; * try { * validateOAuthUserinfoUrl('http://localhost:3000/userinfo'); * } catch (error) { * console.log(error.message); // 'Localhost is not allowed in production' * } * ``` */ export function validateOAuthUserinfoUrl(url: string): boolean { if (!url || typeof url !== 'string') { throw new Error('URL must be a non-empty string'); } let parsedUrl: URL; try { parsedUrl = new URL(url); } catch (error) { throw new Error('Invalid URL format'); } // Check if HTTP is explicitly allowed (for local OAuth testing) const allowHttp = process.env.MIMIR_OAUTH_ALLOW_HTTP === 'true'; // Only allow HTTPS in production (unless explicitly overridden) const isProduction = process.env.NODE_ENV === 'production'; if (isProduction && !allowHttp && parsedUrl.protocol !== 'https:') { throw new Error('Only HTTPS URLs are allowed in production (set MIMIR_OAUTH_ALLOW_HTTP=true to override)'); } if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { throw new Error('Only HTTP/HTTPS protocols are allowed'); } // Block private IP ranges and localhost (except in development) const hostname = parsedUrl.hostname.toLowerCase(); // Allow localhost and host.docker.internal in development ONLY const isDevelopment = process.env.NODE_ENV !== 'production'; if (isDevelopment) { const allowedDevHosts = ['localhost', '127.0.0.1', '::1', 'host.docker.internal']; if (allowedDevHosts.includes(hostname)) { return true; } } // Block private IP ranges in production const privateIpPatterns = [ /^127\./, // 127.0.0.0/8 (loopback) /^10\./, // 10.0.0.0/8 (private) /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 (private) /^192\.168\./, // 192.168.0.0/16 (private) /^169\.254\./, // 169.254.0.0/16 (link-local) /^::1$/, // IPv6 loopback /^fe80:/, // IPv6 link-local /^fc00:/, // IPv6 unique local /^fd00:/, // IPv6 unique local ]; for (const pattern of privateIpPatterns) { if (pattern.test(hostname)) { throw new Error('Private IP addresses are not allowed'); } } // Block localhost variations if (hostname === 'localhost' || hostname.endsWith('.localhost')) { throw new Error('Localhost is not allowed in production'); } return true; } /** * Create secure fetch options with SSL, authentication, and timeout * * Convenience function that combines SSL handling, authentication, * and timeout configuration in one call. * * **Features:** * - SSL certificate handling (respects NODE_TLS_REJECT_UNAUTHORIZED) * - Optional Bearer token authentication from environment * - Automatic request timeout (default: 10 seconds) * * @param url - The URL to fetch * @param options - Base fetch options to extend * @param apiKeyEnvVar - Optional environment variable name for API key * @param timeoutMs - Optional timeout in milliseconds (default: 10000ms = 10s) * @returns Fetch options with SSL, auth, and timeout configured * * @example * ```ts * // Simple usage with defaults * const options = createSecureFetchOptions('https://api.example.com/data'); * const response = await fetch('https://api.example.com/data', options); * * // With authentication * process.env.MIMIR_LLM_API_KEY = 'sk-abc123'; * const options = createSecureFetchOptions( * 'https://api.openai.com/v1/models', * {}, * 'MIMIR_LLM_API_KEY' * ); * // Adds: Authorization: Bearer sk-abc123 * * // With custom timeout (30 seconds) * const options = createSecureFetchOptions( * 'https://slow-api.example.com', * { method: 'POST', body: JSON.stringify(data) }, * undefined, * 30000 * ); * * // Full example with error handling * try { * const options = createSecureFetchOptions( * 'https://api.example.com', * { method: 'GET' }, * 'MY_API_KEY', * 5000 * ); * const response = await fetch('https://api.example.com', options); * const data = await response.json(); * } catch (error) { * if (error.name === 'AbortError') { * console.log('Request timed out'); * } * } * ``` */ export function createSecureFetchOptions( url: string, options: RequestInit = {}, apiKeyEnvVar?: string, timeoutMs: number = 10000 ): RequestInit { let fetchOptions = createFetchOptions(url, options); if (apiKeyEnvVar) { fetchOptions = addAuthHeader(fetchOptions, apiKeyEnvVar); } // Add timeout signal if not already provided in options // Check the original options, not fetchOptions, since signal might not be copied if (!options.signal && !fetchOptions.signal) { fetchOptions.signal = createTimeoutSignal(timeoutMs); } return fetchOptions; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server