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}