{"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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"}