@hasna/domains 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/sedo.d.ts.map +1 -1
- package/dist/cli/index.js +340 -785
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +446 -127
- package/dist/lib/capability.js +46 -0
- package/dist/lib/cloudflare-auth.js +139 -0
- package/dist/lib/cloudflare.js +394 -0
- package/dist/lib/creds-check.d.ts.map +1 -1
- package/dist/lib/delegate.js +12 -0
- package/dist/lib/env-aliases.d.ts +10 -0
- package/dist/lib/env-aliases.d.ts.map +1 -1
- package/dist/lib/registrar.js +2216 -0
- package/dist/lib/route53.d.ts +54 -11
- package/dist/lib/route53.d.ts.map +1 -1
- package/dist/lib/route53.js +678 -0
- package/dist/lib/sedo.d.ts +34 -3
- package/dist/lib/sedo.d.ts.map +1 -1
- package/dist/mcp/index.js +182 -69
- package/package.json +27 -2
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// src/lib/route53.ts
|
|
2
|
+
import {
|
|
3
|
+
Route53Client,
|
|
4
|
+
CreateHostedZoneCommand,
|
|
5
|
+
ListHostedZonesCommand,
|
|
6
|
+
GetHostedZoneCommand,
|
|
7
|
+
DeleteHostedZoneCommand,
|
|
8
|
+
ChangeResourceRecordSetsCommand,
|
|
9
|
+
ListResourceRecordSetsCommand,
|
|
10
|
+
ListHostedZonesByNameCommand
|
|
11
|
+
} from "@aws-sdk/client-route-53";
|
|
12
|
+
import {
|
|
13
|
+
Route53DomainsClient,
|
|
14
|
+
CheckDomainAvailabilityCommand,
|
|
15
|
+
DisableDomainTransferLockCommand,
|
|
16
|
+
GetDomainDetailCommand,
|
|
17
|
+
GetOperationDetailCommand,
|
|
18
|
+
ListDomainsCommand,
|
|
19
|
+
ListPricesCommand,
|
|
20
|
+
RegisterDomainCommand,
|
|
21
|
+
RetrieveDomainAuthCodeCommand,
|
|
22
|
+
TransferDomainCommand,
|
|
23
|
+
UpdateDomainNameserversCommand
|
|
24
|
+
} from "@aws-sdk/client-route-53-domains";
|
|
25
|
+
import { fromIni } from "@aws-sdk/credential-provider-ini";
|
|
26
|
+
function getConfig() {
|
|
27
|
+
return {
|
|
28
|
+
region: process.env["ROUTE53_REGION"] || process.env["ROUTE53_AWS_REGION"] || process.env["AWS_REGION"] || "us-east-1",
|
|
29
|
+
accessKeyId: process.env["ROUTE53_ACCESS_KEY_ID"] || process.env["AWS_ACCESS_KEY_ID"],
|
|
30
|
+
secretAccessKey: process.env["ROUTE53_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_ACCESS_KEY"],
|
|
31
|
+
sessionToken: process.env["ROUTE53_SESSION_TOKEN"] || process.env["AWS_SESSION_TOKEN"],
|
|
32
|
+
profile: process.env["ROUTE53_AWS_PROFILE"] || process.env["AWS_PROFILE"]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function makeClients(config) {
|
|
36
|
+
const cfg = config ?? getConfig();
|
|
37
|
+
const region = cfg.region || "us-east-1";
|
|
38
|
+
const credentials = cfg.accessKeyId && cfg.secretAccessKey ? { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey, sessionToken: cfg.sessionToken } : cfg.profile ? fromIni({ profile: cfg.profile }) : undefined;
|
|
39
|
+
return {
|
|
40
|
+
route53: new Route53Client({ region, credentials }),
|
|
41
|
+
domains: new Route53DomainsClient({ region: "us-east-1", credentials })
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function checkAvailability(domain, config) {
|
|
45
|
+
const { domains } = makeClients(config);
|
|
46
|
+
const result = await domains.send(new CheckDomainAvailabilityCommand({ DomainName: domain }));
|
|
47
|
+
const availability = {
|
|
48
|
+
domain,
|
|
49
|
+
available: result.Availability === "AVAILABLE",
|
|
50
|
+
availability: result.Availability ?? "UNKNOWN"
|
|
51
|
+
};
|
|
52
|
+
if (availability.available) {
|
|
53
|
+
try {
|
|
54
|
+
const tld = domain.split(".").slice(1).join(".");
|
|
55
|
+
const price = await getTldPrice(tld, config);
|
|
56
|
+
if (price) {
|
|
57
|
+
availability.currency = price.currency;
|
|
58
|
+
availability.price = price.registration_price;
|
|
59
|
+
availability.renewal_price = price.renewal_price;
|
|
60
|
+
availability.transfer_price = price.transfer_price;
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
return availability;
|
|
65
|
+
}
|
|
66
|
+
function normalizeTld(tld) {
|
|
67
|
+
return tld.trim().replace(/^\./, "");
|
|
68
|
+
}
|
|
69
|
+
function priceString(value) {
|
|
70
|
+
return value == null ? undefined : value.toString();
|
|
71
|
+
}
|
|
72
|
+
function tldPriceFromRoute53Price(tld, price) {
|
|
73
|
+
return {
|
|
74
|
+
tld: price.Name || tld,
|
|
75
|
+
registration_price: priceString(price.RegistrationPrice?.Price),
|
|
76
|
+
renewal_price: priceString(price.RenewalPrice?.Price),
|
|
77
|
+
transfer_price: priceString(price.TransferPrice?.Price),
|
|
78
|
+
currency: price.RegistrationPrice?.Currency ?? price.RenewalPrice?.Currency ?? price.TransferPrice?.Currency
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function getTldPrice(tld, config) {
|
|
82
|
+
const { domains } = makeClients(config);
|
|
83
|
+
const normalized = normalizeTld(tld);
|
|
84
|
+
const prices = await domains.send(new ListPricesCommand({ Tld: normalized, MaxItems: 1 }));
|
|
85
|
+
const price = prices.Prices?.[0];
|
|
86
|
+
return price ? tldPriceFromRoute53Price(normalized, price) : null;
|
|
87
|
+
}
|
|
88
|
+
async function listTldPrices(config) {
|
|
89
|
+
const { domains } = makeClients(config);
|
|
90
|
+
const prices = [];
|
|
91
|
+
let marker;
|
|
92
|
+
do {
|
|
93
|
+
const result = await domains.send(new ListPricesCommand({ Marker: marker, MaxItems: 100 }));
|
|
94
|
+
for (const price of result.Prices ?? []) {
|
|
95
|
+
prices.push(tldPriceFromRoute53Price(price.Name || "", price));
|
|
96
|
+
}
|
|
97
|
+
marker = result.NextPageMarker;
|
|
98
|
+
} while (marker);
|
|
99
|
+
return prices;
|
|
100
|
+
}
|
|
101
|
+
async function registerDomain(domain, contact, durationYears = 1, autoRenew = true, config, options = {}) {
|
|
102
|
+
const { domains } = makeClients(config);
|
|
103
|
+
const contactDetail = contactToRoute53Contact(contact);
|
|
104
|
+
const result = await domains.send(new RegisterDomainCommand({
|
|
105
|
+
DomainName: domain,
|
|
106
|
+
DurationInYears: durationYears,
|
|
107
|
+
AutoRenew: autoRenew,
|
|
108
|
+
AdminContact: contactDetail,
|
|
109
|
+
RegistrantContact: contactDetail,
|
|
110
|
+
TechContact: contactDetail,
|
|
111
|
+
PrivacyProtectAdminContact: options.privacy_protected ?? true,
|
|
112
|
+
PrivacyProtectRegistrantContact: options.privacy_protected ?? true,
|
|
113
|
+
PrivacyProtectTechContact: options.privacy_protected ?? true,
|
|
114
|
+
...options.nameservers?.length ? { Nameservers: options.nameservers.map((Name) => ({ Name })) } : {}
|
|
115
|
+
}));
|
|
116
|
+
return { operationId: result.OperationId ?? "" };
|
|
117
|
+
}
|
|
118
|
+
function contactToRoute53Contact(contact) {
|
|
119
|
+
return {
|
|
120
|
+
FirstName: contact.first_name,
|
|
121
|
+
LastName: contact.last_name,
|
|
122
|
+
Email: contact.email,
|
|
123
|
+
PhoneNumber: contact.phone,
|
|
124
|
+
AddressLine1: contact.address_line_1,
|
|
125
|
+
...contact.address_line_2 ? { AddressLine2: contact.address_line_2 } : {},
|
|
126
|
+
City: contact.city,
|
|
127
|
+
...contact.state ? { State: contact.state } : {},
|
|
128
|
+
CountryCode: contact.country_code.toUpperCase(),
|
|
129
|
+
ZipCode: contact.zip_code,
|
|
130
|
+
ContactType: contact.organization_name ? "COMPANY" : "PERSON",
|
|
131
|
+
...contact.organization_name ? { OrganizationName: contact.organization_name } : {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function transferDomain(domain, authCode, contact, durationYears = 1, autoRenew = true, config, options = {}) {
|
|
135
|
+
const { domains } = makeClients(config);
|
|
136
|
+
const contactDetail = contactToRoute53Contact(contact);
|
|
137
|
+
const result = await domains.send(new TransferDomainCommand({
|
|
138
|
+
DomainName: domain,
|
|
139
|
+
AuthCode: authCode,
|
|
140
|
+
DurationInYears: durationYears,
|
|
141
|
+
AutoRenew: autoRenew,
|
|
142
|
+
AdminContact: contactDetail,
|
|
143
|
+
RegistrantContact: contactDetail,
|
|
144
|
+
TechContact: contactDetail,
|
|
145
|
+
PrivacyProtectAdminContact: options.privacy_protected ?? true,
|
|
146
|
+
PrivacyProtectRegistrantContact: options.privacy_protected ?? true,
|
|
147
|
+
PrivacyProtectTechContact: options.privacy_protected ?? true,
|
|
148
|
+
...options.nameservers?.length ? { Nameservers: options.nameservers.map((Name) => ({ Name })) } : {}
|
|
149
|
+
}));
|
|
150
|
+
return { operationId: result.OperationId ?? "" };
|
|
151
|
+
}
|
|
152
|
+
async function getRegistrationStatus(operationId, config) {
|
|
153
|
+
const { domains } = makeClients(config);
|
|
154
|
+
const result = await domains.send(new GetOperationDetailCommand({ OperationId: operationId }));
|
|
155
|
+
return {
|
|
156
|
+
status: result.Status ?? "UNKNOWN",
|
|
157
|
+
domain: result.DomainName,
|
|
158
|
+
message: result.Message
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function getDomainDetail(domain, config) {
|
|
162
|
+
const { domains } = makeClients(config);
|
|
163
|
+
const [result, summary] = await Promise.all([
|
|
164
|
+
domains.send(new GetDomainDetailCommand({ DomainName: domain })),
|
|
165
|
+
getDomainSummary(domain, config)
|
|
166
|
+
]);
|
|
167
|
+
const privacy = result;
|
|
168
|
+
return {
|
|
169
|
+
domain: result.DomainName ?? domain,
|
|
170
|
+
expiry: result.ExpirationDate?.toISOString() ?? summary?.expiry ?? "",
|
|
171
|
+
auto_renew: result.AutoRenew ?? summary?.auto_renew ?? null,
|
|
172
|
+
transfer_lock: summary?.transfer_lock ?? (result.StatusList?.includes("TRANSFER_LOCK") ? true : null),
|
|
173
|
+
created: result.CreationDate?.toISOString() ?? "",
|
|
174
|
+
updated: result.UpdatedDate?.toISOString() ?? "",
|
|
175
|
+
nameservers: (result.Nameservers ?? []).map((ns) => ns.Name ?? "").filter(Boolean),
|
|
176
|
+
status_list: result.StatusList ?? [],
|
|
177
|
+
registrar_name: result.RegistrarName,
|
|
178
|
+
privacy_protected: privacy.RegistrantPrivacy ?? privacy.AdminPrivacy ?? privacy.TechPrivacy ?? null
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function getDomainSummary(domain, config) {
|
|
182
|
+
let marker;
|
|
183
|
+
do {
|
|
184
|
+
const { domains } = makeClients(config);
|
|
185
|
+
const result = await domains.send(new ListDomainsCommand({ Marker: marker, MaxItems: 100 }));
|
|
186
|
+
const summary = (result.Domains ?? []).find((item) => item.DomainName === domain);
|
|
187
|
+
if (summary) {
|
|
188
|
+
return {
|
|
189
|
+
domain: summary.DomainName ?? domain,
|
|
190
|
+
expiry: summary.Expiry?.toISOString() ?? "",
|
|
191
|
+
auto_renew: summary.AutoRenew ?? false,
|
|
192
|
+
transfer_lock: summary.TransferLock ?? false
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
marker = result.NextPageMarker;
|
|
196
|
+
} while (marker);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
async function requestTransferOutAuthCode(domain, config) {
|
|
200
|
+
const { domains } = makeClients(config);
|
|
201
|
+
const detail = await getDomainDetail(domain, config);
|
|
202
|
+
let transferLockDisabled = false;
|
|
203
|
+
let transferLockOperationId = null;
|
|
204
|
+
if (detail.transfer_lock) {
|
|
205
|
+
const result = await domains.send(new DisableDomainTransferLockCommand({ DomainName: domain }));
|
|
206
|
+
transferLockDisabled = true;
|
|
207
|
+
transferLockOperationId = result.OperationId ?? null;
|
|
208
|
+
}
|
|
209
|
+
const authCodeResult = await domains.send(new RetrieveDomainAuthCodeCommand({ DomainName: domain }));
|
|
210
|
+
if (!authCodeResult.AuthCode) {
|
|
211
|
+
throw new Error("Route53 did not return a domain transfer authorization code");
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
auth_code: authCodeResult.AuthCode,
|
|
215
|
+
transfer_lock_disabled: transferLockDisabled,
|
|
216
|
+
transfer_lock_operation_id: transferLockOperationId
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function updateNameservers(domain, nameservers, config, client) {
|
|
220
|
+
if (!nameservers.length) {
|
|
221
|
+
throw new Error("updateNameservers requires at least one nameserver");
|
|
222
|
+
}
|
|
223
|
+
const domains = client ?? makeClients(config).domains;
|
|
224
|
+
const result = await domains.send(new UpdateDomainNameserversCommand({
|
|
225
|
+
DomainName: domain,
|
|
226
|
+
Nameservers: nameservers.map((name) => ({ Name: name }))
|
|
227
|
+
}));
|
|
228
|
+
return { operationId: result.OperationId ?? "" };
|
|
229
|
+
}
|
|
230
|
+
async function listRegisteredDomains(config) {
|
|
231
|
+
const { domains } = makeClients(config);
|
|
232
|
+
const all = [];
|
|
233
|
+
let nextPageMarker;
|
|
234
|
+
do {
|
|
235
|
+
const result = await domains.send(new ListDomainsCommand({ Marker: nextPageMarker }));
|
|
236
|
+
for (const d of result.Domains ?? []) {
|
|
237
|
+
all.push({
|
|
238
|
+
domain: d.DomainName ?? "",
|
|
239
|
+
expiry: d.Expiry?.toISOString() ?? "",
|
|
240
|
+
auto_renew: d.AutoRenew ?? false,
|
|
241
|
+
transfer_lock: d.TransferLock ?? false
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
nextPageMarker = result.NextPageMarker;
|
|
245
|
+
} while (nextPageMarker);
|
|
246
|
+
return all;
|
|
247
|
+
}
|
|
248
|
+
function cleanZoneId(id) {
|
|
249
|
+
return id.replace("/hostedzone/", "");
|
|
250
|
+
}
|
|
251
|
+
function normalizeZoneId(id) {
|
|
252
|
+
return cleanZoneId(id.trim());
|
|
253
|
+
}
|
|
254
|
+
function normalizeNameserver(value) {
|
|
255
|
+
return value.trim().toLowerCase().replace(/\.$/, "");
|
|
256
|
+
}
|
|
257
|
+
function nameserversMatch(a, b) {
|
|
258
|
+
if (a.length !== b.length)
|
|
259
|
+
return false;
|
|
260
|
+
const left = [...new Set(a.map(normalizeNameserver))].sort();
|
|
261
|
+
const right = [...new Set(b.map(normalizeNameserver))].sort();
|
|
262
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
263
|
+
}
|
|
264
|
+
function cleanChangeId(id) {
|
|
265
|
+
return id?.replace("/change/", "") || null;
|
|
266
|
+
}
|
|
267
|
+
async function createHostedZone(domain, comment, config, options) {
|
|
268
|
+
const { route53 } = makeClients(config);
|
|
269
|
+
const callerRef = options?.callerReference ?? `domains-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
270
|
+
const result = await route53.send(new CreateHostedZoneCommand({
|
|
271
|
+
Name: domain,
|
|
272
|
+
CallerReference: callerRef,
|
|
273
|
+
HostedZoneConfig: comment ? { Comment: comment } : undefined
|
|
274
|
+
}));
|
|
275
|
+
return {
|
|
276
|
+
id: cleanZoneId(result.HostedZone?.Id ?? ""),
|
|
277
|
+
name: result.HostedZone?.Name ?? domain,
|
|
278
|
+
record_count: result.HostedZone?.ResourceRecordSetCount ?? 0,
|
|
279
|
+
comment,
|
|
280
|
+
name_servers: result.DelegationSet?.NameServers ?? []
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function listHostedZones(config) {
|
|
284
|
+
const { route53 } = makeClients(config);
|
|
285
|
+
const zones = [];
|
|
286
|
+
let marker;
|
|
287
|
+
do {
|
|
288
|
+
const result = await route53.send(new ListHostedZonesCommand({ Marker: marker }));
|
|
289
|
+
for (const z of result.HostedZones ?? []) {
|
|
290
|
+
zones.push({
|
|
291
|
+
id: cleanZoneId(z.Id ?? ""),
|
|
292
|
+
name: z.Name ?? "",
|
|
293
|
+
record_count: z.ResourceRecordSetCount ?? 0,
|
|
294
|
+
comment: z.Config?.Comment,
|
|
295
|
+
private_zone: z.Config?.PrivateZone
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
marker = result.IsTruncated ? result.NextMarker : undefined;
|
|
299
|
+
} while (marker);
|
|
300
|
+
return zones;
|
|
301
|
+
}
|
|
302
|
+
async function getHostedZone(hostedZoneId, config) {
|
|
303
|
+
const { route53 } = makeClients(config);
|
|
304
|
+
const result = await route53.send(new GetHostedZoneCommand({ Id: normalizeZoneId(hostedZoneId) }));
|
|
305
|
+
return {
|
|
306
|
+
id: cleanZoneId(result.HostedZone?.Id ?? ""),
|
|
307
|
+
name: result.HostedZone?.Name ?? "",
|
|
308
|
+
record_count: result.HostedZone?.ResourceRecordSetCount ?? 0,
|
|
309
|
+
comment: result.HostedZone?.Config?.Comment,
|
|
310
|
+
name_servers: result.DelegationSet?.NameServers ?? [],
|
|
311
|
+
private_zone: result.HostedZone?.Config?.PrivateZone
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
async function deleteHostedZone(hostedZoneId, config) {
|
|
315
|
+
const { route53 } = makeClients(config);
|
|
316
|
+
const zoneId = normalizeZoneId(hostedZoneId);
|
|
317
|
+
const managedRecords = [];
|
|
318
|
+
let nextName;
|
|
319
|
+
let nextType;
|
|
320
|
+
let nextIdentifier;
|
|
321
|
+
do {
|
|
322
|
+
const result = await route53.send(new ListResourceRecordSetsCommand({
|
|
323
|
+
HostedZoneId: zoneId,
|
|
324
|
+
StartRecordName: nextName,
|
|
325
|
+
StartRecordType: nextType,
|
|
326
|
+
StartRecordIdentifier: nextIdentifier
|
|
327
|
+
}));
|
|
328
|
+
managedRecords.push(...result.ResourceRecordSets?.filter((record) => record.Type !== "NS" && record.Type !== "SOA") ?? []);
|
|
329
|
+
nextName = result.IsTruncated ? result.NextRecordName : undefined;
|
|
330
|
+
nextType = result.IsTruncated ? result.NextRecordType : undefined;
|
|
331
|
+
nextIdentifier = result.IsTruncated ? result.NextRecordIdentifier : undefined;
|
|
332
|
+
} while (nextName && nextType);
|
|
333
|
+
for (let index = 0;index < managedRecords.length; index += 100) {
|
|
334
|
+
const batch = managedRecords.slice(index, index + 100);
|
|
335
|
+
if (batch.length === 0)
|
|
336
|
+
continue;
|
|
337
|
+
await route53.send(new ChangeResourceRecordSetsCommand({
|
|
338
|
+
HostedZoneId: zoneId,
|
|
339
|
+
ChangeBatch: {
|
|
340
|
+
Changes: batch.map((record) => ({
|
|
341
|
+
Action: "DELETE",
|
|
342
|
+
ResourceRecordSet: record
|
|
343
|
+
}))
|
|
344
|
+
}
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
await route53.send(new DeleteHostedZoneCommand({ Id: zoneId }));
|
|
348
|
+
}
|
|
349
|
+
async function findHostedZoneByDomain(domain, config) {
|
|
350
|
+
const zones = await listHostedZones(config);
|
|
351
|
+
const normalized = domain.endsWith(".") ? domain : `${domain}.`;
|
|
352
|
+
const matches = zones.filter((z) => z.name === normalized);
|
|
353
|
+
if (matches.length === 0)
|
|
354
|
+
return null;
|
|
355
|
+
const publicMatches = matches.filter((z) => !z.private_zone);
|
|
356
|
+
const candidates = publicMatches.length > 0 ? publicMatches : matches;
|
|
357
|
+
if (candidates.length > 1) {
|
|
358
|
+
throw new Error(`Multiple Route 53 hosted zones found for ${domain}; specify hosted zone id`);
|
|
359
|
+
}
|
|
360
|
+
return candidates[0] ?? null;
|
|
361
|
+
}
|
|
362
|
+
async function findHostedZoneByNameservers(domain, nameservers, config) {
|
|
363
|
+
if (!nameservers.length)
|
|
364
|
+
return null;
|
|
365
|
+
const { route53 } = makeClients(config);
|
|
366
|
+
const result = await route53.send(new ListHostedZonesByNameCommand({ DNSName: domain }));
|
|
367
|
+
const zones = (result.HostedZones ?? []).filter((zone) => zone.Name?.replace(/\.$/, "") === domain && !zone.Config?.PrivateZone);
|
|
368
|
+
for (const zone of zones) {
|
|
369
|
+
const id = cleanZoneId(zone.Id ?? "");
|
|
370
|
+
if (!id)
|
|
371
|
+
continue;
|
|
372
|
+
const detail = await getHostedZone(id, config);
|
|
373
|
+
const delegatedNameservers = detail.name_servers ?? [];
|
|
374
|
+
if (nameserversMatch(delegatedNameservers, nameservers)) {
|
|
375
|
+
return { ...detail, id, name_servers: delegatedNameservers };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
function rrsToRecord(rrs) {
|
|
381
|
+
if (rrs.AliasTarget) {
|
|
382
|
+
return {
|
|
383
|
+
name: rrs.Name ?? "",
|
|
384
|
+
type: rrs.Type ?? "",
|
|
385
|
+
ttl: 0,
|
|
386
|
+
values: [],
|
|
387
|
+
alias_target: {
|
|
388
|
+
hosted_zone_id: rrs.AliasTarget.HostedZoneId ?? "",
|
|
389
|
+
dns_name: rrs.AliasTarget.DNSName ?? ""
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
name: rrs.Name ?? "",
|
|
395
|
+
type: rrs.Type ?? "",
|
|
396
|
+
ttl: rrs.TTL ?? 0,
|
|
397
|
+
values: (rrs.ResourceRecords ?? []).map((r) => r.Value ?? "")
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function recordToRrs(record) {
|
|
401
|
+
if (record.alias_target) {
|
|
402
|
+
return {
|
|
403
|
+
Name: record.name,
|
|
404
|
+
Type: record.type,
|
|
405
|
+
AliasTarget: {
|
|
406
|
+
HostedZoneId: record.alias_target.hosted_zone_id,
|
|
407
|
+
DNSName: record.alias_target.dns_name,
|
|
408
|
+
EvaluateTargetHealth: false
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
Name: record.name,
|
|
414
|
+
Type: record.type,
|
|
415
|
+
TTL: record.ttl ?? 300,
|
|
416
|
+
ResourceRecords: record.values.map((v) => ({ Value: v }))
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
async function listRecords(hostedZoneId, config) {
|
|
420
|
+
const { route53 } = makeClients(config);
|
|
421
|
+
const records = [];
|
|
422
|
+
let nextName;
|
|
423
|
+
let nextType;
|
|
424
|
+
do {
|
|
425
|
+
const result = await route53.send(new ListResourceRecordSetsCommand({
|
|
426
|
+
HostedZoneId: normalizeZoneId(hostedZoneId),
|
|
427
|
+
StartRecordName: nextName,
|
|
428
|
+
StartRecordType: nextType
|
|
429
|
+
}));
|
|
430
|
+
for (const rrs of result.ResourceRecordSets ?? []) {
|
|
431
|
+
records.push(rrsToRecord(rrs));
|
|
432
|
+
}
|
|
433
|
+
if (result.IsTruncated) {
|
|
434
|
+
nextName = result.NextRecordName;
|
|
435
|
+
nextType = result.NextRecordType;
|
|
436
|
+
} else {
|
|
437
|
+
nextName = undefined;
|
|
438
|
+
nextType = undefined;
|
|
439
|
+
}
|
|
440
|
+
} while (nextName);
|
|
441
|
+
return records;
|
|
442
|
+
}
|
|
443
|
+
async function upsertRecord(hostedZoneId, record, config, options) {
|
|
444
|
+
const { route53 } = makeClients(config);
|
|
445
|
+
const result = await route53.send(new ChangeResourceRecordSetsCommand({
|
|
446
|
+
HostedZoneId: normalizeZoneId(hostedZoneId),
|
|
447
|
+
ChangeBatch: {
|
|
448
|
+
...options?.comment ? { Comment: options.comment } : {},
|
|
449
|
+
Changes: [{ Action: "UPSERT", ResourceRecordSet: recordToRrs(record) }]
|
|
450
|
+
}
|
|
451
|
+
}));
|
|
452
|
+
return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
|
|
453
|
+
}
|
|
454
|
+
async function deleteRecord(hostedZoneId, record, config, options) {
|
|
455
|
+
const { route53 } = makeClients(config);
|
|
456
|
+
const result = await route53.send(new ChangeResourceRecordSetsCommand({
|
|
457
|
+
HostedZoneId: normalizeZoneId(hostedZoneId),
|
|
458
|
+
ChangeBatch: {
|
|
459
|
+
...options?.comment ? { Comment: options.comment } : {},
|
|
460
|
+
Changes: [{ Action: "DELETE", ResourceRecordSet: recordToRrs(record) }]
|
|
461
|
+
}
|
|
462
|
+
}));
|
|
463
|
+
return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
|
|
464
|
+
}
|
|
465
|
+
async function upsertRecords(hostedZoneId, records, config, options) {
|
|
466
|
+
if (records.length === 0)
|
|
467
|
+
return { changeId: null };
|
|
468
|
+
const { route53 } = makeClients(config);
|
|
469
|
+
const changes = records.map((r) => ({
|
|
470
|
+
Action: "UPSERT",
|
|
471
|
+
ResourceRecordSet: recordToRrs(r)
|
|
472
|
+
}));
|
|
473
|
+
const result = await route53.send(new ChangeResourceRecordSetsCommand({
|
|
474
|
+
HostedZoneId: normalizeZoneId(hostedZoneId),
|
|
475
|
+
ChangeBatch: {
|
|
476
|
+
...options?.comment ? { Comment: options.comment } : {},
|
|
477
|
+
Changes: changes
|
|
478
|
+
}
|
|
479
|
+
}));
|
|
480
|
+
return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
|
|
481
|
+
}
|
|
482
|
+
function createRoute53Provider(config) {
|
|
483
|
+
const cfg = config ?? getConfig();
|
|
484
|
+
const registerWithRoute53 = registerDomain;
|
|
485
|
+
const updateRoute53Nameservers = updateNameservers;
|
|
486
|
+
async function listDomainInventory() {
|
|
487
|
+
const byDomain = new Map;
|
|
488
|
+
try {
|
|
489
|
+
const registered = await listRegisteredDomains(cfg);
|
|
490
|
+
for (const d of registered) {
|
|
491
|
+
byDomain.set(d.domain, {
|
|
492
|
+
domain: d.domain,
|
|
493
|
+
registrar: "AWS Route 53",
|
|
494
|
+
created: "",
|
|
495
|
+
expires: d.expiry,
|
|
496
|
+
nameservers: [],
|
|
497
|
+
status: "active",
|
|
498
|
+
auto_renew: d.auto_renew
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
} catch (error) {
|
|
502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
503
|
+
if (!message.includes("route53domains:ListDomains") && !message.includes("AccessDenied")) {
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const zones = await listHostedZones(cfg);
|
|
508
|
+
for (const z of zones) {
|
|
509
|
+
const zone = z.name_servers?.length ? z : await getHostedZone(z.id, cfg).catch(() => z);
|
|
510
|
+
const domain = zone.name.replace(/\.$/, "");
|
|
511
|
+
const nameservers = zone.name_servers ?? [];
|
|
512
|
+
const existing = byDomain.get(domain);
|
|
513
|
+
if (existing) {
|
|
514
|
+
existing.nameservers = nameservers.length > 0 ? nameservers : existing.nameservers;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
byDomain.set(domain, {
|
|
518
|
+
domain,
|
|
519
|
+
registrar: "AWS Route 53 DNS",
|
|
520
|
+
created: "",
|
|
521
|
+
expires: "",
|
|
522
|
+
nameservers,
|
|
523
|
+
status: "active",
|
|
524
|
+
auto_renew: false
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return Array.from(byDomain.values());
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
name: "route53",
|
|
531
|
+
async listDomains() {
|
|
532
|
+
return listDomainInventory();
|
|
533
|
+
},
|
|
534
|
+
async getDomainInfo(domain) {
|
|
535
|
+
const detail = await getDomainDetail(domain, cfg);
|
|
536
|
+
return {
|
|
537
|
+
domain: detail.domain,
|
|
538
|
+
registrar: "AWS Route 53",
|
|
539
|
+
created: detail.created,
|
|
540
|
+
expires: detail.expiry,
|
|
541
|
+
nameservers: detail.nameservers,
|
|
542
|
+
status: "active",
|
|
543
|
+
auto_renew: detail.auto_renew ?? false
|
|
544
|
+
};
|
|
545
|
+
},
|
|
546
|
+
async registerDomain(domain, contact, options = {}) {
|
|
547
|
+
const result = await registerWithRoute53(domain, contact, options.years ?? 1, options.autoRenew ?? true, cfg);
|
|
548
|
+
return { domain, success: !!result.operationId, operationId: result.operationId };
|
|
549
|
+
},
|
|
550
|
+
async updateNameservers(domain, nameservers) {
|
|
551
|
+
const result = await updateRoute53Nameservers(domain, nameservers, cfg);
|
|
552
|
+
return { domain, success: !!result.operationId, operationId: result.operationId };
|
|
553
|
+
},
|
|
554
|
+
async renewDomain(_domain) {
|
|
555
|
+
return { domain: _domain, success: false, orderId: undefined, chargedAmount: undefined };
|
|
556
|
+
},
|
|
557
|
+
async getDnsRecords(domain) {
|
|
558
|
+
const zone = await findHostedZoneByDomain(domain, cfg);
|
|
559
|
+
if (!zone)
|
|
560
|
+
return [];
|
|
561
|
+
const records = await listRecords(zone.id, cfg);
|
|
562
|
+
const result = [];
|
|
563
|
+
for (const r of records) {
|
|
564
|
+
if (r.alias_target) {
|
|
565
|
+
result.push({ type: r.type, name: r.name, value: r.alias_target.dns_name, ttl: 0 });
|
|
566
|
+
} else {
|
|
567
|
+
for (const v of r.values) {
|
|
568
|
+
result.push({ type: r.type, name: r.name, value: v, ttl: r.ttl });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
},
|
|
574
|
+
async setDnsRecords(domain, records) {
|
|
575
|
+
const zone = await findHostedZoneByDomain(domain, cfg);
|
|
576
|
+
if (!zone)
|
|
577
|
+
throw new Error(`No hosted zone found for ${domain}`);
|
|
578
|
+
const grouped = new Map;
|
|
579
|
+
for (const r of records) {
|
|
580
|
+
const key = `${r.name}|${r.type}`;
|
|
581
|
+
const existing = grouped.get(key);
|
|
582
|
+
if (existing) {
|
|
583
|
+
existing.values.push(r.value);
|
|
584
|
+
} else {
|
|
585
|
+
grouped.set(key, { name: r.name, type: r.type, ttl: r.ttl, values: [r.value] });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
await upsertRecords(zone.id, Array.from(grouped.values()), cfg);
|
|
589
|
+
return true;
|
|
590
|
+
},
|
|
591
|
+
async checkAvailability(domain) {
|
|
592
|
+
const result = await checkAvailability(domain, cfg);
|
|
593
|
+
return {
|
|
594
|
+
domain: result.domain,
|
|
595
|
+
available: result.available,
|
|
596
|
+
standard_price: result.price ? Number(result.price) : undefined,
|
|
597
|
+
currency: result.currency
|
|
598
|
+
};
|
|
599
|
+
},
|
|
600
|
+
async syncToLocalDb(dbFns) {
|
|
601
|
+
const domains = await listDomainInventory();
|
|
602
|
+
let synced = 0;
|
|
603
|
+
let created = 0;
|
|
604
|
+
let updated = 0;
|
|
605
|
+
const errors = [];
|
|
606
|
+
for (const d of domains) {
|
|
607
|
+
try {
|
|
608
|
+
const existing = dbFns.getDomainByName(d.domain);
|
|
609
|
+
if (existing) {
|
|
610
|
+
const existingRoute53 = existing.metadata["route53"];
|
|
611
|
+
const staleDnsOnlyRegistrar = d.registrar !== "AWS Route 53" && (existing.registrar === "AWS Route 53 DNS" || existing.registrar === "AWS Route 53" && existingRoute53?.source === "route53:hosted_zones");
|
|
612
|
+
dbFns.updateDomain(existing.id, {
|
|
613
|
+
...d.registrar === "AWS Route 53" ? { registrar: "AWS Route 53" } : {},
|
|
614
|
+
...staleDnsOnlyRegistrar ? { registrar: null } : {},
|
|
615
|
+
expires_at: d.expires || undefined,
|
|
616
|
+
auto_renew: d.auto_renew,
|
|
617
|
+
nameservers: d.nameservers.length > 0 ? d.nameservers : existing.nameservers,
|
|
618
|
+
metadata: {
|
|
619
|
+
...existing.metadata,
|
|
620
|
+
route53: {
|
|
621
|
+
source: d.registrar === "AWS Route 53" ? "route53domains+hosted_zones" : "route53:hosted_zones",
|
|
622
|
+
synced_at: new Date().toISOString()
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
status: "active"
|
|
626
|
+
});
|
|
627
|
+
updated++;
|
|
628
|
+
} else {
|
|
629
|
+
dbFns.createDomain({
|
|
630
|
+
name: d.domain,
|
|
631
|
+
...d.registrar === "AWS Route 53" ? { registrar: "AWS Route 53" } : {},
|
|
632
|
+
expires_at: d.expires || undefined,
|
|
633
|
+
auto_renew: d.auto_renew,
|
|
634
|
+
nameservers: d.nameservers,
|
|
635
|
+
status: "active",
|
|
636
|
+
notes: d.registrar === "AWS Route 53 DNS" ? "Discovered from Route 53 hosted zones; registrar ownership was not inferred." : undefined,
|
|
637
|
+
metadata: {
|
|
638
|
+
route53: {
|
|
639
|
+
source: d.registrar === "AWS Route 53" ? "route53domains+hosted_zones" : "route53:hosted_zones",
|
|
640
|
+
synced_at: new Date().toISOString()
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
created++;
|
|
645
|
+
}
|
|
646
|
+
synced++;
|
|
647
|
+
} catch (err) {
|
|
648
|
+
errors.push(`${d.domain}: ${err instanceof Error ? err.message : String(err)}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return { synced, created, updated, errors };
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
export {
|
|
656
|
+
upsertRecords,
|
|
657
|
+
upsertRecord,
|
|
658
|
+
updateNameservers,
|
|
659
|
+
transferDomain,
|
|
660
|
+
requestTransferOutAuthCode,
|
|
661
|
+
registerDomain,
|
|
662
|
+
listTldPrices,
|
|
663
|
+
listRegisteredDomains,
|
|
664
|
+
listRecords,
|
|
665
|
+
listHostedZones,
|
|
666
|
+
getTldPrice,
|
|
667
|
+
getRegistrationStatus,
|
|
668
|
+
getHostedZone,
|
|
669
|
+
getDomainDetail,
|
|
670
|
+
getConfig,
|
|
671
|
+
findHostedZoneByNameservers,
|
|
672
|
+
findHostedZoneByDomain,
|
|
673
|
+
deleteRecord,
|
|
674
|
+
deleteHostedZone,
|
|
675
|
+
createRoute53Provider,
|
|
676
|
+
createHostedZone,
|
|
677
|
+
checkAvailability
|
|
678
|
+
};
|