@hasna/domains
Version:
Domain portfolio, registrar, marketplace, and DNS management for AI agents — CLI + MCP + HTTP API + SDK, shared account API and PostgreSQL
828 lines (827 loc) • 32.1 kB
JavaScript
// 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 getConfig() {
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 ?? getConfig();
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 checkAvailability(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 registerDomain(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 updateNameservers(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 ?? getConfig();
const registerWithRoute53 = registerDomain;
const updateRoute53Nameservers = updateNameservers;
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 checkAvailability(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 };
}
};
}
export {
checkAvailability,
createHostedZone,
createRoute53Provider,
deleteDefaultHostedZone,
deleteHostedZone,
deleteRecord,
findHostedZoneByDomain,
findHostedZoneByNameservers,
findRegisteredDomainDetail,
getConfig,
getDomainDetail,
getHostedZone,
getRegistrationContactFromDomain,
getRegistrationStatus,
getTldPrice,
listHostedZones,
listHostedZonesByDomain,
listRecords,
listRegisteredDomains,
listTldPrices,
registerDomain,
requestTransferOutAuthCode,
transferDomain,
updateNameservers,
upsertRecord,
upsertRecords
};