forked from
nekomimi.pet/wisp.place-monorepo
Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
1import { promises as dns } from 'dns'
2
3/**
4 * Result of a domain verification process
5 */
6export interface VerificationResult {
7 /** Whether the verification was successful */
8 verified: boolean
9 /** Error message if verification failed */
10 error?: string
11 /** DNS records found during verification */
12 found?: {
13 /** TXT records found (used for domain verification) */
14 txt?: string[]
15 /** CNAME record found (used for domain pointing) */
16 cname?: string
17 }
18}
19
20/**
21 * Verify domain ownership via TXT record at _wisp.{domain}
22 * Expected format: did:plc:xxx or did:web:xxx
23 */
24export const verifyDomainOwnership = async (
25 domain: string,
26 expectedDid: string
27): Promise<VerificationResult> => {
28 try {
29 const txtDomain = `_wisp.${domain}`
30
31 console.log(`[DNS Verify] Checking TXT record for ${txtDomain}`)
32 console.log(`[DNS Verify] Expected DID: ${expectedDid}`)
33
34 // Query TXT records
35 const records = await dns.resolveTxt(txtDomain)
36
37 // Log what we found
38 const foundTxtValues = records.map((record) => record.join(''))
39 console.log(`[DNS Verify] Found TXT records:`, foundTxtValues)
40
41 // TXT records come as arrays of strings (for multi-part records)
42 // We need to join them and check if any match the expected DID
43 for (const record of records) {
44 const txtValue = record.join('')
45 if (txtValue === expectedDid) {
46 console.log(`[DNS Verify] ✓ TXT record matches!`)
47 return { verified: true, found: { txt: foundTxtValues } }
48 }
49 }
50
51 console.log(`[DNS Verify] ✗ TXT record does not match`)
52 return {
53 verified: false,
54 error: `TXT record at ${txtDomain} does not match expected DID. Expected: ${expectedDid}`,
55 found: { txt: foundTxtValues }
56 }
57 } catch (err: any) {
58 console.log(`[DNS Verify] ✗ TXT lookup error:`, err.message)
59 if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') {
60 return {
61 verified: false,
62 error: `No TXT record found at _wisp.${domain}`,
63 found: { txt: [] }
64 }
65 }
66 return {
67 verified: false,
68 error: `DNS lookup failed: ${err.message}`,
69 found: { txt: [] }
70 }
71 }
72}
73
74/**
75 * Verify CNAME record points to the expected hash target
76 * For custom domains, we expect: domain CNAME -> {hash}.dns.wisp.place
77 */
78export const verifyCNAME = async (
79 domain: string,
80 expectedHash: string
81): Promise<VerificationResult> => {
82 try {
83 console.log(`[DNS Verify] Checking CNAME record for ${domain}`)
84 const expectedTarget = `${expectedHash}.dns.wisp.place`
85 console.log(`[DNS Verify] Expected CNAME: ${expectedTarget}`)
86
87 // Resolve CNAME for the domain
88 const cname = await dns.resolveCname(domain)
89
90 // Log what we found
91 const foundCname =
92 cname.length > 0
93 ? cname[0]?.toLowerCase().replace(/\.$/, '')
94 : null
95 console.log(`[DNS Verify] Found CNAME:`, foundCname || 'none')
96
97 if (cname.length === 0 || !foundCname) {
98 console.log(`[DNS Verify] ✗ No CNAME record found`)
99 return {
100 verified: false,
101 error: `No CNAME record found for ${domain}`,
102 found: { cname: '' }
103 }
104 }
105
106 // Check if CNAME points to the expected target
107 const actualTarget = foundCname
108
109 if (actualTarget === expectedTarget.toLowerCase()) {
110 console.log(`[DNS Verify] ✓ CNAME record matches!`)
111 return { verified: true, found: { cname: actualTarget } }
112 }
113
114 console.log(`[DNS Verify] ✗ CNAME record does not match`)
115 return {
116 verified: false,
117 error: `CNAME for ${domain} points to ${actualTarget}, expected ${expectedTarget}`,
118 found: { cname: actualTarget }
119 }
120 } catch (err: any) {
121 console.log(`[DNS Verify] ✗ CNAME lookup error:`, err.message)
122 if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') {
123 return {
124 verified: false,
125 error: `No CNAME record found for ${domain}`,
126 found: { cname: '' }
127 }
128 }
129 return {
130 verified: false,
131 error: `DNS lookup failed: ${err.message}`,
132 found: { cname: '' }
133 }
134 }
135}
136
137/**
138 * Verify both TXT and CNAME records for a custom domain
139 */
140export const verifyCustomDomain = async (
141 domain: string,
142 expectedDid: string,
143 expectedHash: string
144): Promise<VerificationResult> => {
145 const txtResult = await verifyDomainOwnership(domain, expectedDid)
146 if (!txtResult.verified) {
147 return txtResult
148 }
149
150 const cnameResult = await verifyCNAME(domain, expectedHash)
151 if (!cnameResult.verified) {
152 return cnameResult
153 }
154
155 return { verified: true }
156}