Spaces:
Runtime error
Runtime error
| /** | |
| * AutoRefreshDaemon β Background cookie validity checker for web-cookie providers | |
| * | |
| * Periodically checks stored credentials for web-cookie providers by making | |
| * lightweight requests to their home pages. If a credential is expired, it | |
| * logs a warning and marks the credential for re-authentication. | |
| * | |
| * The daemon does NOT automatically re-login (that requires user interaction | |
| * for security). It alerts the system so higher-level components can decide | |
| * what to do (e.g., fallback to another provider, prompt user to re-login). | |
| */ | |
| import { TOKEN_EXTRACTION_CONFIGS } from "./tokenExtractionConfig"; | |
| // βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface DaemonStatus { | |
| running: boolean; | |
| checkedProviderCount: number; | |
| expiredCredentials: string[]; | |
| lastRun: number | null; | |
| } | |
| interface StoredCredentialEntry { | |
| providerId: string; | |
| value: string; | |
| storedAt: number; | |
| } | |
| // βββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const DEFAULT_CHECK_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes | |
| const MIN_CHECK_INTERVAL_MS = 60 * 1000; // 1 minute minimum | |
| // βββ Daemon βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class AutoRefreshDaemon { | |
| private timerId: ReturnType<typeof setInterval> | null = null; | |
| private running = false; | |
| private checkIntervalMs: number; | |
| private expiredCredentials: string[] = []; | |
| private lastRun: number | null = null; | |
| /** In-memory store of web-cookie credentials (real persistence uses SQLite) */ | |
| private credentialStore = new Map<string, StoredCredentialEntry>(); | |
| constructor(checkIntervalMs = DEFAULT_CHECK_INTERVAL_MS) { | |
| this.checkIntervalMs = Math.max(checkIntervalMs, MIN_CHECK_INTERVAL_MS); | |
| } | |
| /** | |
| * Register a credential for auto-refresh monitoring. | |
| * Called when credentials are extracted/updated. | |
| */ | |
| registerCredential(providerId: string, value: string): void { | |
| this.credentialStore.set(providerId, { | |
| providerId, | |
| value, | |
| storedAt: Date.now(), | |
| }); | |
| } | |
| /** | |
| * Remove a credential from monitoring (e.g., provider deleted) | |
| */ | |
| unregisterCredential(providerId: string): void { | |
| this.credentialStore.delete(providerId); | |
| } | |
| /** | |
| * Start the daemon β begins periodic credential checks | |
| */ | |
| start(): void { | |
| if (this.running) return; | |
| this.running = true; | |
| // Run an initial check immediately | |
| this.check().catch(() => {}); | |
| this.timerId = setInterval(() => { | |
| this.check().catch(() => {}); | |
| }, this.checkIntervalMs); | |
| // Don't keep the process alive solely for this periodic daemon. | |
| (this.timerId as { unref?: () => void })?.unref?.(); | |
| console.log( | |
| `[AutoRefreshDaemon] Started β checking ${this.credentialStore.size} credentials every ${this.checkIntervalMs / 1000}s` | |
| ); | |
| } | |
| /** | |
| * Stop the daemon | |
| */ | |
| stop(): void { | |
| if (!this.running) return; | |
| this.running = false; | |
| if (this.timerId) { | |
| clearInterval(this.timerId); | |
| this.timerId = null; | |
| } | |
| console.log("[AutoRefreshDaemon] Stopped"); | |
| } | |
| /** | |
| * Check all stored credentials for validity. | |
| * Makes a lightweight HEAD/GET request to the provider's home page. | |
| */ | |
| async check(): Promise<void> { | |
| this.lastRun = Date.now(); | |
| const newlyExpired: string[] = []; | |
| const entries = [...this.credentialStore.entries()]; | |
| for (const [providerId] of entries) { | |
| const config = TOKEN_EXTRACTION_CONFIGS.get(providerId); | |
| if (!config) { | |
| this.credentialStore.delete(providerId); | |
| continue; | |
| } | |
| try { | |
| const isValid = await this.validateCredential(providerId, config.homeUrl); | |
| if (!isValid) { | |
| newlyExpired.push(providerId); | |
| console.warn( | |
| `[AutoRefreshDaemon] Credential expired for "${providerId}" (${config.displayName})` | |
| ); | |
| } | |
| } catch { | |
| // Network errors are non-fatal β retry next cycle | |
| } | |
| } | |
| // Update expired list | |
| for (const id of newlyExpired) { | |
| if (!this.expiredCredentials.includes(id)) { | |
| this.expiredCredentials.push(id); | |
| } | |
| } | |
| } | |
| /** | |
| * Validate a credential by making a request to the provider's home page. | |
| * Returns true if the response suggests the credential is still valid. | |
| */ | |
| private async validateCredential(providerId: string, homeUrl: string): Promise<boolean> { | |
| const entry = this.credentialStore.get(providerId); | |
| if (!entry) return false; | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 10000); | |
| try { | |
| const response = await fetch(homeUrl, { | |
| method: "HEAD", | |
| signal: controller.signal, | |
| headers: { | |
| "User-Agent": | |
| "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", | |
| }, | |
| }); | |
| // A valid credential typically returns 200 (occasionally 301/302) | |
| // 401/403 strongly suggest expired credential | |
| if (response.status === 401 || response.status === 403) { | |
| return false; | |
| } | |
| return true; | |
| } catch { | |
| // Network errors (timeout, DNS failure) don't mean the credential is bad | |
| return true; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| } | |
| /** | |
| * Get the current daemon status | |
| */ | |
| getStatus(): DaemonStatus { | |
| return { | |
| running: this.running, | |
| checkedProviderCount: this.credentialStore.size, | |
| expiredCredentials: [...this.expiredCredentials], | |
| lastRun: this.lastRun, | |
| }; | |
| } | |
| /** | |
| * Clear expired credentials list (e.g., after re-authentication) | |
| */ | |
| clearExpired(): void { | |
| this.expiredCredentials = []; | |
| } | |
| /** | |
| * Restart the daemon (useful when config changes) | |
| */ | |
| restart(): void { | |
| this.stop(); | |
| this.start(); | |
| } | |
| } | |
| // βββ Singleton ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const autoRefreshDaemon = new AutoRefreshDaemon(); | |