tarot-mcp-server
Version:
Model Context Protocol server for Rider-Waite tarot card readings
214 lines (213 loc) • 6.82 kB
JavaScript
/**
* 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();