UNPKG

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