UNPKG

@hasna/domains

Version:

Domain portfolio, registrar, marketplace, and DNS management for AI agents — CLI + MCP + HTTP API + SDK, shared account API and PostgreSQL

3,139 lines • 120 kB
// src/lib/namecheap.ts
function getConfig() {
  const apiKey = process.env["NAMECHEAP_API_KEY"];
  const username = process.env["NAMECHEAP_USERNAME"];
  const clientIp = process.env["NAMECHEAP_CLIENT_IP"];
  if (!apiKey)
    throw new Error("NAMECHEAP_API_KEY environment variable is not set");
  if (!username)
    throw new Error("NAMECHEAP_USERNAME environment variable is not set");
  if (!clientIp)
    throw new Error("NAMECHEAP_CLIENT_IP environment variable is not set");
  return {
    apiKey,
    username,
    clientIp,
    sandbox: process.env["NAMECHEAP_SANDBOX"] === "true"
  };
}
async function apiRequest(config, command, params = {}) {
  const base = config.sandbox ? "https://api.sandbox.namecheap.com/xml.response" : "https://api.namecheap.com/xml.response";
  const url = new URL(base);
  url.searchParams.set("ApiUser", config.username);
  url.searchParams.set("ApiKey", config.apiKey);
  url.searchParams.set("UserName", config.username);
  url.searchParams.set("ClientIp", config.clientIp);
  url.searchParams.set("Command", command);
  for (const [k, v] of Object.entries(params)) {
    url.searchParams.set(k, v);
  }
  const response = await fetch(url.toString(), {
    signal: AbortSignal.timeout(30000)
  });
  if (!response.ok) {
    throw new Error(`Namecheap API request failed with status ${response.status}`);
  }
  return response.text();
}
function parseXmlValue(xml, tag) {
  const match = xml.match(new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, "i"));
  return match ? match[1].trim() : null;
}
function parseXmlAttributes(xml, tag) {
  const results = [];
  const tagRegex = new RegExp(`<${tag}([^>]*)>`, "gi");
  let match;
  while ((match = tagRegex.exec(xml)) !== null) {
    const attrs = {};
    const attrStr = match[1];
    const attrRegex = /(\w+)="([^"]*)"/g;
    let attrMatch;
    while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
      attrs[attrMatch[1]] = attrMatch[2];
    }
    results.push(attrs);
  }
  return results;
}
async function listNamecheapDomains(config) {
  const cfg = config || getConfig();
  const xml = await apiRequest(cfg, "namecheap.domains.getList", { PageSize: "100" });
  const domainAttrs = parseXmlAttributes(xml, "Domain");
  return domainAttrs.map((attrs) => ({
    domain: attrs["Name"] || "",
    expiry: attrs["Expires"] || "",
    autoRenew: attrs["AutoRenew"] === "true",
    isExpired: attrs["IsExpired"] === "true",
    isLocked: attrs["IsLocked"] === "true"
  }));
}
async function getDomainInfo(domain, config) {
  const cfg = config || getConfig();
  const xml = await apiRequest(cfg, "namecheap.domains.getInfo", { DomainName: domain });
  const created = parseXmlValue(xml, "CreatedDate") || "";
  const expires = parseXmlValue(xml, "ExpiredDate") || "";
  const nsAttrs = parseXmlAttributes(xml, "Nameserver");
  const nameservers = nsAttrs.map((a) => a["NAME"] || "").filter(Boolean);
  return {
    domain,
    registrar: "Namecheap",
    created,
    expires,
    nameservers
  };
}
async function renewDomain(domain, years = 1, config) {
  const cfg = config || getConfig();
  const xml = await apiRequest(cfg, "namecheap.domains.renew", {
    DomainName: domain,
    Years: String(years)
  });
  const orderId = parseXmlValue(xml, "OrderId");
  const chargedAmount = parseXmlValue(xml, "ChargedAmount");
  return {
    domain,
    success: xml.includes('Status="OK"'),
    orderId: orderId || undefined,
    chargedAmount: chargedAmount || undefined
  };
}
function namecheapContactParams(contact) {
  const organization = contact.organization_name || "NA";
  const base = {
    FirstName: contact.first_name,
    LastName: contact.last_name,
    OrganizationName: organization,
    Address1: contact.address_line_1,
    City: contact.city,
    StateProvince: contact.state,
    PostalCode: contact.zip_code,
    Country: contact.country_code,
    Phone: contact.phone,
    EmailAddress: contact.email
  };
  const params = {};
  for (const prefix of ["Registrant", "Tech", "Admin", "AuxBilling"]) {
    for (const [key, value] of Object.entries(base)) {
      params[prefix + key] = value;
    }
  }
  return params;
}
async function registerDomain(domain, contact, options = {}, config) {
  const cfg = config || getConfig();
  const params = {
    DomainName: domain,
    Years: String(options.years ?? 1),
    AddFreeWhoisguard: options.whoisGuard === false ? "no" : "yes",
    WGEnabled: options.whoisGuard === false ? "no" : "yes",
    ...namecheapContactParams(contact)
  };
  if (options.premiumPrice !== undefined) {
    params.IsPremiumDomain = "true";
    params.PremiumPrice = String(options.premiumPrice);
  }
  const xml = await apiRequest(cfg, "namecheap.domains.create", params);
  return {
    domain,
    success: xml.includes('Status="OK"') || xml.includes('Registered="true"'),
    orderId: parseXmlValue(xml, "OrderId") || undefined,
    chargedAmount: parseXmlValue(xml, "ChargedAmount") || undefined
  };
}
async function updateNameservers(domain, nameservers, config) {
  if (nameservers.length === 0)
    throw new Error("updateNameservers requires at least one nameserver");
  const cfg = config || getConfig();
  const { sld, tld } = splitDomain(domain);
  const xml = await apiRequest(cfg, "namecheap.domains.dns.setCustom", {
    SLD: sld,
    TLD: tld,
    Nameservers: nameservers.join(",")
  });
  return xml.includes('Status="OK"') || xml.includes('Updated="true"');
}
async function getDnsRecords(_domain, sld, tld, config) {
  const cfg = config || getConfig();
  const xml = await apiRequest(cfg, "namecheap.domains.dns.getHosts", {
    SLD: sld,
    TLD: tld
  });
  const hostAttrs = parseXmlAttributes(xml, "host");
  return hostAttrs.map((attrs) => ({
    type: attrs["Type"] || "A",
    name: attrs["Name"] || "@",
    address: attrs["Address"] || "",
    ttl: parseInt(attrs["TTL"] || "3600"),
    mxPref: attrs["MXPref"] ? parseInt(attrs["MXPref"]) : undefined
  }));
}
async function setDnsRecords(_domain, sld, tld, records, config) {
  const cfg = config || getConfig();
  const params = {
    SLD: sld,
    TLD: tld
  };
  records.forEach((r, i) => {
    params[`HostName${i + 1}`] = r.name;
    params[`RecordType${i + 1}`] = r.type;
    params[`Address${i + 1}`] = r.address;
    params[`TTL${i + 1}`] = String(r.ttl);
    if (r.mxPref !== undefined) {
      params[`MXPref${i + 1}`] = String(r.mxPref);
    }
  });
  const xml = await apiRequest(cfg, "namecheap.domains.dns.setHosts", params);
  return xml.includes('IsSuccess="true"');
}
async function checkAvailability(domain, config) {
  const cfg = config || getConfig();
  const xml = await apiRequest(cfg, "namecheap.domains.check", { DomainList: domain });
  const domainAttrs = parseXmlAttributes(xml, "DomainCheckResult");
  const result = domainAttrs[0] || {};
  return {
    domain,
    available: result["Available"] === "true",
    premium: result["IsPremiumName"] === "true",
    price: result["PremiumRegistrationPrice"] ? parseFloat(result["PremiumRegistrationPrice"]) : undefined
  };
}
function splitDomain(domain) {
  const parts = domain.split(".");
  if (parts.length < 2) {
    throw new Error(`Invalid domain: ${domain}`);
  }
  if (parts.length >= 3 && ["co", "com", "org", "net", "ac", "gov"].includes(parts[parts.length - 2])) {
    return {
      sld: parts.slice(0, -2).join("."),
      tld: parts.slice(-2).join(".")
    };
  }
  return {
    sld: parts.slice(0, -1).join("."),
    tld: parts[parts.length - 1]
  };
}
async function syncToLocalDb(dbFunctions, config) {
  const cfg = config || getConfig();
  const result = { synced: 0, errors: [], domains: [] };
  const syncedAt = new Date().toISOString();
  let ncDomains;
  try {
    ncDomains = await listNamecheapDomains(cfg);
  } catch (error) {
    throw new Error(`Failed to list Namecheap domains: ${error instanceof Error ? error.message : String(error)}`);
  }
  for (const ncDomain of ncDomains) {
    try {
      let info;
      try {
        info = await getDomainInfo(ncDomain.domain, cfg);
      } catch {
        info = {
          domain: ncDomain.domain,
          registrar: "Namecheap",
          created: "",
          expires: ncDomain.expiry,
          nameservers: []
        };
      }
      const expiresAt = normalizeDate(info.expires || ncDomain.expiry);
      const createdAt = normalizeDate(info.created);
      const existing = await dbFunctions.getDomainByName(ncDomain.domain);
      if (existing) {
        await dbFunctions.updateDomain(existing.id, {
          registrar: "Namecheap",
          status: "active",
          registered_at: createdAt || undefined,
          expires_at: expiresAt || undefined,
          auto_renew: ncDomain.autoRenew,
          nameservers: info.nameservers.length > 0 ? info.nameservers : undefined,
          expiry_synced_at: syncedAt
        });
      } else {
        await dbFunctions.createDomain({
          name: ncDomain.domain,
          registrar: "Namecheap",
          status: "active",
          registered_at: createdAt || undefined,
          expires_at: expiresAt || undefined,
          auto_renew: ncDomain.autoRenew,
          nameservers: info.nameservers,
          expiry_synced_at: syncedAt
        });
      }
      result.synced++;
      result.domains.push(ncDomain.domain);
    } catch (error) {
      result.errors.push(`${ncDomain.domain}: ${error instanceof Error ? error.message : String(error)}`);
    }
  }
  return result;
}
function normalizeDate(dateStr) {
  if (!dateStr)
    return null;
  try {
    const d = new Date(dateStr);
    if (isNaN(d.getTime()))
      return null;
    return d.toISOString();
  } catch {
    return null;
  }
}

// src/lib/godaddy.ts
class GoDaddyApiError extends Error {
  statusCode;
  details;
  constructor(message, statusCode, details) {
    super(message);
    this.statusCode = statusCode;
    this.details = details;
    this.name = "GoDaddyApiError";
  }
}
var _overriddenFetch = null;
function getCredentials() {
  const apiKey = process.env["GODADDY_API_KEY"];
  const apiSecret = process.env["GODADDY_API_SECRET"];
  if (!apiKey || !apiSecret) {
    throw new Error("GoDaddy API credentials not configured. Set GODADDY_API_KEY and GODADDY_API_SECRET environment variables.");
  }
  return { apiKey, apiSecret };
}
function getHeaders() {
  const { apiKey, apiSecret } = getCredentials();
  return {
    Authorization: `sso-key ${apiKey}:${apiSecret}`,
    "Content-Type": "application/json",
    Accept: "application/json"
  };
}
var GODADDY_API_BASE = "https://api.godaddy.com";
async function apiRequest2(method, path, body) {
  const fetchFn = _overriddenFetch || globalThis.fetch;
  const url = `${GODADDY_API_BASE}${path}`;
  const headers = getHeaders();
  const options = { method, headers };
  if (body !== undefined) {
    options.body = JSON.stringify(body);
  }
  const response = await fetchFn(url, options);
  if (!response.ok) {
    const text = await response.text();
    throw new GoDaddyApiError(`GoDaddy API ${method} ${path} failed with status ${response.status}: ${text}`, response.status, { responseBody: text });
  }
  if (response.status === 204) {
    return;
  }
  return await response.json();
}
async function listGoDaddyDomains() {
  return apiRequest2("GET", "/v1/domains");
}
async function getDomainInfo2(domain) {
  return apiRequest2("GET", `/v1/domains/${encodeURIComponent(domain)}`);
}
async function renewDomain2(domain) {
  return apiRequest2("POST", `/v1/domains/${encodeURIComponent(domain)}/renew`, { period: 1 });
}
async function getDnsRecords2(domain, type) {
  const path = type ? `/v1/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(type)}` : `/v1/domains/${encodeURIComponent(domain)}/records`;
  return apiRequest2("GET", path);
}
async function setDnsRecords2(domain, records) {
  await apiRequest2("PUT", `/v1/domains/${encodeURIComponent(domain)}/records`, records);
}
async function checkAvailability2(domain) {
  return apiRequest2("GET", `/v1/domains/available?domain=${encodeURIComponent(domain)}`);
}
function mapGoDaddyStatus(gdStatus) {
  const s = gdStatus.toUpperCase();
  if (s === "ACTIVE")
    return "active";
  if (s === "EXPIRED")
    return "expired";
  if (s === "TRANSFERRED_OUT" || s === "TRANSFERRING" || s === "PENDING_TRANSFER")
    return "transferring";
  if (s === "REDEMPTION" || s === "PENDING_REDEMPTION")
    return "redemption";
  return "active";
}
async function syncToLocalDb2(dbFns) {
  const result = {
    synced: 0,
    created: 0,
    updated: 0,
    errors: []
  };
  let gdDomains;
  try {
    gdDomains = await listGoDaddyDomains();
  } catch (err) {
    result.errors.push(`Failed to list domains: ${err instanceof Error ? err.message : String(err)}`);
    return result;
  }
  const syncedAt = new Date().toISOString();
  for (const gd of gdDomains) {
    try {
      let detail;
      try {
        detail = await getDomainInfo2(gd.domain);
      } catch {
        detail = gd;
      }
      const existing = await dbFns.getDomainByName(gd.domain);
      const domainData = {
        name: gd.domain,
        registrar: "GoDaddy",
        status: mapGoDaddyStatus(gd.status),
        expires_at: gd.expires ? new Date(gd.expires).toISOString() : undefined,
        auto_renew: gd.renewAuto,
        nameservers: gd.nameServers || [],
        registered_at: detail.createdAt ? new Date(detail.createdAt).toISOString() : undefined,
        expiry_synced_at: syncedAt,
        metadata: {
          godaddy_domain_id: detail.domainId,
          provider: "godaddy",
          locked: detail.locked,
          privacy: detail.privacy
        }
      };
      if (existing) {
        await dbFns.updateDomain(existing.id, domainData);
        result.updated++;
      } else {
        await dbFns.createDomain(domainData);
        result.created++;
      }
      result.synced++;
    } catch (err) {
      result.errors.push(`Failed to sync ${gd.domain}: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
  return result;
}

// src/lib/route53.ts
import {
  Route53Client,
  CreateHostedZoneCommand,
  ListHostedZonesCommand,
  GetHostedZoneCommand,
  DeleteHostedZoneCommand,
  ChangeResourceRecordSetsCommand,
  ListResourceRecordSetsCommand,
  ListHostedZonesByNameCommand
} from "@aws-sdk/client-route-53";
import {
  Route53DomainsClient,
  CheckDomainAvailabilityCommand,
  DisableDomainTransferLockCommand,
  GetDomainDetailCommand,
  GetOperationDetailCommand,
  ListDomainsCommand,
  ListPricesCommand,
  RegisterDomainCommand,
  RetrieveDomainAuthCodeCommand,
  TransferDomainCommand,
  UpdateDomainNameserversCommand
} from "@aws-sdk/client-route-53-domains";
import { fromIni } from "@aws-sdk/credential-provider-ini";
function getConfig2() {
  return {
    region: process.env["ROUTE53_REGION"] || process.env["ROUTE53_AWS_REGION"] || process.env["AWS_REGION"] || "us-east-1",
    accessKeyId: process.env["ROUTE53_ACCESS_KEY_ID"] || process.env["AWS_ACCESS_KEY_ID"],
    secretAccessKey: process.env["ROUTE53_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_ACCESS_KEY"],
    sessionToken: process.env["ROUTE53_SESSION_TOKEN"] || process.env["AWS_SESSION_TOKEN"],
    profile: process.env["ROUTE53_AWS_PROFILE"] || process.env["AWS_PROFILE"]
  };
}
function makeClients(config) {
  const cfg = config ?? getConfig2();
  const region = cfg.region || "us-east-1";
  const credentials = cfg.credentials ?? (cfg.accessKeyId && cfg.secretAccessKey ? { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey, sessionToken: cfg.sessionToken } : cfg.profile ? fromIni({ profile: cfg.profile }) : undefined);
  return {
    route53: new Route53Client({ region, credentials }),
    domains: new Route53DomainsClient({ region: "us-east-1", credentials })
  };
}
async function checkAvailability3(domain, config) {
  const { domains } = makeClients(config);
  const result = await domains.send(new CheckDomainAvailabilityCommand({ DomainName: domain }));
  const availability = {
    domain,
    available: result.Availability === "AVAILABLE",
    availability: result.Availability ?? "UNKNOWN"
  };
  if (availability.available) {
    try {
      const tld = domain.split(".").slice(1).join(".");
      const price = await getTldPrice(tld, config);
      if (price) {
        availability.currency = price.currency;
        availability.price = price.registration_price;
        availability.renewal_price = price.renewal_price;
        availability.transfer_price = price.transfer_price;
      }
    } catch {}
  }
  return availability;
}
function normalizeTld(tld) {
  return tld.trim().replace(/^\./, "");
}
function priceString(value) {
  return value == null ? undefined : value.toString();
}
function tldPriceFromRoute53Price(tld, price) {
  return {
    tld: price.Name || tld,
    registration_price: priceString(price.RegistrationPrice?.Price),
    renewal_price: priceString(price.RenewalPrice?.Price),
    transfer_price: priceString(price.TransferPrice?.Price),
    currency: price.RegistrationPrice?.Currency ?? price.RenewalPrice?.Currency ?? price.TransferPrice?.Currency
  };
}
async function getTldPrice(tld, config) {
  const { domains } = makeClients(config);
  const normalized = normalizeTld(tld);
  const prices = await domains.send(new ListPricesCommand({ Tld: normalized, MaxItems: 1 }));
  const price = prices.Prices?.[0];
  return price ? tldPriceFromRoute53Price(normalized, price) : null;
}
async function listTldPrices(config) {
  const { domains } = makeClients(config);
  const prices = [];
  let marker;
  do {
    const result = await domains.send(new ListPricesCommand({ Marker: marker, MaxItems: 100 }));
    for (const price of result.Prices ?? []) {
      prices.push(tldPriceFromRoute53Price(price.Name || "", price));
    }
    marker = result.NextPageMarker;
  } while (marker);
  return prices;
}
async function registerDomain2(domain, contact, durationYears = 1, autoRenew = true, config, options = {}) {
  const { domains } = makeClients(config);
  const contactDetail = contactToRoute53Contact(contact);
  const result = await domains.send(new RegisterDomainCommand({
    DomainName: domain,
    DurationInYears: durationYears,
    AutoRenew: autoRenew,
    AdminContact: contactDetail,
    RegistrantContact: contactDetail,
    TechContact: contactDetail,
    PrivacyProtectAdminContact: options.privacy_protected ?? true,
    PrivacyProtectRegistrantContact: options.privacy_protected ?? true,
    PrivacyProtectTechContact: options.privacy_protected ?? true,
    ...options.nameservers?.length ? { Nameservers: options.nameservers.map((Name) => ({ Name })) } : {}
  }));
  return { operationId: result.OperationId ?? "" };
}
function contactToRoute53Contact(contact) {
  return {
    FirstName: contact.first_name,
    LastName: contact.last_name,
    Email: contact.email,
    PhoneNumber: contact.phone,
    AddressLine1: contact.address_line_1,
    ...contact.address_line_2 ? { AddressLine2: contact.address_line_2 } : {},
    City: contact.city,
    ...contact.state ? { State: contact.state } : {},
    CountryCode: contact.country_code.toUpperCase(),
    ZipCode: contact.zip_code,
    ContactType: contact.organization_name ? "COMPANY" : "PERSON",
    ...contact.organization_name ? { OrganizationName: contact.organization_name } : {}
  };
}
async function transferDomain(domain, authCode, contact, durationYears = 1, autoRenew = true, config, options = {}) {
  const { domains } = makeClients(config);
  const contactDetail = contactToRoute53Contact(contact);
  const result = await domains.send(new TransferDomainCommand({
    DomainName: domain,
    AuthCode: authCode,
    DurationInYears: durationYears,
    AutoRenew: autoRenew,
    AdminContact: contactDetail,
    RegistrantContact: contactDetail,
    TechContact: contactDetail,
    PrivacyProtectAdminContact: options.privacy_protected ?? true,
    PrivacyProtectRegistrantContact: options.privacy_protected ?? true,
    PrivacyProtectTechContact: options.privacy_protected ?? true,
    ...options.nameservers?.length ? { Nameservers: options.nameservers.map((Name) => ({ Name })) } : {}
  }));
  return { operationId: result.OperationId ?? "" };
}
async function getRegistrationStatus(operationId, config) {
  const { domains } = makeClients(config);
  const result = await domains.send(new GetOperationDetailCommand({ OperationId: operationId }));
  return {
    status: result.Status ?? "UNKNOWN",
    domain: result.DomainName,
    message: result.Message
  };
}
async function getRegistrationContactFromDomain(domain, config) {
  const { domains } = makeClients(config);
  const result = await domains.send(new GetDomainDetailCommand({ DomainName: domain }));
  const contact = result.RegistrantContact ?? result.AdminContact ?? result.TechContact;
  if (!contact)
    throw new Error(`Route53 returned no reusable registration contact for ${domain}`);
  const required = {
    first_name: contact.FirstName,
    last_name: contact.LastName,
    email: contact.Email,
    phone: contact.PhoneNumber,
    address_line_1: contact.AddressLine1,
    city: contact.City,
    country_code: contact.CountryCode,
    zip_code: contact.ZipCode
  };
  for (const [key, value] of Object.entries(required)) {
    if (!value || !String(value).trim())
      throw new Error(`Route53 contact source is missing ${key}`);
  }
  return {
    first_name: String(required.first_name),
    last_name: String(required.last_name),
    email: String(required.email),
    phone: String(required.phone),
    address_line_1: String(required.address_line_1),
    ...contact.AddressLine2 ? { address_line_2: contact.AddressLine2 } : {},
    city: String(required.city),
    ...contact.State ? { state: contact.State } : {},
    country_code: String(required.country_code).toLowerCase(),
    zip_code: String(required.zip_code),
    ...contact.OrganizationName ? { organization_name: contact.OrganizationName } : {}
  };
}
async function findRegisteredDomainDetail(domain, config) {
  const summary = await getDomainSummary(domain, config);
  if (!summary)
    return null;
  return getDomainDetail(domain, config);
}
async function getDomainDetail(domain, config) {
  const { domains } = makeClients(config);
  const [result, summary] = await Promise.all([
    domains.send(new GetDomainDetailCommand({ DomainName: domain })),
    getDomainSummary(domain, config)
  ]);
  const privacy = result;
  return {
    domain: result.DomainName ?? domain,
    expiry: result.ExpirationDate?.toISOString() ?? summary?.expiry ?? "",
    auto_renew: result.AutoRenew ?? summary?.auto_renew ?? null,
    transfer_lock: summary?.transfer_lock ?? (result.StatusList?.includes("TRANSFER_LOCK") ? true : null),
    created: result.CreationDate?.toISOString() ?? "",
    updated: result.UpdatedDate?.toISOString() ?? "",
    nameservers: (result.Nameservers ?? []).map((ns) => ns.Name ?? "").filter(Boolean),
    status_list: result.StatusList ?? [],
    registrar_name: result.RegistrarName,
    privacy_protected: privacy.RegistrantPrivacy ?? privacy.AdminPrivacy ?? privacy.TechPrivacy ?? null
  };
}
async function getDomainSummary(domain, config) {
  let marker;
  do {
    const { domains } = makeClients(config);
    const result = await domains.send(new ListDomainsCommand({ Marker: marker, MaxItems: 100 }));
    const summary = (result.Domains ?? []).find((item) => item.DomainName === domain);
    if (summary) {
      return {
        domain: summary.DomainName ?? domain,
        expiry: summary.Expiry?.toISOString() ?? "",
        auto_renew: summary.AutoRenew ?? false,
        transfer_lock: summary.TransferLock ?? false
      };
    }
    marker = result.NextPageMarker;
  } while (marker);
  return null;
}
async function requestTransferOutAuthCode(domain, config) {
  const { domains } = makeClients(config);
  const detail = await getDomainDetail(domain, config);
  let transferLockDisabled = false;
  let transferLockOperationId = null;
  if (detail.transfer_lock) {
    const result = await domains.send(new DisableDomainTransferLockCommand({ DomainName: domain }));
    transferLockDisabled = true;
    transferLockOperationId = result.OperationId ?? null;
  }
  const authCodeResult = await domains.send(new RetrieveDomainAuthCodeCommand({ DomainName: domain }));
  if (!authCodeResult.AuthCode) {
    throw new Error("Route53 did not return a domain transfer authorization code");
  }
  return {
    auth_code: authCodeResult.AuthCode,
    transfer_lock_disabled: transferLockDisabled,
    transfer_lock_operation_id: transferLockOperationId
  };
}
async function updateNameservers2(domain, nameservers, config, client) {
  if (!nameservers.length) {
    throw new Error("updateNameservers requires at least one nameserver");
  }
  const domains = client ?? makeClients(config).domains;
  const result = await domains.send(new UpdateDomainNameserversCommand({
    DomainName: domain,
    Nameservers: nameservers.map((name) => ({ Name: name }))
  }));
  return { operationId: result.OperationId ?? "" };
}
async function listRegisteredDomains(config) {
  const { domains } = makeClients(config);
  const all = [];
  let nextPageMarker;
  do {
    const result = await domains.send(new ListDomainsCommand({ Marker: nextPageMarker }));
    for (const d of result.Domains ?? []) {
      all.push({
        domain: d.DomainName ?? "",
        expiry: d.Expiry?.toISOString() ?? "",
        auto_renew: d.AutoRenew ?? false,
        transfer_lock: d.TransferLock ?? false
      });
    }
    nextPageMarker = result.NextPageMarker;
  } while (nextPageMarker);
  return all;
}
function cleanZoneId(id) {
  return id.replace("/hostedzone/", "");
}
function normalizeZoneId(id) {
  return cleanZoneId(id.trim());
}
function normalizeNameserver(value) {
  return value.trim().toLowerCase().replace(/\.$/, "");
}
function nameserversMatch(a, b) {
  if (a.length !== b.length)
    return false;
  const left = [...new Set(a.map(normalizeNameserver))].sort();
  const right = [...new Set(b.map(normalizeNameserver))].sort();
  return left.length === right.length && left.every((value, index) => value === right[index]);
}
function cleanChangeId(id) {
  return id?.replace("/change/", "") || null;
}
async function createHostedZone(domain, comment, config, options) {
  const { route53 } = makeClients(config);
  const callerRef = options?.callerReference ?? `domains-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  const result = await route53.send(new CreateHostedZoneCommand({
    Name: domain,
    CallerReference: callerRef,
    HostedZoneConfig: comment ? { Comment: comment } : undefined
  }));
  return {
    id: cleanZoneId(result.HostedZone?.Id ?? ""),
    name: result.HostedZone?.Name ?? domain,
    record_count: result.HostedZone?.ResourceRecordSetCount ?? 0,
    comment,
    name_servers: result.DelegationSet?.NameServers ?? []
  };
}
async function listHostedZones(config) {
  const { route53 } = makeClients(config);
  const zones = [];
  let marker;
  do {
    const result = await route53.send(new ListHostedZonesCommand({ Marker: marker }));
    for (const z of result.HostedZones ?? []) {
      zones.push({
        id: cleanZoneId(z.Id ?? ""),
        name: z.Name ?? "",
        record_count: z.ResourceRecordSetCount ?? 0,
        comment: z.Config?.Comment,
        private_zone: z.Config?.PrivateZone
      });
    }
    marker = result.IsTruncated ? result.NextMarker : undefined;
  } while (marker);
  return zones;
}
async function getHostedZone(hostedZoneId, config) {
  const { route53 } = makeClients(config);
  const result = await route53.send(new GetHostedZoneCommand({ Id: normalizeZoneId(hostedZoneId) }));
  return {
    id: cleanZoneId(result.HostedZone?.Id ?? ""),
    name: result.HostedZone?.Name ?? "",
    record_count: result.HostedZone?.ResourceRecordSetCount ?? 0,
    comment: result.HostedZone?.Config?.Comment,
    name_servers: result.DelegationSet?.NameServers ?? [],
    private_zone: result.HostedZone?.Config?.PrivateZone
  };
}
async function deleteHostedZone(hostedZoneId, config) {
  const { route53 } = makeClients(config);
  const zoneId = normalizeZoneId(hostedZoneId);
  const managedRecords = [];
  let nextName;
  let nextType;
  let nextIdentifier;
  do {
    const result = await route53.send(new ListResourceRecordSetsCommand({
      HostedZoneId: zoneId,
      StartRecordName: nextName,
      StartRecordType: nextType,
      StartRecordIdentifier: nextIdentifier
    }));
    managedRecords.push(...result.ResourceRecordSets?.filter((record) => record.Type !== "NS" && record.Type !== "SOA") ?? []);
    nextName = result.IsTruncated ? result.NextRecordName : undefined;
    nextType = result.IsTruncated ? result.NextRecordType : undefined;
    nextIdentifier = result.IsTruncated ? result.NextRecordIdentifier : undefined;
  } while (nextName && nextType);
  for (let index = 0;index < managedRecords.length; index += 100) {
    const batch = managedRecords.slice(index, index + 100);
    if (batch.length === 0)
      continue;
    await route53.send(new ChangeResourceRecordSetsCommand({
      HostedZoneId: zoneId,
      ChangeBatch: {
        Changes: batch.map((record) => ({
          Action: "DELETE",
          ResourceRecordSet: record
        }))
      }
    }));
  }
  await route53.send(new DeleteHostedZoneCommand({ Id: zoneId }));
}
async function listHostedZonesByDomain(domain, config) {
  const { route53 } = makeClients(config);
  const normalized = domain.trim().toLowerCase().replace(/\.$/, "");
  const dnsName = `${normalized}.`;
  const zones = [];
  let nextDnsName = normalized;
  let nextHostedZoneId;
  do {
    const result = await route53.send(new ListHostedZonesByNameCommand({
      DNSName: nextDnsName,
      HostedZoneId: nextHostedZoneId
    }));
    for (const zone of result.HostedZones ?? []) {
      if (zone.Name?.toLowerCase() !== dnsName || zone.Config?.PrivateZone)
        continue;
      zones.push({
        id: cleanZoneId(zone.Id ?? ""),
        name: zone.Name ?? dnsName,
        record_count: zone.ResourceRecordSetCount ?? 0,
        comment: zone.Config?.Comment,
        private_zone: false
      });
    }
    if (!result.IsTruncated || !result.NextDNSName)
      break;
    if (result.NextDNSName.toLowerCase().replace(/\.$/, "") !== normalized)
      break;
    nextDnsName = result.NextDNSName;
    nextHostedZoneId = result.NextHostedZoneId;
  } while (nextDnsName);
  return zones.filter((zone) => zone.id);
}
async function deleteDefaultHostedZone(hostedZoneId, domain, delegatedNameservers, config) {
  const zoneId = normalizeZoneId(hostedZoneId);
  const expectedName = `${domain.trim().toLowerCase().replace(/\.$/, "")}.`;
  const detail = await getHostedZone(zoneId, config);
  if (detail.id !== zoneId || detail.name.toLowerCase() !== expectedName || detail.private_zone) {
    return { deleted: false, reason: "hosted zone identity does not match the persisted public-zone provenance" };
  }
  if (!delegatedNameservers.length || nameserversMatch(detail.name_servers, delegatedNameservers)) {
    return { deleted: false, reason: "registrar delegation still points at the Route 53 hosted zone" };
  }
  const { route53 } = makeClients(config);
  const records = [];
  let nextName;
  let nextType;
  let nextIdentifier;
  do {
    const result = await route53.send(new ListResourceRecordSetsCommand({
      HostedZoneId: zoneId,
      StartRecordName: nextName,
      StartRecordType: nextType,
      StartRecordIdentifier: nextIdentifier
    }));
    records.push(...result.ResourceRecordSets ?? []);
    nextName = result.IsTruncated ? result.NextRecordName : undefined;
    nextType = result.IsTruncated ? result.NextRecordType : undefined;
    nextIdentifier = result.IsTruncated ? result.NextRecordIdentifier : undefined;
  } while (nextName && nextType);
  const apexRecords = records.filter((record) => record.Name?.toLowerCase() === expectedName);
  const ns = apexRecords.filter((record) => record.Type === "NS");
  const soa = apexRecords.filter((record) => record.Type === "SOA");
  if (records.length !== 2 || apexRecords.length !== 2 || ns.length !== 1 || soa.length !== 1) {
    return { deleted: false, reason: "hosted zone contains records other than the expected apex NS and SOA" };
  }
  const nsValues = ns[0]?.ResourceRecords?.map((record) => record.Value ?? "") ?? [];
  if (!nameserversMatch(nsValues, detail.name_servers)) {
    return { deleted: false, reason: "hosted zone apex NS does not match its delegation set" };
  }
  try {
    await route53.send(new DeleteHostedZoneCommand({ Id: zoneId }));
    return { deleted: true };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    const name = error instanceof Error ? error.name : "";
    if (name === "HostedZoneNotEmpty" || /HostedZoneNotEmpty|not empty/i.test(message)) {
      return { deleted: false, reason: "hosted zone changed after inspection and Route 53 refused deletion" };
    }
    throw error;
  }
}
async function findHostedZoneByDomain(domain, config) {
  const zones = await listHostedZones(config);
  const normalized = domain.endsWith(".") ? domain : `${domain}.`;
  const matches = zones.filter((z) => z.name === normalized);
  if (matches.length === 0)
    return null;
  const publicMatches = matches.filter((z) => !z.private_zone);
  const candidates = publicMatches.length > 0 ? publicMatches : matches;
  if (candidates.length > 1) {
    throw new Error(`Multiple Route 53 hosted zones found for ${domain}; specify hosted zone id`);
  }
  return candidates[0] ?? null;
}
async function findHostedZoneByNameservers(domain, nameservers, config) {
  if (!nameservers.length)
    return null;
  const { route53 } = makeClients(config);
  const result = await route53.send(new ListHostedZonesByNameCommand({ DNSName: domain }));
  const zones = (result.HostedZones ?? []).filter((zone) => zone.Name?.replace(/\.$/, "") === domain && !zone.Config?.PrivateZone);
  for (const zone of zones) {
    const id = cleanZoneId(zone.Id ?? "");
    if (!id)
      continue;
    const detail = await getHostedZone(id, config);
    const delegatedNameservers = detail.name_servers ?? [];
    if (nameserversMatch(delegatedNameservers, nameservers)) {
      return { ...detail, id, name_servers: delegatedNameservers };
    }
  }
  return null;
}
function rrsToRecord(rrs) {
  if (rrs.AliasTarget) {
    return {
      name: rrs.Name ?? "",
      type: rrs.Type ?? "",
      ttl: 0,
      values: [],
      alias_target: {
        hosted_zone_id: rrs.AliasTarget.HostedZoneId ?? "",
        dns_name: rrs.AliasTarget.DNSName ?? ""
      }
    };
  }
  return {
    name: rrs.Name ?? "",
    type: rrs.Type ?? "",
    ttl: rrs.TTL ?? 0,
    values: (rrs.ResourceRecords ?? []).map((r) => r.Value ?? "")
  };
}
function recordToRrs(record) {
  if (record.alias_target) {
    return {
      Name: record.name,
      Type: record.type,
      AliasTarget: {
        HostedZoneId: record.alias_target.hosted_zone_id,
        DNSName: record.alias_target.dns_name,
        EvaluateTargetHealth: false
      }
    };
  }
  return {
    Name: record.name,
    Type: record.type,
    TTL: record.ttl ?? 300,
    ResourceRecords: record.values.map((v) => ({ Value: v }))
  };
}
async function listRecords(hostedZoneId, config, options = {}) {
  const { route53 } = makeClients(config);
  const records = [];
  let nextName;
  let nextType;
  do {
    const result = await route53.send(new ListResourceRecordSetsCommand({
      HostedZoneId: normalizeZoneId(hostedZoneId),
      StartRecordName: nextName,
      StartRecordType: nextType
    }));
    for (const rrs of result.ResourceRecordSets ?? []) {
      if (options.requireSimpleRouting && (rrs.SetIdentifier !== undefined || rrs.Weight !== undefined || rrs.Region !== undefined || rrs.GeoLocation !== undefined || rrs.Failover !== undefined || rrs.HealthCheckId !== undefined || rrs.TrafficPolicyInstanceId !== undefined || rrs.CidrRoutingConfig !== undefined || rrs.GeoProximityLocation !== undefined || rrs.MultiValueAnswer !== undefined && rrs.MultiValueAnswer !== false)) {
        throw new Error("Route 53 routing policy or health check cannot be preserved automatically");
      }
      records.push(rrsToRecord(rrs));
    }
    if (result.IsTruncated) {
      nextName = result.NextRecordName;
      nextType = result.NextRecordType;
    } else {
      nextName = undefined;
      nextType = undefined;
    }
  } while (nextName);
  return records;
}
async function upsertRecord(hostedZoneId, record, config, options) {
  const { route53 } = makeClients(config);
  const result = await route53.send(new ChangeResourceRecordSetsCommand({
    HostedZoneId: normalizeZoneId(hostedZoneId),
    ChangeBatch: {
      ...options?.comment ? { Comment: options.comment } : {},
      Changes: [{ Action: "UPSERT", ResourceRecordSet: recordToRrs(record) }]
    }
  }));
  return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
}
async function deleteRecord(hostedZoneId, record, config, options) {
  const { route53 } = makeClients(config);
  const result = await route53.send(new ChangeResourceRecordSetsCommand({
    HostedZoneId: normalizeZoneId(hostedZoneId),
    ChangeBatch: {
      ...options?.comment ? { Comment: options.comment } : {},
      Changes: [{ Action: "DELETE", ResourceRecordSet: recordToRrs(record) }]
    }
  }));
  return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
}
async function upsertRecords(hostedZoneId, records, config, options) {
  if (records.length === 0)
    return { changeId: null };
  const { route53 } = makeClients(config);
  const changes = records.map((r) => ({
    Action: "UPSERT",
    ResourceRecordSet: recordToRrs(r)
  }));
  const result = await route53.send(new ChangeResourceRecordSetsCommand({
    HostedZoneId: normalizeZoneId(hostedZoneId),
    ChangeBatch: {
      ...options?.comment ? { Comment: options.comment } : {},
      Changes: changes
    }
  }));
  return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
}
function parseMxValue(raw) {
  const match = /^([0-9]+)[ \t]+([^\s]+)$/u.exec(raw.trim());
  const priority = match ? Number(match[1]) : NaN;
  if (!match || !Number.isInteger(priority) || priority < 0 || priority > 65535) {
    throw new Error("Route 53 contains an invalid MX preference or exchange");
  }
  return { priority, value: match[2] };
}
function createRoute53Provider(config) {
  const cfg = config ?? getConfig2();
  const registerWithRoute53 = registerDomain2;
  const updateRoute53Nameservers = updateNameservers2;
  async function listDomainInventory() {
    const byDomain = new Map;
    try {
      const registered = await listRegisteredDomains(cfg);
      for (const d of registered) {
        byDomain.set(d.domain, {
          domain: d.domain,
          registrar: "AWS Route 53",
          created: "",
          expires: d.expiry,
          nameservers: [],
          status: "active",
          auto_renew: d.auto_renew
        });
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      if (!message.includes("route53domains:ListDomains") && !message.includes("AccessDenied")) {
        throw error;
      }
    }
    const zones = await listHostedZones(cfg);
    for (const z of zones) {
      const zone = z.name_servers?.length ? z : await getHostedZone(z.id, cfg).catch(() => z);
      const domain = zone.name.replace(/\.$/, "");
      const nameservers = zone.name_servers ?? [];
      const existing = byDomain.get(domain);
      if (existing) {
        existing.nameservers = nameservers.length > 0 ? nameservers : existing.nameservers;
        continue;
      }
      byDomain.set(domain, {
        domain,
        registrar: "AWS Route 53 DNS",
        created: "",
        expires: "",
        nameservers,
        status: "active",
        auto_renew: false
      });
    }
    return Array.from(byDomain.values());
  }
  return {
    name: "route53",
    dnsWriteScope: "changed-groups",
    async listDomains() {
      return listDomainInventory();
    },
    async getDomainInfo(domain) {
      const detail = await getDomainDetail(domain, cfg);
      return {
        domain: detail.domain,
        registrar: "AWS Route 53",
        created: detail.created,
        expires: detail.expiry,
        nameservers: detail.nameservers,
        status: "active",
        auto_renew: detail.auto_renew ?? false
      };
    },
    async registerDomain(domain, contact, options = {}) {
      const result = await registerWithRoute53(domain, contact, options.years ?? 1, options.autoRenew ?? true, cfg);
      return { domain, success: !!result.operationId, operationId: result.operationId };
    },
    async updateNameservers(domain, nameservers) {
      const result = await updateRoute53Nameservers(domain, nameservers, cfg);
      return { domain, success: !!result.operationId, operationId: result.operationId };
    },
    async renewDomain(_domain) {
      return { domain: _domain, success: false, orderId: undefined, chargedAmount: undefined };
    },
    async getDnsRecords(domain) {
      const zone = await findHostedZoneByDomain(domain, cfg);
      if (!zone)
        return [];
      const records = await listRecords(zone.id, cfg, { requireSimpleRouting: true });
      const result = [];
      for (const r of records) {
        if (r.alias_target) {
          result.push({ type: r.type, name: r.name, value: r.alias_target.dns_name, ttl: 0 });
        } else {
          for (const v of r.values) {
            const mx = r.type === "MX" ? parseMxValue(v) : undefined;
            result.push({
              type: r.type,
              name: r.name,
              value: mx?.value ?? v,
              ttl: r.ttl,
              ...mx === undefined ? {} : { priority: mx.priority }
            });
          }
        }
      }
      return result;
    },
    async setDnsRecords(domain, records) {
      const zone = await findHostedZoneByDomain(domain, cfg);
      if (!zone)
        throw new Error(`No hosted zone found for ${domain}`);
      const grouped = new Map;
      for (const r of records) {
        const mx = r.type === "MX" ? parseMxValue(r.priority === undefined ? r.value : `${r.priority} ${r.value}`) : undefined;
        const value = mx === undefined ? r.value : `${mx.priority} ${mx.value}`;
        const key = `${r.name}|${r.type}`;
        const existing = grouped.get(key);
        if (existing) {
          existing.values.push(value);
        } else {
          grouped.set(key, { name: r.name, type: r.type, ttl: r.ttl, values: [value] });
        }
      }
      await upsertRecords(zone.id, Array.from(grouped.values()), cfg);
      return true;
    },
    async checkAvailability(domain) {
      const result = await checkAvailability3(domain, cfg);
      return {
        domain: result.domain,
        available: result.available,
        standard_price: result.price ? Number(result.price) : undefined,
        currency: result.currency
      };
    },
    async syncToLocalDb(dbFns) {
      const domains = await listDomainInventory();
      const syncedAt = new Date().toISOString();
      let synced = 0;
      let created = 0;
      let updated = 0;
      const errors = [];
      for (const d of domains) {
        try {
          const existing = await dbFns.getDomainByName(d.domain);
          if (existing) {
            const existingRoute53 = existing.metadata["route53"];
            const staleDnsOnlyRegistrar = d.registrar !== "AWS Route 53" && (existing.registrar === "AWS Route 53 DNS" || existing.registrar === "AWS Route 53" && existingRoute53?.source === "route53:hosted_zones");
            await dbFns.updateDomain(existing.id, {
              ...d.registrar === "AWS Route 53" ? { registrar: "AWS Route 53" } : {},
              ...staleDnsOnlyRegistrar ? { registrar: null } : {},
              ...d.registrar === "AWS Route 53" ? { expiry_synced_at: syncedAt } : {},
              expires_at: d.expires || undefined,
              auto_renew: d.auto_renew,
              nameservers: d.nameservers.length > 0 ? d.nameservers : existing.nameservers,
              metadata: {
                ...existing.metadata,
                route53: {
                  source: d.registrar === "AWS Route 53" ? "route53domains+hosted_zones" : "route53:hosted_zones",
                  synced_at: syncedAt
                }
              },
              status: "active"
            });
            updated++;
          } else {
            await dbFns.createDomain({
              name: d.domain,
              ...d.registrar === "AWS Route 53" ? { registrar: "AWS Route 53" } : {},
              ...d.registrar === "AWS Route 53" ? { expiry_synced_at: syncedAt } : {},
              expires_at: d.expires || undefined,
              auto_renew: d.auto_renew,
              nameservers: d.nameservers,
              status: "active",
              notes: d.registrar === "AWS Route 53 DNS" ? "Discovered from Route 53 hosted zones; registrar ownership was not inferred." : undefined,
              metadata: {
                route53: {
                  source: d.registrar === "AWS Route 53" ? "route53domains+hosted_zones" : "route53:hosted_zones",
                  synced_at: syncedAt
                }
              }
            });
            created++;
          }
          synced++;
        } catch (err) {
          errors.push(`${d.domain}: ${err instanceof Error ? err.message : String(err)}`);
        }
      }
      return { synced, created, updated, errors };
    }
  };
}

// src/lib/cloudflare.ts
import { createHash } from "node:crypto";

// src/lib/dns-txt.ts
function decodeDnsTxt(value) {
  const input = value.trim();
  if (!input.startsWith('"'))
    return value;
  let result = "";
  let i = 0;
  while (i < input.length) {
    if (input[i++] !== '"')
      throw new Error("Malformed DNS TXT presentation");
    let closed = false;
    while (i < input.length) {
      let char = input[i++];
      if (char === '"') {
        closed = true;
        break;
      }
      if (char === "\\") {
        if (i === input.length)
          throw new Error("Malformed DNS TXT escape");
        if (/\d/u.test(input[i])) {
          const digits = input.slice(i, i + 3);
          if (!/^\d{3}$/u.test(digits) || Number(digits) > 255)
            throw new Error("Malformed DNS TXT octet");
          char = String.fromCharCode(Number(digits));
          i += 3;
        } else
          char = input[i++];
      }
      if (/[\x00-\x1f\x7f]/u.test(char))
        throw new Error("DNS TXT control character is not supported");
      result += char;
    }
    if (!closed)
      throw new Error("Unterminated DNS TXT string");
    if (i < input.length && !/\s/u.test(input[i]))
      throw new Error("Malformed DNS TXT string separator");
    while (i < input.length && /\s/u.test(input[i]))
      i++;
  }
  return result;
}
function mailPolicyKind(value) {
  const semantic = decodeDnsTxt(value).trim();
  if (/^v=spf1(?:\s|$)/iu.test(semantic))
    return "spf";
  if (/^v=DMARC1(?:;|\s|$)/iu.test(semantic))
    return "dmarc";
  return null;
}

// src/lib/env-aliases.ts
function firstEnv(env, names) {
  for (const key of names) {
    const value = env[key];
    if (value)
      return { key, value };
  }
  return;
}
function hasEveryEnv(env, groups) {
  return groups.every((names) => !!firstEnv(env, names));
}
function flattenEnvNames(groups) {
  return Array.from(new Set(groups.flatMap((names) => [...names])));
}
var NAMECHEAP_ENV = {
  apiKey: ["NAMECHEAP_API_KEY"],
  username: ["NAMECHEAP_USERNAME"],
  clientIp: ["NAMECHEAP_CLIENT_IP"]
};
var GODADDY_ENV = {
  apiKey: ["GODADDY_API_KEY"],
  apiSecret: ["GODADDY_API_SECRET"]
};
var BRANDSIGHT_ENV = {
  apiKey: ["BRANDSIGHT_API_KEY"],
  apiSecret: ["BRANDSIGHT_API_SECRET"],
  customerId: ["BRANDSIGHT_CUSTOMER_ID"],
  shopperId: ["BRANDSIGHT_SHOPPER_ID"],
  accountId: ["BRANDSIGHT_ACCOUNT_ID"]
};
var SEDO_ENV = {
  partnerId: ["SEDO_PARTNER_ID"],
  signKey: ["SEDO_API_KEY", "SEDO_SIGN_KEY"],
  username: ["SEDO_USERNAME", "SEDO_EMAIL"],
  password: ["SEDO_PASSWORD"]
};
var CLOUDFLARE_ENV = {
  apiToken: ["CLOUDFLARE_API_TOKEN"],
  apiKey: ["CLOUDFLARE_API_KEY"],
  email: ["CLOUDFLARE_EMAIL"],
  accountId: ["CLOUDFLARE_ACCOUNT_ID"]
};
var ROUTE53_ENV = {
  accessKeyId: ["ROUTE53_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"],
  secretAccessKey: ["ROUTE53_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"],
  sessionToken: ["ROUTE53_SESSION_TOKEN", "AWS_SESSION_TOKEN"],
  profile: ["ROUTE53_AWS_PROFILE", "AWS_PROFILE"],
  region: ["ROUTE53_REGION", "ROUTE53_AWS_REGION", "AWS_REGION"],
  webIdentityTokenFile: ["AWS_WEB_IDENTITY_TOKEN_FILE"],
  roleArn: ["AWS_ROLE_ARN"],
  containerCredentials: [
    "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
    "AWS_CONTAINER_CREDENTIALS_FULL_URI"
  ]
};
function providerEnvNames(provider) {
  switch (provider.toLowerCase()) {
    case "namecheap":
      return flattenEnvNames([NAMECHEAP_ENV.apiKey, NAMECHEAP_ENV.username, NAMECHEAP_ENV.clientIp]);
    case "godaddy":
      return flattenEnvNames([GODADDY_ENV.apiKey, GODADDY_ENV.apiSecret]);
    case "brandsight":
      return flattenEnvNames([
        BRANDSIGHT_ENV.apiKey,
        BRANDSIGHT_ENV.apiSecret,
        BRANDSIGHT_ENV.customerId,
        BRANDSIGHT_ENV.shopperId,
        BRANDSIGHT_ENV.accountId
      ]);
    case "sedo":
      return flattenEnvNames([SEDO_ENV.partnerId, SEDO_ENV.signKey, SEDO_ENV.username, SEDO_ENV.password]);
    case "cloudflare":
      return flattenEnvNames([CLOUDFLARE_ENV.apiToken, CLOUDFLARE_ENV.apiKey, CLOUDFLARE_ENV.email, CLOUDFLARE_ENV.accountId]);
    case "route53":
      return flattenEnvNames([
        ROUTE53_ENV.profile,
        ROUTE53_ENV.accessKeyId,
        ROUTE53_ENV.secretAccessKey,
        ROUTE53_ENV.sessionToken,
        ROUTE53_ENV.region,
        ROUTE53_ENV.webIdentityTokenFile,
        ROUTE53_ENV.roleArn,
        ROUTE53_ENV.containerCredentials
      ]);
    default:
      return [];
  }
}
function hasProviderCredentials(provider, env = process.env) {
  switch (provider.toLowerCase()) {
    case "namecheap":
      return hasEveryEnv(env, [NAMECHEAP_ENV.apiKey, NAMECHEAP_ENV.username, NAMECHEAP_ENV.clientIp]);
    case "godaddy":
      return hasEveryEnv(env, [GODADDY_ENV.apiKey, GODADDY_ENV.apiSecret]);
    case "brandsight":
      return hasEveryEnv(env, [BRANDSIGHT_ENV.apiKey, BRANDSIGHT_ENV.apiSecret, BRANDSIGHT_ENV.customerId]);
    case "sedo":
      return hasEveryEnv(env, [SEDO_ENV.partnerId, SEDO_ENV.signKey, SEDO_ENV.username, SEDO_ENV.password]);
    case "cloudflare": {
      const tokenMode = !!firstEnv(env, CLOUDFLARE_ENV.apiToken);
      const keyMode = hasEveryEnv(env, [CLOUDFLARE_ENV.apiKey, CLOUDFLARE_ENV.email]);
      return tokenMode || keyMode;
    }
    case "route53": {
      const profileMode = !!firstEnv(env, ROUTE53_ENV.profile);
      const keyMode = hasEveryEnv(env, [ROUTE53_ENV.accessKeyId, ROUTE53_ENV.secretAccessKey]);
      const webIdentityMode = hasEveryEnv(env, [ROUTE53_ENV.webIdentityTokenFile, ROUTE53_ENV.roleArn]);
      const containerMode = !!firstEnv(env, ROUTE53_ENV.containerCredentials);
      return profileMode || keyMode || webIdentityMode || containerMode;
    }
    default:
      return false;
  }
}

// src/lib/cloudflare-auth.ts
function resolveCloudflareConfig(env = process.env) {
  const accountId = firstEnv(env, CLOUDFLARE_ENV.accountId)?.value;
  const apiToken = firstEnv(env, CLOUDFLARE_ENV.apiToken)?.value;
  if (apiToken)
    return { apiToken, accountId };
  const apiKey = firstEnv(env, CLOUDFLARE_ENV.apiKey)?.value;
  const email = firstEnv(env, CLOUDFLARE_ENV.email)?.value;
  if (apiKey && email)
    return { apiKey, email, accountId };
  return accountId ? { accountId } : {};
}
function cloudflareAuthHeaders(cfg) {
  if (cfg.apiToken)
    return { Authorization: `Bearer ${cfg.apiToken}` };
  if (cfg.apiKey && cfg.email)
    return { "X-Auth-Key": cfg.apiKey, "X-Auth-Email": cfg.email };
  throw new Error("Cloudflare credentials not configured. Set CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL.");
}

// src/lib/cloudflare.ts
class CloudflareApiError extends Error {
  statusCode;
  providerCode;
  constructor(message, statusCode, providerCode) {
    const code = providerCode ? `, code ${providerCode}` : "";
    super(`Cloudflare API error (HTTP ${statusCode}${code}): ${message}`);
    this.statusCode = statusCode;
    this.providerCode = providerCode;
    this.name = "CloudflareApiError";
  }
}
function getConfig3() {
  return resolveCloudflareConfig();
}
function checkCredentials(cfg) {
  cloudflareAuthHeaders(cfg);
}
var CF_BASE = "https://api.cloudflare.com/client/v4";
var DEFAULT_CF_TIMEOUT_MS = 30000;
var MAX_CF_TIMEOUT_MS = 120000;
var DEFAULT_CF_MAX_RESPONSE_BYTES = 1048576;
var MAX_CF_RESPONSE_BYTES = 4194304;
function positiveBoundedEnv(name, fallback, maximum) {
  const raw = process.env[name]?.trim();
  if (!raw)
    return fallback;
  const value = Number(raw);
  if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
    throw new Error(`${name} must be an integer between 1 and ${maximum}`);
  }
  return value;
}
async function boundedResponseText(response, maxBytes, signal) {
  const contentLength = Number(response.headers.get("content-length"));
  if (Number.isFinite(contentLength) && contentLength > maxBytes) {
    throw new Error(`Cloudflare API response exceeds ${maxBytes} bytes`);
  }
  if (!response.body)
    return "";
  const reader = response.body.getReader();
  const abort = () => void reader.cancel("Cloudflare API request aborted");
  if (signal.aborted)
    abort();
  signal.addEventListener("abort", abort, { once: true });
  const chunks = [];
  let length = 0;
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done)
        break;
      length += value.byteLength;
      if (length > maxBytes) {
        await reader.cancel("Cloudflare API response exceeds bounded size");
        throw new Error(`Cloudflare API response exceeds ${maxBytes} bytes`);
      }
      chunks.push(value);
    }
  } finally {
    signal.removeEventListener("abort", abort);
  }
  const bytes = new Uint8Array(length);
  let offset = 0;
  for (const chunk of chunks) {
    bytes.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return new TextDecoder().decode(bytes);
}
async function cfFetch(path, opts = {}) {
  const cfg = opts.config ?? getConfig3();
  checkCredentials(cfg);
  const timeoutMs = Math.min(opts.timeoutMs ?? MAX_CF_TIMEOUT_MS, positiveBoundedEnv("DOMAINS_PROVIDER_HTTP_TIMEOUT_MS", DEFAULT_CF_TIMEOUT_MS, MAX_CF_TIMEOUT_MS));
  const maxResponseBytes = positiveBoundedEnv("DOMAINS_PROVIDER_MAX_RESPONSE_BYTES", DEFAULT_CF_MAX_RESPONSE_BYTES, MAX_CF_RESPONSE_BYTES);
  const controller = new AbortController;
  let timer;
  const timeout = new Promise((_resolve, reject) => {
    timer = setTimeout(() => {
      controller.abort();
      reject(new Error(`Cloudflare API request exceeded ${timeoutMs}ms`));
    }, timeoutMs);
  });
  let json;
  let status = 0;
  try {
    ({ json, status } = await Promise.race([
      (async () => {
        const response = await fetch(`${CF_BASE}${path}`, {
          method: opts.method ?? "GET",
          headers: {
            ...cloudflareAuthHeaders(cfg),
            "Content-Type": "application/json"
          },
          body: opts.body ? JSON.stringify(opts.body) : undefined,
          signal: controller.signal
        });
        const text = await boundedResponseText(response, maxResponseBytes, controller.signal);
        try {
          return {
            status: response.status,
            json: JSON.parse(text)
          };
        } catch {
          throw new Error(`Cloudflare API returned invalid JSON (${response.status})`);
        }
      })(),
      timeout
    ]));
  } finally {
    if (timer !== undefined)
      clearTimeout(timer);
  }
  if (status < 200 || status >= 300 || json.success !== true) {
    const first = json.errors?.[0];
    const msg = first?.message ?? "request failed";
    const providerCode = typeof first?.code === "string" || typeof first?.code === "number" ? String(first.code) : undefined;
    throw new CloudflareApiError(msg, status, providerCode);
  }
  opts.onResultInfo?.(json.result_info);
  return json.result;
}
function exactDnsName(value, label) {
  if (typeof value !== "string")
    throw new Error(`${label} must be a fully qualified DNS name`);
  const name = value.toLowerCase().replace(/\.$/, "");
  if (name.length > 253 || !name.includes(".") || !name.split(".").every((part) => part.length > 0 && part.length <= 63 && /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/.test(part))) {
    throw new Error(`${label} must be a fully qualified DNS name without wildcards`);
  }
  return name;
}
async function createRecordIfAbsent(input, config) {
  if (!/^[a-f0-9]{32}$/.test(input.zoneId))
    throw new Error("zoneId must be an exact Cloudflare zone ID");
  const zoneName = exactDnsName(input.zoneName, "zoneName");
  const name = exactDnsName(input.name, "name");
  const content = exactDnsName(input.content, "content");
  if (!name.endsWith(`.${zoneName}`))
    throw new Error("name must be a fully qualified subdomain of the exact zone");
  if (name === content)
    throw new Error("CNAME cannot point to itself");
  if (input.type !== "CNAME" || input.proxied !== false)
    throw new Error("create-only currently requires type CNAME and explicit proxied false");
  if (!Number.isSafeInteger(input.ttl) || !(input.ttl === 1 || input.ttl >= 60 && input.ttl <= 86400)) {
    throw new Error("ttl must be 1 (automatic) or an integer between 60 and 86400");
  }
  const base = `/zones/${input.zoneId}`;
  const zone = await cfFetch(base, { config });
  if (!zone || zone.id !== input.zoneId || exactDnsName(zone.name, "provider zone name") !== zoneName || zone.status !== "active") {
    throw new Error("Provider zone identity or active status does not match the requested zone");
  }
  const readName = async () => {
    const records = await cfFetch(`${base}/dns_records?name=${encodeURIComponent(name)}&per_page=100&page=1`, { config });
    if (!Array.isArray(records) || records.some((record) => !record || exactDnsName(record.name, "provider record name") !== name)) {
      throw new Error("Provider returned invalid exact-name DNS records");
    }
    return records;
  };
  if ((await readName()).length !== 0)
    throw new Error("DNS name already exists; create-only refuses every existing record");
  const body = { type: "CNAME", name, content, ttl: input.ttl, proxied: false };
  const matches = (record, id) => !!record && typeof record.id === "string" && /^[a-f0-9]{32}$/.test(record.id) && (!id || record.id === id) && record.type === "CNAME" && exactDnsName(record.name, "provider record name") === name && exactDnsName(record.content, "provider record content") === content && record.ttl === input.ttl && record.proxied === false;
  try {
    const created = await cfFetch(`${base}/dns_records`, { method: "POST", body, config });
    if (!matches(created))
      throw new Error("Provider create result does not match the exact requested record");
    const record = await cfFetch(`${base}/dns_records/${created.id}`, { config });
    const atName = await readName();
    if (!matches(record, created.id) || atName.length !== 1 || !matches(atName[0], created.id)) {
      throw new Error("Provider readback does not match the exact created record ID and content");
    }
    return { created: true, provider: "cloudflare", zone: { id: zone.id, name: zoneName }, record };
  } catch (error) {
    throw new Error(`DNS create is unverified; no retry or rollback was attempted. Read the exact provider name before retrying. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
  }
}
async function updateRecordInPlace(input, config) {
  if (!/^[a-f0-9]{32}$/.test(input.zoneId))
    throw new Error("zoneId must be an exact Cloudflare zone ID");
  if (!/^[a-f0-9]{32}$/.test(input.recordId))
    throw new Error("recordId must be an exact Cloudflare record ID");
  const zoneName = exactDnsName(input.zoneName, "zoneName");
  const { update } = input;
  const body = {};
  if (update.content !== undefined) {
    if (typeof update.content !== "string" || !update.content.trim() || update.content.length > 2048 || /[\u0000-\u001f\u007f]/u.test(update.content)) {
      throw new Error("content must be a non-empty DNS value without control characters");
    }
    body.content = update.content.trim();
  }
  if (update.ttl !== undefined) {
    if (!Number.isSafeInteger(update.ttl) || !(update.ttl === 1 || update.ttl >= 60 && update.ttl <= 86400)) {
      throw new Error("ttl must be 1 (automatic) or an integer between 60 and 86400");
    }
    body.ttl = update.ttl;
  }
  if (update.proxied !== undefined) {
    if (typeof update.proxied !== "boolean")
      throw new Error("proxied must be a boolean");
    body.proxied = update.proxied;
  }
  if (Object.keys(body).length === 0)
    throw new Error("At least one of content, ttl or proxied must be given");
  if (update.expectContent !== undefined && (typeof update.expectContent !== "string" || !update.expectContent.trim())) {
    throw new Error("expectContent must be a non-empty DNS value");
  }
  const base = `/zones/${input.zoneId}`;
  const zone = await cfFetch(base, { config });
  if (!zone || zone.id !== input.zoneId || exactDnsName(zone.name, "provider zone name") !== zoneName || zone.status !== "active") {
    throw new Error("Provider zone identity or active status does not match the requested zone");
  }
  const readRecord = async () => {
    const record = await cfFetch(`${base}/dns_records/${input.recordId}`, { config });
    if (!record || record.id !== input.recordId) {
      throw new Error("Provider record identity does not match the exact requested record ID");
    }
    return record;
  };
  const previous = await readRecord();
  if (update.expectContent !== undefined && previous.content !== update.expectContent.trim()) {
    throw new Error(`DNS record content is ${previous.content}, not the expected ${update.expectContent}; refusing the change`);
  }
  try {
    await cfFetch(`${base}/dns_records/${input.recordId}`, { method: "PATCH", body, config });
    const record = await readRecord();
    if (record.type !== previous.type || record.name !== previous.name) {
      throw new Error("Provider readback changed the record identity (type or name)");
    }
    if (body.content !== undefined && record.content !== body.content || body.ttl !== undefined && record.ttl !== body.ttl || body.proxied !== undefined && record.proxied !== body.proxied) {
      throw new Error("Provider readback does not match the requested record fields");
    }
    return { changed: true, provider: "cloudflare", zone: { id: zone.id, name: zoneName }, previous, record };
  } catch (error) {
    throw new Error(`DNS change is unverified; no retry or rollback was attempted. Read the exact provider record before retrying. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
  }
}
async function listZones(config) {
  const zones = [];
  let page = 1;
  while (true) {
    const result = await cfFetch(`/zones?per_page=50&page=${page}`, { config });
    if (!result || result.length === 0)
      break;
    for (const z of result) {
      zones.push({ id: z.id, name: z.name, status: z.status, nameservers: z.name_servers, original_nameservers: z.original_name_servers });
    }
    if (result.length < 50)
      break;
    page++;
  }
  return zones;
}
async function getZone(domain, config) {
  const result = await cfFetch(`/zones?name=${encodeURIComponent(domain)}`, { config });
  if (!result || result.length === 0)
    return null;
  const z = result[0];
  return { id: z.id, name: z.name, status: z.status, nameservers: z.name_servers, original_nameservers: z.original_name_servers };
}
async function createZone(domain, config) {
  const cfg = config ?? getConfig3();
  if (!cfg.accountId) {
    throw new Error("CLOUDFLARE_ACCOUNT_ID is required to create a zone.");
  }
  const result = await cfFetch("/zones", { method: "POST", body: { name: domain, account: { id: cfg.accountId }, jump_start: false }, config: cfg });
  return { id: result.id, name: result.name, status: result.status, nameservers: result.name_servers };
}
async function ensureZone(domain, config, deps) {
  const get = deps?.getZone ?? getZone;
  const create = deps?.createZone ?? createZone;
  const existing = await get(domain, config);
  const zone = existing ?? await create(domain, config);
  if (!zone.nameservers || zone.nameservers.length === 0) {
    throw new Error(`Cloudflare zone for ${domain} has no nameservers yet`);
  }
  return zone;
}
async function deleteZone(zoneId, config) {
  await cfFetch(`/zones/${zoneId}`, { method: "DELETE", config });
}
async function getZoneOriginTlsMode(zoneId, config) {
  const setting = await cfFetch(`/zones/${encodeURIComponent(zoneId)}/settings/ssl`, { config });
  if (setting.id !== "ssl" || typeof setting.value !== "string" || !setting.value.trim()) {
    throw new Error("Cloudflare returned an invalid zone SSL setting");
  }
  const mode = setting.value.trim().toLowerCase();
  if (!["off", "flexible", "full", "strict", "origin_pull"].includes(mode)) {
    throw new Error(`Cloudflare returned unsupported zone SSL setting ${mode}`);
  }
  return mode;
}
async function ensureZoneOriginTlsMode(zoneId, requested, config) {
  const current = await getZoneOriginTlsMode(zoneId, config);
  if (current === "origin_pull" || current === "strict" && requested === "full") {
    return { mode: current, changed: false, downgradeRefused: true };
  }
  let changed = false;
  if (current !== requested) {
    await cfFetch(`/zones/${encodeURIComponent(zoneId)}/settings/ssl`, { method: "PATCH", body: { value: requested }, config });
    changed = true;
  }
  const readback = await getZoneOriginTlsMode(zoneId, config);
  if (readback !== requested) {
    throw new Error(`Cloudflare zone SSL setting readback is ${readback}, expected ${requested}`);
  }
  return { mode: requested, changed, downgradeRefused: false };
}
var CLOUDFLARE_ZONE_SETTINGS = {
  email_obfuscation: ["on", "off"]
};
function requireZoneSettingName(setting) {
  if (typeof setting !== "string" || !Object.hasOwn(CLOUDFLARE_ZONE_SETTINGS, setting)) {
    throw new Error(`Unsupported Cloudflare zone setting '${setting}'; supported: ${Object.keys(CLOUDFLARE_ZONE_SETTINGS).join(", ")}`);
  }
  return setting;
}
function requireZoneSettingValue(setting, value) {
  const allowed = CLOUDFLARE_ZONE_SETTINGS[setting];
  if (typeof value !== "string" || !allowed.includes(value.trim().toLowerCase())) {
    throw new Error(`Unsupported value '${value}' for zone setting ${setting}; supported: ${allowed.join(", ")}`);
  }
  return value.trim().toLowerCase();
}
function requireZoneId(zoneId) {
  const id = typeof zoneId === "string" ? zoneId.trim() : "";
  if (!/^[a-f0-9]{32}$/.test(id)) {
    throw new Error("zoneId must be an exact Cloudflare zone ID");
  }
  return id;
}
async function getZoneSetting(zoneId, setting, config) {
  const name = requireZoneSettingName(setting);
  const id = requireZoneId(zoneId);
  const result = await cfFetch(`/zones/${encodeURIComponent(id)}/settings/${name}`, { config });
  if (!result || result.id !== name || typeof result.value !== "string" || !result.value.trim()) {
    throw new Error(`Cloudflare returned an invalid zone setting for ${name}`);
  }
  const value = result.value.trim().toLowerCase();
  const allowed = CLOUDFLARE_ZONE_SETTINGS[name];
  if (!allowed.includes(value)) {
    throw new Error(`Cloudflare returned unsupported ${name} value ${value}`);
  }
  return value;
}
async function ensureZoneSetting(zoneId, setting, value, config) {
  const name = requireZoneSettingName(setting);
  const requested = requireZoneSettingValue(name, value);
  const id = requireZoneId(zoneId);
  const current = await getZoneSetting(id, name, config);
  if (current === requested)
    return { setting: name, value: requested, changed: false };
  await cfFetch(`/zones/${encodeURIComponent(id)}/settings/${name}`, { method: "PATCH", body: { value: requested }, config });
  const readback = await getZoneSetting(id, name, config);
  if (readback !== requested) {
    throw new Error(`Cloudflare zone setting ${name} readback is ${readback}, expected ${requested}`);
  }
  return { setting: name, value: requested, changed: true };
}

class CloudflareDnsPaginationError extends Error {
  constructor(reason, page, count, metadata) {
    const numeric = metadata ? Object.fromEntries(["page", "per_page", "count", "total_count", "total_pages"].map((key) => [key, typeof metadata[key] === "number" && Number.isFinite(metadata[key]) ? metadata[key] : null])) : null;
    super(`Cloudflare DNS pagination ${reason} (page=${page}, returned=${count}, metadata=${JSON.stringify(numeric)})`);
    this.name = "CloudflareDnsPaginationError";
  }
}
var DNS_MAIL_RECONCILIATION_TIMEOUT_MS = 25000;
async function listRecords2(zoneId, config, options = {}) {
  const deadline = Math.min(performance.now() + 1e4, options.deadline ?? Infinity);
  if (!Number.isFinite(deadline))
    throw new Error("Invalid DNS enumeration deadline");
  const assertRead = async () => {
    await options.assertLease?.();
    if (performance.now() >= deadline)
      throw new Error("Cloudflare DNS enumeration time budget exhausted");
  };
  for (let attempt = 1;; attempt++) {
    await assertRead();
    try {
      const records = await listRecordsAttempt(zoneId, config, assertRead, deadline);
      await assertRead();
      return records;
    } catch (error) {
      if (!(error instanceof CloudflareDnsPaginationError) || attempt >= 6)
        throw error;
      await assertRead();
      const delay = 250 * 2 ** (attempt - 1);
      if (performance.now() + delay >= deadline)
        throw error;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}
var CF_DNS_READ_TYPES = new Set(["A", "AAAA", "CAA", "CERT", "CNAME", "DNSKEY", "DS", "HTTPS", "LOC", "MX", "NAPTR", "NS", "OPENPGPKEY", "PTR", "SMIMEA", "SRV", "SSHFP", "SVCB", "TLSA", "TXT", "URI"]);
function readDnsRecord(value) {
  const malformed = () => new Error("Cloudflare returned a malformed DNS record");
  if (!value || typeof value !== "object" || Array.isArray(value))
    throw malformed();
  const r = value;
  if (typeof r.id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/u.test(r.id) || typeof r.type !== "string" || !CF_DNS_READ_TYPES.has(r.type) || typeof r.name !== "string" || r.name.length > 253 || !/^(?:\*|[A-Za-z0-9_-]+)(?:\.[A-Za-z0-9_-]+)+\.?$/u.test(r.name) || typeof r.content !== "string" || r.content.length === 0 && r.type !== "TXT" || typeof r.ttl !== "number" || !Number.isSafeInteger(r.ttl) || !(r.ttl === 1 || r.ttl >= 30 && r.ttl <= 86400) || (r.type === "MX" || r.priority !== undefined) && (typeof r.priority !== "number" || !Number.isSafeInteger(r.priority) || r.priority < 0 || r.priority > 65535) || r.proxied !== undefined && typeof r.proxied !== "boolean")
    throw malformed();
  return {
    id: r.id,
    type: r.type,
    name: r.name,
    content: r.content,
    ttl: r.ttl,
    priority: r.priority,
    proxied: r.proxied
  };
}
async function listRecordsAttempt(zoneId, config, assertRead, deadline) {
  const records = [];
  let page = 1;
  const seenIds = new Set;
  let pagination;
  while (true) {
    await assertRead();
    let resultInfo;
    const result = await cfFetch(`/zones/${zoneId}/dns_records?per_page=100&page=${page}`, { config, onResultInfo: (value) => {
      resultInfo = value;
    }, timeoutMs: Math.max(1, Math.ceil(deadline - performance.now())) });
    await assertRead();
    if (!Array.isArray(result))
      throw new Error("Cloudflare DNS list result is not an array");
    const pageRecords = result.map(readDnsRecord);
    for (const record of pageRecords) {
      if (seenIds.has(record.id))
        throw new Error("Cloudflare DNS pagination repeated a record identity");
      seenIds.add(record.id);
    }
    if (resultInfo !== undefined) {
      if (!resultInfo || typeof resultInfo !== "object")
        throw new Error("Cloudflare DNS pagination is malformed");
      const info = resultInfo;
      const values = [info.page, info.per_page, info.count, info.total_count, info.total_pages];
      if (!values.every((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0))
        throw new CloudflareDnsPaginationError("is incomplete", page, result.length, info);
      const current = { perPage: info.per_page, totalCount: info.total_count, totalPages: info.total_pages };
      const expectedPages = Math.ceil(current.totalCount / current.perPage);
      if (info.page !== page || current.perPage < 1 || current.perPage > 100)
        throw new Error("Cloudflare DNS pagination has invalid page identity or size");
      if (info.count !== result.length || current.totalPages !== expectedPages && !(current.totalCount === 0 && current.totalPages === 1) || result.length !== Math.min(current.perPage, Math.max(0, current.totalCount - (page - 1) * current.perPage)) || pagination && JSON.stringify(pagination) !== JSON.stringify(current))
        throw new CloudflareDnsPaginationError("changed or contradicts the result", page, result.length, info);
      pagination = current;
    } else if (pagination)
      throw new CloudflareDnsPaginationError("disappeared before completion", page, result.length);
    records.push(...pageRecords);
    if (pagination ? page >= pagination.totalPages : result.length < 100) {
      if (pagination && records.length !== pagination.totalCount)
        throw new CloudflareDnsPaginationError("did not yield the complete population", page, result.length);
      break;
    }
    page++;
  }
  return records;
}
async function listRecordsByNameType(zoneId, type, name, config) {
  const result = await cfFetch(`/zones/${zoneId}/dns_records?type=${encodeURIComponent(type)}&name=${encodeURIComponent(name)}`, { config });
  return (result ?? []).map((r) => ({
    id: r.id,
    type: r.type,
    name: r.name,
    content: r.content,
    ttl: r.ttl,
    priority: r.priority,
    proxied: r.proxied
  }));
}
async function upsertRecord2(zoneId, record, config) {
  const existing = await listRecordsByNameType(zoneId, record.type, record.name, config);
  const body = {
    type: record.type,
    name: record.name,
    content: record.content,
    ttl: record.ttl ?? 1,
    priority: record.priority,
    proxied: record.proxied ?? false
  };
  const sameRecord = existing.find((r) => r.content === record.content && (r.priority ?? undefined) === (record.priority ?? undefined) && (r.proxied ?? false) === (record.proxied ?? false));
  if (sameRecord?.id) {
    await cfFetch(`/zones/${zoneId}/dns_records/${sameRecord.id}`, { method: "PUT", body, config });
  } else {
    await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body, config });
  }
}
function sameRecordIdentity(a, b) {
  return a.type === b.type && a.name === b.name && a.content === b.content && (a.priority ?? undefined) === (b.priority ?? undefined);
}
function resolveDesiredProxied(records, existing) {
  const omitted = records.filter((record) => record.proxied === undefined);
  if (omitted.length === 0)
    return records;
  const { type, name } = records[0];
  if (omitted.length === records.length) {
    const proxyStates = new Set(existing.map((record) => record.proxied));
    if (proxyStates.size > 1) {
      throw new Error(`Cannot safely preserve Cloudflare proxied state for ${type} ${name}: ` + "existing records have mixed proxied values while desired records omit proxied; " + "set proxied explicitly on every desired record");
    }
    const inherited = existing[0]?.proxied;
    return inherited === undefined ? records : records.map((record) => ({ ...record, proxied: inherited }));
  }
  const existingHasProxyState = existing.some((record) => record.proxied !== undefined);
  return records.map((record) => {
    if (record.proxied !== undefined)
      return record;
    const matches = existing.filter((candidate) => sameRecordIdentity(candidate, record));
    if (matches.length === 1 && matches[0].proxied !== undefined) {
      return { ...record, proxied: matches[0].proxied };
    }
    if (!existingHasProxyState)
      return record;
    throw new Error(`Cannot safely preserve Cloudflare proxied state for ${type} ${name} record ${record.content}: ` + "omitted proxied does not resolve to exactly one existing value; " + "set proxied explicitly on this desired record");
  });
}
async function prepareRecordsByNameType(zoneId, records, config) {
  if (records.length === 0)
    return;
  const { type, name } = records[0];
  const existing = await listRecordsByNameType(zoneId, type, name, config);
  if (existing.length === records.length && records.every((record) => existing.some((candidate) => sameRecordIdentity(candidate, record) && candidate.ttl === record.ttl && (record.proxied === undefined || candidate.proxied === record.proxied)))) {
    return;
  }
  return { existing, desired: resolveDesiredProxied(records, existing) };
}
async function replaceRecordsByNameType(zoneId, replacement, config) {
  for (const record of replacement.existing) {
    if (record.id)
      await deleteRecord2(zoneId, record.id, config);
  }
  for (const record of replacement.desired) {
    await cfFetch(`/zones/${zoneId}/dns_records`, {
      method: "POST",
      body: {
        type: record.type,
        name: record.name,
        content: record.content,
        ttl: record.ttl ?? 1,
        priority: record.priority,
        ...record.proxied === undefined ? {} : { proxied: record.proxied }
      },
      config
    });
  }
}
async function reconcileRecords(zoneId, records, config) {
  const grouped = new Map;
  for (const record of records) {
    const key = `${record.type}|${record.name}`;
    const group = grouped.get(key) ?? [];
    group.push(record);
    grouped.set(key, group);
  }
  const replacements = [];
  for (const group of grouped.values()) {
    const replacement = await prepareRecordsByNameType(zoneId, group, config);
    if (replacement)
      replacements.push(replacement);
  }
  for (const replacement of replacements) {
    await replaceRecordsByNameType(zoneId, replacement, config);
  }
}
async function getDnsRecordGroupState(zoneId, type, name, config, options = {}) {
  const records = (await listRecords2(zoneId, config, options)).filter((record) => record.type === type && record.name.toLowerCase().replace(/\.$/u, "") === name).map((record) => {
    if (!record.id)
      throw new Error("DNS state record has no provider identity");
    return {
      id: record.id,
      type: record.type,
      name: record.name,
      value: record.content,
      ttl: record.ttl,
      priority: record.priority ?? null,
      proxied: record.proxied ?? null
    };
  }).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
  if (records.length > 1000 || new Set(records.map((record) => record.id)).size !== records.length) {
    throw new Error("DNS state group is oversized or has ambiguous provider identities");
  }
  const state = { hash_scheme: "domains-dns-group-v1", zone_id: zoneId, record_name: name, record_type: type, records };
  return { ...state, records_sha256: createHash("sha256").update(JSON.stringify(state)).digest("hex") };
}
async function reconcileGuardedRecords(zoneId, desired, expectedHash, config, assertLease = async () => {}) {
  const first = desired[0];
  if (!first || desired.some((record) => record.type !== first.type || record.name !== first.name))
    throw new Error("DNS guard requires exactly one record group");
  await assertLease();
  const before = await getDnsRecordGroupState(zoneId, first.type, first.name, config, { assertLease });
  if (before.records_sha256 !== expectedHash)
    throw new Error("DNS current record state does not match expected_current_records_sha256");
  const existing = before.records.map((record) => ({
    id: record.id,
    type: record.type,
    name: record.name,
    content: record.value,
    ttl: record.ttl,
    ...record.priority === null ? {} : { priority: record.priority },
    ...record.proxied === null ? {} : { proxied: record.proxied }
  }));
  const targets = resolveDesiredProxied(desired, existing);
  const matches = (actual, target) => sameRecordIdentity(actual, target) && actual.ttl === target.ttl && (target.proxied === undefined || actual.proxied === target.proxied);
  const remaining = [...existing];
  const retained = [];
  const additions = [];
  for (const target of targets) {
    const index = remaining.findIndex((record) => matches(record, target));
    if (index < 0)
      additions.push(target);
    else
      retained.push(remaining.splice(index, 1)[0]);
  }
  if (additions.some((record) => record.ttl === 1))
    throw new Error("Guarded automatic TTL requires an exact retained provider record");
  await assertLease();
  if ((await getDnsRecordGroupState(zoneId, first.type, first.name, config, { assertLease })).records_sha256 !== expectedHash)
    throw new Error("DNS record group changed before mutation");
  for (const record of remaining) {
    await assertLease();
    await deleteRecord2(zoneId, record.id, config);
  }
  for (const record of additions) {
    await assertLease();
    await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body: record, config });
  }
  await assertLease();
  const after = await getDnsRecordGroupState(zoneId, first.type, first.name, config, { assertLease });
  const actual = after.records.map((record) => ({
    id: record.id,
    type: record.type,
    name: record.name,
    content: record.value,
    ttl: record.ttl,
    ...record.priority === null ? {} : { priority: record.priority },
    ...record.proxied === null ? {} : { proxied: record.proxied }
  }));
  if (actual.length !== targets.length || !targets.every((target) => actual.some((record) => matches(record, target))) || !retained.every((original) => actual.some((record) => record.id === original.id && matches(record, original)))) {
    throw new Error("Guarded DNS complete record group readback mismatch");
  }
  return after;
}
async function reconcileMailRecords(zoneId, records, config, assertLease = async () => {}, operationDeadline = performance.now() + DNS_MAIL_RECONCILIATION_TIMEOUT_MS) {
  const deadline = Math.min(operationDeadline, performance.now() + DNS_MAIL_RECONCILIATION_TIMEOUT_MS);
  const remaining = () => {
    const milliseconds = deadline - performance.now();
    if (!Number.isFinite(milliseconds) || milliseconds < 1)
      throw new Error("DNS reconciliation time budget exhausted");
    return Math.floor(milliseconds);
  };
  const assertOperation = async () => {
    remaining();
    await assertLease();
    remaining();
  };
  const applied = [];
  for (const desired of records) {
    const all = await listRecords2(zoneId, config, { assertLease: assertOperation, deadline });
    const named = all.filter((record) => record.name.toLowerCase().replace(/\.$/u, "") === desired.name);
    const existing = named.filter((record) => record.type === desired.type);
    const kind = desired.type === "TXT" ? mailPolicyKind(desired.content) : null;
    const dmarc = kind === "dmarc";
    const spf = kind === "spf";
    const policies = existing.filter((record) => kind !== null && mailPolicyKind(record.content) === kind);
    if ((dmarc || spf) && policies.length > 1)
      throw new Error("Multiple existing mail policies require review");
    if (dmarc && policies[0]) {
      applied.push(policies[0]);
      continue;
    }
    const exact = existing.find((record) => sameRecordIdentity(record, desired) && (desired.type !== "CNAME" || record.proxied === false));
    if (exact) {
      applied.push(exact);
      continue;
    }
    if (spf && policies[0]) {
      const current = policies[0];
      const tokens = decodeDnsTxt(current.content).trim().split(/\s+/u);
      const known = new Set(tokens.map((token) => token.toLowerCase()));
      const inserts = decodeDnsTxt(desired.content).trim().split(/\s+/u).slice(1).filter((token) => !/^[+?~-]?all$/iu.test(token) && !known.has(token.toLowerCase()));
      if (inserts.some((token) => !/^include:[a-z0-9.-]+$/iu.test(token))) {
        throw new Error("Mail preservation accepts only additional SPF includes");
      }
      if (inserts.length === 0) {
        applied.push(current);
        continue;
      }
      const terminal = tokens.findIndex((token) => /^[+?~-]?all$/iu.test(token));
      tokens.splice(terminal < 0 ? tokens.length : terminal, 0, ...inserts);
      if (!current.id)
        throw new Error("Existing SPF record has no provider identity");
      const updated = { ...current, content: tokens.join(" ") };
      await assertOperation();
      const fresh = await listRecords2(zoneId, config, { assertLease: assertOperation, deadline });
      const freshPolicies = fresh.filter((record) => record.type === "TXT" && record.name === current.name && mailPolicyKind(record.content) === "spf");
      const same = freshPolicies.length === 1 ? freshPolicies.find((record) => record.id === current.id) : undefined;
      if (!same || JSON.stringify(same) !== JSON.stringify(current))
        throw new Error("SPF record changed during reconciliation");
      await assertOperation();
      await cfFetch(`/zones/${zoneId}/dns_records/${current.id}`, {
        method: "PATCH",
        body: { content: updated.content },
        config,
        timeoutMs: remaining()
      });
      await assertOperation();
      applied.push(updated);
      continue;
    }
    if (desired.type !== "TXT" && existing.length > 0)
      throw new Error("Existing mail record conflicts with requested authorization");
    if (named.some((record) => record.type === "CNAME") && desired.type !== "CNAME" && named.some((record) => record.type === "CNAME" && record.proxied !== true)) {
      throw new Error("Mail record conflicts with an existing CNAME");
    }
    if (desired.type === "CNAME" && named.length > 0)
      throw new Error("Mail CNAME conflicts with existing records");
    await assertOperation();
    await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body: desired, config, timeoutMs: remaining() });
    await assertOperation();
    applied.push(desired);
  }
  return applied;
}
async function deleteRecord2(zoneId, recordId, config) {
  await cfFetch(`/zones/${zoneId}/dns_records/${recordId}`, { method: "DELETE", config });
}
async function deleteRecordByNameType(zoneId, name, type, config) {
  const existing = await cfFetch(`/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(name)}`, { config });
  for (const r of existing ?? []) {
    await deleteRecord2(zoneId, r.id, config);
  }
}
function zoneToDomainInfo(zone) {
  return {
    domain: zone.name,
    registrar: "Cloudflare DNS",
    created: "",
    expires: "",
    nameservers: zone.nameservers,
    status: zone.status === "active" ? "active" : "discovered",
    auto_renew: false
  };
}
function withCloudflareMetadata(existing, zone) {
  return {
    ...existing,
    cloudflare: {
      zone_id: zone.id,
      zone_status: zone.status,
      source: "cloudflare:zones",
      synced_at: new Date().toISOString()
    }
  };
}
function createCloudflareProvider(config) {
  const cfg = config ?? getConfig3();
  return {
    name: "cloudflare",
    dnsWriteScope: "changed-groups",
    async listDomains() {
      const zones = await listZones(cfg);
      return zones.map(zoneToDomainInfo);
    },
    async syncToLocalDb(dbFns) {
      const zones = await listZones(cfg);
      let synced = 0;
      let created = 0;
      let updated = 0;
      const errors = [];
      for (const zone of zones) {
        try {
          const info = zoneToDomainInfo(zone);
          const existing = await dbFns.getDomainByName(zone.name);
          if (existing) {
            await dbFns.updateDomain(existing.id, {
              ...existing.registrar === "Cloudflare DNS" ? { registrar: null } : {},
              status: existing.status === "discovered" && info.status === "active" ? "active" : existing.status,
              nameservers: zone.nameservers,
              metadata: withCloudflareMetadata(existing.metadata, zone)
            });
            updated++;
          } else {
            await dbFns.createDomain({
              name: zone.name,
              status: info.status === "active" ? "active" : "discovered",
              auto_renew: false,
              nameservers: zone.nameservers,
              notes: "Discovered from Cloudflare zones; registrar ownership was not inferred.",
              metadata: withCloudflareMetadata({}, zone)
            });
            created++;
          }
          synced++;
        } catch (err) {
          errors.push(`${zone.name}: ${err instanceof Error ? err.message : String(err)}`);
        }
      }
      return { synced, created, updated, errors };
    },
    async getDnsRecords(domain) {
      const zone = await getZone(domain, cfg);
      if (!zone)
        return [];
      const records = await listRecords2(zone.id, cfg);
      return records.map((r) => ({
        type: r.type,
        name: r.name,
        value: r.content,
        ttl: r.ttl === 1 ? 0 : r.ttl,
        priority: r.priority,
        ...r.proxied === undefined ? {} : { proxied: r.proxied }
      }));
    },
    async setDnsRecords(domain, records) {
      const zone = await getZone(domain, cfg);
      if (!zone)
        throw new Error(`No Cloudflare zone found for ${domain}`);
      await reconcileRecords(zone.id, records.map((r) => ({
        type: r.type,
        name: r.name,
        content: r.value,
        ttl: r.ttl || 1,
        priority: r.priority,
        ...r.proxied === undefined ? {} : { proxied: r.proxied }
      })), cfg);
      return true;
    },
    async deleteDnsRecords(domain, records) {
      const zone = await getZone(domain, cfg);
      if (!zone)
        throw new Error(`No Cloudflare zone found for ${domain}`);
      const grouped = new Map;
      for (const record of records) {
        const key = `${record.type}|${record.name}`;
        const group = grouped.get(key) ?? [];
        group.push(record);
        grouped.set(key, group);
      }
      for (const group of grouped.values()) {
        const { type, name } = group[0];
        const existing = await listRecordsByNameType(zone.id, type, name, cfg);
        for (const record of group) {
          const match = existing.find((candidate) => candidate.content === record.value && (candidate.priority ?? undefined) === (record.priority ?? undefined));
          if (match?.id) {
            await deleteRecord2(zone.id, match.id, cfg);
          }
        }
      }
      return true;
    }
  };
}
async function listWorkerCustomDomains(config) {
  const cfg = config ?? getConfig3();
  if (!cfg.accountId)
    throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Worker Custom Domains.");
  return cfFetch(`/accounts/${encodeURIComponent(cfg.accountId)}/workers/domains`, { config: cfg });
}
async function bindWorkerCustomDomain(hostname, zoneId, workerName = "shortlink-router", config) {
  const cfg = config ?? getConfig3();
  if (!cfg.accountId)
    throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Worker Custom Domains.");
  if (!zoneId.trim())
    throw new Error("Cloudflare zone id is required for Worker Custom Domains.");
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(workerName))
    throw new Error("Invalid Cloudflare Worker name.");
  return cfFetch(`/accounts/${encodeURIComponent(cfg.accountId)}/workers/domains`, {
    method: "PUT",
    body: { hostname, service: workerName, zone_id: zoneId },
    config: cfg
  });
}
async function workerCustomDomainReady(hostname, zoneId, workerName = "shortlink-router", config, fetchImpl = fetch, readiness = {}) {
  const domains = await listWorkerCustomDomains(config);
  const bound = domains.some((domain) => domain.hostname.toLowerCase() === hostname.toLowerCase() && domain.service === workerName && (domain.zone_id === undefined || domain.zone_id === zoneId) && domain.enabled !== false);
  if (!bound)
    return false;
  try {
    const path = readiness.path ?? "/.well-known/shortlink-router";
    const header = readiness.header ?? "x-shortlink-router";
    const expectedValue = readiness.value ?? workerName;
    const response = await fetchImpl(`https://${hostname}${path}`, {
      method: "GET",
      redirect: "manual",
      headers: { accept: "application/json" }
    });
    const ready = response.status === 200 && response.headers.get(header) === expectedValue;
    await response.body?.cancel();
    return ready;
  } catch {
    return false;
  }
}

// src/lib/version.ts
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
var cachedVersion = null;
function getPackageVersion() {
  if (cachedVersion)
    return cachedVersion;
  try {
    const moduleDir = dirname(fileURLToPath(import.meta.url));
    const packageJsonPath = resolve(moduleDir, "../../package.json");
    const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
    cachedVersion = pkg.version ?? "0.0.0";
  } catch {
    cachedVersion = "0.0.0";
  }
  return cachedVersion;
}
var USER_AGENT = `open-domains/${getPackageVersion()}`;

// src/lib/brandsight.ts
function resolveBrandsightConfig(env = process.env) {
  const apiKey = firstEnv(env, BRANDSIGHT_ENV.apiKey)?.value ?? "";
  const apiSecret = firstEnv(env, BRANDSIGHT_ENV.apiSecret)?.value;
  const customerId = firstEnv(env, BRANDSIGHT_ENV.customerId)?.value;
  const shopperId = firstEnv(env, BRANDSIGHT_ENV.shopperId)?.value;
  const accountId = firstEnv(env, BRANDSIGHT_ENV.accountId)?.value;
  return {
    apiKey,
    apiSecret,
    customerId,
    shopperId,
    accountId,
    baseUrl: env["BRANDSIGHT_BASE_URL"]
  };
}
class BrandsightApiError extends Error {
  statusCode;
  responseBody;
  constructor(message, statusCode, responseBody) {
    super(message);
    this.statusCode = statusCode;
    this.responseBody = responseBody;
    this.name = "BrandsightApiError";
  }
}
var _fetchFn = null;
function getConfig4() {
  return resolveBrandsightConfig();
}
var BRANDSIGHT_DOMAIN_BASE = "https://api.godaddy.com/v2";
var BRANDSIGHT_DOMAIN_MAX_RESPONSE_BYTES = 1048576;
function brandsightDomainTimeoutMs() {
  const raw = process.env["DOMAINS_PROVIDER_HTTP_TIMEOUT_MS"]?.trim();
  if (!raw)
    return 30000;
  const timeout = Number(raw);
  if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 120000) {
    throw new BrandsightApiError("DOMAINS_PROVIDER_HTTP_TIMEOUT_MS must be an integer between 1 and 120000");
  }
  return timeout;
}
async function boundedDomainResponseText(response, signal) {
  const declaredLength = response.headers.get("content-length");
  if (declaredLength !== null) {
    const parsed = Number(declaredLength);
    if (Number.isFinite(parsed) && parsed > BRANDSIGHT_DOMAIN_MAX_RESPONSE_BYTES) {
      throw new BrandsightApiError(`Brandsight Domain API response exceeds ${BRANDSIGHT_DOMAIN_MAX_RESPONSE_BYTES} bytes`, response.status);
    }
  }
  if (!response.body)
    return "";
  const reader = response.body.getReader();
  const abort = () => void reader.cancel("Brandsight Domain API request aborted");
  if (signal.aborted)
    abort();
  signal.addEventListener("abort", abort, { once: true });
  const chunks = [];
  let total = 0;
  try {
    while (true) {
      if (signal.aborted)
        throw signal.reason;
      const { done, value } = await reader.read();
      if (done)
        break;
      if (!value)
        continue;
      total += value.byteLength;
      if (total > BRANDSIGHT_DOMAIN_MAX_RESPONSE_BYTES) {
        await reader.cancel();
        throw new BrandsightApiError(`Brandsight Domain API response exceeds ${BRANDSIGHT_DOMAIN_MAX_RESPONSE_BYTES} bytes`, response.status);
      }
      chunks.push(value);
    }
  } finally {
    signal.removeEventListener("abort", abort);
    reader.releaseLock();
  }
  if (signal.aborted)
    throw signal.reason;
  const joined = new Uint8Array(total);
  let offset = 0;
  for (const chunk of chunks) {
    joined.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return new TextDecoder().decode(joined);
}
function requireDomainConfig(config) {
  const cfg = config ?? getConfig4();
  if (!cfg.apiKey || !cfg.apiSecret || !cfg.customerId) {
    throw new BrandsightApiError("Brandsight Domain API credentials are not configured. Set BRANDSIGHT_API_KEY, BRANDSIGHT_API_SECRET, and BRANDSIGHT_CUSTOMER_ID.");
  }
  return cfg;
}
function domainBaseUrl(cfg) {
  return cfg.baseUrl ?? BRANDSIGHT_DOMAIN_BASE;
}
function domainHeaders(cfg) {
  return {
    Authorization: `sso-key ${cfg.apiKey}:${cfg.apiSecret}`,
    "Content-Type": "application/json",
    Accept: "application/json",
    "User-Agent": USER_AGENT
  };
}
async function domainApiRequest(method, path, config, body) {
  const cfg = requireDomainConfig(config);
  const fetchFn = _fetchFn || globalThis.fetch;
  const signal = AbortSignal.timeout(brandsightDomainTimeoutMs());
  const response = await fetchFn(`${domainBaseUrl(cfg)}${path}`, {
    method,
    headers: domainHeaders(cfg),
    body: body ? JSON.stringify(body) : undefined,
    signal
  });
  const text = await boundedDomainResponseText(response, signal);
  if (!response.ok) {
    throw new BrandsightApiError(`Brandsight Domain API ${method} ${path} failed with status ${response.status}`, response.status, text);
  }
  if (!text.trim())
    return {};
  return JSON.parse(text);
}
function normalizeBrandsightDomain(raw) {
  const nameServers = raw.nameServers ?? raw.nameservers ?? [];
  const expiresAt = raw.expiresAt ?? raw.expires ?? "";
  const createdAt = raw.createdAt ?? raw.created ?? "";
  const renewAuto = raw.renewAuto ?? raw.auto_renew ?? false;
  return {
    ...raw,
    domain: String(raw.domain ?? ""),
    status: String(raw.status ?? "UNKNOWN"),
    created: createdAt,
    expires: expiresAt,
    auto_renew: renewAuto,
    locked: Boolean(raw.locked),
    nameservers: nameServers,
    createdAt,
    expiresAt,
    nameServers,
    renewAuto
  };
}
function customerPath(cfg, path) {
  return `/customers/${encodeURIComponent(cfg.customerId)}${path}`;
}
function domainTld(domain) {
  const parts = domain.split(".").filter(Boolean);
  if (parts.length < 2)
    throw new BrandsightApiError(`Invalid domain name: ${domain}`);
  return parts.slice(1).join(".");
}
function brandsightContact(contact) {
  return {
    addressMailing: {
      address1: contact.address_line_1,
      city: contact.city,
      country: contact.country_code,
      postalCode: contact.zip_code,
      state: contact.state
    },
    email: contact.email,
    encoding: "ASCII",
    nameFirst: contact.first_name,
    nameLast: contact.last_name,
    organization: contact.organization_name,
    phone: contact.phone
  };
}
async function domainAvailability(domain, cfg, type, period = 1) {
  const params = new URLSearchParams({
    domain,
    period: String(period),
    type,
    optimizeFor: "ACCURACY"
  });
  const result = await domainApiRequest("GET", `/domains/available?${params.toString()}`, cfg);
  return {
    domain: result.domain ?? domain,
    available: Boolean(result.available),
    price: result.price,
    currency: result.currency,
    registryPremiumPricing: result.registryPremiumPricing
  };
}
async function listDomains(config) {
  const cfg = requireDomainConfig(config);
  const domains = [];
  const seenDomains = new Set;
  const seenMarkers = new Set;
  let marker;
  while (true) {
    const params = new URLSearchParams({ limit: "500" });
    if (marker)
      params.set("marker", marker);
    const batch = await domainApiRequest("GET", customerPath(cfg, `/domains?${params.toString()}`), cfg);
    if (!Array.isArray(batch) || batch.length === 0)
      break;
    for (const normalized of batch.map(normalizeBrandsightDomain)) {
      if (!normalized.domain || seenDomains.has(normalized.domain))
        continue;
      seenDomains.add(normalized.domain);
      domains.push(normalized);
    }
    if (batch.length < 500)
      break;
    const nextMarker = String(batch[batch.length - 1]?.domain ?? "");
    if (!nextMarker || seenMarkers.has(nextMarker))
      break;
    seenMarkers.add(nextMarker);
    marker = nextMarker;
  }
  return domains;
}
async function getDomainInfo3(domain, config) {
  const cfg = requireDomainConfig(config);
  const result = await domainApiRequest("GET", customerPath(cfg, `/domains/${encodeURIComponent(domain)}`), cfg);
  return normalizeBrandsightDomain(result);
}
async function checkAvailability4(domain, config) {
  const cfg = requireDomainConfig(config);
  return domainAvailability(domain, cfg, "REGISTRATION", 1);
}
async function renewDomain3(domain, years = 1, config) {
  const cfg = requireDomainConfig(config);
  const current = await getDomainInfo3(domain, cfg);
  if (!current?.expires)
    throw new BrandsightApiError(`Cannot renew ${domain}: current expiry is unavailable`);
  const quote = await domainAvailability(domain, cfg, "RENEWAL", years);
  const price = quote.price ?? current.renewal?.price;
  const currency = quote.currency ?? current.renewal?.currency;
  if (price == null || !currency) {
    throw new BrandsightApiError(`Cannot renew ${domain}: renewal quote did not include an exact price and currency`);
  }
  const result = await domainApiRequest("POST", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/renew`), cfg, {
    consent: {
      agreedAt: new Date().toISOString(),
      agreedBy: cfg.shopperId ?? "domains-cli",
      currency,
      price,
      registryPremiumPricing: quote.registryPremiumPricing ?? false
    },
    expires: current.expires,
    period: years
  });
  return { success: true, orderId: result.orderId ?? result.id };
}
async function getLegalAgreements(tld, privacy = false, config) {
  const cfg = requireDomainConfig(config);
  const params = new URLSearchParams({
    privacy: String(privacy),
    tlds: tld
  });
  return domainApiRequest("GET", customerPath(cfg, `/domains/agreements?${params.toString()}`), cfg);
}
async function getRegistrationSchema(tld, config) {
  const cfg = requireDomainConfig(config);
  return domainApiRequest("GET", customerPath(cfg, `/domains/register/schema/${encodeURIComponent(tld)}`), cfg);
}
async function validateRegistrationRequest(payload, config) {
  const cfg = requireDomainConfig(config);
  await domainApiRequest("POST", customerPath(cfg, "/domains/register/validate"), cfg, payload);
  return true;
}
async function registerBrandsightDomain(domain, contact, options = {}, config) {
  const cfg = requireDomainConfig(config);
  const period = options.years ?? 1;
  const availability = await domainAvailability(domain, cfg, "REGISTRATION", period);
  if (!availability.available)
    throw new BrandsightApiError(`${domain} is not available for registration`);
  const price = options.premiumPrice ?? availability.price;
  if (price == null || !availability.currency) {
    throw new BrandsightApiError(`Cannot register ${domain}: availability quote did not include an exact price and currency`);
  }
  const tld = domainTld(domain);
  const schema = await getRegistrationSchema(tld, cfg);
  const required = Array.isArray(schema["required"]) ? schema["required"] : [];
  if (required.includes("metadata") && !options.metadata) {
    throw new BrandsightApiError(`Cannot register ${domain}: ${tld} requires TLD-specific metadata; pass registration metadata before validation`);
  }
  const privacy = options.privacy ?? false;
  const agreements = await getLegalAgreements(tld, privacy, cfg);
  const agreementKeys = agreements.map((a) => a.agreementKey).filter(Boolean);
  const c = brandsightContact(contact);
  const payload = {
    consent: {
      agreedAt: new Date().toISOString(),
      agreedBy: contact.email || cfg.shopperId || "domains-cli",
      agreementKeys,
      currency: availability.currency,
      price,
      registryPremiumPricing: availability.registryPremiumPricing ?? !!options.premiumPrice
    },
    contacts: {
      admin: c,
      billing: c,
      registrant: c,
      tech: c
    },
    domain,
    metadata: options.metadata ?? {},
    nameServers: options.nameservers ?? [],
    period,
    privacy,
    renewAuto: options.autoRenew ?? true
  };
  await validateRegistrationRequest(payload, cfg);
  const result = await domainApiRequest("POST", customerPath(cfg, "/domains/register"), cfg, payload);
  return {
    success: true,
    orderId: result.orderId ?? result.id,
    operationId: result.operationId ?? result.id,
    chargedAmount: String(price)
  };
}
async function updateNameservers3(domain, nameservers, config) {
  const cfg = requireDomainConfig(config);
  const result = await domainApiRequest("PUT", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/nameServers`), cfg, { nameServers: nameservers });
  return { success: true, operationId: result.operationId ?? result.id };
}
async function getDnsRecords3(domain, config) {
  const cfg = requireDomainConfig(config);
  const records = [];
  let offset = 0;
  const limit = 1000;
  while (true) {
    const batch = await domainApiRequest("GET", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/records?offset=${offset}&limit=${limit}`), cfg);
    if (!Array.isArray(batch) || batch.length === 0)
      break;
    if (records.length + batch.length > 5000) {
      throw new BrandsightApiError("Brandsight DNS record inventory exceeds the 5000-record safety bound");
    }
    records.push(...batch);
    if (batch.length < limit)
      break;
    offset += limit;
  }
  return records;
}
function normalizeBrandsightDnsRecord(record) {
  return {
    ...record,
    ttl: Math.max(record.ttl || 600, 600)
  };
}
async function setDnsRecords3(domain, records, config) {
  const cfg = requireDomainConfig(config);
  await domainApiRequest("PUT", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/records`), cfg, records.map(normalizeBrandsightDnsRecord));
  return true;
}
async function syncToLocalDb3(dbFns, config) {
  const domains = await listDomains(config);
  let synced = 0, created = 0, updated = 0;
  const errors = [];
  const syncedAt = new Date().toISOString();
  for (const d of domains) {
    try {
      const existing = await dbFns.getDomainByName(d.domain);
      if (existing) {
        await dbFns.updateDomain(existing.id, {
          registrar: "Brandsight",
          expires_at: d.expires || undefined,
          auto_renew: d.auto_renew,
          status: "active",
          nameservers: d.nameservers,
          expiry_synced_at: syncedAt
        });
        updated++;
      } else {
        await dbFns.createDomain({
          name: d.domain,
          registrar: "Brandsight",
          expires_at: d.expires || undefined,
          auto_renew: d.auto_renew,
          status: "active",
          nameservers: d.nameservers,
          expiry_synced_at: syncedAt
        });
        created++;
      }
      synced++;
    } catch (err) {
      errors.push(`${d.domain}: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
  return { synced, created, updated, errors };
}
function createBrandsightProvider(config) {
  const cfg = config ?? getConfig4();
  return {
    name: "brandsight",
    async listDomains() {
      const domains = await listDomains(cfg);
      return domains.map((d) => ({
        domain: d.domain,
        registrar: "Brandsight",
        created: d.created ?? "",
        expires: d.expires,
        nameservers: d.nameservers,
        status: d.status === "ACTIVE" ? "active" : d.status.toLowerCase(),
        auto_renew: d.auto_renew
      }));
    },
    async getDomainInfo(domain) {
      const d = await getDomainInfo3(domain, cfg);
      if (!d)
        throw new Error(`Domain not found in Brandsight: ${domain}`);
      return {
        domain: d.domain,
        registrar: "Brandsight",
        created: d.created ?? "",
        expires: d.expires,
        nameservers: d.nameservers,
        status: d.status === "ACTIVE" ? "active" : d.status.toLowerCase(),
        auto_renew: d.auto_renew
      };
    },
    async renewDomain(domain, years = 1) {
      const result = await renewDomain3(domain, years, cfg);
      return { domain, success: result.success, orderId: result.orderId };
    },
    async registerDomain(domain, contact, options) {
      const result = await registerBrandsightDomain(domain, contact, options, cfg);
      return {
        domain,
        success: result.success,
        orderId: result.orderId,
        operationId: result.operationId,
        chargedAmount: result.chargedAmount
      };
    },
    async updateNameservers(domain, nameservers) {
      const result = await updateNameservers3(domain, nameservers, cfg);
      return { domain, success: result.success, operationId: result.operationId };
    },
    async getDnsRecords(domain) {
      const records = await getDnsRecords3(domain, cfg);
      return records.map((r) => ({
        type: r.type,
        name: r.name,
        value: r.data,
        ttl: r.ttl,
        priority: r.priority
      }));
    },
    async setDnsRecords(domain, records) {
      return setDnsRecords3(domain, records.map((r) => ({
        type: r.type,
        name: r.name,
        data: r.value,
        ttl: r.ttl,
        priority: r.priority
      })), cfg);
    },
    async checkAvailability(domain) {
      const result = await checkAvailability4(domain, cfg);
      return {
        domain: result.domain,
        available: result.available,
        is_premium: result.registryPremiumPricing,
        premium_price: result.registryPremiumPricing ? result.price : undefined,
        standard_price: result.registryPremiumPricing ? undefined : result.price,
        currency: result.currency
      };
    },
    async syncToLocalDb(dbFns) {
      return syncToLocalDb3(dbFns, cfg);
    }
  };
}

// src/lib/registrar.ts
function createNamecheapProvider() {
  return {
    name: "namecheap",
    async listDomains() {
      const config = getConfig();
      const domains = await listNamecheapDomains(config);
      return domains.map((d) => ({
        domain: d.domain,
        registrar: "Namecheap",
        created: "",
        expires: d.expiry,
        nameservers: [],
        status: "active",
        auto_renew: d.autoRenew
      }));
    },
    async getDomainInfo(domain) {
      const config = getConfig();
      const info = await getDomainInfo(domain, config);
      return {
        domain: info.domain,
        registrar: info.registrar,
        created: info.created,
        expires: info.expires,
        nameservers: info.nameservers,
        status: "active",
        auto_renew: true
      };
    },
    async registerDomain(domain, contact, options = {}) {
      const config = getConfig();
      const result = await registerDomain(domain, contact, {
        years: options.years,
        premiumPrice: options.premiumPrice
      }, config);
      return {
        domain: result.domain,
        success: result.success,
        orderId: result.orderId,
        chargedAmount: result.chargedAmount
      };
    },
    async updateNameservers(domain, nameservers) {
      const config = getConfig();
      const success = await updateNameservers(domain, nameservers, config);
      return { domain, success };
    },
    async renewDomain(domain, years = 1) {
      const config = getConfig();
      const result = await renewDomain(domain, years, config);
      return {
        domain: result.domain,
        success: result.success,
        orderId: result.orderId,
        chargedAmount: result.chargedAmount
      };
    },
    async getDnsRecords(domain) {
      const config = getConfig();
      const { sld, tld } = splitDomain(domain);
      const records = await getDnsRecords(domain, sld, tld, config);
      return records.map((r) => ({
        type: r.type,
        name: r.name,
        value: r.address,
        ttl: r.ttl,
        priority: r.mxPref
      }));
    },
    async setDnsRecords(domain, records) {
      const config = getConfig();
      const { sld, tld } = splitDomain(domain);
      const ncRecords = records.map((r) => ({
        type: r.type,
        name: r.name,
        address: r.value,
        ttl: r.ttl,
        mxPref: r.priority
      }));
      return setDnsRecords(domain, sld, tld, ncRecords, config);
    },
    async checkAvailability(domain) {
      const config = getConfig();
      const result = await checkAvailability(domain, config);
      return {
        domain: result.domain,
        available: result.available,
        is_premium: result.premium,
        premium_price: result.price
      };
    },
    async syncToLocalDb(dbFns) {
      const result = await syncToLocalDb(dbFns);
      return {
        synced: result.synced,
        created: 0,
        updated: 0,
        errors: result.errors
      };
    }
  };
}
function createGoDaddyProvider() {
  return {
    name: "godaddy",
    async listDomains() {
      const domains = await listGoDaddyDomains();
      return domains.map((d) => ({
        domain: d.domain,
        registrar: "GoDaddy",
        created: "",
        expires: d.expires,
        nameservers: d.nameServers || [],
        status: d.status.toLowerCase(),
        auto_renew: d.renewAuto
      }));
    },
    async getDomainInfo(domain) {
      const detail = await getDomainInfo2(domain);
      return {
        domain: detail.domain,
        registrar: "GoDaddy",
        created: detail.createdAt || "",
        expires: detail.expires,
        nameservers: detail.nameServers || [],
        status: detail.status.toLowerCase(),
        auto_renew: detail.renewAuto
      };
    },
    async renewDomain(domain) {
      const result = await renewDomain2(domain);
      return {
        domain,
        success: true,
        orderId: String(result.orderId),
        chargedAmount: String(result.total)
      };
    },
    async getDnsRecords(domain) {
      const records = await getDnsRecords2(domain);
      return records.map((r) => ({
        type: r.type,
        name: r.name,
        value: r.data,
        ttl: r.ttl,
        priority: r.priority
      }));
    },
    async setDnsRecords(domain, records) {
      const gdRecords = records.map((r) => ({
        type: r.type,
        name: r.name,
        data: r.value,
        ttl: r.ttl,
        priority: r.priority
      }));
      await setDnsRecords2(domain, gdRecords);
      return true;
    },
    async checkAvailability(domain) {
      const result = await checkAvailability2(domain);
      return {
        domain: result.domain,
        available: result.available,
        standard_price: result.price,
        currency: result.currency
      };
    },
    async syncToLocalDb(dbFns) {
      const result = await syncToLocalDb2(dbFns);
      return {
        synced: result.synced,
        created: result.created,
        updated: result.updated,
        errors: result.errors
      };
    }
  };
}
var providerRegistry = new Map([
  ["namecheap", {
    info: {
      name: "namecheap",
      type: "full",
      configured: false,
      envVars: providerEnvNames("namecheap")
    },
    createInventory: createNamecheapProvider,
    createRegistrar: createNamecheapProvider,
    createDns: createNamecheapProvider
  }],
  ["godaddy", {
    info: {
      name: "godaddy",
      type: "full",
      configured: false,
      envVars: providerEnvNames("godaddy")
    },
    createInventory: createGoDaddyProvider,
    createRegistrar: createGoDaddyProvider,
    createDns: createGoDaddyProvider
  }],
  ["route53", {
    info: {
      name: "route53",
      type: "full",
      configured: false,
      envVars: providerEnvNames("route53")
    },
    createInventory: () => createRoute53Provider(),
    createRegistrar: () => createRoute53Provider(),
    createDns: () => createRoute53Provider()
  }],
  ["cloudflare", {
    info: {
      name: "cloudflare",
      type: "dns",
      configured: false,
      envVars: providerEnvNames("cloudflare")
    },
    createInventory: () => createCloudflareProvider(),
    createDns: createCloudflareProvider
  }],
  ["brandsight", {
    info: {
      name: "brandsight",
      type: "full",
      configured: false,
      envVars: providerEnvNames("brandsight")
    },
    createInventory: createBrandsightProvider,
    createRegistrar: createBrandsightProvider,
    createDns: createBrandsightProvider
  }],
  ["sedo", {
    info: {
      name: "sedo",
      type: "marketplace",
      configured: false,
      envVars: providerEnvNames("sedo")
    }
  }]
]);
function isConfigured(providerName) {
  return hasProviderCredentials(providerName);
}
function registerProvider(entry) {
  providerRegistry.set(entry.info.name.toLowerCase(), entry);
}
function getAvailableProviders() {
  return Array.from(providerRegistry.values()).map((e) => ({
    ...e.info,
    configured: isConfigured(e.info.name),
    inventory: !!e.createInventory
  }));
}
function getProviderInfo(name) {
  const entry = providerRegistry.get(name.toLowerCase());
  if (!entry)
    return null;
  return { ...entry.info, configured: isConfigured(entry.info.name), inventory: !!entry.createInventory };
}
function providerHasRegistrar(name) {
  return !!providerRegistry.get(name.toLowerCase())?.createRegistrar;
}
function providerHasDns(name) {
  return !!providerRegistry.get(name.toLowerCase())?.createDns;
}
function providerHasInventory(name) {
  return !!providerRegistry.get(name.toLowerCase())?.createInventory;
}
function getDomainInventoryProvider(name) {
  const entry = providerRegistry.get(name.toLowerCase());
  if (!entry?.createInventory)
    throw new Error(`No domain inventory provider: ${name}`);
  return entry.createInventory();
}
function getRegistrarProvider(name) {
  const entry = providerRegistry.get(name.toLowerCase());
  if (!entry?.createRegistrar)
    throw new Error(`No registrar provider: ${name}`);
  return entry.createRegistrar();
}
function getDnsProvider(name) {
  const entry = providerRegistry.get(name.toLowerCase());
  if (!entry?.createDns)
    throw new Error(`No DNS provider: ${name}`);
  return entry.createDns();
}
function getProvider(name) {
  return getRegistrarProvider(name);
}
async function syncAll(dbFns) {
  const available = getAvailableProviders().filter((p) => p.configured && providerHasInventory(p.name));
  const result = { providers: [], totalSynced: 0, totalErrors: [] };
  for (const info of available) {
    try {
      const provider = getDomainInventoryProvider(info.name);
      const syncResult = await provider.syncToLocalDb(dbFns);
      result.providers.push({ name: info.name, result: syncResult });
      result.totalSynced += syncResult.synced;
      result.totalErrors.push(...syncResult.errors.map((e) => `[${info.name}] ${e}`));
    } catch (error) {
      const msg = `[${info.name}] Sync failed: ${error instanceof Error ? error.message : String(error)}`;
      result.totalErrors.push(msg);
      result.providers.push({ name: info.name, result: { synced: 0, created: 0, updated: 0, errors: [msg] } });
    }
  }
  return result;
}
async function autoDetectRegistrar(domain, getDomainByName) {
  const dbDomain = await getDomainByName(domain);
  if (!dbDomain?.registrar)
    return null;
  const r = dbDomain.registrar.toLowerCase();
  if (r.includes("cloudflare dns") || r.includes("route 53 dns") || r.includes("route53 dns"))
    return null;
  if (r.includes("namecheap"))
    return "namecheap";
  if (r.includes("godaddy"))
    return "godaddy";
  if (r.includes("route 53") || r.includes("route53"))
    return "route53";
  if (r.includes("cloudflare"))
    return null;
  if (r.includes("brandsight"))
    return "brandsight";
  return null;
}
export {
  autoDetectRegistrar,
  getAvailableProviders,
  getDnsProvider,
  getDomainInventoryProvider,
  getProvider,
  getProviderInfo,
  getRegistrarProvider,
  providerHasDns,
  providerHasInventory,
  providerHasRegistrar,
  registerProvider,
  syncAll
};