@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.
@@ -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
+ };