@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
JavaScript
// 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
};