@hasna/domains 0.0.24 → 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,2216 @@
1
+ // src/lib/env-aliases.ts
2
+ function firstEnv(env, names) {
3
+ for (const key of names) {
4
+ const value = env[key];
5
+ if (value)
6
+ return { key, value };
7
+ }
8
+ return;
9
+ }
10
+ function hasEveryEnv(env, groups) {
11
+ return groups.every((names) => !!firstEnv(env, names));
12
+ }
13
+ function flattenEnvNames(groups) {
14
+ return Array.from(new Set(groups.flatMap((names) => [...names])));
15
+ }
16
+ var NAMECHEAP_ENV = {
17
+ apiKey: ["NAMECHEAP_API_KEY"],
18
+ username: ["NAMECHEAP_USERNAME"],
19
+ clientIp: ["NAMECHEAP_CLIENT_IP"]
20
+ };
21
+ var GODADDY_ENV = {
22
+ apiKey: ["GODADDY_API_KEY"],
23
+ apiSecret: ["GODADDY_API_SECRET"]
24
+ };
25
+ var BRANDSIGHT_ENV = {
26
+ apiKey: ["BRANDSIGHT_API_KEY"],
27
+ apiSecret: ["BRANDSIGHT_API_SECRET"],
28
+ customerId: ["BRANDSIGHT_CUSTOMER_ID"],
29
+ shopperId: ["BRANDSIGHT_SHOPPER_ID"],
30
+ accountId: ["BRANDSIGHT_ACCOUNT_ID"]
31
+ };
32
+ var SEDO_ENV = {
33
+ partnerId: ["SEDO_PARTNER_ID"],
34
+ signKey: ["SEDO_API_KEY", "SEDO_SIGN_KEY"],
35
+ username: ["SEDO_USERNAME", "SEDO_EMAIL"],
36
+ password: ["SEDO_PASSWORD"]
37
+ };
38
+ var CLOUDFLARE_ENV = {
39
+ apiToken: ["CLOUDFLARE_API_TOKEN"],
40
+ apiKey: ["CLOUDFLARE_API_KEY"],
41
+ email: ["CLOUDFLARE_EMAIL"],
42
+ accountId: ["CLOUDFLARE_ACCOUNT_ID"]
43
+ };
44
+ var ROUTE53_ENV = {
45
+ accessKeyId: ["ROUTE53_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"],
46
+ secretAccessKey: ["ROUTE53_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"],
47
+ sessionToken: ["ROUTE53_SESSION_TOKEN", "AWS_SESSION_TOKEN"],
48
+ profile: ["ROUTE53_AWS_PROFILE", "AWS_PROFILE"],
49
+ region: ["ROUTE53_REGION", "ROUTE53_AWS_REGION", "AWS_REGION"],
50
+ webIdentityTokenFile: ["AWS_WEB_IDENTITY_TOKEN_FILE"],
51
+ roleArn: ["AWS_ROLE_ARN"],
52
+ containerCredentials: [
53
+ "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
54
+ "AWS_CONTAINER_CREDENTIALS_FULL_URI"
55
+ ]
56
+ };
57
+ function providerEnvNames(provider) {
58
+ switch (provider.toLowerCase()) {
59
+ case "namecheap":
60
+ return flattenEnvNames([NAMECHEAP_ENV.apiKey, NAMECHEAP_ENV.username, NAMECHEAP_ENV.clientIp]);
61
+ case "godaddy":
62
+ return flattenEnvNames([GODADDY_ENV.apiKey, GODADDY_ENV.apiSecret]);
63
+ case "brandsight":
64
+ return flattenEnvNames([
65
+ BRANDSIGHT_ENV.apiKey,
66
+ BRANDSIGHT_ENV.apiSecret,
67
+ BRANDSIGHT_ENV.customerId,
68
+ BRANDSIGHT_ENV.shopperId,
69
+ BRANDSIGHT_ENV.accountId
70
+ ]);
71
+ case "sedo":
72
+ return flattenEnvNames([SEDO_ENV.partnerId, SEDO_ENV.signKey, SEDO_ENV.username, SEDO_ENV.password]);
73
+ case "cloudflare":
74
+ return flattenEnvNames([CLOUDFLARE_ENV.apiToken, CLOUDFLARE_ENV.apiKey, CLOUDFLARE_ENV.email, CLOUDFLARE_ENV.accountId]);
75
+ case "route53":
76
+ return flattenEnvNames([
77
+ ROUTE53_ENV.profile,
78
+ ROUTE53_ENV.accessKeyId,
79
+ ROUTE53_ENV.secretAccessKey,
80
+ ROUTE53_ENV.sessionToken,
81
+ ROUTE53_ENV.region,
82
+ ROUTE53_ENV.webIdentityTokenFile,
83
+ ROUTE53_ENV.roleArn,
84
+ ROUTE53_ENV.containerCredentials
85
+ ]);
86
+ default:
87
+ return [];
88
+ }
89
+ }
90
+ function hasProviderCredentials(provider, env = process.env) {
91
+ switch (provider.toLowerCase()) {
92
+ case "namecheap":
93
+ return hasEveryEnv(env, [NAMECHEAP_ENV.apiKey, NAMECHEAP_ENV.username, NAMECHEAP_ENV.clientIp]);
94
+ case "godaddy":
95
+ return hasEveryEnv(env, [GODADDY_ENV.apiKey, GODADDY_ENV.apiSecret]);
96
+ case "brandsight":
97
+ return hasEveryEnv(env, [BRANDSIGHT_ENV.apiKey, BRANDSIGHT_ENV.apiSecret, BRANDSIGHT_ENV.customerId]);
98
+ case "sedo":
99
+ return hasEveryEnv(env, [SEDO_ENV.partnerId, SEDO_ENV.signKey, SEDO_ENV.username, SEDO_ENV.password]);
100
+ case "cloudflare": {
101
+ const tokenMode = !!firstEnv(env, CLOUDFLARE_ENV.apiToken);
102
+ const keyMode = hasEveryEnv(env, [CLOUDFLARE_ENV.apiKey, CLOUDFLARE_ENV.email]);
103
+ return tokenMode || keyMode;
104
+ }
105
+ case "route53": {
106
+ const profileMode = !!firstEnv(env, ROUTE53_ENV.profile);
107
+ const keyMode = hasEveryEnv(env, [ROUTE53_ENV.accessKeyId, ROUTE53_ENV.secretAccessKey]);
108
+ const webIdentityMode = hasEveryEnv(env, [ROUTE53_ENV.webIdentityTokenFile, ROUTE53_ENV.roleArn]);
109
+ const containerMode = !!firstEnv(env, ROUTE53_ENV.containerCredentials);
110
+ return profileMode || keyMode || webIdentityMode || containerMode;
111
+ }
112
+ default:
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // src/lib/cloudflare-auth.ts
118
+ function resolveCloudflareConfig(env = process.env) {
119
+ const accountId = firstEnv(env, CLOUDFLARE_ENV.accountId)?.value;
120
+ const apiToken = firstEnv(env, CLOUDFLARE_ENV.apiToken)?.value;
121
+ if (apiToken)
122
+ return { apiToken, accountId };
123
+ const apiKey = firstEnv(env, CLOUDFLARE_ENV.apiKey)?.value;
124
+ const email = firstEnv(env, CLOUDFLARE_ENV.email)?.value;
125
+ if (apiKey && email)
126
+ return { apiKey, email, accountId };
127
+ return accountId ? { accountId } : {};
128
+ }
129
+ function cloudflareAuthHeaders(cfg) {
130
+ if (cfg.apiToken)
131
+ return { Authorization: `Bearer ${cfg.apiToken}` };
132
+ if (cfg.apiKey && cfg.email)
133
+ return { "X-Auth-Key": cfg.apiKey, "X-Auth-Email": cfg.email };
134
+ throw new Error("Cloudflare credentials not configured. Set CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL.");
135
+ }
136
+
137
+ // src/lib/cloudflare.ts
138
+ function getConfig() {
139
+ return resolveCloudflareConfig();
140
+ }
141
+ function checkCredentials(cfg) {
142
+ cloudflareAuthHeaders(cfg);
143
+ }
144
+ var CF_BASE = "https://api.cloudflare.com/client/v4";
145
+ async function cfFetch(path, opts = {}) {
146
+ const cfg = opts.config ?? getConfig();
147
+ checkCredentials(cfg);
148
+ const res = await fetch(`${CF_BASE}${path}`, {
149
+ method: opts.method ?? "GET",
150
+ headers: {
151
+ ...cloudflareAuthHeaders(cfg),
152
+ "Content-Type": "application/json"
153
+ },
154
+ body: opts.body ? JSON.stringify(opts.body) : undefined
155
+ });
156
+ const json = await res.json();
157
+ if (!json.success) {
158
+ const msg = json.errors?.[0]?.message ?? `Cloudflare API error (${res.status})`;
159
+ throw new Error(msg);
160
+ }
161
+ return json.result;
162
+ }
163
+ async function listZones(config) {
164
+ const zones = [];
165
+ let page = 1;
166
+ while (true) {
167
+ const result = await cfFetch(`/zones?per_page=50&page=${page}`, { config });
168
+ if (!result || result.length === 0)
169
+ break;
170
+ for (const z of result) {
171
+ zones.push({ id: z.id, name: z.name, status: z.status, nameservers: z.name_servers, original_nameservers: z.original_name_servers });
172
+ }
173
+ if (result.length < 50)
174
+ break;
175
+ page++;
176
+ }
177
+ return zones;
178
+ }
179
+ async function getZone(domain, config) {
180
+ const result = await cfFetch(`/zones?name=${encodeURIComponent(domain)}`, { config });
181
+ if (!result || result.length === 0)
182
+ return null;
183
+ const z = result[0];
184
+ return { id: z.id, name: z.name, status: z.status, nameservers: z.name_servers, original_nameservers: z.original_name_servers };
185
+ }
186
+ async function createZone(domain, config) {
187
+ const cfg = config ?? getConfig();
188
+ if (!cfg.accountId) {
189
+ throw new Error("CLOUDFLARE_ACCOUNT_ID is required to create a zone.");
190
+ }
191
+ const result = await cfFetch("/zones", { method: "POST", body: { name: domain, account: { id: cfg.accountId }, jump_start: false }, config: cfg });
192
+ return { id: result.id, name: result.name, status: result.status, nameservers: result.name_servers };
193
+ }
194
+ async function ensureZone(domain, config, deps) {
195
+ const get = deps?.getZone ?? getZone;
196
+ const create = deps?.createZone ?? createZone;
197
+ const existing = await get(domain, config);
198
+ const zone = existing ?? await create(domain, config);
199
+ if (!zone.nameservers || zone.nameservers.length === 0) {
200
+ throw new Error(`Cloudflare zone for ${domain} has no nameservers yet`);
201
+ }
202
+ return zone;
203
+ }
204
+ async function deleteZone(zoneId, config) {
205
+ await cfFetch(`/zones/${zoneId}`, { method: "DELETE", config });
206
+ }
207
+ async function listRecords(zoneId, config) {
208
+ const records = [];
209
+ let page = 1;
210
+ while (true) {
211
+ const result = await cfFetch(`/zones/${zoneId}/dns_records?per_page=100&page=${page}`, { config });
212
+ if (!result || result.length === 0)
213
+ break;
214
+ for (const r of result) {
215
+ records.push({ id: r.id, type: r.type, name: r.name, content: r.content, ttl: r.ttl, priority: r.priority, proxied: r.proxied });
216
+ }
217
+ if (result.length < 100)
218
+ break;
219
+ page++;
220
+ }
221
+ return records;
222
+ }
223
+ async function listRecordsByNameType(zoneId, type, name, config) {
224
+ const result = await cfFetch(`/zones/${zoneId}/dns_records?type=${encodeURIComponent(type)}&name=${encodeURIComponent(name)}`, { config });
225
+ return (result ?? []).map((r) => ({
226
+ id: r.id,
227
+ type: r.type,
228
+ name: r.name,
229
+ content: r.content,
230
+ ttl: r.ttl,
231
+ priority: r.priority,
232
+ proxied: r.proxied
233
+ }));
234
+ }
235
+ async function upsertRecord(zoneId, record, config) {
236
+ const existing = await listRecordsByNameType(zoneId, record.type, record.name, config);
237
+ const body = {
238
+ type: record.type,
239
+ name: record.name,
240
+ content: record.content,
241
+ ttl: record.ttl ?? 1,
242
+ priority: record.priority,
243
+ proxied: record.proxied ?? false
244
+ };
245
+ const sameRecord = existing.find((r) => r.content === record.content && (r.priority ?? undefined) === (record.priority ?? undefined) && (r.proxied ?? false) === (record.proxied ?? false));
246
+ if (sameRecord?.id) {
247
+ await cfFetch(`/zones/${zoneId}/dns_records/${sameRecord.id}`, { method: "PUT", body, config });
248
+ } else {
249
+ await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body, config });
250
+ }
251
+ }
252
+ async function replaceRecordsByNameType(zoneId, records, config) {
253
+ if (records.length === 0)
254
+ return;
255
+ const { type, name } = records[0];
256
+ const existing = await listRecordsByNameType(zoneId, type, name, config);
257
+ for (const record of existing) {
258
+ if (record.id)
259
+ await deleteRecord(zoneId, record.id, config);
260
+ }
261
+ for (const record of records) {
262
+ await cfFetch(`/zones/${zoneId}/dns_records`, {
263
+ method: "POST",
264
+ body: {
265
+ type: record.type,
266
+ name: record.name,
267
+ content: record.content,
268
+ ttl: record.ttl ?? 1,
269
+ priority: record.priority,
270
+ proxied: record.proxied ?? false
271
+ },
272
+ config
273
+ });
274
+ }
275
+ }
276
+ async function deleteRecord(zoneId, recordId, config) {
277
+ await cfFetch(`/zones/${zoneId}/dns_records/${recordId}`, { method: "DELETE", config });
278
+ }
279
+ async function deleteRecordByNameType(zoneId, name, type, config) {
280
+ const existing = await cfFetch(`/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(name)}`, { config });
281
+ for (const r of existing ?? []) {
282
+ await deleteRecord(zoneId, r.id, config);
283
+ }
284
+ }
285
+ function zoneToDomainInfo(zone) {
286
+ return {
287
+ domain: zone.name,
288
+ registrar: "Cloudflare DNS",
289
+ created: "",
290
+ expires: "",
291
+ nameservers: zone.nameservers,
292
+ status: zone.status === "active" ? "active" : "discovered",
293
+ auto_renew: false
294
+ };
295
+ }
296
+ function withCloudflareMetadata(existing, zone) {
297
+ return {
298
+ ...existing,
299
+ cloudflare: {
300
+ zone_id: zone.id,
301
+ zone_status: zone.status,
302
+ source: "cloudflare:zones",
303
+ synced_at: new Date().toISOString()
304
+ }
305
+ };
306
+ }
307
+ function createCloudflareProvider(config) {
308
+ const cfg = config ?? getConfig();
309
+ return {
310
+ name: "cloudflare",
311
+ async listDomains() {
312
+ const zones = await listZones(cfg);
313
+ return zones.map(zoneToDomainInfo);
314
+ },
315
+ async syncToLocalDb(dbFns) {
316
+ const zones = await listZones(cfg);
317
+ let synced = 0;
318
+ let created = 0;
319
+ let updated = 0;
320
+ const errors = [];
321
+ for (const zone of zones) {
322
+ try {
323
+ const info = zoneToDomainInfo(zone);
324
+ const existing = dbFns.getDomainByName(zone.name);
325
+ if (existing) {
326
+ dbFns.updateDomain(existing.id, {
327
+ ...existing.registrar === "Cloudflare DNS" ? { registrar: null } : {},
328
+ status: existing.status === "discovered" && info.status === "active" ? "active" : existing.status,
329
+ nameservers: zone.nameservers,
330
+ metadata: withCloudflareMetadata(existing.metadata, zone)
331
+ });
332
+ updated++;
333
+ } else {
334
+ dbFns.createDomain({
335
+ name: zone.name,
336
+ status: info.status === "active" ? "active" : "discovered",
337
+ auto_renew: false,
338
+ nameservers: zone.nameservers,
339
+ notes: "Discovered from Cloudflare zones; registrar ownership was not inferred.",
340
+ metadata: withCloudflareMetadata({}, zone)
341
+ });
342
+ created++;
343
+ }
344
+ synced++;
345
+ } catch (err) {
346
+ errors.push(`${zone.name}: ${err instanceof Error ? err.message : String(err)}`);
347
+ }
348
+ }
349
+ return { synced, created, updated, errors };
350
+ },
351
+ async getDnsRecords(domain) {
352
+ const zone = await getZone(domain, cfg);
353
+ if (!zone)
354
+ return [];
355
+ const records = await listRecords(zone.id, cfg);
356
+ return records.map((r) => ({
357
+ type: r.type,
358
+ name: r.name,
359
+ value: r.content,
360
+ ttl: r.ttl === 1 ? 0 : r.ttl,
361
+ priority: r.priority
362
+ }));
363
+ },
364
+ async setDnsRecords(domain, records) {
365
+ const zone = await getZone(domain, cfg);
366
+ if (!zone)
367
+ throw new Error(`No Cloudflare zone found for ${domain}`);
368
+ const grouped = new Map;
369
+ for (const r of records) {
370
+ const key = `${r.type}|${r.name}`;
371
+ const existing = grouped.get(key) ?? [];
372
+ existing.push({ type: r.type, name: r.name, content: r.value, ttl: r.ttl || 1, priority: r.priority });
373
+ grouped.set(key, existing);
374
+ }
375
+ for (const group of grouped.values()) {
376
+ await replaceRecordsByNameType(zone.id, group, cfg);
377
+ }
378
+ return true;
379
+ }
380
+ };
381
+ }
382
+
383
+ // src/lib/route53.ts
384
+ import {
385
+ Route53Client,
386
+ CreateHostedZoneCommand,
387
+ ListHostedZonesCommand,
388
+ GetHostedZoneCommand,
389
+ DeleteHostedZoneCommand,
390
+ ChangeResourceRecordSetsCommand,
391
+ ListResourceRecordSetsCommand,
392
+ ListHostedZonesByNameCommand
393
+ } from "@aws-sdk/client-route-53";
394
+ import {
395
+ Route53DomainsClient,
396
+ CheckDomainAvailabilityCommand,
397
+ DisableDomainTransferLockCommand,
398
+ GetDomainDetailCommand,
399
+ GetOperationDetailCommand,
400
+ ListDomainsCommand,
401
+ ListPricesCommand,
402
+ RegisterDomainCommand,
403
+ RetrieveDomainAuthCodeCommand,
404
+ TransferDomainCommand,
405
+ UpdateDomainNameserversCommand
406
+ } from "@aws-sdk/client-route-53-domains";
407
+ import { fromIni } from "@aws-sdk/credential-provider-ini";
408
+ function getConfig2() {
409
+ return {
410
+ region: process.env["ROUTE53_REGION"] || process.env["ROUTE53_AWS_REGION"] || process.env["AWS_REGION"] || "us-east-1",
411
+ accessKeyId: process.env["ROUTE53_ACCESS_KEY_ID"] || process.env["AWS_ACCESS_KEY_ID"],
412
+ secretAccessKey: process.env["ROUTE53_SECRET_ACCESS_KEY"] || process.env["AWS_SECRET_ACCESS_KEY"],
413
+ sessionToken: process.env["ROUTE53_SESSION_TOKEN"] || process.env["AWS_SESSION_TOKEN"],
414
+ profile: process.env["ROUTE53_AWS_PROFILE"] || process.env["AWS_PROFILE"]
415
+ };
416
+ }
417
+ function makeClients(config) {
418
+ const cfg = config ?? getConfig2();
419
+ const region = cfg.region || "us-east-1";
420
+ const credentials = cfg.accessKeyId && cfg.secretAccessKey ? { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey, sessionToken: cfg.sessionToken } : cfg.profile ? fromIni({ profile: cfg.profile }) : undefined;
421
+ return {
422
+ route53: new Route53Client({ region, credentials }),
423
+ domains: new Route53DomainsClient({ region: "us-east-1", credentials })
424
+ };
425
+ }
426
+ async function checkAvailability(domain, config) {
427
+ const { domains } = makeClients(config);
428
+ const result = await domains.send(new CheckDomainAvailabilityCommand({ DomainName: domain }));
429
+ const availability = {
430
+ domain,
431
+ available: result.Availability === "AVAILABLE",
432
+ availability: result.Availability ?? "UNKNOWN"
433
+ };
434
+ if (availability.available) {
435
+ try {
436
+ const tld = domain.split(".").slice(1).join(".");
437
+ const price = await getTldPrice(tld, config);
438
+ if (price) {
439
+ availability.currency = price.currency;
440
+ availability.price = price.registration_price;
441
+ availability.renewal_price = price.renewal_price;
442
+ availability.transfer_price = price.transfer_price;
443
+ }
444
+ } catch {}
445
+ }
446
+ return availability;
447
+ }
448
+ function normalizeTld(tld) {
449
+ return tld.trim().replace(/^\./, "");
450
+ }
451
+ function priceString(value) {
452
+ return value == null ? undefined : value.toString();
453
+ }
454
+ function tldPriceFromRoute53Price(tld, price) {
455
+ return {
456
+ tld: price.Name || tld,
457
+ registration_price: priceString(price.RegistrationPrice?.Price),
458
+ renewal_price: priceString(price.RenewalPrice?.Price),
459
+ transfer_price: priceString(price.TransferPrice?.Price),
460
+ currency: price.RegistrationPrice?.Currency ?? price.RenewalPrice?.Currency ?? price.TransferPrice?.Currency
461
+ };
462
+ }
463
+ async function getTldPrice(tld, config) {
464
+ const { domains } = makeClients(config);
465
+ const normalized = normalizeTld(tld);
466
+ const prices = await domains.send(new ListPricesCommand({ Tld: normalized, MaxItems: 1 }));
467
+ const price = prices.Prices?.[0];
468
+ return price ? tldPriceFromRoute53Price(normalized, price) : null;
469
+ }
470
+ async function listTldPrices(config) {
471
+ const { domains } = makeClients(config);
472
+ const prices = [];
473
+ let marker;
474
+ do {
475
+ const result = await domains.send(new ListPricesCommand({ Marker: marker, MaxItems: 100 }));
476
+ for (const price of result.Prices ?? []) {
477
+ prices.push(tldPriceFromRoute53Price(price.Name || "", price));
478
+ }
479
+ marker = result.NextPageMarker;
480
+ } while (marker);
481
+ return prices;
482
+ }
483
+ async function registerDomain(domain, contact, durationYears = 1, autoRenew = true, config, options = {}) {
484
+ const { domains } = makeClients(config);
485
+ const contactDetail = contactToRoute53Contact(contact);
486
+ const result = await domains.send(new RegisterDomainCommand({
487
+ DomainName: domain,
488
+ DurationInYears: durationYears,
489
+ AutoRenew: autoRenew,
490
+ AdminContact: contactDetail,
491
+ RegistrantContact: contactDetail,
492
+ TechContact: contactDetail,
493
+ PrivacyProtectAdminContact: options.privacy_protected ?? true,
494
+ PrivacyProtectRegistrantContact: options.privacy_protected ?? true,
495
+ PrivacyProtectTechContact: options.privacy_protected ?? true,
496
+ ...options.nameservers?.length ? { Nameservers: options.nameservers.map((Name) => ({ Name })) } : {}
497
+ }));
498
+ return { operationId: result.OperationId ?? "" };
499
+ }
500
+ function contactToRoute53Contact(contact) {
501
+ return {
502
+ FirstName: contact.first_name,
503
+ LastName: contact.last_name,
504
+ Email: contact.email,
505
+ PhoneNumber: contact.phone,
506
+ AddressLine1: contact.address_line_1,
507
+ ...contact.address_line_2 ? { AddressLine2: contact.address_line_2 } : {},
508
+ City: contact.city,
509
+ ...contact.state ? { State: contact.state } : {},
510
+ CountryCode: contact.country_code.toUpperCase(),
511
+ ZipCode: contact.zip_code,
512
+ ContactType: contact.organization_name ? "COMPANY" : "PERSON",
513
+ ...contact.organization_name ? { OrganizationName: contact.organization_name } : {}
514
+ };
515
+ }
516
+ async function transferDomain(domain, authCode, contact, durationYears = 1, autoRenew = true, config, options = {}) {
517
+ const { domains } = makeClients(config);
518
+ const contactDetail = contactToRoute53Contact(contact);
519
+ const result = await domains.send(new TransferDomainCommand({
520
+ DomainName: domain,
521
+ AuthCode: authCode,
522
+ DurationInYears: durationYears,
523
+ AutoRenew: autoRenew,
524
+ AdminContact: contactDetail,
525
+ RegistrantContact: contactDetail,
526
+ TechContact: contactDetail,
527
+ PrivacyProtectAdminContact: options.privacy_protected ?? true,
528
+ PrivacyProtectRegistrantContact: options.privacy_protected ?? true,
529
+ PrivacyProtectTechContact: options.privacy_protected ?? true,
530
+ ...options.nameservers?.length ? { Nameservers: options.nameservers.map((Name) => ({ Name })) } : {}
531
+ }));
532
+ return { operationId: result.OperationId ?? "" };
533
+ }
534
+ async function getRegistrationStatus(operationId, config) {
535
+ const { domains } = makeClients(config);
536
+ const result = await domains.send(new GetOperationDetailCommand({ OperationId: operationId }));
537
+ return {
538
+ status: result.Status ?? "UNKNOWN",
539
+ domain: result.DomainName,
540
+ message: result.Message
541
+ };
542
+ }
543
+ async function getDomainDetail(domain, config) {
544
+ const { domains } = makeClients(config);
545
+ const [result, summary] = await Promise.all([
546
+ domains.send(new GetDomainDetailCommand({ DomainName: domain })),
547
+ getDomainSummary(domain, config)
548
+ ]);
549
+ const privacy = result;
550
+ return {
551
+ domain: result.DomainName ?? domain,
552
+ expiry: result.ExpirationDate?.toISOString() ?? summary?.expiry ?? "",
553
+ auto_renew: result.AutoRenew ?? summary?.auto_renew ?? null,
554
+ transfer_lock: summary?.transfer_lock ?? (result.StatusList?.includes("TRANSFER_LOCK") ? true : null),
555
+ created: result.CreationDate?.toISOString() ?? "",
556
+ updated: result.UpdatedDate?.toISOString() ?? "",
557
+ nameservers: (result.Nameservers ?? []).map((ns) => ns.Name ?? "").filter(Boolean),
558
+ status_list: result.StatusList ?? [],
559
+ registrar_name: result.RegistrarName,
560
+ privacy_protected: privacy.RegistrantPrivacy ?? privacy.AdminPrivacy ?? privacy.TechPrivacy ?? null
561
+ };
562
+ }
563
+ async function getDomainSummary(domain, config) {
564
+ let marker;
565
+ do {
566
+ const { domains } = makeClients(config);
567
+ const result = await domains.send(new ListDomainsCommand({ Marker: marker, MaxItems: 100 }));
568
+ const summary = (result.Domains ?? []).find((item) => item.DomainName === domain);
569
+ if (summary) {
570
+ return {
571
+ domain: summary.DomainName ?? domain,
572
+ expiry: summary.Expiry?.toISOString() ?? "",
573
+ auto_renew: summary.AutoRenew ?? false,
574
+ transfer_lock: summary.TransferLock ?? false
575
+ };
576
+ }
577
+ marker = result.NextPageMarker;
578
+ } while (marker);
579
+ return null;
580
+ }
581
+ async function requestTransferOutAuthCode(domain, config) {
582
+ const { domains } = makeClients(config);
583
+ const detail = await getDomainDetail(domain, config);
584
+ let transferLockDisabled = false;
585
+ let transferLockOperationId = null;
586
+ if (detail.transfer_lock) {
587
+ const result = await domains.send(new DisableDomainTransferLockCommand({ DomainName: domain }));
588
+ transferLockDisabled = true;
589
+ transferLockOperationId = result.OperationId ?? null;
590
+ }
591
+ const authCodeResult = await domains.send(new RetrieveDomainAuthCodeCommand({ DomainName: domain }));
592
+ if (!authCodeResult.AuthCode) {
593
+ throw new Error("Route53 did not return a domain transfer authorization code");
594
+ }
595
+ return {
596
+ auth_code: authCodeResult.AuthCode,
597
+ transfer_lock_disabled: transferLockDisabled,
598
+ transfer_lock_operation_id: transferLockOperationId
599
+ };
600
+ }
601
+ async function updateNameservers(domain, nameservers, config, client) {
602
+ if (!nameservers.length) {
603
+ throw new Error("updateNameservers requires at least one nameserver");
604
+ }
605
+ const domains = client ?? makeClients(config).domains;
606
+ const result = await domains.send(new UpdateDomainNameserversCommand({
607
+ DomainName: domain,
608
+ Nameservers: nameservers.map((name) => ({ Name: name }))
609
+ }));
610
+ return { operationId: result.OperationId ?? "" };
611
+ }
612
+ async function listRegisteredDomains(config) {
613
+ const { domains } = makeClients(config);
614
+ const all = [];
615
+ let nextPageMarker;
616
+ do {
617
+ const result = await domains.send(new ListDomainsCommand({ Marker: nextPageMarker }));
618
+ for (const d of result.Domains ?? []) {
619
+ all.push({
620
+ domain: d.DomainName ?? "",
621
+ expiry: d.Expiry?.toISOString() ?? "",
622
+ auto_renew: d.AutoRenew ?? false,
623
+ transfer_lock: d.TransferLock ?? false
624
+ });
625
+ }
626
+ nextPageMarker = result.NextPageMarker;
627
+ } while (nextPageMarker);
628
+ return all;
629
+ }
630
+ function cleanZoneId(id) {
631
+ return id.replace("/hostedzone/", "");
632
+ }
633
+ function normalizeZoneId(id) {
634
+ return cleanZoneId(id.trim());
635
+ }
636
+ function normalizeNameserver(value) {
637
+ return value.trim().toLowerCase().replace(/\.$/, "");
638
+ }
639
+ function nameserversMatch(a, b) {
640
+ if (a.length !== b.length)
641
+ return false;
642
+ const left = [...new Set(a.map(normalizeNameserver))].sort();
643
+ const right = [...new Set(b.map(normalizeNameserver))].sort();
644
+ return left.length === right.length && left.every((value, index) => value === right[index]);
645
+ }
646
+ function cleanChangeId(id) {
647
+ return id?.replace("/change/", "") || null;
648
+ }
649
+ async function createHostedZone(domain, comment, config, options) {
650
+ const { route53 } = makeClients(config);
651
+ const callerRef = options?.callerReference ?? `domains-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
652
+ const result = await route53.send(new CreateHostedZoneCommand({
653
+ Name: domain,
654
+ CallerReference: callerRef,
655
+ HostedZoneConfig: comment ? { Comment: comment } : undefined
656
+ }));
657
+ return {
658
+ id: cleanZoneId(result.HostedZone?.Id ?? ""),
659
+ name: result.HostedZone?.Name ?? domain,
660
+ record_count: result.HostedZone?.ResourceRecordSetCount ?? 0,
661
+ comment,
662
+ name_servers: result.DelegationSet?.NameServers ?? []
663
+ };
664
+ }
665
+ async function listHostedZones(config) {
666
+ const { route53 } = makeClients(config);
667
+ const zones = [];
668
+ let marker;
669
+ do {
670
+ const result = await route53.send(new ListHostedZonesCommand({ Marker: marker }));
671
+ for (const z of result.HostedZones ?? []) {
672
+ zones.push({
673
+ id: cleanZoneId(z.Id ?? ""),
674
+ name: z.Name ?? "",
675
+ record_count: z.ResourceRecordSetCount ?? 0,
676
+ comment: z.Config?.Comment,
677
+ private_zone: z.Config?.PrivateZone
678
+ });
679
+ }
680
+ marker = result.IsTruncated ? result.NextMarker : undefined;
681
+ } while (marker);
682
+ return zones;
683
+ }
684
+ async function getHostedZone(hostedZoneId, config) {
685
+ const { route53 } = makeClients(config);
686
+ const result = await route53.send(new GetHostedZoneCommand({ Id: normalizeZoneId(hostedZoneId) }));
687
+ return {
688
+ id: cleanZoneId(result.HostedZone?.Id ?? ""),
689
+ name: result.HostedZone?.Name ?? "",
690
+ record_count: result.HostedZone?.ResourceRecordSetCount ?? 0,
691
+ comment: result.HostedZone?.Config?.Comment,
692
+ name_servers: result.DelegationSet?.NameServers ?? [],
693
+ private_zone: result.HostedZone?.Config?.PrivateZone
694
+ };
695
+ }
696
+ async function deleteHostedZone(hostedZoneId, config) {
697
+ const { route53 } = makeClients(config);
698
+ const zoneId = normalizeZoneId(hostedZoneId);
699
+ const managedRecords = [];
700
+ let nextName;
701
+ let nextType;
702
+ let nextIdentifier;
703
+ do {
704
+ const result = await route53.send(new ListResourceRecordSetsCommand({
705
+ HostedZoneId: zoneId,
706
+ StartRecordName: nextName,
707
+ StartRecordType: nextType,
708
+ StartRecordIdentifier: nextIdentifier
709
+ }));
710
+ managedRecords.push(...result.ResourceRecordSets?.filter((record) => record.Type !== "NS" && record.Type !== "SOA") ?? []);
711
+ nextName = result.IsTruncated ? result.NextRecordName : undefined;
712
+ nextType = result.IsTruncated ? result.NextRecordType : undefined;
713
+ nextIdentifier = result.IsTruncated ? result.NextRecordIdentifier : undefined;
714
+ } while (nextName && nextType);
715
+ for (let index = 0;index < managedRecords.length; index += 100) {
716
+ const batch = managedRecords.slice(index, index + 100);
717
+ if (batch.length === 0)
718
+ continue;
719
+ await route53.send(new ChangeResourceRecordSetsCommand({
720
+ HostedZoneId: zoneId,
721
+ ChangeBatch: {
722
+ Changes: batch.map((record) => ({
723
+ Action: "DELETE",
724
+ ResourceRecordSet: record
725
+ }))
726
+ }
727
+ }));
728
+ }
729
+ await route53.send(new DeleteHostedZoneCommand({ Id: zoneId }));
730
+ }
731
+ async function findHostedZoneByDomain(domain, config) {
732
+ const zones = await listHostedZones(config);
733
+ const normalized = domain.endsWith(".") ? domain : `${domain}.`;
734
+ const matches = zones.filter((z) => z.name === normalized);
735
+ if (matches.length === 0)
736
+ return null;
737
+ const publicMatches = matches.filter((z) => !z.private_zone);
738
+ const candidates = publicMatches.length > 0 ? publicMatches : matches;
739
+ if (candidates.length > 1) {
740
+ throw new Error(`Multiple Route 53 hosted zones found for ${domain}; specify hosted zone id`);
741
+ }
742
+ return candidates[0] ?? null;
743
+ }
744
+ async function findHostedZoneByNameservers(domain, nameservers, config) {
745
+ if (!nameservers.length)
746
+ return null;
747
+ const { route53 } = makeClients(config);
748
+ const result = await route53.send(new ListHostedZonesByNameCommand({ DNSName: domain }));
749
+ const zones = (result.HostedZones ?? []).filter((zone) => zone.Name?.replace(/\.$/, "") === domain && !zone.Config?.PrivateZone);
750
+ for (const zone of zones) {
751
+ const id = cleanZoneId(zone.Id ?? "");
752
+ if (!id)
753
+ continue;
754
+ const detail = await getHostedZone(id, config);
755
+ const delegatedNameservers = detail.name_servers ?? [];
756
+ if (nameserversMatch(delegatedNameservers, nameservers)) {
757
+ return { ...detail, id, name_servers: delegatedNameservers };
758
+ }
759
+ }
760
+ return null;
761
+ }
762
+ function rrsToRecord(rrs) {
763
+ if (rrs.AliasTarget) {
764
+ return {
765
+ name: rrs.Name ?? "",
766
+ type: rrs.Type ?? "",
767
+ ttl: 0,
768
+ values: [],
769
+ alias_target: {
770
+ hosted_zone_id: rrs.AliasTarget.HostedZoneId ?? "",
771
+ dns_name: rrs.AliasTarget.DNSName ?? ""
772
+ }
773
+ };
774
+ }
775
+ return {
776
+ name: rrs.Name ?? "",
777
+ type: rrs.Type ?? "",
778
+ ttl: rrs.TTL ?? 0,
779
+ values: (rrs.ResourceRecords ?? []).map((r) => r.Value ?? "")
780
+ };
781
+ }
782
+ function recordToRrs(record) {
783
+ if (record.alias_target) {
784
+ return {
785
+ Name: record.name,
786
+ Type: record.type,
787
+ AliasTarget: {
788
+ HostedZoneId: record.alias_target.hosted_zone_id,
789
+ DNSName: record.alias_target.dns_name,
790
+ EvaluateTargetHealth: false
791
+ }
792
+ };
793
+ }
794
+ return {
795
+ Name: record.name,
796
+ Type: record.type,
797
+ TTL: record.ttl ?? 300,
798
+ ResourceRecords: record.values.map((v) => ({ Value: v }))
799
+ };
800
+ }
801
+ async function listRecords2(hostedZoneId, config) {
802
+ const { route53 } = makeClients(config);
803
+ const records = [];
804
+ let nextName;
805
+ let nextType;
806
+ do {
807
+ const result = await route53.send(new ListResourceRecordSetsCommand({
808
+ HostedZoneId: normalizeZoneId(hostedZoneId),
809
+ StartRecordName: nextName,
810
+ StartRecordType: nextType
811
+ }));
812
+ for (const rrs of result.ResourceRecordSets ?? []) {
813
+ records.push(rrsToRecord(rrs));
814
+ }
815
+ if (result.IsTruncated) {
816
+ nextName = result.NextRecordName;
817
+ nextType = result.NextRecordType;
818
+ } else {
819
+ nextName = undefined;
820
+ nextType = undefined;
821
+ }
822
+ } while (nextName);
823
+ return records;
824
+ }
825
+ async function upsertRecord2(hostedZoneId, record, config, options) {
826
+ const { route53 } = makeClients(config);
827
+ const result = await route53.send(new ChangeResourceRecordSetsCommand({
828
+ HostedZoneId: normalizeZoneId(hostedZoneId),
829
+ ChangeBatch: {
830
+ ...options?.comment ? { Comment: options.comment } : {},
831
+ Changes: [{ Action: "UPSERT", ResourceRecordSet: recordToRrs(record) }]
832
+ }
833
+ }));
834
+ return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
835
+ }
836
+ async function deleteRecord2(hostedZoneId, record, config, options) {
837
+ const { route53 } = makeClients(config);
838
+ const result = await route53.send(new ChangeResourceRecordSetsCommand({
839
+ HostedZoneId: normalizeZoneId(hostedZoneId),
840
+ ChangeBatch: {
841
+ ...options?.comment ? { Comment: options.comment } : {},
842
+ Changes: [{ Action: "DELETE", ResourceRecordSet: recordToRrs(record) }]
843
+ }
844
+ }));
845
+ return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
846
+ }
847
+ async function upsertRecords(hostedZoneId, records, config, options) {
848
+ if (records.length === 0)
849
+ return { changeId: null };
850
+ const { route53 } = makeClients(config);
851
+ const changes = records.map((r) => ({
852
+ Action: "UPSERT",
853
+ ResourceRecordSet: recordToRrs(r)
854
+ }));
855
+ const result = await route53.send(new ChangeResourceRecordSetsCommand({
856
+ HostedZoneId: normalizeZoneId(hostedZoneId),
857
+ ChangeBatch: {
858
+ ...options?.comment ? { Comment: options.comment } : {},
859
+ Changes: changes
860
+ }
861
+ }));
862
+ return { changeId: cleanChangeId(result.ChangeInfo?.Id) };
863
+ }
864
+ function createRoute53Provider(config) {
865
+ const cfg = config ?? getConfig2();
866
+ const registerWithRoute53 = registerDomain;
867
+ const updateRoute53Nameservers = updateNameservers;
868
+ async function listDomainInventory() {
869
+ const byDomain = new Map;
870
+ try {
871
+ const registered = await listRegisteredDomains(cfg);
872
+ for (const d of registered) {
873
+ byDomain.set(d.domain, {
874
+ domain: d.domain,
875
+ registrar: "AWS Route 53",
876
+ created: "",
877
+ expires: d.expiry,
878
+ nameservers: [],
879
+ status: "active",
880
+ auto_renew: d.auto_renew
881
+ });
882
+ }
883
+ } catch (error) {
884
+ const message = error instanceof Error ? error.message : String(error);
885
+ if (!message.includes("route53domains:ListDomains") && !message.includes("AccessDenied")) {
886
+ throw error;
887
+ }
888
+ }
889
+ const zones = await listHostedZones(cfg);
890
+ for (const z of zones) {
891
+ const zone = z.name_servers?.length ? z : await getHostedZone(z.id, cfg).catch(() => z);
892
+ const domain = zone.name.replace(/\.$/, "");
893
+ const nameservers = zone.name_servers ?? [];
894
+ const existing = byDomain.get(domain);
895
+ if (existing) {
896
+ existing.nameservers = nameservers.length > 0 ? nameservers : existing.nameservers;
897
+ continue;
898
+ }
899
+ byDomain.set(domain, {
900
+ domain,
901
+ registrar: "AWS Route 53 DNS",
902
+ created: "",
903
+ expires: "",
904
+ nameservers,
905
+ status: "active",
906
+ auto_renew: false
907
+ });
908
+ }
909
+ return Array.from(byDomain.values());
910
+ }
911
+ return {
912
+ name: "route53",
913
+ async listDomains() {
914
+ return listDomainInventory();
915
+ },
916
+ async getDomainInfo(domain) {
917
+ const detail = await getDomainDetail(domain, cfg);
918
+ return {
919
+ domain: detail.domain,
920
+ registrar: "AWS Route 53",
921
+ created: detail.created,
922
+ expires: detail.expiry,
923
+ nameservers: detail.nameservers,
924
+ status: "active",
925
+ auto_renew: detail.auto_renew ?? false
926
+ };
927
+ },
928
+ async registerDomain(domain, contact, options = {}) {
929
+ const result = await registerWithRoute53(domain, contact, options.years ?? 1, options.autoRenew ?? true, cfg);
930
+ return { domain, success: !!result.operationId, operationId: result.operationId };
931
+ },
932
+ async updateNameservers(domain, nameservers) {
933
+ const result = await updateRoute53Nameservers(domain, nameservers, cfg);
934
+ return { domain, success: !!result.operationId, operationId: result.operationId };
935
+ },
936
+ async renewDomain(_domain) {
937
+ return { domain: _domain, success: false, orderId: undefined, chargedAmount: undefined };
938
+ },
939
+ async getDnsRecords(domain) {
940
+ const zone = await findHostedZoneByDomain(domain, cfg);
941
+ if (!zone)
942
+ return [];
943
+ const records = await listRecords2(zone.id, cfg);
944
+ const result = [];
945
+ for (const r of records) {
946
+ if (r.alias_target) {
947
+ result.push({ type: r.type, name: r.name, value: r.alias_target.dns_name, ttl: 0 });
948
+ } else {
949
+ for (const v of r.values) {
950
+ result.push({ type: r.type, name: r.name, value: v, ttl: r.ttl });
951
+ }
952
+ }
953
+ }
954
+ return result;
955
+ },
956
+ async setDnsRecords(domain, records) {
957
+ const zone = await findHostedZoneByDomain(domain, cfg);
958
+ if (!zone)
959
+ throw new Error(`No hosted zone found for ${domain}`);
960
+ const grouped = new Map;
961
+ for (const r of records) {
962
+ const key = `${r.name}|${r.type}`;
963
+ const existing = grouped.get(key);
964
+ if (existing) {
965
+ existing.values.push(r.value);
966
+ } else {
967
+ grouped.set(key, { name: r.name, type: r.type, ttl: r.ttl, values: [r.value] });
968
+ }
969
+ }
970
+ await upsertRecords(zone.id, Array.from(grouped.values()), cfg);
971
+ return true;
972
+ },
973
+ async checkAvailability(domain) {
974
+ const result = await checkAvailability(domain, cfg);
975
+ return {
976
+ domain: result.domain,
977
+ available: result.available,
978
+ standard_price: result.price ? Number(result.price) : undefined,
979
+ currency: result.currency
980
+ };
981
+ },
982
+ async syncToLocalDb(dbFns) {
983
+ const domains = await listDomainInventory();
984
+ let synced = 0;
985
+ let created = 0;
986
+ let updated = 0;
987
+ const errors = [];
988
+ for (const d of domains) {
989
+ try {
990
+ const existing = dbFns.getDomainByName(d.domain);
991
+ if (existing) {
992
+ const existingRoute53 = existing.metadata["route53"];
993
+ const staleDnsOnlyRegistrar = d.registrar !== "AWS Route 53" && (existing.registrar === "AWS Route 53 DNS" || existing.registrar === "AWS Route 53" && existingRoute53?.source === "route53:hosted_zones");
994
+ dbFns.updateDomain(existing.id, {
995
+ ...d.registrar === "AWS Route 53" ? { registrar: "AWS Route 53" } : {},
996
+ ...staleDnsOnlyRegistrar ? { registrar: null } : {},
997
+ expires_at: d.expires || undefined,
998
+ auto_renew: d.auto_renew,
999
+ nameservers: d.nameservers.length > 0 ? d.nameservers : existing.nameservers,
1000
+ metadata: {
1001
+ ...existing.metadata,
1002
+ route53: {
1003
+ source: d.registrar === "AWS Route 53" ? "route53domains+hosted_zones" : "route53:hosted_zones",
1004
+ synced_at: new Date().toISOString()
1005
+ }
1006
+ },
1007
+ status: "active"
1008
+ });
1009
+ updated++;
1010
+ } else {
1011
+ dbFns.createDomain({
1012
+ name: d.domain,
1013
+ ...d.registrar === "AWS Route 53" ? { registrar: "AWS Route 53" } : {},
1014
+ expires_at: d.expires || undefined,
1015
+ auto_renew: d.auto_renew,
1016
+ nameservers: d.nameservers,
1017
+ status: "active",
1018
+ notes: d.registrar === "AWS Route 53 DNS" ? "Discovered from Route 53 hosted zones; registrar ownership was not inferred." : undefined,
1019
+ metadata: {
1020
+ route53: {
1021
+ source: d.registrar === "AWS Route 53" ? "route53domains+hosted_zones" : "route53:hosted_zones",
1022
+ synced_at: new Date().toISOString()
1023
+ }
1024
+ }
1025
+ });
1026
+ created++;
1027
+ }
1028
+ synced++;
1029
+ } catch (err) {
1030
+ errors.push(`${d.domain}: ${err instanceof Error ? err.message : String(err)}`);
1031
+ }
1032
+ }
1033
+ return { synced, created, updated, errors };
1034
+ }
1035
+ };
1036
+ }
1037
+
1038
+ // src/lib/namecheap.ts
1039
+ function getConfig3() {
1040
+ const apiKey = process.env["NAMECHEAP_API_KEY"];
1041
+ const username = process.env["NAMECHEAP_USERNAME"];
1042
+ const clientIp = process.env["NAMECHEAP_CLIENT_IP"];
1043
+ if (!apiKey)
1044
+ throw new Error("NAMECHEAP_API_KEY environment variable is not set");
1045
+ if (!username)
1046
+ throw new Error("NAMECHEAP_USERNAME environment variable is not set");
1047
+ if (!clientIp)
1048
+ throw new Error("NAMECHEAP_CLIENT_IP environment variable is not set");
1049
+ return {
1050
+ apiKey,
1051
+ username,
1052
+ clientIp,
1053
+ sandbox: process.env["NAMECHEAP_SANDBOX"] === "true"
1054
+ };
1055
+ }
1056
+ async function apiRequest(config, command, params = {}) {
1057
+ const base = config.sandbox ? "https://api.sandbox.namecheap.com/xml.response" : "https://api.namecheap.com/xml.response";
1058
+ const url = new URL(base);
1059
+ url.searchParams.set("ApiUser", config.username);
1060
+ url.searchParams.set("ApiKey", config.apiKey);
1061
+ url.searchParams.set("UserName", config.username);
1062
+ url.searchParams.set("ClientIp", config.clientIp);
1063
+ url.searchParams.set("Command", command);
1064
+ for (const [k, v] of Object.entries(params)) {
1065
+ url.searchParams.set(k, v);
1066
+ }
1067
+ const response = await fetch(url.toString(), {
1068
+ signal: AbortSignal.timeout(30000)
1069
+ });
1070
+ if (!response.ok) {
1071
+ throw new Error(`Namecheap API request failed with status ${response.status}`);
1072
+ }
1073
+ return response.text();
1074
+ }
1075
+ function parseXmlValue(xml, tag) {
1076
+ const match = xml.match(new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, "i"));
1077
+ return match ? match[1].trim() : null;
1078
+ }
1079
+ function parseXmlAttributes(xml, tag) {
1080
+ const results = [];
1081
+ const tagRegex = new RegExp(`<${tag}([^>]*)>`, "gi");
1082
+ let match;
1083
+ while ((match = tagRegex.exec(xml)) !== null) {
1084
+ const attrs = {};
1085
+ const attrStr = match[1];
1086
+ const attrRegex = /(\w+)="([^"]*)"/g;
1087
+ let attrMatch;
1088
+ while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
1089
+ attrs[attrMatch[1]] = attrMatch[2];
1090
+ }
1091
+ results.push(attrs);
1092
+ }
1093
+ return results;
1094
+ }
1095
+ async function listNamecheapDomains(config) {
1096
+ const cfg = config || getConfig3();
1097
+ const xml = await apiRequest(cfg, "namecheap.domains.getList", { PageSize: "100" });
1098
+ const domainAttrs = parseXmlAttributes(xml, "Domain");
1099
+ return domainAttrs.map((attrs) => ({
1100
+ domain: attrs["Name"] || "",
1101
+ expiry: attrs["Expires"] || "",
1102
+ autoRenew: attrs["AutoRenew"] === "true",
1103
+ isExpired: attrs["IsExpired"] === "true",
1104
+ isLocked: attrs["IsLocked"] === "true"
1105
+ }));
1106
+ }
1107
+ async function getDomainInfo(domain, config) {
1108
+ const cfg = config || getConfig3();
1109
+ const xml = await apiRequest(cfg, "namecheap.domains.getInfo", { DomainName: domain });
1110
+ const created = parseXmlValue(xml, "CreatedDate") || "";
1111
+ const expires = parseXmlValue(xml, "ExpiredDate") || "";
1112
+ const nsAttrs = parseXmlAttributes(xml, "Nameserver");
1113
+ const nameservers = nsAttrs.map((a) => a["NAME"] || "").filter(Boolean);
1114
+ return {
1115
+ domain,
1116
+ registrar: "Namecheap",
1117
+ created,
1118
+ expires,
1119
+ nameservers
1120
+ };
1121
+ }
1122
+ async function renewDomain(domain, years = 1, config) {
1123
+ const cfg = config || getConfig3();
1124
+ const xml = await apiRequest(cfg, "namecheap.domains.renew", {
1125
+ DomainName: domain,
1126
+ Years: String(years)
1127
+ });
1128
+ const orderId = parseXmlValue(xml, "OrderId");
1129
+ const chargedAmount = parseXmlValue(xml, "ChargedAmount");
1130
+ return {
1131
+ domain,
1132
+ success: xml.includes('Status="OK"'),
1133
+ orderId: orderId || undefined,
1134
+ chargedAmount: chargedAmount || undefined
1135
+ };
1136
+ }
1137
+ function namecheapContactParams(contact) {
1138
+ const organization = contact.organization_name || "NA";
1139
+ const base = {
1140
+ FirstName: contact.first_name,
1141
+ LastName: contact.last_name,
1142
+ OrganizationName: organization,
1143
+ Address1: contact.address_line_1,
1144
+ City: contact.city,
1145
+ StateProvince: contact.state,
1146
+ PostalCode: contact.zip_code,
1147
+ Country: contact.country_code,
1148
+ Phone: contact.phone,
1149
+ EmailAddress: contact.email
1150
+ };
1151
+ const params = {};
1152
+ for (const prefix of ["Registrant", "Tech", "Admin", "AuxBilling"]) {
1153
+ for (const [key, value] of Object.entries(base)) {
1154
+ params[prefix + key] = value;
1155
+ }
1156
+ }
1157
+ return params;
1158
+ }
1159
+ async function registerDomain2(domain, contact, options = {}, config) {
1160
+ const cfg = config || getConfig3();
1161
+ const params = {
1162
+ DomainName: domain,
1163
+ Years: String(options.years ?? 1),
1164
+ AddFreeWhoisguard: options.whoisGuard === false ? "no" : "yes",
1165
+ WGEnabled: options.whoisGuard === false ? "no" : "yes",
1166
+ ...namecheapContactParams(contact)
1167
+ };
1168
+ if (options.premiumPrice !== undefined) {
1169
+ params.IsPremiumDomain = "true";
1170
+ params.PremiumPrice = String(options.premiumPrice);
1171
+ }
1172
+ const xml = await apiRequest(cfg, "namecheap.domains.create", params);
1173
+ return {
1174
+ domain,
1175
+ success: xml.includes('Status="OK"') || xml.includes('Registered="true"'),
1176
+ orderId: parseXmlValue(xml, "OrderId") || undefined,
1177
+ chargedAmount: parseXmlValue(xml, "ChargedAmount") || undefined
1178
+ };
1179
+ }
1180
+ async function updateNameservers2(domain, nameservers, config) {
1181
+ if (nameservers.length === 0)
1182
+ throw new Error("updateNameservers requires at least one nameserver");
1183
+ const cfg = config || getConfig3();
1184
+ const { sld, tld } = splitDomain(domain);
1185
+ const xml = await apiRequest(cfg, "namecheap.domains.dns.setCustom", {
1186
+ SLD: sld,
1187
+ TLD: tld,
1188
+ Nameservers: nameservers.join(",")
1189
+ });
1190
+ return xml.includes('Status="OK"') || xml.includes('Updated="true"');
1191
+ }
1192
+ async function getDnsRecords(_domain, sld, tld, config) {
1193
+ const cfg = config || getConfig3();
1194
+ const xml = await apiRequest(cfg, "namecheap.domains.dns.getHosts", {
1195
+ SLD: sld,
1196
+ TLD: tld
1197
+ });
1198
+ const hostAttrs = parseXmlAttributes(xml, "host");
1199
+ return hostAttrs.map((attrs) => ({
1200
+ type: attrs["Type"] || "A",
1201
+ name: attrs["Name"] || "@",
1202
+ address: attrs["Address"] || "",
1203
+ ttl: parseInt(attrs["TTL"] || "3600"),
1204
+ mxPref: attrs["MXPref"] ? parseInt(attrs["MXPref"]) : undefined
1205
+ }));
1206
+ }
1207
+ async function setDnsRecords(_domain, sld, tld, records, config) {
1208
+ const cfg = config || getConfig3();
1209
+ const params = {
1210
+ SLD: sld,
1211
+ TLD: tld
1212
+ };
1213
+ records.forEach((r, i) => {
1214
+ params[`HostName${i + 1}`] = r.name;
1215
+ params[`RecordType${i + 1}`] = r.type;
1216
+ params[`Address${i + 1}`] = r.address;
1217
+ params[`TTL${i + 1}`] = String(r.ttl);
1218
+ if (r.mxPref !== undefined) {
1219
+ params[`MXPref${i + 1}`] = String(r.mxPref);
1220
+ }
1221
+ });
1222
+ const xml = await apiRequest(cfg, "namecheap.domains.dns.setHosts", params);
1223
+ return xml.includes('IsSuccess="true"');
1224
+ }
1225
+ async function checkAvailability2(domain, config) {
1226
+ const cfg = config || getConfig3();
1227
+ const xml = await apiRequest(cfg, "namecheap.domains.check", { DomainList: domain });
1228
+ const domainAttrs = parseXmlAttributes(xml, "DomainCheckResult");
1229
+ const result = domainAttrs[0] || {};
1230
+ return {
1231
+ domain,
1232
+ available: result["Available"] === "true",
1233
+ premium: result["IsPremiumName"] === "true",
1234
+ price: result["PremiumRegistrationPrice"] ? parseFloat(result["PremiumRegistrationPrice"]) : undefined
1235
+ };
1236
+ }
1237
+ function splitDomain(domain) {
1238
+ const parts = domain.split(".");
1239
+ if (parts.length < 2) {
1240
+ throw new Error(`Invalid domain: ${domain}`);
1241
+ }
1242
+ if (parts.length >= 3 && ["co", "com", "org", "net", "ac", "gov"].includes(parts[parts.length - 2])) {
1243
+ return {
1244
+ sld: parts.slice(0, -2).join("."),
1245
+ tld: parts.slice(-2).join(".")
1246
+ };
1247
+ }
1248
+ return {
1249
+ sld: parts.slice(0, -1).join("."),
1250
+ tld: parts[parts.length - 1]
1251
+ };
1252
+ }
1253
+ async function syncToLocalDb(dbFunctions, config) {
1254
+ const cfg = config || getConfig3();
1255
+ const result = { synced: 0, errors: [], domains: [] };
1256
+ let ncDomains;
1257
+ try {
1258
+ ncDomains = await listNamecheapDomains(cfg);
1259
+ } catch (error) {
1260
+ throw new Error(`Failed to list Namecheap domains: ${error instanceof Error ? error.message : String(error)}`);
1261
+ }
1262
+ for (const ncDomain of ncDomains) {
1263
+ try {
1264
+ let info;
1265
+ try {
1266
+ info = await getDomainInfo(ncDomain.domain, cfg);
1267
+ } catch {
1268
+ info = {
1269
+ domain: ncDomain.domain,
1270
+ registrar: "Namecheap",
1271
+ created: "",
1272
+ expires: ncDomain.expiry,
1273
+ nameservers: []
1274
+ };
1275
+ }
1276
+ const expiresAt = normalizeDate(info.expires || ncDomain.expiry);
1277
+ const createdAt = normalizeDate(info.created);
1278
+ const existing = dbFunctions.getDomainByName(ncDomain.domain);
1279
+ if (existing) {
1280
+ dbFunctions.updateDomain(existing.id, {
1281
+ registrar: "Namecheap",
1282
+ status: "active",
1283
+ registered_at: createdAt || undefined,
1284
+ expires_at: expiresAt || undefined,
1285
+ auto_renew: ncDomain.autoRenew,
1286
+ nameservers: info.nameservers.length > 0 ? info.nameservers : undefined
1287
+ });
1288
+ } else {
1289
+ dbFunctions.createDomain({
1290
+ name: ncDomain.domain,
1291
+ registrar: "Namecheap",
1292
+ status: "active",
1293
+ registered_at: createdAt || undefined,
1294
+ expires_at: expiresAt || undefined,
1295
+ auto_renew: ncDomain.autoRenew,
1296
+ nameservers: info.nameservers
1297
+ });
1298
+ }
1299
+ result.synced++;
1300
+ result.domains.push(ncDomain.domain);
1301
+ } catch (error) {
1302
+ result.errors.push(`${ncDomain.domain}: ${error instanceof Error ? error.message : String(error)}`);
1303
+ }
1304
+ }
1305
+ return result;
1306
+ }
1307
+ function normalizeDate(dateStr) {
1308
+ if (!dateStr)
1309
+ return null;
1310
+ try {
1311
+ const d = new Date(dateStr);
1312
+ if (isNaN(d.getTime()))
1313
+ return null;
1314
+ return d.toISOString();
1315
+ } catch {
1316
+ return null;
1317
+ }
1318
+ }
1319
+
1320
+ // src/lib/godaddy.ts
1321
+ class GoDaddyApiError extends Error {
1322
+ statusCode;
1323
+ details;
1324
+ constructor(message, statusCode, details) {
1325
+ super(message);
1326
+ this.statusCode = statusCode;
1327
+ this.details = details;
1328
+ this.name = "GoDaddyApiError";
1329
+ }
1330
+ }
1331
+ var _overriddenFetch = null;
1332
+ function getCredentials() {
1333
+ const apiKey = process.env["GODADDY_API_KEY"];
1334
+ const apiSecret = process.env["GODADDY_API_SECRET"];
1335
+ if (!apiKey || !apiSecret) {
1336
+ throw new Error("GoDaddy API credentials not configured. Set GODADDY_API_KEY and GODADDY_API_SECRET environment variables.");
1337
+ }
1338
+ return { apiKey, apiSecret };
1339
+ }
1340
+ function getHeaders() {
1341
+ const { apiKey, apiSecret } = getCredentials();
1342
+ return {
1343
+ Authorization: `sso-key ${apiKey}:${apiSecret}`,
1344
+ "Content-Type": "application/json",
1345
+ Accept: "application/json"
1346
+ };
1347
+ }
1348
+ var GODADDY_API_BASE = "https://api.godaddy.com";
1349
+ async function apiRequest2(method, path, body) {
1350
+ const fetchFn = _overriddenFetch || globalThis.fetch;
1351
+ const url = `${GODADDY_API_BASE}${path}`;
1352
+ const headers = getHeaders();
1353
+ const options = { method, headers };
1354
+ if (body !== undefined) {
1355
+ options.body = JSON.stringify(body);
1356
+ }
1357
+ const response = await fetchFn(url, options);
1358
+ if (!response.ok) {
1359
+ const text = await response.text();
1360
+ throw new GoDaddyApiError(`GoDaddy API ${method} ${path} failed with status ${response.status}: ${text}`, response.status, { responseBody: text });
1361
+ }
1362
+ if (response.status === 204) {
1363
+ return;
1364
+ }
1365
+ return await response.json();
1366
+ }
1367
+ async function listGoDaddyDomains() {
1368
+ return apiRequest2("GET", "/v1/domains");
1369
+ }
1370
+ async function getDomainInfo2(domain) {
1371
+ return apiRequest2("GET", `/v1/domains/${encodeURIComponent(domain)}`);
1372
+ }
1373
+ async function renewDomain2(domain) {
1374
+ return apiRequest2("POST", `/v1/domains/${encodeURIComponent(domain)}/renew`, { period: 1 });
1375
+ }
1376
+ async function getDnsRecords2(domain, type) {
1377
+ const path = type ? `/v1/domains/${encodeURIComponent(domain)}/records/${encodeURIComponent(type)}` : `/v1/domains/${encodeURIComponent(domain)}/records`;
1378
+ return apiRequest2("GET", path);
1379
+ }
1380
+ async function setDnsRecords2(domain, records) {
1381
+ await apiRequest2("PUT", `/v1/domains/${encodeURIComponent(domain)}/records`, records);
1382
+ }
1383
+ async function checkAvailability3(domain) {
1384
+ return apiRequest2("GET", `/v1/domains/available?domain=${encodeURIComponent(domain)}`);
1385
+ }
1386
+ function mapGoDaddyStatus(gdStatus) {
1387
+ const s = gdStatus.toUpperCase();
1388
+ if (s === "ACTIVE")
1389
+ return "active";
1390
+ if (s === "EXPIRED")
1391
+ return "expired";
1392
+ if (s === "TRANSFERRED_OUT" || s === "TRANSFERRING" || s === "PENDING_TRANSFER")
1393
+ return "transferring";
1394
+ if (s === "REDEMPTION" || s === "PENDING_REDEMPTION")
1395
+ return "redemption";
1396
+ return "active";
1397
+ }
1398
+ async function syncToLocalDb2(dbFns) {
1399
+ const result = {
1400
+ synced: 0,
1401
+ created: 0,
1402
+ updated: 0,
1403
+ errors: []
1404
+ };
1405
+ let gdDomains;
1406
+ try {
1407
+ gdDomains = await listGoDaddyDomains();
1408
+ } catch (err) {
1409
+ result.errors.push(`Failed to list domains: ${err instanceof Error ? err.message : String(err)}`);
1410
+ return result;
1411
+ }
1412
+ for (const gd of gdDomains) {
1413
+ try {
1414
+ let detail;
1415
+ try {
1416
+ detail = await getDomainInfo2(gd.domain);
1417
+ } catch {
1418
+ detail = gd;
1419
+ }
1420
+ const existing = dbFns.getDomainByName(gd.domain);
1421
+ const domainData = {
1422
+ name: gd.domain,
1423
+ registrar: "GoDaddy",
1424
+ status: mapGoDaddyStatus(gd.status),
1425
+ expires_at: gd.expires ? new Date(gd.expires).toISOString() : undefined,
1426
+ auto_renew: gd.renewAuto,
1427
+ nameservers: gd.nameServers || [],
1428
+ registered_at: detail.createdAt ? new Date(detail.createdAt).toISOString() : undefined,
1429
+ metadata: {
1430
+ godaddy_domain_id: detail.domainId,
1431
+ provider: "godaddy",
1432
+ locked: detail.locked,
1433
+ privacy: detail.privacy
1434
+ }
1435
+ };
1436
+ if (existing) {
1437
+ dbFns.updateDomain(existing.id, domainData);
1438
+ result.updated++;
1439
+ } else {
1440
+ dbFns.createDomain(domainData);
1441
+ result.created++;
1442
+ }
1443
+ result.synced++;
1444
+ } catch (err) {
1445
+ result.errors.push(`Failed to sync ${gd.domain}: ${err instanceof Error ? err.message : String(err)}`);
1446
+ }
1447
+ }
1448
+ return result;
1449
+ }
1450
+
1451
+ // src/lib/version.ts
1452
+ import { readFileSync } from "node:fs";
1453
+ import { dirname, resolve } from "node:path";
1454
+ import { fileURLToPath } from "node:url";
1455
+ var cachedVersion = null;
1456
+ function getPackageVersion() {
1457
+ if (cachedVersion)
1458
+ return cachedVersion;
1459
+ try {
1460
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
1461
+ const packageJsonPath = resolve(moduleDir, "../../package.json");
1462
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1463
+ cachedVersion = pkg.version ?? "0.0.0";
1464
+ } catch {
1465
+ cachedVersion = "0.0.0";
1466
+ }
1467
+ return cachedVersion;
1468
+ }
1469
+ var USER_AGENT = `open-domains/${getPackageVersion()}`;
1470
+
1471
+ // src/lib/brandsight.ts
1472
+ function resolveBrandsightConfig(env = process.env) {
1473
+ const apiKey = firstEnv(env, BRANDSIGHT_ENV.apiKey)?.value ?? "";
1474
+ const apiSecret = firstEnv(env, BRANDSIGHT_ENV.apiSecret)?.value;
1475
+ const customerId = firstEnv(env, BRANDSIGHT_ENV.customerId)?.value;
1476
+ const shopperId = firstEnv(env, BRANDSIGHT_ENV.shopperId)?.value;
1477
+ const accountId = firstEnv(env, BRANDSIGHT_ENV.accountId)?.value;
1478
+ return {
1479
+ apiKey,
1480
+ apiSecret,
1481
+ customerId,
1482
+ shopperId,
1483
+ accountId,
1484
+ baseUrl: env["BRANDSIGHT_BASE_URL"]
1485
+ };
1486
+ }
1487
+ class BrandsightApiError extends Error {
1488
+ statusCode;
1489
+ responseBody;
1490
+ constructor(message, statusCode, responseBody) {
1491
+ super(message);
1492
+ this.statusCode = statusCode;
1493
+ this.responseBody = responseBody;
1494
+ this.name = "BrandsightApiError";
1495
+ }
1496
+ }
1497
+ var _fetchFn = null;
1498
+ function getConfig4() {
1499
+ return resolveBrandsightConfig();
1500
+ }
1501
+ var BRANDSIGHT_DOMAIN_BASE = "https://api.godaddy.com/v2";
1502
+ function requireDomainConfig(config) {
1503
+ const cfg = config ?? getConfig4();
1504
+ if (!cfg.apiKey || !cfg.apiSecret || !cfg.customerId) {
1505
+ throw new BrandsightApiError("Brandsight Domain API credentials are not configured. Set BRANDSIGHT_API_KEY, BRANDSIGHT_API_SECRET, and BRANDSIGHT_CUSTOMER_ID.");
1506
+ }
1507
+ return cfg;
1508
+ }
1509
+ function domainBaseUrl(cfg) {
1510
+ return cfg.baseUrl ?? BRANDSIGHT_DOMAIN_BASE;
1511
+ }
1512
+ function domainHeaders(cfg) {
1513
+ return {
1514
+ Authorization: `sso-key ${cfg.apiKey}:${cfg.apiSecret}`,
1515
+ "Content-Type": "application/json",
1516
+ Accept: "application/json",
1517
+ "User-Agent": USER_AGENT
1518
+ };
1519
+ }
1520
+ async function domainApiRequest(method, path, config, body) {
1521
+ const cfg = requireDomainConfig(config);
1522
+ const fetchFn = _fetchFn || globalThis.fetch;
1523
+ const response = await fetchFn(`${domainBaseUrl(cfg)}${path}`, {
1524
+ method,
1525
+ headers: domainHeaders(cfg),
1526
+ body: body ? JSON.stringify(body) : undefined,
1527
+ signal: AbortSignal.timeout(30000)
1528
+ });
1529
+ const text = await response.text();
1530
+ if (!response.ok) {
1531
+ throw new BrandsightApiError(`Brandsight Domain API ${method} ${path} failed with status ${response.status}`, response.status, text);
1532
+ }
1533
+ if (!text.trim())
1534
+ return {};
1535
+ return JSON.parse(text);
1536
+ }
1537
+ function normalizeBrandsightDomain(raw) {
1538
+ const nameServers = raw.nameServers ?? raw.nameservers ?? [];
1539
+ const expiresAt = raw.expiresAt ?? raw.expires ?? "";
1540
+ const createdAt = raw.createdAt ?? raw.created ?? "";
1541
+ const renewAuto = raw.renewAuto ?? raw.auto_renew ?? false;
1542
+ return {
1543
+ ...raw,
1544
+ domain: String(raw.domain ?? ""),
1545
+ status: String(raw.status ?? "UNKNOWN"),
1546
+ created: createdAt,
1547
+ expires: expiresAt,
1548
+ auto_renew: renewAuto,
1549
+ locked: Boolean(raw.locked),
1550
+ nameservers: nameServers,
1551
+ createdAt,
1552
+ expiresAt,
1553
+ nameServers,
1554
+ renewAuto
1555
+ };
1556
+ }
1557
+ function customerPath(cfg, path) {
1558
+ return `/customers/${encodeURIComponent(cfg.customerId)}${path}`;
1559
+ }
1560
+ function domainTld(domain) {
1561
+ const parts = domain.split(".").filter(Boolean);
1562
+ if (parts.length < 2)
1563
+ throw new BrandsightApiError(`Invalid domain name: ${domain}`);
1564
+ return parts.slice(1).join(".");
1565
+ }
1566
+ function brandsightContact(contact) {
1567
+ return {
1568
+ addressMailing: {
1569
+ address1: contact.address_line_1,
1570
+ city: contact.city,
1571
+ country: contact.country_code,
1572
+ postalCode: contact.zip_code,
1573
+ state: contact.state
1574
+ },
1575
+ email: contact.email,
1576
+ encoding: "ASCII",
1577
+ nameFirst: contact.first_name,
1578
+ nameLast: contact.last_name,
1579
+ organization: contact.organization_name,
1580
+ phone: contact.phone
1581
+ };
1582
+ }
1583
+ async function domainAvailability(domain, cfg, type, period = 1) {
1584
+ const params = new URLSearchParams({
1585
+ domain,
1586
+ period: String(period),
1587
+ type,
1588
+ optimizeFor: "ACCURACY"
1589
+ });
1590
+ const result = await domainApiRequest("GET", `/domains/available?${params.toString()}`, cfg);
1591
+ return {
1592
+ domain: result.domain ?? domain,
1593
+ available: Boolean(result.available),
1594
+ price: result.price,
1595
+ currency: result.currency,
1596
+ registryPremiumPricing: result.registryPremiumPricing
1597
+ };
1598
+ }
1599
+ async function listDomains(config) {
1600
+ const cfg = requireDomainConfig(config);
1601
+ const domains = [];
1602
+ const seenMarkers = new Set;
1603
+ let marker;
1604
+ while (true) {
1605
+ const params = new URLSearchParams({ limit: "500" });
1606
+ if (marker)
1607
+ params.set("marker", marker);
1608
+ const batch = await domainApiRequest("GET", customerPath(cfg, `/domains?${params.toString()}`), cfg);
1609
+ if (!Array.isArray(batch) || batch.length === 0)
1610
+ break;
1611
+ domains.push(...batch.map(normalizeBrandsightDomain).filter((d) => d.domain));
1612
+ if (batch.length < 500)
1613
+ break;
1614
+ const nextMarker = String(batch[batch.length - 1]?.domain ?? "");
1615
+ if (!nextMarker || seenMarkers.has(nextMarker))
1616
+ break;
1617
+ seenMarkers.add(nextMarker);
1618
+ marker = nextMarker;
1619
+ }
1620
+ return domains;
1621
+ }
1622
+ async function getDomainInfo3(domain, config) {
1623
+ const cfg = requireDomainConfig(config);
1624
+ const result = await domainApiRequest("GET", customerPath(cfg, `/domains/${encodeURIComponent(domain)}`), cfg);
1625
+ return normalizeBrandsightDomain(result);
1626
+ }
1627
+ async function checkAvailability4(domain, config) {
1628
+ const cfg = requireDomainConfig(config);
1629
+ return domainAvailability(domain, cfg, "REGISTRATION", 1);
1630
+ }
1631
+ async function renewDomain3(domain, years = 1, config) {
1632
+ const cfg = requireDomainConfig(config);
1633
+ const current = await getDomainInfo3(domain, cfg);
1634
+ if (!current?.expires)
1635
+ throw new BrandsightApiError(`Cannot renew ${domain}: current expiry is unavailable`);
1636
+ const quote = await domainAvailability(domain, cfg, "RENEWAL", years);
1637
+ const price = quote.price ?? current.renewal?.price;
1638
+ const currency = quote.currency ?? current.renewal?.currency;
1639
+ if (price == null || !currency) {
1640
+ throw new BrandsightApiError(`Cannot renew ${domain}: renewal quote did not include an exact price and currency`);
1641
+ }
1642
+ const result = await domainApiRequest("POST", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/renew`), cfg, {
1643
+ consent: {
1644
+ agreedAt: new Date().toISOString(),
1645
+ agreedBy: cfg.shopperId ?? "domains-cli",
1646
+ currency,
1647
+ price,
1648
+ registryPremiumPricing: quote.registryPremiumPricing ?? false
1649
+ },
1650
+ expires: current.expires,
1651
+ period: years
1652
+ });
1653
+ return { success: true, orderId: result.orderId ?? result.id };
1654
+ }
1655
+ async function getLegalAgreements(tld, privacy = false, config) {
1656
+ const cfg = requireDomainConfig(config);
1657
+ const params = new URLSearchParams({
1658
+ privacy: String(privacy),
1659
+ tlds: tld
1660
+ });
1661
+ return domainApiRequest("GET", customerPath(cfg, `/domains/agreements?${params.toString()}`), cfg);
1662
+ }
1663
+ async function getRegistrationSchema(tld, config) {
1664
+ const cfg = requireDomainConfig(config);
1665
+ return domainApiRequest("GET", customerPath(cfg, `/domains/register/schema/${encodeURIComponent(tld)}`), cfg);
1666
+ }
1667
+ async function validateRegistrationRequest(payload, config) {
1668
+ const cfg = requireDomainConfig(config);
1669
+ await domainApiRequest("POST", customerPath(cfg, "/domains/register/validate"), cfg, payload);
1670
+ return true;
1671
+ }
1672
+ async function registerBrandsightDomain(domain, contact, options = {}, config) {
1673
+ const cfg = requireDomainConfig(config);
1674
+ const period = options.years ?? 1;
1675
+ const availability = await domainAvailability(domain, cfg, "REGISTRATION", period);
1676
+ if (!availability.available)
1677
+ throw new BrandsightApiError(`${domain} is not available for registration`);
1678
+ const price = options.premiumPrice ?? availability.price;
1679
+ if (price == null || !availability.currency) {
1680
+ throw new BrandsightApiError(`Cannot register ${domain}: availability quote did not include an exact price and currency`);
1681
+ }
1682
+ const tld = domainTld(domain);
1683
+ const schema = await getRegistrationSchema(tld, cfg);
1684
+ const required = Array.isArray(schema["required"]) ? schema["required"] : [];
1685
+ if (required.includes("metadata") && !options.metadata) {
1686
+ throw new BrandsightApiError(`Cannot register ${domain}: ${tld} requires TLD-specific metadata; pass registration metadata before validation`);
1687
+ }
1688
+ const privacy = options.privacy ?? false;
1689
+ const agreements = await getLegalAgreements(tld, privacy, cfg);
1690
+ const agreementKeys = agreements.map((a) => a.agreementKey).filter(Boolean);
1691
+ const c = brandsightContact(contact);
1692
+ const payload = {
1693
+ consent: {
1694
+ agreedAt: new Date().toISOString(),
1695
+ agreedBy: contact.email || cfg.shopperId || "domains-cli",
1696
+ agreementKeys,
1697
+ currency: availability.currency,
1698
+ price,
1699
+ registryPremiumPricing: availability.registryPremiumPricing ?? !!options.premiumPrice
1700
+ },
1701
+ contacts: {
1702
+ admin: c,
1703
+ billing: c,
1704
+ registrant: c,
1705
+ tech: c
1706
+ },
1707
+ domain,
1708
+ metadata: options.metadata ?? {},
1709
+ nameServers: options.nameservers ?? [],
1710
+ period,
1711
+ privacy,
1712
+ renewAuto: options.autoRenew ?? true
1713
+ };
1714
+ await validateRegistrationRequest(payload, cfg);
1715
+ const result = await domainApiRequest("POST", customerPath(cfg, "/domains/register"), cfg, payload);
1716
+ return {
1717
+ success: true,
1718
+ orderId: result.orderId ?? result.id,
1719
+ operationId: result.operationId ?? result.id,
1720
+ chargedAmount: String(price)
1721
+ };
1722
+ }
1723
+ async function updateNameservers3(domain, nameservers, config) {
1724
+ const cfg = requireDomainConfig(config);
1725
+ const result = await domainApiRequest("PUT", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/nameServers`), cfg, { nameServers: nameservers });
1726
+ return { success: true, operationId: result.operationId ?? result.id };
1727
+ }
1728
+ async function getDnsRecords3(domain, config) {
1729
+ const cfg = requireDomainConfig(config);
1730
+ const records = [];
1731
+ let offset = 0;
1732
+ const limit = 1000;
1733
+ while (true) {
1734
+ const batch = await domainApiRequest("GET", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/records?offset=${offset}&limit=${limit}`), cfg);
1735
+ if (!Array.isArray(batch) || batch.length === 0)
1736
+ break;
1737
+ records.push(...batch);
1738
+ if (batch.length < limit)
1739
+ break;
1740
+ offset++;
1741
+ }
1742
+ return records;
1743
+ }
1744
+ function normalizeBrandsightDnsRecord(record) {
1745
+ return {
1746
+ ...record,
1747
+ ttl: Math.max(record.ttl || 600, 600)
1748
+ };
1749
+ }
1750
+ async function setDnsRecords3(domain, records, config) {
1751
+ const cfg = requireDomainConfig(config);
1752
+ await domainApiRequest("PUT", customerPath(cfg, `/domains/${encodeURIComponent(domain)}/records`), cfg, records.map(normalizeBrandsightDnsRecord));
1753
+ return true;
1754
+ }
1755
+ async function syncToLocalDb3(dbFns, config) {
1756
+ const domains = await listDomains(config);
1757
+ let synced = 0, created = 0, updated = 0;
1758
+ const errors = [];
1759
+ for (const d of domains) {
1760
+ try {
1761
+ const existing = dbFns.getDomainByName(d.domain);
1762
+ if (existing) {
1763
+ dbFns.updateDomain(existing.id, {
1764
+ registrar: "Brandsight",
1765
+ expires_at: d.expires || undefined,
1766
+ auto_renew: d.auto_renew,
1767
+ status: "active",
1768
+ nameservers: d.nameservers
1769
+ });
1770
+ updated++;
1771
+ } else {
1772
+ dbFns.createDomain({
1773
+ name: d.domain,
1774
+ registrar: "Brandsight",
1775
+ expires_at: d.expires || undefined,
1776
+ auto_renew: d.auto_renew,
1777
+ status: "active",
1778
+ nameservers: d.nameservers
1779
+ });
1780
+ created++;
1781
+ }
1782
+ synced++;
1783
+ } catch (err) {
1784
+ errors.push(`${d.domain}: ${err instanceof Error ? err.message : String(err)}`);
1785
+ }
1786
+ }
1787
+ return { synced, created, updated, errors };
1788
+ }
1789
+ function createBrandsightProvider(config) {
1790
+ const cfg = config ?? getConfig4();
1791
+ return {
1792
+ name: "brandsight",
1793
+ async listDomains() {
1794
+ const domains = await listDomains(cfg);
1795
+ return domains.map((d) => ({
1796
+ domain: d.domain,
1797
+ registrar: "Brandsight",
1798
+ created: d.created ?? "",
1799
+ expires: d.expires,
1800
+ nameservers: d.nameservers,
1801
+ status: d.status === "ACTIVE" ? "active" : d.status.toLowerCase(),
1802
+ auto_renew: d.auto_renew
1803
+ }));
1804
+ },
1805
+ async getDomainInfo(domain) {
1806
+ const d = await getDomainInfo3(domain, cfg);
1807
+ if (!d)
1808
+ throw new Error(`Domain not found in Brandsight: ${domain}`);
1809
+ return {
1810
+ domain: d.domain,
1811
+ registrar: "Brandsight",
1812
+ created: d.created ?? "",
1813
+ expires: d.expires,
1814
+ nameservers: d.nameservers,
1815
+ status: d.status === "ACTIVE" ? "active" : d.status.toLowerCase(),
1816
+ auto_renew: d.auto_renew
1817
+ };
1818
+ },
1819
+ async renewDomain(domain, years = 1) {
1820
+ const result = await renewDomain3(domain, years, cfg);
1821
+ return { domain, success: result.success, orderId: result.orderId };
1822
+ },
1823
+ async registerDomain(domain, contact, options) {
1824
+ const result = await registerBrandsightDomain(domain, contact, options, cfg);
1825
+ return {
1826
+ domain,
1827
+ success: result.success,
1828
+ orderId: result.orderId,
1829
+ operationId: result.operationId,
1830
+ chargedAmount: result.chargedAmount
1831
+ };
1832
+ },
1833
+ async updateNameservers(domain, nameservers) {
1834
+ const result = await updateNameservers3(domain, nameservers, cfg);
1835
+ return { domain, success: result.success, operationId: result.operationId };
1836
+ },
1837
+ async getDnsRecords(domain) {
1838
+ const records = await getDnsRecords3(domain, cfg);
1839
+ return records.map((r) => ({
1840
+ type: r.type,
1841
+ name: r.name,
1842
+ value: r.data,
1843
+ ttl: r.ttl,
1844
+ priority: r.priority
1845
+ }));
1846
+ },
1847
+ async setDnsRecords(domain, records) {
1848
+ return setDnsRecords3(domain, records.map((r) => ({
1849
+ type: r.type,
1850
+ name: r.name,
1851
+ data: r.value,
1852
+ ttl: r.ttl,
1853
+ priority: r.priority
1854
+ })), cfg);
1855
+ },
1856
+ async checkAvailability(domain) {
1857
+ const result = await checkAvailability4(domain, cfg);
1858
+ return {
1859
+ domain: result.domain,
1860
+ available: result.available,
1861
+ is_premium: result.registryPremiumPricing,
1862
+ premium_price: result.registryPremiumPricing ? result.price : undefined,
1863
+ standard_price: result.registryPremiumPricing ? undefined : result.price,
1864
+ currency: result.currency
1865
+ };
1866
+ },
1867
+ async syncToLocalDb(dbFns) {
1868
+ return syncToLocalDb3(dbFns, cfg);
1869
+ }
1870
+ };
1871
+ }
1872
+
1873
+ // src/lib/registrar.ts
1874
+ function createNamecheapProvider() {
1875
+ return {
1876
+ name: "namecheap",
1877
+ async listDomains() {
1878
+ const config = getConfig3();
1879
+ const domains = await listNamecheapDomains(config);
1880
+ return domains.map((d) => ({
1881
+ domain: d.domain,
1882
+ registrar: "Namecheap",
1883
+ created: "",
1884
+ expires: d.expiry,
1885
+ nameservers: [],
1886
+ status: "active",
1887
+ auto_renew: d.autoRenew
1888
+ }));
1889
+ },
1890
+ async getDomainInfo(domain) {
1891
+ const config = getConfig3();
1892
+ const info = await getDomainInfo(domain, config);
1893
+ return {
1894
+ domain: info.domain,
1895
+ registrar: info.registrar,
1896
+ created: info.created,
1897
+ expires: info.expires,
1898
+ nameservers: info.nameservers,
1899
+ status: "active",
1900
+ auto_renew: true
1901
+ };
1902
+ },
1903
+ async registerDomain(domain, contact, options = {}) {
1904
+ const config = getConfig3();
1905
+ const result = await registerDomain2(domain, contact, {
1906
+ years: options.years,
1907
+ premiumPrice: options.premiumPrice
1908
+ }, config);
1909
+ return {
1910
+ domain: result.domain,
1911
+ success: result.success,
1912
+ orderId: result.orderId,
1913
+ chargedAmount: result.chargedAmount
1914
+ };
1915
+ },
1916
+ async updateNameservers(domain, nameservers) {
1917
+ const config = getConfig3();
1918
+ const success = await updateNameservers2(domain, nameservers, config);
1919
+ return { domain, success };
1920
+ },
1921
+ async renewDomain(domain, years = 1) {
1922
+ const config = getConfig3();
1923
+ const result = await renewDomain(domain, years, config);
1924
+ return {
1925
+ domain: result.domain,
1926
+ success: result.success,
1927
+ orderId: result.orderId,
1928
+ chargedAmount: result.chargedAmount
1929
+ };
1930
+ },
1931
+ async getDnsRecords(domain) {
1932
+ const config = getConfig3();
1933
+ const { sld, tld } = splitDomain(domain);
1934
+ const records = await getDnsRecords(domain, sld, tld, config);
1935
+ return records.map((r) => ({
1936
+ type: r.type,
1937
+ name: r.name,
1938
+ value: r.address,
1939
+ ttl: r.ttl,
1940
+ priority: r.mxPref
1941
+ }));
1942
+ },
1943
+ async setDnsRecords(domain, records) {
1944
+ const config = getConfig3();
1945
+ const { sld, tld } = splitDomain(domain);
1946
+ const ncRecords = records.map((r) => ({
1947
+ type: r.type,
1948
+ name: r.name,
1949
+ address: r.value,
1950
+ ttl: r.ttl,
1951
+ mxPref: r.priority
1952
+ }));
1953
+ return setDnsRecords(domain, sld, tld, ncRecords, config);
1954
+ },
1955
+ async checkAvailability(domain) {
1956
+ const config = getConfig3();
1957
+ const result = await checkAvailability2(domain, config);
1958
+ return {
1959
+ domain: result.domain,
1960
+ available: result.available,
1961
+ is_premium: result.premium,
1962
+ premium_price: result.price
1963
+ };
1964
+ },
1965
+ async syncToLocalDb(dbFns) {
1966
+ const result = await syncToLocalDb(dbFns);
1967
+ return {
1968
+ synced: result.synced,
1969
+ created: 0,
1970
+ updated: 0,
1971
+ errors: result.errors
1972
+ };
1973
+ }
1974
+ };
1975
+ }
1976
+ function createGoDaddyProvider() {
1977
+ return {
1978
+ name: "godaddy",
1979
+ async listDomains() {
1980
+ const domains = await listGoDaddyDomains();
1981
+ return domains.map((d) => ({
1982
+ domain: d.domain,
1983
+ registrar: "GoDaddy",
1984
+ created: "",
1985
+ expires: d.expires,
1986
+ nameservers: d.nameServers || [],
1987
+ status: d.status.toLowerCase(),
1988
+ auto_renew: d.renewAuto
1989
+ }));
1990
+ },
1991
+ async getDomainInfo(domain) {
1992
+ const detail = await getDomainInfo2(domain);
1993
+ return {
1994
+ domain: detail.domain,
1995
+ registrar: "GoDaddy",
1996
+ created: detail.createdAt || "",
1997
+ expires: detail.expires,
1998
+ nameservers: detail.nameServers || [],
1999
+ status: detail.status.toLowerCase(),
2000
+ auto_renew: detail.renewAuto
2001
+ };
2002
+ },
2003
+ async renewDomain(domain) {
2004
+ const result = await renewDomain2(domain);
2005
+ return {
2006
+ domain,
2007
+ success: true,
2008
+ orderId: String(result.orderId),
2009
+ chargedAmount: String(result.total)
2010
+ };
2011
+ },
2012
+ async getDnsRecords(domain) {
2013
+ const records = await getDnsRecords2(domain);
2014
+ return records.map((r) => ({
2015
+ type: r.type,
2016
+ name: r.name,
2017
+ value: r.data,
2018
+ ttl: r.ttl,
2019
+ priority: r.priority
2020
+ }));
2021
+ },
2022
+ async setDnsRecords(domain, records) {
2023
+ const gdRecords = records.map((r) => ({
2024
+ type: r.type,
2025
+ name: r.name,
2026
+ data: r.value,
2027
+ ttl: r.ttl,
2028
+ priority: r.priority
2029
+ }));
2030
+ await setDnsRecords2(domain, gdRecords);
2031
+ return true;
2032
+ },
2033
+ async checkAvailability(domain) {
2034
+ const result = await checkAvailability3(domain);
2035
+ return {
2036
+ domain: result.domain,
2037
+ available: result.available,
2038
+ standard_price: result.price,
2039
+ currency: result.currency
2040
+ };
2041
+ },
2042
+ async syncToLocalDb(dbFns) {
2043
+ const result = await syncToLocalDb2(dbFns);
2044
+ return {
2045
+ synced: result.synced,
2046
+ created: result.created,
2047
+ updated: result.updated,
2048
+ errors: result.errors
2049
+ };
2050
+ }
2051
+ };
2052
+ }
2053
+ var providerRegistry = new Map([
2054
+ ["namecheap", {
2055
+ info: {
2056
+ name: "namecheap",
2057
+ type: "full",
2058
+ configured: false,
2059
+ envVars: providerEnvNames("namecheap")
2060
+ },
2061
+ createInventory: createNamecheapProvider,
2062
+ createRegistrar: createNamecheapProvider,
2063
+ createDns: createNamecheapProvider
2064
+ }],
2065
+ ["godaddy", {
2066
+ info: {
2067
+ name: "godaddy",
2068
+ type: "full",
2069
+ configured: false,
2070
+ envVars: providerEnvNames("godaddy")
2071
+ },
2072
+ createInventory: createGoDaddyProvider,
2073
+ createRegistrar: createGoDaddyProvider,
2074
+ createDns: createGoDaddyProvider
2075
+ }],
2076
+ ["route53", {
2077
+ info: {
2078
+ name: "route53",
2079
+ type: "full",
2080
+ configured: false,
2081
+ envVars: providerEnvNames("route53")
2082
+ },
2083
+ createInventory: () => createRoute53Provider(),
2084
+ createRegistrar: () => createRoute53Provider(),
2085
+ createDns: () => createRoute53Provider()
2086
+ }],
2087
+ ["cloudflare", {
2088
+ info: {
2089
+ name: "cloudflare",
2090
+ type: "dns",
2091
+ configured: false,
2092
+ envVars: providerEnvNames("cloudflare")
2093
+ },
2094
+ createInventory: () => createCloudflareProvider(),
2095
+ createDns: createCloudflareProvider
2096
+ }],
2097
+ ["brandsight", {
2098
+ info: {
2099
+ name: "brandsight",
2100
+ type: "full",
2101
+ configured: false,
2102
+ envVars: providerEnvNames("brandsight")
2103
+ },
2104
+ createInventory: createBrandsightProvider,
2105
+ createRegistrar: createBrandsightProvider,
2106
+ createDns: createBrandsightProvider
2107
+ }],
2108
+ ["sedo", {
2109
+ info: {
2110
+ name: "sedo",
2111
+ type: "marketplace",
2112
+ configured: false,
2113
+ envVars: providerEnvNames("sedo")
2114
+ }
2115
+ }]
2116
+ ]);
2117
+ function isConfigured(providerName) {
2118
+ return hasProviderCredentials(providerName);
2119
+ }
2120
+ function registerProvider(entry) {
2121
+ providerRegistry.set(entry.info.name.toLowerCase(), entry);
2122
+ }
2123
+ function getAvailableProviders() {
2124
+ return Array.from(providerRegistry.values()).map((e) => ({
2125
+ ...e.info,
2126
+ configured: isConfigured(e.info.name),
2127
+ inventory: !!e.createInventory
2128
+ }));
2129
+ }
2130
+ function getProviderInfo(name) {
2131
+ const entry = providerRegistry.get(name.toLowerCase());
2132
+ if (!entry)
2133
+ return null;
2134
+ return { ...entry.info, configured: isConfigured(entry.info.name), inventory: !!entry.createInventory };
2135
+ }
2136
+ function providerHasRegistrar(name) {
2137
+ return !!providerRegistry.get(name.toLowerCase())?.createRegistrar;
2138
+ }
2139
+ function providerHasDns(name) {
2140
+ return !!providerRegistry.get(name.toLowerCase())?.createDns;
2141
+ }
2142
+ function providerHasInventory(name) {
2143
+ return !!providerRegistry.get(name.toLowerCase())?.createInventory;
2144
+ }
2145
+ function getDomainInventoryProvider(name) {
2146
+ const entry = providerRegistry.get(name.toLowerCase());
2147
+ if (!entry?.createInventory)
2148
+ throw new Error(`No domain inventory provider: ${name}`);
2149
+ return entry.createInventory();
2150
+ }
2151
+ function getRegistrarProvider(name) {
2152
+ const entry = providerRegistry.get(name.toLowerCase());
2153
+ if (!entry?.createRegistrar)
2154
+ throw new Error(`No registrar provider: ${name}`);
2155
+ return entry.createRegistrar();
2156
+ }
2157
+ function getDnsProvider(name) {
2158
+ const entry = providerRegistry.get(name.toLowerCase());
2159
+ if (!entry?.createDns)
2160
+ throw new Error(`No DNS provider: ${name}`);
2161
+ return entry.createDns();
2162
+ }
2163
+ function getProvider(name) {
2164
+ return getRegistrarProvider(name);
2165
+ }
2166
+ async function syncAll(dbFns) {
2167
+ const available = getAvailableProviders().filter((p) => p.configured && providerHasInventory(p.name));
2168
+ const result = { providers: [], totalSynced: 0, totalErrors: [] };
2169
+ for (const info of available) {
2170
+ try {
2171
+ const provider = getDomainInventoryProvider(info.name);
2172
+ const syncResult = await provider.syncToLocalDb(dbFns);
2173
+ result.providers.push({ name: info.name, result: syncResult });
2174
+ result.totalSynced += syncResult.synced;
2175
+ result.totalErrors.push(...syncResult.errors.map((e) => `[${info.name}] ${e}`));
2176
+ } catch (error) {
2177
+ const msg = `[${info.name}] Sync failed: ${error instanceof Error ? error.message : String(error)}`;
2178
+ result.totalErrors.push(msg);
2179
+ result.providers.push({ name: info.name, result: { synced: 0, created: 0, updated: 0, errors: [msg] } });
2180
+ }
2181
+ }
2182
+ return result;
2183
+ }
2184
+ function autoDetectRegistrar(domain, getDomainByName) {
2185
+ const dbDomain = getDomainByName(domain);
2186
+ if (!dbDomain?.registrar)
2187
+ return null;
2188
+ const r = dbDomain.registrar.toLowerCase();
2189
+ if (r.includes("cloudflare dns") || r.includes("route 53 dns") || r.includes("route53 dns"))
2190
+ return null;
2191
+ if (r.includes("namecheap"))
2192
+ return "namecheap";
2193
+ if (r.includes("godaddy"))
2194
+ return "godaddy";
2195
+ if (r.includes("route 53") || r.includes("route53"))
2196
+ return "route53";
2197
+ if (r.includes("cloudflare"))
2198
+ return null;
2199
+ if (r.includes("brandsight"))
2200
+ return "brandsight";
2201
+ return null;
2202
+ }
2203
+ export {
2204
+ syncAll,
2205
+ registerProvider,
2206
+ providerHasRegistrar,
2207
+ providerHasInventory,
2208
+ providerHasDns,
2209
+ getRegistrarProvider,
2210
+ getProviderInfo,
2211
+ getProvider,
2212
+ getDomainInventoryProvider,
2213
+ getDnsProvider,
2214
+ getAvailableProviders,
2215
+ autoDetectRegistrar
2216
+ };