@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.
- package/README.md +73 -24
- package/dist/cli/commands/dns.d.ts.map +1 -1
- package/dist/cli/commands/domain.d.ts.map +1 -1
- package/dist/cli/commands/sedo.d.ts.map +1 -1
- package/dist/cli/index.js +36988 -35301
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20185 -17966
- package/dist/lib/brandsight.d.ts.map +1 -1
- package/dist/lib/capability.js +46 -0
- package/dist/lib/cloudflare-auth.js +139 -0
- package/dist/lib/cloudflare.js +394 -0
- package/dist/lib/creds-check.d.ts.map +1 -1
- package/dist/lib/delegate.js +12 -0
- package/dist/lib/dns-plan.d.ts +29 -0
- package/dist/lib/dns-plan.d.ts.map +1 -0
- package/dist/lib/env-aliases.d.ts +10 -0
- package/dist/lib/env-aliases.d.ts.map +1 -1
- package/dist/lib/registrar.js +2216 -0
- package/dist/lib/route53.d.ts +54 -11
- package/dist/lib/route53.d.ts.map +1 -1
- package/dist/lib/route53.js +678 -0
- package/dist/lib/sedo.d.ts +34 -3
- package/dist/lib/sedo.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +20353 -18269
- package/dist/mcp/tool-filter.d.ts +4 -0
- package/dist/mcp/tool-filter.d.ts.map +1 -0
- package/dist/storage.js +78 -96
- package/package.json +37 -10
|
@@ -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
|
+
};
|