Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { verifyCustomDomain } from './dns-verify'; 2import { db } from './db'; 3 4interface VerificationStats { 5 totalChecked: number; 6 verified: number; 7 failed: number; 8 errors: number; 9} 10 11export class DNSVerificationWorker { 12 private interval: Timer | null = null; 13 private isRunning = false; 14 private lastRunTime: number | null = null; 15 private stats: VerificationStats = { 16 totalChecked: 0, 17 verified: 0, 18 failed: 0, 19 errors: 0, 20 }; 21 22 constructor( 23 private checkIntervalMs: number = 60 * 60 * 1000, // 1 hour default 24 private onLog?: (message: string, data?: any) => void 25 ) {} 26 27 private log(message: string, data?: any) { 28 if (this.onLog) { 29 this.onLog(message, data); 30 } 31 } 32 33 async start() { 34 if (this.isRunning) { 35 this.log('DNS verification worker already running'); 36 return; 37 } 38 39 this.isRunning = true; 40 this.log('Starting DNS verification worker', { 41 intervalMinutes: this.checkIntervalMs / 60000, 42 }); 43 44 // Run immediately on start 45 await this.verifyAllDomains(); 46 47 // Then run on interval 48 this.interval = setInterval(() => { 49 this.verifyAllDomains(); 50 }, this.checkIntervalMs); 51 } 52 53 stop() { 54 if (this.interval) { 55 clearInterval(this.interval); 56 this.interval = null; 57 } 58 this.isRunning = false; 59 this.log('DNS verification worker stopped'); 60 } 61 62 private async verifyAllDomains() { 63 this.log('Starting DNS verification check'); 64 const startTime = Date.now(); 65 66 const runStats: VerificationStats = { 67 totalChecked: 0, 68 verified: 0, 69 failed: 0, 70 errors: 0, 71 }; 72 73 try { 74 // Get all custom domains (both verified and pending) 75 const domains = await db<Array<{ 76 id: string; 77 domain: string; 78 did: string; 79 verified: boolean; 80 }>>` 81 SELECT id, domain, did, verified FROM custom_domains 82 `; 83 84 if (!domains || domains.length === 0) { 85 this.log('No custom domains to check'); 86 this.lastRunTime = Date.now(); 87 return; 88 } 89 90 const verifiedCount = domains.filter(d => d.verified).length; 91 const pendingCount = domains.filter(d => !d.verified).length; 92 this.log(`Checking ${domains.length} custom domains (${verifiedCount} verified, ${pendingCount} pending)`); 93 94 // Verify each domain 95 for (const row of domains) { 96 runStats.totalChecked++; 97 const { id, domain, did, verified: wasVerified } = row; 98 99 try { 100 // Extract hash from id (SHA256 of did:domain) 101 const expectedHash = id.substring(0, 16); 102 103 // Verify DNS records - this will only verify if TXT record matches this specific DID 104 const result = await verifyCustomDomain(domain, did, expectedHash); 105 106 if (result.verified) { 107 // Double-check: ensure this record is still the current owner in database 108 // This prevents race conditions where domain ownership changed during verification 109 const currentOwner = await db<Array<{ id: string; did: string; verified: boolean }>>` 110 SELECT id, did, verified FROM custom_domains WHERE domain = ${domain} 111 `; 112 113 const isStillOwner = currentOwner.length > 0 && currentOwner[0].id === id; 114 115 if (!isStillOwner) { 116 this.log(`⚠️ Domain ownership changed during verification: ${domain}`, { 117 expectedId: id, 118 expectedDid: did, 119 actualId: currentOwner[0]?.id, 120 actualDid: currentOwner[0]?.did 121 }); 122 runStats.failed++; 123 continue; 124 } 125 126 // Update verified status and last_verified_at timestamp 127 await db` 128 UPDATE custom_domains 129 SET verified = true, 130 last_verified_at = EXTRACT(EPOCH FROM NOW()) 131 WHERE id = ${id} 132 `; 133 runStats.verified++; 134 if (!wasVerified) { 135 this.log(`Domain newly verified: ${domain}`, { did }); 136 } else { 137 this.log(`Domain re-verified: ${domain}`, { did }); 138 } 139 } else { 140 // Mark domain as unverified or keep it pending 141 await db` 142 UPDATE custom_domains 143 SET verified = false, 144 last_verified_at = EXTRACT(EPOCH FROM NOW()) 145 WHERE id = ${id} 146 `; 147 runStats.failed++; 148 if (wasVerified) { 149 this.log(`Domain verification failed (was verified): ${domain}`, { 150 did, 151 error: result.error, 152 found: result.found, 153 }); 154 } else { 155 this.log(`Domain still pending: ${domain}`, { 156 did, 157 error: result.error, 158 found: result.found, 159 }); 160 } 161 } 162 } catch (error) { 163 runStats.errors++; 164 this.log(`Error verifying domain: ${domain}`, { 165 did, 166 error: error instanceof Error ? error.message : String(error), 167 }); 168 } 169 } 170 171 // Update cumulative stats 172 this.stats.totalChecked += runStats.totalChecked; 173 this.stats.verified += runStats.verified; 174 this.stats.failed += runStats.failed; 175 this.stats.errors += runStats.errors; 176 177 const duration = Date.now() - startTime; 178 this.lastRunTime = Date.now(); 179 180 this.log('DNS verification check completed', { 181 duration: `${duration}ms`, 182 ...runStats, 183 }); 184 } catch (error) { 185 this.log('Fatal error in DNS verification worker', { 186 error: error instanceof Error ? error.message : String(error), 187 }); 188 } 189 } 190 191 getHealth() { 192 return { 193 isRunning: this.isRunning, 194 lastRunTime: this.lastRunTime, 195 intervalMs: this.checkIntervalMs, 196 stats: this.stats, 197 healthy: this.isRunning && ( 198 this.lastRunTime === null || 199 Date.now() - this.lastRunTime < this.checkIntervalMs * 2 200 ), 201 }; 202 } 203 204 // Manual trigger for testing 205 async trigger() { 206 this.log('Manual DNS verification triggered'); 207 await this.verifyAllDomains(); 208 } 209}