UNPKG

tarot-mcp-server

Version:

Model Context Protocol server for Rider-Waite tarot card readings

214 lines (213 loc) 6.82 kB
/** * Physical Random Number Provider * Provides true randomness from natural physical phenomena */ export class PhysicalRandomProvider { cache = []; cacheSize = 1000; isRefilling = false; sources = [ { name: "Quantum", description: "ANU Quantum Random Numbers (quantum vacuum fluctuations)", isAvailable: true, errorCount: 0 }, { name: "Atmospheric", description: "RANDOM.ORG (atmospheric electromagnetic noise)", isAvailable: true, errorCount: 0 }, { name: "Cryptographic", description: "OS cryptographic random number generator", isAvailable: true, errorCount: 0 } ]; constructor() { // Pre-fill cache on initialization this.refillCache(); } /** * Get a random number between 0 and 1 from physical sources */ async getPhysicalRandom() { // If cache is low, trigger async refill if (this.cache.length < 100 && !this.isRefilling) { this.refillCache(); } // Use cached value if available if (this.cache.length > 0) { const randomByte = this.cache.shift(); return randomByte / 255; } // If no cache, get directly (blocking) return await this.getDirectRandom(); } /** * Get current status of random sources */ getSourceStatus() { return [...this.sources]; } /** * Get the currently active source name */ getCurrentSource() { const available = this.sources.find(s => s.isAvailable && s.errorCount < 3); return available?.name || "Fallback"; } /** * Refill the random number cache asynchronously */ async refillCache() { if (this.isRefilling) return; this.isRefilling = true; try { const newNumbers = await this.fetchPhysicalRandomNumbers(this.cacheSize); this.cache.push(...newNumbers); // Keep cache size reasonable if (this.cache.length > this.cacheSize * 2) { this.cache = this.cache.slice(-this.cacheSize); } } catch (error) { console.warn('Failed to refill physical random cache:', error); } finally { this.isRefilling = false; } } /** * Get random number directly (blocking) */ async getDirectRandom() { try { const numbers = await this.fetchPhysicalRandomNumbers(1); return numbers[0] / 255; } catch (error) { console.warn('Physical random failed, using cryptographic fallback'); return this.getCryptographicRandom(); } } /** * Fetch random numbers from physical sources */ async fetchPhysicalRandomNumbers(count) { // Try quantum source first try { const quantum = await this.getQuantumRandom(count); this.markSourceSuccess("Quantum"); return quantum; } catch (error) { this.markSourceError("Quantum"); console.warn('Quantum random failed:', error); } // Try atmospheric source try { const atmospheric = await this.getAtmosphericRandom(count); this.markSourceSuccess("Atmospheric"); return atmospheric; } catch (error) { this.markSourceError("Atmospheric"); console.warn('Atmospheric random failed:', error); } // Fallback to cryptographic this.markSourceSuccess("Cryptographic"); return Array.from({ length: count }, () => Math.floor(this.getCryptographicRandom() * 256)); } /** * Get quantum random numbers from ANU */ async getQuantumRandom(count) { const response = await fetch(`https://qrng.anu.edu.au/API/jsonI.php?length=${count}&type=uint8`, { method: 'GET', headers: { 'User-Agent': 'TarotMCP/1.0' }, signal: AbortSignal.timeout(5000) // 5 second timeout }); if (!response.ok) { throw new Error(`Quantum API error: ${response.status}`); } const data = await response.json(); if (!data.success || !Array.isArray(data.data)) { throw new Error('Invalid quantum random response'); } return data.data; } /** * Get atmospheric random numbers from RANDOM.ORG */ async getAtmosphericRandom(count) { const response = await fetch(`https://www.random.org/integers/?num=${count}&min=0&max=255&col=1&base=10&format=plain&rnd=new`, { method: 'GET', headers: { 'User-Agent': 'TarotMCP/1.0' }, signal: AbortSignal.timeout(5000) // 5 second timeout }); if (!response.ok) { throw new Error(`Atmospheric API error: ${response.status}`); } const text = await response.text(); const numbers = text.trim().split('\n').map(n => parseInt(n, 10)); if (numbers.some(n => isNaN(n) || n < 0 || n > 255)) { throw new Error('Invalid atmospheric random response'); } return numbers; } /** * Cryptographic random fallback */ getCryptographicRandom() { if (typeof crypto !== 'undefined' && crypto.getRandomValues) { const array = new Uint32Array(1); crypto.getRandomValues(array); return array[0] / (0xffffffff + 1); } else if (typeof require !== 'undefined') { try { const crypto = require('crypto'); return crypto.randomBytes(4).readUInt32BE(0) / (0xffffffff + 1); } catch (e) { return Math.random(); } } else { return Math.random(); } } /** * Mark source as successful */ markSourceSuccess(sourceName) { const source = this.sources.find(s => s.name === sourceName); if (source) { source.isAvailable = true; source.lastUsed = new Date(); source.errorCount = Math.max(0, source.errorCount - 1); } } /** * Mark source as having an error */ markSourceError(sourceName) { const source = this.sources.find(s => s.name === sourceName); if (source) { source.errorCount++; if (source.errorCount >= 3) { source.isAvailable = false; } } } } // Global instance export const physicalRandom = new PhysicalRandomProvider();