txt-domain-verification
Version:
A production-ready Node.js package for verifying domain ownership through DNS TXT records with authoritative nameserver queries, security features, and comprehensive error handling.
1 lines • 11.9 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/index.js"],"sourcesContent":["const dns = require('dns');\nconst { Client } = require('dns2');\nconst crypto = require('crypto');\n\n// Configuration constants\nconst CONFIG = {\n VERIFICATION_PREFIX: '_verify',\n TOKEN_EXPIRY_HOURS: 72,\n MAX_RETRIES: 3,\n RETRY_DELAY_MS: 1000,\n DNS_TIMEOUT_MS: 10000,\n MIN_NAMESERVERS_VERIFIED: 1,\n};\n\n/**\n * Normalizes a domain name for consistent processing\n */\nfunction normalizeDomain(domain) {\n if (typeof domain !== 'string') {\n throw new Error('Domain must be a string');\n }\n return domain.trim().toLowerCase().replace(/\\.$/, '');\n}\n\n/**\n * Generates a secure verification token using HMAC\n */\nfunction generateSecureToken(secret, userId, domain, issuedAt = new Date()) {\n if (!secret || !userId || !domain) {\n throw new Error('Secret, userId, and domain are required');\n }\n\n const payload = `${userId}|${domain}|${issuedAt.toISOString()}`;\n const hmac = crypto.createHmac('sha256', secret);\n hmac.update(payload);\n\n return hmac.digest('base64url');\n}\n\n/**\n * Generates verification instructions with a secure token\n */\nfunction generateVerificationCode(\n domain,\n secret,\n userId,\n businessName = '',\n format = '{{businessName}} Domain Verification: {{token}}',\n issuedAt = new Date()\n) {\n const normalizedDomain = normalizeDomain(domain);\n const token = generateSecureToken(secret, userId, normalizedDomain, issuedAt);\n\n const formattedString = format\n .replace('{{businessName}}', businessName)\n .replace('{{token}}', token);\n\n return {\n token,\n formattedString,\n domain: normalizedDomain,\n issuedAt,\n expiresAt: new Date(\n issuedAt.getTime() + CONFIG.TOKEN_EXPIRY_HOURS * 60 * 60 * 1000\n ),\n instructions: `Please add the following TXT record to your DNS settings for ${normalizedDomain}:\\nName: ${CONFIG.VERIFICATION_PREFIX}\\nValue: ${formattedString}`,\n verificationRecord: {\n name: `${CONFIG.VERIFICATION_PREFIX}.${normalizedDomain}`,\n value: formattedString,\n },\n };\n}\n\n/**\n * Resolves authoritative nameservers for a domain\n */\nasync function resolveAuthoritativeNameservers(domain) {\n const normalizedDomain = normalizeDomain(domain);\n\n try {\n const nameservers = await dns.promises.resolveNs(normalizedDomain);\n if (!nameservers || nameservers.length === 0) {\n throw new Error(`No nameservers found for ${normalizedDomain}`);\n }\n\n // Resolve IP addresses for nameservers\n const nameserverIPs = [];\n for (const ns of nameservers) {\n try {\n const ips = await dns.promises.resolve4(ns);\n nameserverIPs.push(...ips);\n } catch (err) {\n console.warn(\n `Warning: Could not resolve nameserver ${ns}: ${err.message}`\n );\n }\n }\n\n if (nameserverIPs.length === 0) {\n throw new Error(\n `Could not resolve any nameserver IPs for ${normalizedDomain}`\n );\n }\n\n return nameserverIPs;\n } catch (error) {\n throw new Error(\n `Failed to resolve nameservers for ${normalizedDomain}: ${error.message}`\n );\n }\n}\n\n/**\n * Queries a specific nameserver for TXT records\n */\nasync function queryNameserver(nameserverIP, domain, verificationPrefix) {\n return new Promise((resolve, reject) => {\n const client = new Client({\n nameServers: [nameserverIP],\n timeout: CONFIG.DNS_TIMEOUT_MS,\n });\n\n const queryName = `${verificationPrefix}.${domain}`;\n\n client.on('message', (response) => {\n const answers = response.answers || [];\n const txtRecords = answers\n .filter((answer) => answer.type === 'TXT')\n .map((answer) => answer.data.join(''));\n\n resolve(txtRecords);\n });\n\n client.on('error', (error) => {\n reject(\n new Error(\n `Nameserver query failed for ${nameserverIP}: ${error.message}`\n )\n );\n });\n\n client.on('timeout', () => {\n reject(new Error(`Nameserver query timeout for ${nameserverIP}`));\n });\n\n client.query(queryName, 'TXT');\n });\n}\n\n/**\n * Verifies domain ownership by querying authoritative nameservers\n */\nasync function verifyDomain(\n domain,\n expectedToken,\n verificationPrefix = CONFIG.VERIFICATION_PREFIX,\n options = {}\n) {\n const normalizedDomain = normalizeDomain(domain);\n const maxRetries = options.maxRetries || CONFIG.MAX_RETRIES;\n const retryDelay = options.retryDelay || CONFIG.RETRY_DELAY_MS;\n\n let lastError;\n\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n try {\n // Get authoritative nameservers\n const nameserverIPs = await resolveAuthoritativeNameservers(\n normalizedDomain\n );\n\n // Query each nameserver for TXT records\n const verificationPromises = nameserverIPs.map(async (nsIP) => {\n try {\n const txtRecords = await queryNameserver(\n nsIP,\n normalizedDomain,\n verificationPrefix\n );\n return { nameserver: nsIP, records: txtRecords, success: true };\n } catch (error) {\n return { nameserver: nsIP, error: error.message, success: false };\n }\n });\n\n const results = await Promise.all(verificationPromises);\n const successfulQueries = results.filter((r) => r.success);\n const failedQueries = results.filter((r) => !r.success);\n\n // Check if any successful query contains the expected token\n let tokenFound = false;\n let matchingRecords = [];\n\n for (const query of successfulQueries) {\n for (const record of query.records) {\n if (record.includes(expectedToken)) {\n tokenFound = true;\n matchingRecords.push({\n nameserver: query.nameserver,\n record: record,\n });\n }\n }\n }\n\n const verificationResult = {\n verified: tokenFound,\n domain: normalizedDomain,\n attempt: attempt,\n totalNameservers: nameserverIPs.length,\n successfulQueries: successfulQueries.length,\n failedQueries: failedQueries.length,\n matchingRecords: matchingRecords,\n partialPropagation:\n successfulQueries.length > 0 &&\n successfulQueries.length < nameserverIPs.length,\n nameserverResults: results,\n timestamp: new Date(),\n };\n\n if (tokenFound) {\n return verificationResult;\n }\n\n // If we have some successful queries but no token found, this might indicate\n // the record is still propagating or there's a mismatch\n if (successfulQueries.length > 0) {\n return verificationResult;\n }\n\n // All queries failed, this might be a temporary issue\n lastError = new Error(\n `All nameserver queries failed for ${normalizedDomain}`\n );\n } catch (error) {\n lastError = error;\n\n if (attempt < maxRetries) {\n // Exponential backoff\n const delay = retryDelay * Math.pow(2, attempt - 1);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw (\n lastError || new Error(`Verification failed after ${maxRetries} attempts`)\n );\n}\n\nmodule.exports = {\n generateVerificationCode,\n verifyDomain,\n generateSecureToken,\n normalizeDomain,\n resolveAuthoritativeNameservers,\n CONFIG,\n};\n"],"names":["dns","require$$0","Client","require$$1","crypto","require$$2","CONFIG","VERIFICATION_PREFIX","TOKEN_EXPIRY_HOURS","MAX_RETRIES","RETRY_DELAY_MS","DNS_TIMEOUT_MS","MIN_NAMESERVERS_VERIFIED","normalizeDomain","domain","Error","trim","toLowerCase","replace","generateSecureToken","secret","userId","issuedAt","Date","payload","toISOString","hmac","createHmac","update","digest","async","resolveAuthoritativeNameservers","normalizedDomain","nameservers","promises","resolveNs","length","nameserverIPs","ns","ips","resolve4","push","err","console","warn","message","error","queryNameserver","nameserverIP","verificationPrefix","Promise","resolve","reject","client","nameServers","timeout","queryName","on","response","txtRecords","answers","filter","answer","type","map","data","join","query","generateVerificationCode","businessName","format","token","formattedString","expiresAt","getTime","instructions","verificationRecord","name","value","verifyDomain","expectedToken","options","maxRetries","retryDelay","lastError","attempt","verificationPromises","nsIP","nameserver","records","success","results","all","successfulQueries","r","failedQueries","tokenFound","matchingRecords","record","includes","verificationResult","verified","totalNameservers","partialPropagation","nameserverResults","timestamp","delay","Math","pow","setTimeout"],"mappings":"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"}