Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
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}