UNPKG

nostr-deploy-server

Version:

Node.js server for hosting static websites under npub subdomains using Nostr protocol and Blossom servers

250 lines (207 loc) 9.29 kB
import * as dotenv from 'dotenv'; // @ts-ignore - xbytes doesn't have TypeScript declarations import xbytes from 'xbytes'; import { ServerConfig } from '../types'; // Load environment variables dotenv.config(); export class ConfigManager { private static instance: ConfigManager; private config: ServerConfig; private constructor() { this.config = { port: parseInt(process.env.PORT || '3000', 10), baseDomain: process.env.BASE_DOMAIN || '', defaultRelays: this.parseCommaSeparated( process.env.DEFAULT_RELAYS || 'wss://relay.nostr.band,wss://nostrue.com,wss://purplerelay.com,wss://relay.primal.net,wss://nos.lol,wss://relay.damus.io,wss://relay.nsite.lol' ), defaultBlossomServers: this.parseCommaSeparated( process.env.DEFAULT_BLOSSOM_SERVERS || 'https://cdn.hzrd149.com,https://blossom.primal.net,https://blossom.band,https://loratu.bitcointxoko.com,https://blossom.f7z.io,https://cdn.sovbit.host' ), cacheTtlSeconds: parseInt(process.env.CACHE_TTL_SECONDS || '300', 10), maxCacheSize: parseInt(process.env.MAX_CACHE_SIZE || '100', 10), rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000', 10), rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10), logLevel: process.env.LOG_LEVEL || 'info', corsOrigin: process.env.CORS_ORIGIN || '*', trustProxy: process.env.TRUST_PROXY === 'true', requestTimeoutMs: parseInt(process.env.REQUEST_TIMEOUT_MS || '30000', 10), maxFileSizeMB: parseInt(process.env.MAX_FILE_SIZE_MB || '50', 10), // SSR Configuration ssrEnabled: process.env.SSR_ENABLED === 'true', // Default is false ssrTimeoutMs: parseInt(process.env.SSR_TIMEOUT_MS || '60000', 10), // Increased to 60 seconds ssrCacheTtlSeconds: parseInt(process.env.SSR_CACHE_TTL_SECONDS || '1800', 10), // 30 minutes ssrViewportWidth: parseInt(process.env.SSR_VIEWPORT_WIDTH || '1920', 10), ssrViewportHeight: parseInt(process.env.SSR_VIEWPORT_HEIGHT || '1080', 10), ssrMaxConcurrentPages: parseInt(process.env.SSR_MAX_CONCURRENT_PAGES || '3', 10), // WebSocket Connection Pooling Configuration wsConnectionTimeoutMs: parseInt(process.env.WS_CONNECTION_TIMEOUT_MS || '3600000', 10), // 1 hour default wsCleanupIntervalMs: parseInt(process.env.WS_CLEANUP_INTERVAL_MS || '300000', 10), // 5 minutes default // Cache TTL Configuration negativeCacheTtlMs: parseInt(process.env.NEGATIVE_CACHE_TTL_MS || '10000', 10), // 10 seconds default positiveCacheTtlMs: parseInt(process.env.POSITIVE_CACHE_TTL_MS || '300000', 10), // 5 minutes default fileContentCacheTtlMs: parseInt(process.env.FILE_CONTENT_CACHE_TTL_MS || '1800000', 10), // 30 minutes default errorCacheTtlMs: parseInt(process.env.ERROR_CACHE_TTL_MS || '60000', 10), // 1 minute default // Query Timeout Configuration relayQueryTimeoutMs: parseInt(process.env.RELAY_QUERY_TIMEOUT_MS || '3000', 10), // Reduced from 10s to 3s for faster responses // Advanced Cache Configuration cachePath: process.env.CACHE_PATH, cacheTime: parseInt(process.env.CACHE_TIME || '3600', 10), maxFileSize: process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.MAX_FILE_SIZE) : Infinity, // Real-time Cache Invalidation Configuration realtimeCacheInvalidation: process.env.REALTIME_CACHE_INVALIDATION === 'true', invalidationRelays: this.parseCommaSeparated( process.env.INVALIDATION_RELAYS || 'wss://relay.primal.net,wss://relay.damus.io,wss://relay.nostr.band' // Use fast, reliable relays ), invalidationTimeoutMs: parseInt(process.env.INVALIDATION_TIMEOUT_MS || '30000', 10), // 30 seconds invalidationReconnectDelayMs: parseInt( process.env.INVALIDATION_RECONNECT_DELAY_MS || '5000', 10 ), // 5 seconds // Sliding Expiration Configuration slidingExpiration: process.env.SLIDING_EXPIRATION === 'true', // Default is false for backward compatibility }; this.validateConfig(); } public static getInstance(): ConfigManager { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); } return ConfigManager.instance; } public getConfig(): ServerConfig { return { ...this.config }; } public updateConfig(updates: Partial<ServerConfig>): void { this.config = { ...this.config, ...updates }; this.validateConfig(); } private parseCommaSeparated(value: string): string[] { return value .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0); } private validateConfig(): void { const { config } = this; if (config.port < 1 || config.port > 65535) { throw new Error(`Invalid port: ${config.port}. Must be between 1-65535`); } if (!config.baseDomain) { throw new Error('BASE_DOMAIN is required'); } if (config.defaultRelays.length === 0) { throw new Error('At least one default relay is required'); } if (config.defaultBlossomServers.length === 0) { throw new Error('At least one default Blossom server is required'); } // Validate relay URLs config.defaultRelays.forEach((relay) => { if (!relay.startsWith('wss://') && !relay.startsWith('ws://')) { throw new Error(`Invalid relay URL: ${relay}. Must start with ws:// or wss://`); } }); // Validate Blossom server URLs config.defaultBlossomServers.forEach((server) => { if (!server.startsWith('https://') && !server.startsWith('http://')) { throw new Error( `Invalid Blossom server URL: ${server}. Must start with http:// or https://` ); } }); if (config.cacheTtlSeconds < 0) { throw new Error('Cache TTL cannot be negative'); } if (config.maxCacheSize < 1) { throw new Error('Max cache size must be at least 1'); } if (config.rateLimitWindowMs < 1000) { throw new Error('Rate limit window must be at least 1000ms'); } if (config.rateLimitMaxRequests < 1) { throw new Error('Rate limit max requests must be at least 1'); } if (config.requestTimeoutMs < 1000) { throw new Error('Request timeout must be at least 1000ms'); } if (config.maxFileSizeMB < 1) { throw new Error('Max file size must be at least 1MB'); } // SSR validation if (config.ssrTimeoutMs < 1000) { throw new Error('SSR timeout must be at least 1000ms'); } if (config.ssrCacheTtlSeconds < 0) { throw new Error('SSR cache TTL cannot be negative'); } if (config.ssrViewportWidth < 320 || config.ssrViewportWidth > 3840) { throw new Error('SSR viewport width must be between 320-3840 pixels'); } if (config.ssrViewportHeight < 240 || config.ssrViewportHeight > 2160) { throw new Error('SSR viewport height must be between 240-2160 pixels'); } if (config.ssrMaxConcurrentPages < 1 || config.ssrMaxConcurrentPages > 10) { throw new Error('SSR max concurrent pages must be between 1-10'); } // WebSocket Connection Pooling Configuration if (config.wsConnectionTimeoutMs < 1000) { throw new Error('WS connection timeout must be at least 1000ms'); } if (config.wsCleanupIntervalMs < 1000) { throw new Error('WS cleanup interval must be at least 1000ms'); } // Cache TTL Configuration if (config.negativeCacheTtlMs < 0) { throw new Error('Negative cache TTL cannot be negative'); } if (config.positiveCacheTtlMs < 1000) { throw new Error('Positive cache TTL must be at least 1000ms'); } if (config.fileContentCacheTtlMs < 1000) { throw new Error('File content cache TTL must be at least 1000ms'); } if (config.errorCacheTtlMs < 0) { throw new Error('Error cache TTL cannot be negative'); } // Query Timeout Configuration if (config.relayQueryTimeoutMs < 1000) { throw new Error('Relay query timeout must be at least 1000ms'); } // Real-time Cache Invalidation Configuration if (config.realtimeCacheInvalidation) { if (config.invalidationRelays.length === 0) { throw new Error( 'At least one invalidation relay is required when real-time cache invalidation is enabled' ); } // Validate invalidation relay URLs config.invalidationRelays.forEach((relay) => { if (!relay.startsWith('wss://') && !relay.startsWith('ws://')) { throw new Error( `Invalid invalidation relay URL: ${relay}. Must start with ws:// or wss://` ); } }); if (config.invalidationTimeoutMs < 5000) { throw new Error('Invalidation timeout must be at least 5000ms'); } if (config.invalidationReconnectDelayMs < 1000) { throw new Error('Invalidation reconnect delay must be at least 1000ms'); } } } public isProduction(): boolean { return process.env.NODE_ENV === 'production'; } public isDevelopment(): boolean { return process.env.NODE_ENV === 'development'; } public isTest(): boolean { return process.env.NODE_ENV === 'test'; } }