nodaddy 1.0.0
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 +118 -0
- package/dist/index.js +1977 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1977 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/services/state-manager.ts
|
|
13
|
+
var state_manager_exports = {};
|
|
14
|
+
__export(state_manager_exports, {
|
|
15
|
+
clearAll: () => clearAll,
|
|
16
|
+
clearConfig: () => clearConfig,
|
|
17
|
+
createMigration: () => createMigration,
|
|
18
|
+
getActiveMigration: () => getActiveMigration,
|
|
19
|
+
getAllMigrations: () => getAllMigrations,
|
|
20
|
+
getConfig: () => getConfig,
|
|
21
|
+
getMigration: () => getMigration,
|
|
22
|
+
getResumableDomains: () => getResumableDomains,
|
|
23
|
+
getStorePath: () => getStorePath,
|
|
24
|
+
saveDnsBackup: () => saveDnsBackup,
|
|
25
|
+
setConfig: () => setConfig,
|
|
26
|
+
updateDomainStatus: () => updateDomainStatus
|
|
27
|
+
});
|
|
28
|
+
import Conf from "conf";
|
|
29
|
+
import crypto from "crypto";
|
|
30
|
+
function getConfig() {
|
|
31
|
+
return store.get("config");
|
|
32
|
+
}
|
|
33
|
+
function setConfig(config) {
|
|
34
|
+
const current = store.get("config");
|
|
35
|
+
store.set("config", { ...current, ...config });
|
|
36
|
+
}
|
|
37
|
+
function clearConfig() {
|
|
38
|
+
store.set("config", {});
|
|
39
|
+
}
|
|
40
|
+
function clearAll() {
|
|
41
|
+
store.clear();
|
|
42
|
+
}
|
|
43
|
+
function getStorePath() {
|
|
44
|
+
return store.path;
|
|
45
|
+
}
|
|
46
|
+
function createMigration(domains) {
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
49
|
+
const domainStates = {};
|
|
50
|
+
for (const domain of domains) {
|
|
51
|
+
domainStates[domain] = {
|
|
52
|
+
domain,
|
|
53
|
+
status: "pending",
|
|
54
|
+
dnsRecordsBackup: [],
|
|
55
|
+
lastUpdated: now
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const migration = {
|
|
59
|
+
id,
|
|
60
|
+
startedAt: now,
|
|
61
|
+
domains: domainStates
|
|
62
|
+
};
|
|
63
|
+
const migrations = store.get("migrations");
|
|
64
|
+
migrations[id] = migration;
|
|
65
|
+
store.set("migrations", migrations);
|
|
66
|
+
store.set("activeMigrationId", id);
|
|
67
|
+
return migration;
|
|
68
|
+
}
|
|
69
|
+
function sanitizeMigration(migration) {
|
|
70
|
+
for (const domain of Object.values(migration.domains)) {
|
|
71
|
+
delete domain.authCode;
|
|
72
|
+
}
|
|
73
|
+
return migration;
|
|
74
|
+
}
|
|
75
|
+
function getActiveMigration() {
|
|
76
|
+
const id = store.get("activeMigrationId");
|
|
77
|
+
if (!id) return null;
|
|
78
|
+
const migrations = store.get("migrations");
|
|
79
|
+
const migration = migrations[id];
|
|
80
|
+
return migration ? sanitizeMigration(migration) : null;
|
|
81
|
+
}
|
|
82
|
+
function getMigration(id) {
|
|
83
|
+
const migrations = store.get("migrations");
|
|
84
|
+
const migration = migrations[id];
|
|
85
|
+
return migration ? sanitizeMigration(migration) : null;
|
|
86
|
+
}
|
|
87
|
+
function updateDomainStatus(migrationId, domain, status, extra) {
|
|
88
|
+
const migrations = store.get("migrations");
|
|
89
|
+
const migration = migrations[migrationId];
|
|
90
|
+
if (!migration) throw new Error(`Migration ${migrationId} not found`);
|
|
91
|
+
const domainState = migration.domains[domain];
|
|
92
|
+
if (!domainState) throw new Error(`Domain ${domain} not in migration`);
|
|
93
|
+
migration.domains[domain] = {
|
|
94
|
+
...domainState,
|
|
95
|
+
// Clear stale error when transitioning to a non-failed status
|
|
96
|
+
...status !== "failed" ? { error: void 0 } : {},
|
|
97
|
+
...extra,
|
|
98
|
+
status,
|
|
99
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
100
|
+
};
|
|
101
|
+
store.set("migrations", migrations);
|
|
102
|
+
}
|
|
103
|
+
function saveDnsBackup(migrationId, domain, records) {
|
|
104
|
+
updateDomainStatus(migrationId, domain, "pending", {
|
|
105
|
+
dnsRecordsBackup: records
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function getAllMigrations() {
|
|
109
|
+
const migrations = store.get("migrations");
|
|
110
|
+
return Object.values(migrations).sort(
|
|
111
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
function getResumableDomains(migrationId) {
|
|
115
|
+
const migration = getMigration(migrationId);
|
|
116
|
+
if (!migration) return [];
|
|
117
|
+
return Object.values(migration.domains).filter(
|
|
118
|
+
(d) => d.status !== "completed" && d.status !== "transfer_initiated"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
var store;
|
|
122
|
+
var init_state_manager = __esm({
|
|
123
|
+
"src/services/state-manager.ts"() {
|
|
124
|
+
"use strict";
|
|
125
|
+
store = new Conf({
|
|
126
|
+
projectName: "nodaddy",
|
|
127
|
+
configFileMode: 384,
|
|
128
|
+
defaults: {
|
|
129
|
+
config: {},
|
|
130
|
+
migrations: {},
|
|
131
|
+
activeMigrationId: null
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/index.ts
|
|
138
|
+
import { Command } from "commander";
|
|
139
|
+
|
|
140
|
+
// src/commands/migrate.ts
|
|
141
|
+
import * as p4 from "@clack/prompts";
|
|
142
|
+
import chalk5 from "chalk";
|
|
143
|
+
|
|
144
|
+
// src/providers/godaddy.ts
|
|
145
|
+
import { z as z2 } from "zod/v4";
|
|
146
|
+
|
|
147
|
+
// src/types/godaddy.ts
|
|
148
|
+
import { z } from "zod/v4";
|
|
149
|
+
var GoDaddyDomainSchema = z.object({
|
|
150
|
+
domain: z.string(),
|
|
151
|
+
domainId: z.number(),
|
|
152
|
+
status: z.string(),
|
|
153
|
+
expires: z.string().optional(),
|
|
154
|
+
expirationProtected: z.boolean().optional(),
|
|
155
|
+
holdRegistrar: z.boolean().optional(),
|
|
156
|
+
locked: z.boolean().optional(),
|
|
157
|
+
privacy: z.boolean().optional(),
|
|
158
|
+
renewAuto: z.boolean().optional(),
|
|
159
|
+
renewable: z.boolean().optional(),
|
|
160
|
+
transferProtected: z.boolean().optional(),
|
|
161
|
+
createdAt: z.string().optional(),
|
|
162
|
+
authCode: z.string().optional(),
|
|
163
|
+
nameServers: z.array(z.string()).nullable().optional()
|
|
164
|
+
});
|
|
165
|
+
var GoDaddyDnsRecordSchema = z.object({
|
|
166
|
+
type: z.string(),
|
|
167
|
+
name: z.string(),
|
|
168
|
+
data: z.string(),
|
|
169
|
+
ttl: z.number(),
|
|
170
|
+
priority: z.number().optional(),
|
|
171
|
+
weight: z.number().optional(),
|
|
172
|
+
port: z.number().optional(),
|
|
173
|
+
service: z.string().optional(),
|
|
174
|
+
protocol: z.string().optional()
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// src/services/rate-limiter.ts
|
|
178
|
+
var RateLimiter = class {
|
|
179
|
+
timestamps = [];
|
|
180
|
+
config;
|
|
181
|
+
constructor(config) {
|
|
182
|
+
this.config = config;
|
|
183
|
+
}
|
|
184
|
+
async acquire() {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
this.timestamps = this.timestamps.filter(
|
|
187
|
+
(t) => now - t < this.config.windowMs
|
|
188
|
+
);
|
|
189
|
+
if (this.timestamps.length >= this.config.requests) {
|
|
190
|
+
const oldest = this.timestamps[0];
|
|
191
|
+
const waitMs = this.config.windowMs - (now - oldest) + 50;
|
|
192
|
+
await sleep(waitMs);
|
|
193
|
+
return this.acquire();
|
|
194
|
+
}
|
|
195
|
+
this.timestamps.push(now);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
function sleep(ms) {
|
|
199
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
200
|
+
}
|
|
201
|
+
var godaddyRateLimiter = new RateLimiter({
|
|
202
|
+
requests: 55,
|
|
203
|
+
// 60/min with buffer
|
|
204
|
+
windowMs: 6e4
|
|
205
|
+
});
|
|
206
|
+
var cloudflareRateLimiter = new RateLimiter({
|
|
207
|
+
requests: 1100,
|
|
208
|
+
// 1200/5min with buffer
|
|
209
|
+
windowMs: 3e5
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/services/validation.ts
|
|
213
|
+
var DOMAIN_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
|
214
|
+
function assertValidDomain(domain) {
|
|
215
|
+
if (domain.length === 0 || domain.length > 253) {
|
|
216
|
+
throw new Error(`Invalid domain name: ${domain}`);
|
|
217
|
+
}
|
|
218
|
+
const labels = domain.split(".");
|
|
219
|
+
for (const label of labels) {
|
|
220
|
+
if (label.length === 0 || label.length > 63) {
|
|
221
|
+
throw new Error(`Invalid domain name: ${domain}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!DOMAIN_RE.test(domain)) {
|
|
225
|
+
throw new Error(`Invalid domain name: ${domain}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/providers/godaddy.ts
|
|
230
|
+
var BASE_URL = "https://api.godaddy.com";
|
|
231
|
+
var RESOURCE_LOCK_MAX_RETRIES = 6;
|
|
232
|
+
var RESOURCE_LOCK_BASE_DELAY_MS = 5e3;
|
|
233
|
+
var GoDaddyClient = class {
|
|
234
|
+
credentials;
|
|
235
|
+
constructor(credentials) {
|
|
236
|
+
this.credentials = credentials;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Core fetch with 422 resource lock retry. All request methods use this.
|
|
240
|
+
*/
|
|
241
|
+
async fetchWithRetry(path, options = {}, onRetry) {
|
|
242
|
+
for (let attempt = 0; attempt <= RESOURCE_LOCK_MAX_RETRIES; attempt++) {
|
|
243
|
+
await godaddyRateLimiter.acquire();
|
|
244
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
245
|
+
...options,
|
|
246
|
+
headers: {
|
|
247
|
+
Authorization: `sso-key ${this.credentials.apiKey}:${this.credentials.apiSecret}`,
|
|
248
|
+
"Content-Type": "application/json",
|
|
249
|
+
...options.headers
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
if (res.ok) return res;
|
|
253
|
+
const body = await res.text();
|
|
254
|
+
if (res.status === 422 && body.includes("Resource is being used") && attempt < RESOURCE_LOCK_MAX_RETRIES) {
|
|
255
|
+
const delay = RESOURCE_LOCK_BASE_DELAY_MS * (attempt + 1);
|
|
256
|
+
onRetry?.(attempt + 1, RESOURCE_LOCK_MAX_RETRIES, delay / 1e3);
|
|
257
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
throw new GoDaddyApiError(
|
|
261
|
+
`GoDaddy API error ${res.status}: ${body}`,
|
|
262
|
+
res.status,
|
|
263
|
+
body
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
throw new Error("Exhausted retries");
|
|
267
|
+
}
|
|
268
|
+
async request(path, options = {}, onRetry) {
|
|
269
|
+
const res = await this.fetchWithRetry(path, options, onRetry);
|
|
270
|
+
const text2 = await res.text();
|
|
271
|
+
if (!text2) throw new Error("Expected JSON response but got empty body");
|
|
272
|
+
return JSON.parse(text2);
|
|
273
|
+
}
|
|
274
|
+
async requestVoid(path, options = {}, onRetry) {
|
|
275
|
+
const res = await this.fetchWithRetry(path, options, onRetry);
|
|
276
|
+
await res.text();
|
|
277
|
+
}
|
|
278
|
+
async requestText(path, onRetry) {
|
|
279
|
+
const res = await this.fetchWithRetry(path, {}, onRetry);
|
|
280
|
+
return res.text();
|
|
281
|
+
}
|
|
282
|
+
async listDomains() {
|
|
283
|
+
const data = await this.request(
|
|
284
|
+
"/v1/domains?limit=1000&statuses=ACTIVE"
|
|
285
|
+
);
|
|
286
|
+
return z2.array(GoDaddyDomainSchema).parse(data);
|
|
287
|
+
}
|
|
288
|
+
async getDomainDetail(domain) {
|
|
289
|
+
assertValidDomain(domain);
|
|
290
|
+
const data = await this.request(`/v1/domains/${domain}`);
|
|
291
|
+
return GoDaddyDomainSchema.parse(data);
|
|
292
|
+
}
|
|
293
|
+
async getDnsRecords(domain) {
|
|
294
|
+
assertValidDomain(domain);
|
|
295
|
+
const data = await this.request(
|
|
296
|
+
`/v1/domains/${domain}/records`
|
|
297
|
+
);
|
|
298
|
+
return z2.array(GoDaddyDnsRecordSchema).parse(data);
|
|
299
|
+
}
|
|
300
|
+
async removePrivacy(domain, onRetry) {
|
|
301
|
+
assertValidDomain(domain);
|
|
302
|
+
await this.requestVoid(
|
|
303
|
+
`/v1/domains/${domain}/privacy`,
|
|
304
|
+
{ method: "DELETE" },
|
|
305
|
+
onRetry
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
async prepareForTransfer(domain, onRetry) {
|
|
309
|
+
assertValidDomain(domain);
|
|
310
|
+
try {
|
|
311
|
+
await this.requestVoid(
|
|
312
|
+
`/v1/domains/${domain}`,
|
|
313
|
+
{ method: "PATCH", body: JSON.stringify({ locked: false, renewAuto: false }) },
|
|
314
|
+
onRetry
|
|
315
|
+
);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err instanceof GoDaddyApiError && !err.responseBody.includes("Resource is being used")) {
|
|
318
|
+
await this.requestVoid(
|
|
319
|
+
`/v1/domains/${domain}`,
|
|
320
|
+
{ method: "PATCH", body: JSON.stringify({ locked: false }) },
|
|
321
|
+
onRetry
|
|
322
|
+
);
|
|
323
|
+
} else {
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async getAuthCode(domain) {
|
|
329
|
+
assertValidDomain(domain);
|
|
330
|
+
try {
|
|
331
|
+
const text2 = await this.requestText(
|
|
332
|
+
`/v1/domains/${domain}/transferAuthCode`
|
|
333
|
+
);
|
|
334
|
+
try {
|
|
335
|
+
const parsed = JSON.parse(text2);
|
|
336
|
+
if (typeof parsed === "string") return parsed;
|
|
337
|
+
if (Array.isArray(parsed) && typeof parsed[0] === "string")
|
|
338
|
+
return parsed[0];
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
return text2.replace(/^"|"$/g, "");
|
|
342
|
+
} catch (err) {
|
|
343
|
+
if (err instanceof GoDaddyApiError && err.statusCode === 404) {
|
|
344
|
+
const detail = await this.getDomainDetail(domain);
|
|
345
|
+
if (detail.authCode) return detail.authCode;
|
|
346
|
+
throw new GoDaddyApiError(
|
|
347
|
+
`No auth code available for ${domain} \u2014 check GoDaddy dashboard`,
|
|
348
|
+
404,
|
|
349
|
+
""
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async updateNameservers(domain, nameservers, onRetry) {
|
|
356
|
+
assertValidDomain(domain);
|
|
357
|
+
await this.requestVoid(
|
|
358
|
+
`/v1/domains/${domain}`,
|
|
359
|
+
{ method: "PATCH", body: JSON.stringify({ nameServers: nameservers }) },
|
|
360
|
+
onRetry
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
async verifyCredentials() {
|
|
364
|
+
try {
|
|
365
|
+
await this.request("/v1/domains?limit=1");
|
|
366
|
+
return true;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var GoDaddyApiError = class extends Error {
|
|
373
|
+
constructor(message, statusCode, responseBody) {
|
|
374
|
+
super(message);
|
|
375
|
+
this.statusCode = statusCode;
|
|
376
|
+
this.responseBody = responseBody;
|
|
377
|
+
this.name = "GoDaddyApiError";
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/providers/cloudflare.ts
|
|
382
|
+
import { z as z4 } from "zod/v4";
|
|
383
|
+
|
|
384
|
+
// src/types/cloudflare.ts
|
|
385
|
+
import { z as z3 } from "zod/v4";
|
|
386
|
+
var CloudflareZoneSchema = z3.object({
|
|
387
|
+
id: z3.string(),
|
|
388
|
+
name: z3.string(),
|
|
389
|
+
status: z3.string(),
|
|
390
|
+
name_servers: z3.array(z3.string()).optional()
|
|
391
|
+
});
|
|
392
|
+
var CloudflareDnsRecordSchema = z3.object({
|
|
393
|
+
id: z3.string().optional(),
|
|
394
|
+
type: z3.string(),
|
|
395
|
+
name: z3.string(),
|
|
396
|
+
content: z3.string(),
|
|
397
|
+
ttl: z3.number(),
|
|
398
|
+
proxied: z3.boolean().optional(),
|
|
399
|
+
priority: z3.number().optional(),
|
|
400
|
+
data: z3.record(z3.string(), z3.unknown()).optional()
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// src/providers/cloudflare.ts
|
|
404
|
+
var BASE_URL2 = "https://api.cloudflare.com/client/v4";
|
|
405
|
+
var CloudflareClient = class {
|
|
406
|
+
credentials;
|
|
407
|
+
constructor(credentials) {
|
|
408
|
+
this.credentials = credentials;
|
|
409
|
+
}
|
|
410
|
+
authHeaders() {
|
|
411
|
+
if (this.credentials.authType === "global-key") {
|
|
412
|
+
return {
|
|
413
|
+
"X-Auth-Key": this.credentials.apiKey,
|
|
414
|
+
"X-Auth-Email": this.credentials.email
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
Authorization: `Bearer ${this.credentials.apiToken}`
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async request(path, options = {}) {
|
|
422
|
+
await cloudflareRateLimiter.acquire();
|
|
423
|
+
const res = await fetch(`${BASE_URL2}${path}`, {
|
|
424
|
+
...options,
|
|
425
|
+
headers: {
|
|
426
|
+
...this.authHeaders(),
|
|
427
|
+
"Content-Type": "application/json",
|
|
428
|
+
...options.headers
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
const body = await res.text();
|
|
433
|
+
throw new CloudflareApiError(
|
|
434
|
+
`Cloudflare API error ${res.status}: ${body}`,
|
|
435
|
+
res.status,
|
|
436
|
+
body
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const text2 = await res.text();
|
|
440
|
+
if (!text2) throw new Error("Expected JSON response but got empty body");
|
|
441
|
+
const json = JSON.parse(text2);
|
|
442
|
+
if (!json.success) {
|
|
443
|
+
const errorMsg = json.errors.map((e) => `${e.code}: ${e.message}`).join(", ");
|
|
444
|
+
throw new CloudflareApiError(
|
|
445
|
+
`Cloudflare API error: ${errorMsg}`,
|
|
446
|
+
res.status,
|
|
447
|
+
JSON.stringify(json)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return json.result;
|
|
451
|
+
}
|
|
452
|
+
async createZone(domain) {
|
|
453
|
+
assertValidDomain(domain);
|
|
454
|
+
const data = await this.request("/zones", {
|
|
455
|
+
method: "POST",
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
name: domain,
|
|
458
|
+
account: { id: this.credentials.accountId },
|
|
459
|
+
jump_start: true,
|
|
460
|
+
type: "full"
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
return CloudflareZoneSchema.parse(data);
|
|
464
|
+
}
|
|
465
|
+
async getZoneByName(domain) {
|
|
466
|
+
assertValidDomain(domain);
|
|
467
|
+
const data = await this.request(
|
|
468
|
+
`/zones?name=${encodeURIComponent(domain)}&account.id=${this.credentials.accountId}`
|
|
469
|
+
);
|
|
470
|
+
const zones = z4.array(CloudflareZoneSchema).parse(data);
|
|
471
|
+
return zones[0] ?? null;
|
|
472
|
+
}
|
|
473
|
+
async getZoneStatus(zoneId) {
|
|
474
|
+
const data = await this.request(`/zones/${zoneId}`);
|
|
475
|
+
return CloudflareZoneSchema.parse(data);
|
|
476
|
+
}
|
|
477
|
+
async createDnsRecord(zoneId, record) {
|
|
478
|
+
const data = await this.request(
|
|
479
|
+
`/zones/${zoneId}/dns_records`,
|
|
480
|
+
{
|
|
481
|
+
method: "POST",
|
|
482
|
+
body: JSON.stringify(record)
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
return CloudflareDnsRecordSchema.parse(data);
|
|
486
|
+
}
|
|
487
|
+
async verifyCredentials() {
|
|
488
|
+
try {
|
|
489
|
+
const endpoint = this.credentials.authType === "global-key" ? "/user" : "/user/tokens/verify";
|
|
490
|
+
await this.request(endpoint);
|
|
491
|
+
return true;
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async checkAuthCode(domain, authCode) {
|
|
497
|
+
assertValidDomain(domain);
|
|
498
|
+
const encoded = Buffer.from(authCode).toString("base64");
|
|
499
|
+
return this.request(
|
|
500
|
+
`/accounts/${this.credentials.accountId}/registrar/domains/${domain}/check_auth`,
|
|
501
|
+
{
|
|
502
|
+
method: "POST",
|
|
503
|
+
body: JSON.stringify({ auth_code: encoded })
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
async initiateTransfer(zoneId, domain, authCode, contact) {
|
|
508
|
+
assertValidDomain(domain);
|
|
509
|
+
const encoded = Buffer.from(authCode).toString("base64");
|
|
510
|
+
return this.request(
|
|
511
|
+
`/zones/${zoneId}/registrar/domains/${domain}/transfer`,
|
|
512
|
+
{
|
|
513
|
+
method: "POST",
|
|
514
|
+
body: JSON.stringify({
|
|
515
|
+
auth_code: encoded,
|
|
516
|
+
auto_renew: true,
|
|
517
|
+
years: 1,
|
|
518
|
+
privacy: true,
|
|
519
|
+
import_dns: true,
|
|
520
|
+
registrant: contact,
|
|
521
|
+
fee_acknowledgement: {
|
|
522
|
+
transfer_fee: 0,
|
|
523
|
+
icann_fee: 0
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
async waitForZoneActive(zoneId, timeoutMs = 3e5, pollIntervalMs = 1e4) {
|
|
530
|
+
const start = Date.now();
|
|
531
|
+
while (Date.now() - start < timeoutMs) {
|
|
532
|
+
const zone = await this.getZoneStatus(zoneId);
|
|
533
|
+
if (zone.status === "active") return zone;
|
|
534
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
535
|
+
}
|
|
536
|
+
throw new CloudflareApiError(
|
|
537
|
+
`Zone ${zoneId} did not become active within ${timeoutMs / 1e3}s`,
|
|
538
|
+
408,
|
|
539
|
+
""
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
var CloudflareApiError = class extends Error {
|
|
544
|
+
constructor(message, statusCode, responseBody) {
|
|
545
|
+
super(message);
|
|
546
|
+
this.statusCode = statusCode;
|
|
547
|
+
this.responseBody = responseBody;
|
|
548
|
+
this.name = "CloudflareApiError";
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/ui/wizard.ts
|
|
553
|
+
init_state_manager();
|
|
554
|
+
import * as p from "@clack/prompts";
|
|
555
|
+
import chalk from "chalk";
|
|
556
|
+
function credentialsFromEnv() {
|
|
557
|
+
const gdKey = process.env.GODADDY_API_KEY;
|
|
558
|
+
const gdSecret = process.env.GODADDY_API_SECRET;
|
|
559
|
+
const cfAccountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
560
|
+
if (!gdKey || !gdSecret || !cfAccountId) return null;
|
|
561
|
+
const cfApiKey = process.env.CLOUDFLARE_API_KEY;
|
|
562
|
+
const cfEmail = process.env.CLOUDFLARE_EMAIL;
|
|
563
|
+
const cfToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
564
|
+
let cloudflare;
|
|
565
|
+
if (cfApiKey && cfEmail) {
|
|
566
|
+
cloudflare = { authType: "global-key", apiKey: cfApiKey, email: cfEmail, accountId: cfAccountId };
|
|
567
|
+
} else if (cfToken) {
|
|
568
|
+
cloudflare = { authType: "token", apiToken: cfToken, accountId: cfAccountId };
|
|
569
|
+
} else {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
godaddy: { apiKey: gdKey, apiSecret: gdSecret },
|
|
574
|
+
cloudflare
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function collectCredentials() {
|
|
578
|
+
const envCreds = credentialsFromEnv();
|
|
579
|
+
if (envCreds) {
|
|
580
|
+
p.log.info("Using credentials from environment variables.");
|
|
581
|
+
return envCreds;
|
|
582
|
+
}
|
|
583
|
+
const config = getConfig();
|
|
584
|
+
const hasGoDaddy = config.godaddy?.apiKey && config.godaddy?.apiSecret;
|
|
585
|
+
const hasCloudflare = config.cloudflare?.accountId && (config.cloudflare?.apiToken || config.cloudflare?.apiKey);
|
|
586
|
+
let useStored = false;
|
|
587
|
+
if (hasGoDaddy && hasCloudflare) {
|
|
588
|
+
useStored = await p.confirm({
|
|
589
|
+
message: "Use stored API credentials?",
|
|
590
|
+
initialValue: true
|
|
591
|
+
});
|
|
592
|
+
if (p.isCancel(useStored)) {
|
|
593
|
+
p.cancel("Migration cancelled.");
|
|
594
|
+
process.exit(0);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (useStored && hasGoDaddy && hasCloudflare) {
|
|
598
|
+
const cf = config.cloudflare;
|
|
599
|
+
const cloudflare2 = cf.authType === "global-key" ? { authType: "global-key", apiKey: cf.apiKey, email: cf.email, accountId: cf.accountId } : { authType: "token", apiToken: cf.apiToken, accountId: cf.accountId };
|
|
600
|
+
return {
|
|
601
|
+
godaddy: config.godaddy,
|
|
602
|
+
cloudflare: cloudflare2
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
p.note(
|
|
606
|
+
"GoDaddy: Create a Production API key (not OTE/Test) at\n https://developer.godaddy.com/keys\n\nCloudflare: Use your Global API Key (bottom of page) at\n https://dash.cloudflare.com/profile/api-tokens\n Account ID is on any zone overview page.",
|
|
607
|
+
"API Credentials Required"
|
|
608
|
+
);
|
|
609
|
+
const gdCreds = await p.group(
|
|
610
|
+
{
|
|
611
|
+
gdKey: () => p.text({
|
|
612
|
+
message: "GoDaddy API Key",
|
|
613
|
+
placeholder: "e.g. dLf3...",
|
|
614
|
+
validate: (v) => {
|
|
615
|
+
if (!v?.trim()) return "API key is required";
|
|
616
|
+
}
|
|
617
|
+
}),
|
|
618
|
+
gdSecret: () => p.password({
|
|
619
|
+
message: "GoDaddy API Secret",
|
|
620
|
+
validate: (v) => {
|
|
621
|
+
if (!v?.trim()) return "API secret is required";
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
},
|
|
625
|
+
{ onCancel: () => {
|
|
626
|
+
p.cancel("Migration cancelled.");
|
|
627
|
+
process.exit(0);
|
|
628
|
+
} }
|
|
629
|
+
);
|
|
630
|
+
const cfAuthType = await p.select({
|
|
631
|
+
message: "Cloudflare auth method",
|
|
632
|
+
options: [
|
|
633
|
+
{ value: "global-key", label: "Global API Key", hint: "recommended \u2014 supports registrar transfers" },
|
|
634
|
+
{ value: "token", label: "Scoped API Token", hint: "limited \u2014 no registrar transfer support" }
|
|
635
|
+
]
|
|
636
|
+
});
|
|
637
|
+
if (p.isCancel(cfAuthType)) {
|
|
638
|
+
p.cancel("Migration cancelled.");
|
|
639
|
+
process.exit(0);
|
|
640
|
+
}
|
|
641
|
+
let cloudflare;
|
|
642
|
+
if (cfAuthType === "global-key") {
|
|
643
|
+
const cfCreds = await p.group(
|
|
644
|
+
{
|
|
645
|
+
email: () => p.text({
|
|
646
|
+
message: "Cloudflare account email",
|
|
647
|
+
placeholder: "you@example.com",
|
|
648
|
+
validate: (v) => {
|
|
649
|
+
if (!v?.trim()) return "Email is required";
|
|
650
|
+
}
|
|
651
|
+
}),
|
|
652
|
+
apiKey: () => p.password({
|
|
653
|
+
message: "Cloudflare Global API Key",
|
|
654
|
+
validate: (v) => {
|
|
655
|
+
if (!v?.trim()) return "API key is required";
|
|
656
|
+
}
|
|
657
|
+
}),
|
|
658
|
+
accountId: () => p.text({
|
|
659
|
+
message: "Cloudflare Account ID",
|
|
660
|
+
placeholder: "Found on any zone overview page",
|
|
661
|
+
validate: (v) => {
|
|
662
|
+
if (!v?.trim()) return "Account ID is required";
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
},
|
|
666
|
+
{ onCancel: () => {
|
|
667
|
+
p.cancel("Migration cancelled.");
|
|
668
|
+
process.exit(0);
|
|
669
|
+
} }
|
|
670
|
+
);
|
|
671
|
+
cloudflare = {
|
|
672
|
+
authType: "global-key",
|
|
673
|
+
apiKey: cfCreds.apiKey,
|
|
674
|
+
email: cfCreds.email,
|
|
675
|
+
accountId: cfCreds.accountId
|
|
676
|
+
};
|
|
677
|
+
} else {
|
|
678
|
+
const cfCreds = await p.group(
|
|
679
|
+
{
|
|
680
|
+
apiToken: () => p.password({
|
|
681
|
+
message: "Cloudflare API Token",
|
|
682
|
+
validate: (v) => {
|
|
683
|
+
if (!v?.trim()) return "API token is required";
|
|
684
|
+
}
|
|
685
|
+
}),
|
|
686
|
+
accountId: () => p.text({
|
|
687
|
+
message: "Cloudflare Account ID",
|
|
688
|
+
placeholder: "Found on any zone overview page",
|
|
689
|
+
validate: (v) => {
|
|
690
|
+
if (!v?.trim()) return "Account ID is required";
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
},
|
|
694
|
+
{ onCancel: () => {
|
|
695
|
+
p.cancel("Migration cancelled.");
|
|
696
|
+
process.exit(0);
|
|
697
|
+
} }
|
|
698
|
+
);
|
|
699
|
+
cloudflare = {
|
|
700
|
+
authType: "token",
|
|
701
|
+
apiToken: cfCreds.apiToken,
|
|
702
|
+
accountId: cfCreds.accountId
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const save = await p.confirm({
|
|
706
|
+
message: "Save credentials for future use?",
|
|
707
|
+
initialValue: true
|
|
708
|
+
});
|
|
709
|
+
if (p.isCancel(save)) {
|
|
710
|
+
p.cancel("Migration cancelled.");
|
|
711
|
+
process.exit(0);
|
|
712
|
+
}
|
|
713
|
+
const result = {
|
|
714
|
+
godaddy: {
|
|
715
|
+
apiKey: gdCreds.gdKey,
|
|
716
|
+
apiSecret: gdCreds.gdSecret
|
|
717
|
+
},
|
|
718
|
+
cloudflare
|
|
719
|
+
};
|
|
720
|
+
if (save) {
|
|
721
|
+
setConfig({
|
|
722
|
+
godaddy: result.godaddy,
|
|
723
|
+
cloudflare: cloudflare.authType === "global-key" ? { authType: "global-key", apiKey: cloudflare.apiKey, email: cloudflare.email, accountId: cloudflare.accountId } : { authType: "token", apiToken: cloudflare.apiToken, accountId: cloudflare.accountId }
|
|
724
|
+
});
|
|
725
|
+
p.log.success("Credentials saved.");
|
|
726
|
+
}
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
async function collectMigrationOptions(overrides) {
|
|
730
|
+
const options = await p.group(
|
|
731
|
+
{
|
|
732
|
+
migrateRecords: () => p.confirm({
|
|
733
|
+
message: "Migrate DNS records to Cloudflare?",
|
|
734
|
+
initialValue: true
|
|
735
|
+
}),
|
|
736
|
+
proxied: () => p.confirm({
|
|
737
|
+
message: "Proxy records through Cloudflare (orange cloud)?",
|
|
738
|
+
initialValue: false
|
|
739
|
+
})
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
onCancel: () => {
|
|
743
|
+
p.cancel("Migration cancelled.");
|
|
744
|
+
process.exit(0);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
let dryRun = overrides?.dryRun ?? false;
|
|
749
|
+
if (!overrides?.dryRun) {
|
|
750
|
+
const answer = await p.confirm({
|
|
751
|
+
message: "Dry run first? (preview without making changes)",
|
|
752
|
+
initialValue: false
|
|
753
|
+
});
|
|
754
|
+
if (p.isCancel(answer)) {
|
|
755
|
+
p.cancel("Migration cancelled.");
|
|
756
|
+
process.exit(0);
|
|
757
|
+
}
|
|
758
|
+
dryRun = answer;
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
migrateRecords: options.migrateRecords,
|
|
762
|
+
proxied: options.proxied,
|
|
763
|
+
dryRun
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
async function collectRegistrantContact() {
|
|
767
|
+
const config = getConfig();
|
|
768
|
+
if (config.registrantContact) {
|
|
769
|
+
const saved = config.registrantContact;
|
|
770
|
+
const useStored = await p.confirm({
|
|
771
|
+
message: `Use saved registrant contact? (${saved.first_name} ${saved.last_name}, ${saved.email})`,
|
|
772
|
+
initialValue: true
|
|
773
|
+
});
|
|
774
|
+
if (p.isCancel(useStored)) {
|
|
775
|
+
p.cancel("Migration cancelled.");
|
|
776
|
+
process.exit(0);
|
|
777
|
+
}
|
|
778
|
+
if (useStored) return saved;
|
|
779
|
+
}
|
|
780
|
+
p.note(
|
|
781
|
+
"ICANN requires registrant contact info for all domain transfers.\nCloudflare enables free WHOIS privacy by default, so this\ninformation will not be publicly visible after the transfer.",
|
|
782
|
+
"Registrant Contact"
|
|
783
|
+
);
|
|
784
|
+
const contact = await p.group(
|
|
785
|
+
{
|
|
786
|
+
first_name: () => p.text({
|
|
787
|
+
message: "First name",
|
|
788
|
+
validate: (v) => {
|
|
789
|
+
if (!v?.trim()) return "Required";
|
|
790
|
+
}
|
|
791
|
+
}),
|
|
792
|
+
last_name: () => p.text({
|
|
793
|
+
message: "Last name",
|
|
794
|
+
validate: (v) => {
|
|
795
|
+
if (!v?.trim()) return "Required";
|
|
796
|
+
}
|
|
797
|
+
}),
|
|
798
|
+
email: () => p.text({
|
|
799
|
+
message: "Email",
|
|
800
|
+
placeholder: "you@example.com",
|
|
801
|
+
validate: (v) => {
|
|
802
|
+
if (!v?.trim()) return "Required";
|
|
803
|
+
}
|
|
804
|
+
}),
|
|
805
|
+
phone: () => p.text({
|
|
806
|
+
message: "Phone",
|
|
807
|
+
placeholder: "+1.5551234567",
|
|
808
|
+
validate: (v) => {
|
|
809
|
+
if (!v?.trim()) return "Required";
|
|
810
|
+
}
|
|
811
|
+
}),
|
|
812
|
+
address: () => p.text({
|
|
813
|
+
message: "Street address",
|
|
814
|
+
validate: (v) => {
|
|
815
|
+
if (!v?.trim()) return "Required";
|
|
816
|
+
}
|
|
817
|
+
}),
|
|
818
|
+
address2: () => p.text({
|
|
819
|
+
message: "Address line 2 (optional)",
|
|
820
|
+
defaultValue: ""
|
|
821
|
+
}),
|
|
822
|
+
city: () => p.text({
|
|
823
|
+
message: "City",
|
|
824
|
+
validate: (v) => {
|
|
825
|
+
if (!v?.trim()) return "Required";
|
|
826
|
+
}
|
|
827
|
+
}),
|
|
828
|
+
state: () => p.text({
|
|
829
|
+
message: "State / Province",
|
|
830
|
+
validate: (v) => {
|
|
831
|
+
if (!v?.trim()) return "Required";
|
|
832
|
+
}
|
|
833
|
+
}),
|
|
834
|
+
zip: () => p.text({
|
|
835
|
+
message: "Postal code",
|
|
836
|
+
validate: (v) => {
|
|
837
|
+
if (!v?.trim()) return "Required";
|
|
838
|
+
}
|
|
839
|
+
}),
|
|
840
|
+
country: () => p.text({
|
|
841
|
+
message: "Country code",
|
|
842
|
+
placeholder: "US",
|
|
843
|
+
validate: (v) => {
|
|
844
|
+
if (!v?.trim()) return "Required";
|
|
845
|
+
}
|
|
846
|
+
})
|
|
847
|
+
},
|
|
848
|
+
{ onCancel: () => {
|
|
849
|
+
p.cancel("Migration cancelled.");
|
|
850
|
+
process.exit(0);
|
|
851
|
+
} }
|
|
852
|
+
);
|
|
853
|
+
const result = {
|
|
854
|
+
first_name: contact.first_name,
|
|
855
|
+
last_name: contact.last_name,
|
|
856
|
+
organization: "",
|
|
857
|
+
address: contact.address,
|
|
858
|
+
address2: contact.address2 ?? "",
|
|
859
|
+
city: contact.city,
|
|
860
|
+
state: contact.state,
|
|
861
|
+
zip: contact.zip,
|
|
862
|
+
country: contact.country,
|
|
863
|
+
phone: contact.phone,
|
|
864
|
+
email: contact.email
|
|
865
|
+
};
|
|
866
|
+
setConfig({ registrantContact: result });
|
|
867
|
+
p.log.success("Registrant contact saved for future transfers.");
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
async function confirmTransferCost(domainCount) {
|
|
871
|
+
p.note(
|
|
872
|
+
`Each domain transfer includes a 1-year renewal charged at
|
|
873
|
+
Cloudflare's at-cost pricing. Cost varies by TLD \u2014 common
|
|
874
|
+
examples: .com ~$9.15, .net ~$10.50, .org ~$10.00/year.
|
|
875
|
+
Other TLDs may cost more. Check Cloudflare's pricing for details.
|
|
876
|
+
|
|
877
|
+
Payment is billed to the card on file in your Cloudflare account.
|
|
878
|
+
Domains to transfer: ${chalk.bold(domainCount)}`,
|
|
879
|
+
"Transfer Cost"
|
|
880
|
+
);
|
|
881
|
+
const confirmed = await p.confirm({
|
|
882
|
+
message: `I understand that ${chalk.bold(domainCount)} domain transfer${domainCount === 1 ? "" : "s"} will be charged to my Cloudflare account`,
|
|
883
|
+
initialValue: true
|
|
884
|
+
});
|
|
885
|
+
if (p.isCancel(confirmed)) {
|
|
886
|
+
p.cancel("Migration cancelled.");
|
|
887
|
+
process.exit(0);
|
|
888
|
+
}
|
|
889
|
+
return confirmed;
|
|
890
|
+
}
|
|
891
|
+
async function confirmMigration(domainCount, dryRun) {
|
|
892
|
+
const action = dryRun ? "preview migration for" : "start migration for";
|
|
893
|
+
const confirmed = await p.confirm({
|
|
894
|
+
message: `Proceed to ${action} ${chalk.bold(domainCount)} domain${domainCount === 1 ? "" : "s"}?`,
|
|
895
|
+
initialValue: true
|
|
896
|
+
});
|
|
897
|
+
if (p.isCancel(confirmed)) {
|
|
898
|
+
p.cancel("Migration cancelled.");
|
|
899
|
+
process.exit(0);
|
|
900
|
+
}
|
|
901
|
+
return confirmed;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/ui/domain-selector.ts
|
|
905
|
+
import * as p2 from "@clack/prompts";
|
|
906
|
+
import chalk2 from "chalk";
|
|
907
|
+
async function selectDomains(domains, selectAll) {
|
|
908
|
+
if (selectAll) {
|
|
909
|
+
return domains.map((d) => d.domain);
|
|
910
|
+
}
|
|
911
|
+
const mode = await p2.select({
|
|
912
|
+
message: `${domains.length} domains available \u2014 migrate all or choose?`,
|
|
913
|
+
options: [
|
|
914
|
+
{ value: "all", label: "All domains" },
|
|
915
|
+
{ value: "pick", label: "Let me choose" }
|
|
916
|
+
]
|
|
917
|
+
});
|
|
918
|
+
if (p2.isCancel(mode)) {
|
|
919
|
+
p2.cancel("Migration cancelled.");
|
|
920
|
+
process.exit(0);
|
|
921
|
+
}
|
|
922
|
+
if (mode === "all") {
|
|
923
|
+
return domains.map((d) => d.domain);
|
|
924
|
+
}
|
|
925
|
+
const options = domains.map((d) => {
|
|
926
|
+
const expires = d.expires ? new Date(d.expires).toLocaleDateString() : "unknown";
|
|
927
|
+
const locked = d.locked ? "locked" : "unlocked";
|
|
928
|
+
const privacy = d.privacy ? "privacy" : "no privacy";
|
|
929
|
+
let expiryWarning = "";
|
|
930
|
+
if (d.expires) {
|
|
931
|
+
const daysUntilExpiry = (new Date(d.expires).getTime() - Date.now()) / (1e3 * 60 * 60 * 24);
|
|
932
|
+
if (daysUntilExpiry < 30) {
|
|
933
|
+
expiryWarning = chalk2.yellow(" \u26A0 expires soon");
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
value: d.domain,
|
|
938
|
+
label: d.domain,
|
|
939
|
+
hint: `expires ${expires}, ${locked}, ${privacy}${expiryWarning}`
|
|
940
|
+
};
|
|
941
|
+
});
|
|
942
|
+
const selected = await p2.multiselect({
|
|
943
|
+
message: `Select domains to migrate (${domains.length} available)`,
|
|
944
|
+
options,
|
|
945
|
+
required: true
|
|
946
|
+
});
|
|
947
|
+
if (p2.isCancel(selected)) {
|
|
948
|
+
p2.cancel("Migration cancelled.");
|
|
949
|
+
process.exit(0);
|
|
950
|
+
}
|
|
951
|
+
return selected;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/ui/dns-preview.ts
|
|
955
|
+
import * as p3 from "@clack/prompts";
|
|
956
|
+
import chalk3 from "chalk";
|
|
957
|
+
|
|
958
|
+
// src/services/dns-migrator.ts
|
|
959
|
+
var SKIP_TYPES = /* @__PURE__ */ new Set(["SOA"]);
|
|
960
|
+
function isGoDaddyParking(record) {
|
|
961
|
+
if (record.type === "A" && record.name === "@" && record.data === "Parked") {
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
if (record.type === "CNAME" && (record.data.endsWith(".secureserver.net") || record.data.endsWith(".domaincontrol.com"))) {
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
function mapGoDaddyToCloudflare(records, domain, proxied = false) {
|
|
970
|
+
const mapped = [];
|
|
971
|
+
for (const record of records) {
|
|
972
|
+
if (SKIP_TYPES.has(record.type)) continue;
|
|
973
|
+
if (record.type === "NS" && record.name === "@") continue;
|
|
974
|
+
if (isGoDaddyParking(record)) continue;
|
|
975
|
+
const cfRecord = mapRecord(record, domain, proxied);
|
|
976
|
+
if (cfRecord) mapped.push(cfRecord);
|
|
977
|
+
}
|
|
978
|
+
return mapped;
|
|
979
|
+
}
|
|
980
|
+
function mapRecord(record, domain, proxied) {
|
|
981
|
+
const name = record.name === "@" ? domain : `${record.name}.${domain}`;
|
|
982
|
+
const ttl = record.ttl < 120 ? 1 : record.ttl;
|
|
983
|
+
switch (record.type) {
|
|
984
|
+
case "A":
|
|
985
|
+
case "AAAA":
|
|
986
|
+
return {
|
|
987
|
+
type: record.type,
|
|
988
|
+
name,
|
|
989
|
+
content: record.data,
|
|
990
|
+
ttl,
|
|
991
|
+
proxied
|
|
992
|
+
};
|
|
993
|
+
case "CNAME":
|
|
994
|
+
return {
|
|
995
|
+
type: "CNAME",
|
|
996
|
+
name,
|
|
997
|
+
content: record.data === "@" ? domain : record.data,
|
|
998
|
+
ttl,
|
|
999
|
+
proxied
|
|
1000
|
+
};
|
|
1001
|
+
case "MX":
|
|
1002
|
+
return {
|
|
1003
|
+
type: "MX",
|
|
1004
|
+
name,
|
|
1005
|
+
content: record.data,
|
|
1006
|
+
ttl,
|
|
1007
|
+
priority: record.priority ?? 10
|
|
1008
|
+
};
|
|
1009
|
+
case "TXT":
|
|
1010
|
+
return {
|
|
1011
|
+
type: "TXT",
|
|
1012
|
+
name,
|
|
1013
|
+
content: record.data,
|
|
1014
|
+
ttl
|
|
1015
|
+
};
|
|
1016
|
+
case "SRV": {
|
|
1017
|
+
const srvName = record.service && record.protocol ? `${record.service}.${record.protocol}.${name}` : name;
|
|
1018
|
+
return {
|
|
1019
|
+
type: "SRV",
|
|
1020
|
+
name: srvName,
|
|
1021
|
+
content: `${record.priority ?? 0} ${record.weight ?? 0} ${record.port ?? 0} ${record.data}`,
|
|
1022
|
+
ttl,
|
|
1023
|
+
data: {
|
|
1024
|
+
priority: record.priority ?? 0,
|
|
1025
|
+
weight: record.weight ?? 0,
|
|
1026
|
+
port: record.port ?? 0,
|
|
1027
|
+
target: record.data,
|
|
1028
|
+
service: record.service ?? "",
|
|
1029
|
+
proto: record.protocol ?? "",
|
|
1030
|
+
name
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
case "CAA": {
|
|
1035
|
+
const parts = record.data.match(/^(\d+)\s+(\S+)\s+(.+)$/);
|
|
1036
|
+
if (!parts) return null;
|
|
1037
|
+
return {
|
|
1038
|
+
type: "CAA",
|
|
1039
|
+
name,
|
|
1040
|
+
content: record.data,
|
|
1041
|
+
ttl,
|
|
1042
|
+
data: {
|
|
1043
|
+
flags: parseInt(parts[1], 10),
|
|
1044
|
+
tag: parts[2],
|
|
1045
|
+
value: parts[3]
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
case "NS":
|
|
1050
|
+
return {
|
|
1051
|
+
type: "NS",
|
|
1052
|
+
name,
|
|
1053
|
+
content: record.data,
|
|
1054
|
+
ttl
|
|
1055
|
+
};
|
|
1056
|
+
default:
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function migrateDnsRecords(cloudflare, zoneId, records) {
|
|
1061
|
+
let created = 0;
|
|
1062
|
+
const failed = [];
|
|
1063
|
+
for (const record of records) {
|
|
1064
|
+
try {
|
|
1065
|
+
await cloudflare.createDnsRecord(zoneId, record);
|
|
1066
|
+
created++;
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1069
|
+
if (message.includes("already exists")) {
|
|
1070
|
+
created++;
|
|
1071
|
+
} else {
|
|
1072
|
+
failed.push({ record, error: message });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return { created, failed };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/ui/dns-preview.ts
|
|
1080
|
+
function summarizeRecords(records) {
|
|
1081
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1082
|
+
for (const r of records) {
|
|
1083
|
+
counts.set(r.type, (counts.get(r.type) ?? 0) + 1);
|
|
1084
|
+
}
|
|
1085
|
+
const parts = [...counts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([type, count]) => `${count} ${type}`);
|
|
1086
|
+
return `${parts.join(", ")} ${chalk3.dim(`(${records.length} total)`)}`;
|
|
1087
|
+
}
|
|
1088
|
+
function formatRecordDetail(record) {
|
|
1089
|
+
const type = chalk3.cyan(record.type.padEnd(6));
|
|
1090
|
+
const name = chalk3.bold(record.name);
|
|
1091
|
+
const content = record.content.length > 60 ? record.content.slice(0, 57) + "..." : record.content;
|
|
1092
|
+
const priority = record.priority !== void 0 ? ` (pri: ${record.priority})` : "";
|
|
1093
|
+
return ` ${type} ${name} \u2192 ${content}${priority}`;
|
|
1094
|
+
}
|
|
1095
|
+
async function previewDnsRecords(godaddy, domains) {
|
|
1096
|
+
const s = p3.spinner();
|
|
1097
|
+
s.start(`Fetching DNS records for ${domains.length} domain${domains.length === 1 ? "" : "s"}...`);
|
|
1098
|
+
const previews = [];
|
|
1099
|
+
const errors = [];
|
|
1100
|
+
for (const domain of domains) {
|
|
1101
|
+
try {
|
|
1102
|
+
const gdRecords = await godaddy.getDnsRecords(domain);
|
|
1103
|
+
const cfRecords = mapGoDaddyToCloudflare(gdRecords, domain);
|
|
1104
|
+
previews.push({ domain, records: cfRecords });
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
errors.push({ domain, error: err instanceof Error ? err.message : String(err) });
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
s.stop("DNS records fetched");
|
|
1110
|
+
p3.log.message(chalk3.bold("DNS records to migrate:"));
|
|
1111
|
+
for (const { domain, records } of previews) {
|
|
1112
|
+
if (records.length === 0) {
|
|
1113
|
+
p3.log.message(` ${chalk3.bold(domain)} \u2192 ${chalk3.dim("no records to migrate")}`);
|
|
1114
|
+
} else {
|
|
1115
|
+
p3.log.message(` ${chalk3.bold(domain)} \u2192 ${summarizeRecords(records)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
for (const { domain, error } of errors) {
|
|
1119
|
+
p3.log.message(` ${chalk3.bold(domain)} \u2192 ${chalk3.red(`failed: ${error}`)}`);
|
|
1120
|
+
}
|
|
1121
|
+
const domainsWithRecords = previews.filter((p9) => p9.records.length > 0);
|
|
1122
|
+
if (domainsWithRecords.length === 0) return;
|
|
1123
|
+
let viewMore = true;
|
|
1124
|
+
while (viewMore) {
|
|
1125
|
+
const choice = await p3.select({
|
|
1126
|
+
message: "View detailed records for a domain?",
|
|
1127
|
+
options: [
|
|
1128
|
+
{ value: "__skip__", label: "Continue", hint: "proceed to migration options" },
|
|
1129
|
+
...domainsWithRecords.map((d) => ({
|
|
1130
|
+
value: d.domain,
|
|
1131
|
+
label: d.domain,
|
|
1132
|
+
hint: `${d.records.length} records`
|
|
1133
|
+
}))
|
|
1134
|
+
]
|
|
1135
|
+
});
|
|
1136
|
+
if (p3.isCancel(choice) || choice === "__skip__") {
|
|
1137
|
+
viewMore = false;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
const preview = domainsWithRecords.find((d) => d.domain === choice);
|
|
1141
|
+
if (preview) {
|
|
1142
|
+
p3.log.message(chalk3.bold(`
|
|
1143
|
+
${preview.domain} records:`));
|
|
1144
|
+
for (const record of preview.records) {
|
|
1145
|
+
p3.log.message(formatRecordDetail(record));
|
|
1146
|
+
}
|
|
1147
|
+
p3.log.message("");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/services/transfer-engine.ts
|
|
1153
|
+
init_state_manager();
|
|
1154
|
+
|
|
1155
|
+
// src/services/errors.ts
|
|
1156
|
+
import chalk4 from "chalk";
|
|
1157
|
+
var GODADDY_HINTS = [
|
|
1158
|
+
{
|
|
1159
|
+
match: (_, status) => status === 401 || status === 403,
|
|
1160
|
+
suggestion: "Check your GoDaddy API key and secret at https://developer.godaddy.com/keys"
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
match: (_, status) => status === 429,
|
|
1164
|
+
suggestion: "GoDaddy rate limit hit. Wait a minute and try again, or reduce concurrency."
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
match: (msg) => msg.includes("DOMAIN_LOCKED"),
|
|
1168
|
+
suggestion: "Domain is locked. It may take a few minutes after unlocking before GoDaddy reflects the change."
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
match: (msg) => msg.includes("409") || msg.includes("Conflict"),
|
|
1172
|
+
suggestion: "The auth code may have been sent to your email instead. Check your inbox and use `nodaddy resume` to continue."
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
match: (msg) => msg.includes("UNABLE_TO_AUTHENTICATE") || msg.includes("NOT_FOUND"),
|
|
1176
|
+
suggestion: "Make sure you are using a Production API key (not OTE/Test) at https://developer.godaddy.com/keys"
|
|
1177
|
+
}
|
|
1178
|
+
];
|
|
1179
|
+
var CLOUDFLARE_HINTS = [
|
|
1180
|
+
{
|
|
1181
|
+
match: (_, status) => status === 401 || status === 403,
|
|
1182
|
+
suggestion: "Check your Cloudflare API token permissions. Required: Zone:Edit, DNS:Edit, Registrar Domains:Edit"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
match: (msg) => msg.includes("already exists"),
|
|
1186
|
+
suggestion: "This zone already exists in Cloudflare. You may need to delete it first or use the existing zone."
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
match: (_, status) => status === 429,
|
|
1190
|
+
suggestion: "Cloudflare rate limit hit. The tool will automatically retry with backoff."
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
match: (msg) => msg.includes("not_registrable"),
|
|
1194
|
+
suggestion: "This TLD cannot be transferred to Cloudflare Registrar. DNS-only setup is still possible."
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
match: (msg) => msg.includes("did not become active"),
|
|
1198
|
+
suggestion: "Nameserver changes can take up to 48 hours to propagate. Run `nodaddy resume` to retry later."
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
function formatError(err, provider, plain = false) {
|
|
1202
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1203
|
+
const statusCode = err.statusCode;
|
|
1204
|
+
const hints = provider === "godaddy" ? GODADDY_HINTS : provider === "cloudflare" ? CLOUDFLARE_HINTS : [...GODADDY_HINTS, ...CLOUDFLARE_HINTS];
|
|
1205
|
+
const hint = hints.find((h) => h.match(message, statusCode));
|
|
1206
|
+
if (hint) {
|
|
1207
|
+
return plain ? `${message} | Suggestion: ${hint.suggestion}` : `${message}
|
|
1208
|
+
${chalk4.yellow("Suggestion:")} ${hint.suggestion}`;
|
|
1209
|
+
}
|
|
1210
|
+
return message;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/services/transfer-engine.ts
|
|
1214
|
+
var GODADDY_RESOURCE_LOCK_DELAY_MS = 5e3;
|
|
1215
|
+
var UNSUPPORTED_TLDS = /* @__PURE__ */ new Set([
|
|
1216
|
+
"uk",
|
|
1217
|
+
"co.uk",
|
|
1218
|
+
"org.uk",
|
|
1219
|
+
"me.uk",
|
|
1220
|
+
"de",
|
|
1221
|
+
"ca",
|
|
1222
|
+
"au",
|
|
1223
|
+
"com.au",
|
|
1224
|
+
"net.au",
|
|
1225
|
+
"jp",
|
|
1226
|
+
"eu",
|
|
1227
|
+
"be",
|
|
1228
|
+
"fr",
|
|
1229
|
+
"nl"
|
|
1230
|
+
]);
|
|
1231
|
+
function preflightCheck(domain) {
|
|
1232
|
+
const reasons = [];
|
|
1233
|
+
if (domain.status !== "ACTIVE") {
|
|
1234
|
+
reasons.push(
|
|
1235
|
+
`Status is ${domain.status} \u2014 domain must be ACTIVE to transfer. Check your GoDaddy dashboard for holds or suspensions.`
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
if (domain.createdAt) {
|
|
1239
|
+
const created = new Date(domain.createdAt);
|
|
1240
|
+
const daysSinceCreation = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
1241
|
+
if (daysSinceCreation < 60) {
|
|
1242
|
+
const daysRemaining = Math.ceil(60 - daysSinceCreation);
|
|
1243
|
+
reasons.push(
|
|
1244
|
+
`Domain is only ${Math.floor(daysSinceCreation)} days old \u2014 ICANN requires 60 days before transfer. Try again in ${daysRemaining} day${daysRemaining === 1 ? "" : "s"}.`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const tld = domain.domain.split(".").slice(1).join(".");
|
|
1249
|
+
if (UNSUPPORTED_TLDS.has(tld)) {
|
|
1250
|
+
reasons.push(
|
|
1251
|
+
`TLD .${tld} is not supported by Cloudflare Registrar \u2014 see https://www.cloudflare.com/tld-policies/ for supported TLDs`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (domain.transferProtected) {
|
|
1255
|
+
reasons.push(
|
|
1256
|
+
"Domain Protection is enabled \u2014 disable at https://dcc.godaddy.com \u2192 select domain \u2192 Secure \u2192 downgrade to None (requires identity verification)"
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
domain: domain.domain,
|
|
1261
|
+
eligible: reasons.length === 0,
|
|
1262
|
+
reasons
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
async function transferDomain(godaddy, cloudflare, domain, migrationId, options, contact, onProgress) {
|
|
1266
|
+
const report = (step, status, error) => {
|
|
1267
|
+
onProgress?.({ domain, step, status, error });
|
|
1268
|
+
};
|
|
1269
|
+
const retryReporter = (action, currentStatus) => (attempt, maxRetries, delaySec) => {
|
|
1270
|
+
report(
|
|
1271
|
+
`${action} \u2014 GoDaddy resource locked, retrying in ${delaySec}s (${attempt}/${maxRetries})`,
|
|
1272
|
+
currentStatus
|
|
1273
|
+
);
|
|
1274
|
+
};
|
|
1275
|
+
try {
|
|
1276
|
+
report("Exporting DNS records", "pending");
|
|
1277
|
+
const dnsRecords = await godaddy.getDnsRecords(domain);
|
|
1278
|
+
saveDnsBackup(migrationId, domain, dnsRecords);
|
|
1279
|
+
if (options.dryRun) {
|
|
1280
|
+
report("Dry run \u2014 would migrate DNS records", "pending");
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
report("Creating Cloudflare zone", "pending");
|
|
1284
|
+
let zone;
|
|
1285
|
+
try {
|
|
1286
|
+
zone = await cloudflare.createZone(domain);
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
const message = err instanceof Error ? err.message : "";
|
|
1289
|
+
if (message.includes("already exists")) {
|
|
1290
|
+
report("Zone already exists, looking up", "pending");
|
|
1291
|
+
const existing = await cloudflare.getZoneByName(domain);
|
|
1292
|
+
if (existing) {
|
|
1293
|
+
zone = existing;
|
|
1294
|
+
} else {
|
|
1295
|
+
throw err;
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
throw err;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const zoneId = zone.id;
|
|
1302
|
+
const nameservers = zone.name_servers ?? [];
|
|
1303
|
+
updateDomainStatus(migrationId, domain, "pending", {
|
|
1304
|
+
cloudflareZoneId: zoneId,
|
|
1305
|
+
cloudflareNameservers: nameservers
|
|
1306
|
+
});
|
|
1307
|
+
if (options.migrateRecords) {
|
|
1308
|
+
report("Migrating DNS records", "pending");
|
|
1309
|
+
const cfRecords = mapGoDaddyToCloudflare(dnsRecords, domain, options.proxied);
|
|
1310
|
+
const result = await migrateDnsRecords(cloudflare, zoneId, cfRecords);
|
|
1311
|
+
if (result.failed.length > 0) {
|
|
1312
|
+
report(
|
|
1313
|
+
`DNS migration: ${result.created} created, ${result.failed.length} failed`,
|
|
1314
|
+
"pending"
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
updateDomainStatus(migrationId, domain, "dns_migrated");
|
|
1319
|
+
report("DNS migrated", "dns_migrated");
|
|
1320
|
+
report("Removing WHOIS privacy", "dns_migrated");
|
|
1321
|
+
try {
|
|
1322
|
+
await godaddy.removePrivacy(domain, retryReporter("Removing WHOIS privacy", "dns_migrated"));
|
|
1323
|
+
} catch (privacyErr) {
|
|
1324
|
+
const msg = privacyErr instanceof Error ? privacyErr.message : "";
|
|
1325
|
+
if (!msg.includes("404") && !msg.includes("409")) {
|
|
1326
|
+
report(`Privacy removal failed (non-blocking): ${msg}`, "dns_migrated");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
await new Promise((r) => setTimeout(r, GODADDY_RESOURCE_LOCK_DELAY_MS));
|
|
1330
|
+
report("Unlocking + disabling auto-renew", "dns_migrated");
|
|
1331
|
+
await godaddy.prepareForTransfer(domain, retryReporter("Unlocking + disabling auto-renew", "dns_migrated"));
|
|
1332
|
+
report("Waiting for unlock to propagate", "dns_migrated");
|
|
1333
|
+
let unlocked = false;
|
|
1334
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
1335
|
+
await new Promise((r) => setTimeout(r, GODADDY_RESOURCE_LOCK_DELAY_MS));
|
|
1336
|
+
const detail = await godaddy.getDomainDetail(domain);
|
|
1337
|
+
if (!detail.locked) {
|
|
1338
|
+
unlocked = true;
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (!unlocked) {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
`Domain ${domain} is still locked after prepareForTransfer (unlock + disable auto-renew)`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
updateDomainStatus(migrationId, domain, "unlocked");
|
|
1348
|
+
report("Domain unlocked", "unlocked");
|
|
1349
|
+
report("Fetching auth code", "unlocked");
|
|
1350
|
+
const authCode = await godaddy.getAuthCode(domain);
|
|
1351
|
+
updateDomainStatus(migrationId, domain, "auth_obtained");
|
|
1352
|
+
report("Auth code obtained", "auth_obtained");
|
|
1353
|
+
if (nameservers.length > 0) {
|
|
1354
|
+
report("Updating nameservers", "auth_obtained");
|
|
1355
|
+
await godaddy.updateNameservers(domain, nameservers, retryReporter("Updating nameservers", "auth_obtained"));
|
|
1356
|
+
updateDomainStatus(migrationId, domain, "ns_changed");
|
|
1357
|
+
report("Nameservers updated", "ns_changed");
|
|
1358
|
+
}
|
|
1359
|
+
if (contact) {
|
|
1360
|
+
report("Waiting for zone activation (may take a few minutes)", "ns_changed");
|
|
1361
|
+
await cloudflare.waitForZoneActive(zoneId);
|
|
1362
|
+
report("Validating auth code", "ns_changed");
|
|
1363
|
+
await cloudflare.checkAuthCode(domain, authCode);
|
|
1364
|
+
report("Initiating transfer", "ns_changed");
|
|
1365
|
+
await cloudflare.initiateTransfer(zoneId, domain, authCode, contact);
|
|
1366
|
+
updateDomainStatus(migrationId, domain, "transfer_initiated");
|
|
1367
|
+
report("Transfer initiated", "transfer_initiated");
|
|
1368
|
+
} else {
|
|
1369
|
+
updateDomainStatus(migrationId, domain, "completed");
|
|
1370
|
+
report("DNS migrated (no registrar transfer \u2014 use Global API Key to enable)", "completed");
|
|
1371
|
+
}
|
|
1372
|
+
return { authCode };
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
1375
|
+
const provider = rawMessage.includes("GoDaddy") ? "godaddy" : rawMessage.includes("Cloudflare") ? "cloudflare" : void 0;
|
|
1376
|
+
const message = formatError(err, provider);
|
|
1377
|
+
const persistedError = formatError(err, provider, true);
|
|
1378
|
+
updateDomainStatus(migrationId, domain, "failed", {
|
|
1379
|
+
error: persistedError
|
|
1380
|
+
});
|
|
1381
|
+
report(message, "failed", message);
|
|
1382
|
+
throw err;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/ui/progress.ts
|
|
1387
|
+
import { Listr } from "listr2";
|
|
1388
|
+
function createMigrationTasks(domains, godaddy, cloudflare, migrationId, options, contact) {
|
|
1389
|
+
return new Listr(
|
|
1390
|
+
domains.map((domain) => ({
|
|
1391
|
+
title: domain,
|
|
1392
|
+
task: async (ctx, task) => {
|
|
1393
|
+
try {
|
|
1394
|
+
const result = await transferDomain(
|
|
1395
|
+
godaddy,
|
|
1396
|
+
cloudflare,
|
|
1397
|
+
domain,
|
|
1398
|
+
migrationId,
|
|
1399
|
+
options,
|
|
1400
|
+
contact,
|
|
1401
|
+
(progress) => {
|
|
1402
|
+
task.title = `${domain} \u2014 ${progress.step}`;
|
|
1403
|
+
}
|
|
1404
|
+
);
|
|
1405
|
+
ctx.results.set(domain, {
|
|
1406
|
+
success: true,
|
|
1407
|
+
authCode: result?.authCode
|
|
1408
|
+
});
|
|
1409
|
+
task.title = `${domain} \u2713`;
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1412
|
+
ctx.results.set(domain, { success: false, error: message });
|
|
1413
|
+
task.title = `${domain} \u2717 ${message}`;
|
|
1414
|
+
throw err;
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
exitOnError: false
|
|
1418
|
+
})),
|
|
1419
|
+
{
|
|
1420
|
+
concurrent: 8,
|
|
1421
|
+
exitOnError: false,
|
|
1422
|
+
rendererOptions: {
|
|
1423
|
+
collapseErrors: false
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/commands/migrate.ts
|
|
1430
|
+
init_state_manager();
|
|
1431
|
+
async function migrateCommand(opts) {
|
|
1432
|
+
p4.intro(chalk5.bgCyan.black(" nodaddy "));
|
|
1433
|
+
const creds = await collectCredentials();
|
|
1434
|
+
const s = p4.spinner();
|
|
1435
|
+
s.start("Verifying API credentials...");
|
|
1436
|
+
const godaddy = new GoDaddyClient(creds.godaddy);
|
|
1437
|
+
const cloudflare = new CloudflareClient(creds.cloudflare);
|
|
1438
|
+
const [gdValid, cfValid] = await Promise.all([
|
|
1439
|
+
godaddy.verifyCredentials(),
|
|
1440
|
+
cloudflare.verifyCredentials()
|
|
1441
|
+
]);
|
|
1442
|
+
if (!gdValid) {
|
|
1443
|
+
s.stop("GoDaddy credentials invalid");
|
|
1444
|
+
p4.log.error("Failed to authenticate with GoDaddy API. Check your API key and secret.");
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
if (!cfValid) {
|
|
1448
|
+
s.stop("Cloudflare credentials invalid");
|
|
1449
|
+
p4.log.error("Failed to authenticate with Cloudflare API. Check your API token.");
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
s.stop("API credentials verified");
|
|
1453
|
+
s.start("Fetching domains from GoDaddy...");
|
|
1454
|
+
let domains;
|
|
1455
|
+
try {
|
|
1456
|
+
domains = await godaddy.listDomains();
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
s.stop("Failed to fetch domains");
|
|
1459
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1460
|
+
process.exit(1);
|
|
1461
|
+
}
|
|
1462
|
+
s.stop(`Found ${chalk5.bold(domains.length)} active domains`);
|
|
1463
|
+
if (domains.length === 0) {
|
|
1464
|
+
p4.log.warn("No active domains found in your GoDaddy account.");
|
|
1465
|
+
p4.outro("Nothing to migrate.");
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const selected = await selectDomains(domains, opts.all ?? false);
|
|
1469
|
+
if (selected.length === 0) {
|
|
1470
|
+
p4.outro("No domains selected.");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
s.start("Running preflight checks...");
|
|
1474
|
+
const selectedDomainDetails = await Promise.all(
|
|
1475
|
+
selected.map((d) => godaddy.getDomainDetail(d))
|
|
1476
|
+
);
|
|
1477
|
+
const preflightResults = selectedDomainDetails.map(preflightCheck);
|
|
1478
|
+
const eligible = preflightResults.filter((r) => r.eligible);
|
|
1479
|
+
const ineligible = preflightResults.filter((r) => !r.eligible);
|
|
1480
|
+
s.stop(
|
|
1481
|
+
`${chalk5.green(eligible.length)} eligible, ${chalk5.red(ineligible.length)} ineligible`
|
|
1482
|
+
);
|
|
1483
|
+
if (ineligible.length > 0) {
|
|
1484
|
+
p4.log.warn("Ineligible domains:");
|
|
1485
|
+
for (const result of ineligible) {
|
|
1486
|
+
p4.log.message(
|
|
1487
|
+
` ${chalk5.red("\u2717")} ${result.domain}: ${result.reasons.join(", ")}`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (eligible.length === 0) {
|
|
1492
|
+
p4.outro("No eligible domains to migrate.");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const eligibleDomains = eligible.map((r) => r.domain);
|
|
1496
|
+
await previewDnsRecords(godaddy, eligibleDomains);
|
|
1497
|
+
const migrationOptions = await collectMigrationOptions(
|
|
1498
|
+
opts.dryRun ? { dryRun: true } : void 0
|
|
1499
|
+
);
|
|
1500
|
+
const canTransfer = creds.cloudflare.authType === "global-key";
|
|
1501
|
+
if (!migrationOptions.dryRun && canTransfer) {
|
|
1502
|
+
const costConfirmed = await confirmTransferCost(eligible.length);
|
|
1503
|
+
if (!costConfirmed) {
|
|
1504
|
+
p4.outro("Migration cancelled.");
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
let contact;
|
|
1509
|
+
if (migrationOptions.dryRun) {
|
|
1510
|
+
contact = void 0;
|
|
1511
|
+
} else if (canTransfer) {
|
|
1512
|
+
contact = await collectRegistrantContact();
|
|
1513
|
+
} else {
|
|
1514
|
+
p4.log.warn(
|
|
1515
|
+
"Scoped API tokens do not support registrar transfers. DNS will be migrated but domains will not be transferred."
|
|
1516
|
+
);
|
|
1517
|
+
contact = void 0;
|
|
1518
|
+
}
|
|
1519
|
+
const confirmed = await confirmMigration(
|
|
1520
|
+
eligible.length,
|
|
1521
|
+
migrationOptions.dryRun
|
|
1522
|
+
);
|
|
1523
|
+
if (!confirmed) {
|
|
1524
|
+
p4.outro("Migration cancelled.");
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const migration = createMigration(eligibleDomains);
|
|
1528
|
+
p4.log.step(
|
|
1529
|
+
migrationOptions.dryRun ? "Starting dry run..." : "Starting migration..."
|
|
1530
|
+
);
|
|
1531
|
+
const tasks = createMigrationTasks(
|
|
1532
|
+
eligibleDomains,
|
|
1533
|
+
godaddy,
|
|
1534
|
+
cloudflare,
|
|
1535
|
+
migration.id,
|
|
1536
|
+
migrationOptions,
|
|
1537
|
+
contact
|
|
1538
|
+
);
|
|
1539
|
+
const ctx = { results: /* @__PURE__ */ new Map() };
|
|
1540
|
+
try {
|
|
1541
|
+
await tasks.run(ctx);
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
const succeeded = eligibleDomains.filter(
|
|
1545
|
+
(d) => ctx.results.get(d)?.success
|
|
1546
|
+
);
|
|
1547
|
+
const failed = eligibleDomains.filter(
|
|
1548
|
+
(d) => !ctx.results.get(d)?.success
|
|
1549
|
+
);
|
|
1550
|
+
if (migrationOptions.dryRun) {
|
|
1551
|
+
p4.log.success(`Preview run finished for ${succeeded.length}/${eligible.length} domains`);
|
|
1552
|
+
} else if (succeeded.length === eligible.length) {
|
|
1553
|
+
p4.log.success(`Migration run finished for ${succeeded.length}/${eligible.length} domains`);
|
|
1554
|
+
} else if (succeeded.length > 0) {
|
|
1555
|
+
p4.log.warn(
|
|
1556
|
+
`Migration run finished: ${chalk5.green(succeeded.length)} succeeded, ${chalk5.red(failed.length)} failed`
|
|
1557
|
+
);
|
|
1558
|
+
} else {
|
|
1559
|
+
p4.log.error(
|
|
1560
|
+
`Migration failed for all ${eligible.length} domain${eligible.length === 1 ? "" : "s"}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (!migrationOptions.dryRun && succeeded.length > 0) {
|
|
1564
|
+
p4.note(
|
|
1565
|
+
`Transfers initiated for ${succeeded.length} domain${succeeded.length === 1 ? "" : "s"}.
|
|
1566
|
+
|
|
1567
|
+
Track progress:
|
|
1568
|
+
${chalk5.cyan("nodaddy status")}
|
|
1569
|
+
|
|
1570
|
+
https://dash.cloudflare.com/?to=/:account/domains/transfer
|
|
1571
|
+
|
|
1572
|
+
Transfers typically take 1-5 days to complete.
|
|
1573
|
+
|
|
1574
|
+
When you're done transferring domains, run
|
|
1575
|
+
${chalk5.cyan("nodaddy cleanup")}
|
|
1576
|
+
to remove stored credentials and personal info from this machine.`,
|
|
1577
|
+
"Next Steps"
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
if (!migrationOptions.dryRun && failed.length > 0) {
|
|
1581
|
+
p4.note(
|
|
1582
|
+
`${failed.length} domain${failed.length === 1 ? "" : "s"} failed. Your progress has been saved.
|
|
1583
|
+
|
|
1584
|
+
To retry failed domains:
|
|
1585
|
+
${chalk5.cyan("nodaddy resume")}
|
|
1586
|
+
|
|
1587
|
+
To see what went wrong:
|
|
1588
|
+
${chalk5.cyan("nodaddy status")}
|
|
1589
|
+
|
|
1590
|
+
Common fixes:
|
|
1591
|
+
\u2022 "Resource is being used" \u2014 GoDaddy is still processing a
|
|
1592
|
+
recent change. Wait a few minutes and run resume.
|
|
1593
|
+
\u2022 Domain Protection \u2014 disable at https://dcc.godaddy.com
|
|
1594
|
+
\u2022 Auth code issues \u2014 check your GoDaddy email inbox`,
|
|
1595
|
+
"Failed Domains"
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
p4.outro(chalk5.green("Done!"));
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/commands/list.ts
|
|
1602
|
+
import * as p5 from "@clack/prompts";
|
|
1603
|
+
import chalk6 from "chalk";
|
|
1604
|
+
import Table from "cli-table3";
|
|
1605
|
+
init_state_manager();
|
|
1606
|
+
async function listCommand() {
|
|
1607
|
+
p5.intro(chalk6.bgCyan.black(" nodaddy \u2014 list domains "));
|
|
1608
|
+
const gdKey = process.env.GODADDY_API_KEY;
|
|
1609
|
+
const gdSecret = process.env.GODADDY_API_SECRET;
|
|
1610
|
+
const config = getConfig();
|
|
1611
|
+
const apiKey = gdKey || config.godaddy?.apiKey;
|
|
1612
|
+
const apiSecret = gdSecret || config.godaddy?.apiSecret;
|
|
1613
|
+
if (!apiKey || !apiSecret) {
|
|
1614
|
+
p5.log.error(
|
|
1615
|
+
"GoDaddy credentials not configured. Set GODADDY_API_KEY/GODADDY_API_SECRET env vars, or run `nodaddy migrate`."
|
|
1616
|
+
);
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
const godaddy = new GoDaddyClient({ apiKey, apiSecret });
|
|
1620
|
+
const s = p5.spinner();
|
|
1621
|
+
s.start("Fetching domains from GoDaddy...");
|
|
1622
|
+
try {
|
|
1623
|
+
const domains = await godaddy.listDomains();
|
|
1624
|
+
s.stop(`Found ${chalk6.bold(domains.length)} active domains`);
|
|
1625
|
+
if (domains.length === 0) {
|
|
1626
|
+
p5.log.info("No active domains found.");
|
|
1627
|
+
p5.outro("");
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const table = new Table({
|
|
1631
|
+
head: ["Domain", "Expires", "Locked", "Privacy", "Auto-Renew"],
|
|
1632
|
+
style: { head: ["cyan"] }
|
|
1633
|
+
});
|
|
1634
|
+
for (const d of domains) {
|
|
1635
|
+
const expires = d.expires ? new Date(d.expires).toLocaleDateString() : "\u2014";
|
|
1636
|
+
const locked = d.locked ? chalk6.yellow("Yes") : chalk6.green("No");
|
|
1637
|
+
const privacy = d.privacy ? chalk6.yellow("Yes") : chalk6.dim("No");
|
|
1638
|
+
const autoRenew = d.renewAuto ? chalk6.yellow("Yes") : chalk6.dim("No");
|
|
1639
|
+
table.push([d.domain, expires, locked, privacy, autoRenew]);
|
|
1640
|
+
}
|
|
1641
|
+
console.log(table.toString());
|
|
1642
|
+
p5.outro(`${domains.length} domains total`);
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
s.stop("Failed to fetch domains");
|
|
1645
|
+
p5.log.error(err instanceof Error ? err.message : String(err));
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// src/commands/status.ts
|
|
1651
|
+
init_state_manager();
|
|
1652
|
+
import * as p6 from "@clack/prompts";
|
|
1653
|
+
import chalk7 from "chalk";
|
|
1654
|
+
import Table2 from "cli-table3";
|
|
1655
|
+
var STATUS_COLORS = {
|
|
1656
|
+
pending: chalk7.dim,
|
|
1657
|
+
dns_migrated: chalk7.blue,
|
|
1658
|
+
unlocked: chalk7.blue,
|
|
1659
|
+
auth_obtained: chalk7.blue,
|
|
1660
|
+
ns_changed: chalk7.cyan,
|
|
1661
|
+
transfer_initiated: chalk7.yellow,
|
|
1662
|
+
completed: chalk7.green,
|
|
1663
|
+
failed: chalk7.red
|
|
1664
|
+
};
|
|
1665
|
+
var STATUS_LABELS = {
|
|
1666
|
+
pending: "Pending",
|
|
1667
|
+
dns_migrated: "DNS Migrated",
|
|
1668
|
+
unlocked: "Unlocked",
|
|
1669
|
+
auth_obtained: "Auth Obtained",
|
|
1670
|
+
ns_changed: "NS Changed",
|
|
1671
|
+
transfer_initiated: "Transferring (1-5 days)",
|
|
1672
|
+
completed: "Completed",
|
|
1673
|
+
failed: "Failed"
|
|
1674
|
+
};
|
|
1675
|
+
var MEANINGFUL_STATUSES = /* @__PURE__ */ new Set([
|
|
1676
|
+
"transfer_initiated",
|
|
1677
|
+
"completed"
|
|
1678
|
+
]);
|
|
1679
|
+
async function statusCommand() {
|
|
1680
|
+
p6.intro(chalk7.bgCyan.black(" nodaddy \u2014 migration status "));
|
|
1681
|
+
const allMigrations = getAllMigrations();
|
|
1682
|
+
if (allMigrations.length === 0) {
|
|
1683
|
+
p6.log.info("No migrations found. Run `nodaddy migrate` to start one.");
|
|
1684
|
+
p6.outro("");
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const meaningful = allMigrations.filter(
|
|
1688
|
+
(m) => Object.values(m.domains).some((d) => MEANINGFUL_STATUSES.has(d.status))
|
|
1689
|
+
);
|
|
1690
|
+
if (meaningful.length === 0) {
|
|
1691
|
+
p6.log.info("No completed transfers yet. Run `nodaddy migrate` to start one.");
|
|
1692
|
+
p6.outro("");
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
1696
|
+
for (const migration of [...meaningful].reverse()) {
|
|
1697
|
+
for (const d of Object.values(migration.domains)) {
|
|
1698
|
+
if (MEANINGFUL_STATUSES.has(d.status)) {
|
|
1699
|
+
domainMap.set(d.domain, d);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const domains = [...domainMap.values()].sort(
|
|
1704
|
+
(a, b) => new Date(a.lastUpdated).getTime() - new Date(b.lastUpdated).getTime()
|
|
1705
|
+
);
|
|
1706
|
+
const table = new Table2({
|
|
1707
|
+
head: ["Domain", "Status", "Last Updated"],
|
|
1708
|
+
style: { head: ["cyan"] }
|
|
1709
|
+
});
|
|
1710
|
+
for (const d of domains) {
|
|
1711
|
+
const colorFn = STATUS_COLORS[d.status] ?? chalk7.dim;
|
|
1712
|
+
const label = STATUS_LABELS[d.status] ?? d.status;
|
|
1713
|
+
const updated = new Date(d.lastUpdated).toLocaleString();
|
|
1714
|
+
const statusText = d.error ? `${colorFn(label)} \u2014 ${chalk7.red(d.error.slice(0, 60))}` : colorFn(label);
|
|
1715
|
+
table.push([d.domain, statusText, updated]);
|
|
1716
|
+
}
|
|
1717
|
+
console.log(table.toString());
|
|
1718
|
+
const counts = domains.reduce(
|
|
1719
|
+
(acc, d) => {
|
|
1720
|
+
acc[d.status] = (acc[d.status] ?? 0) + 1;
|
|
1721
|
+
return acc;
|
|
1722
|
+
},
|
|
1723
|
+
{}
|
|
1724
|
+
);
|
|
1725
|
+
const summary = Object.entries(counts).map(([status, count]) => {
|
|
1726
|
+
const colorFn = STATUS_COLORS[status] ?? chalk7.dim;
|
|
1727
|
+
return colorFn(`${STATUS_LABELS[status] ?? status}: ${count}`);
|
|
1728
|
+
}).join(" | ");
|
|
1729
|
+
p6.outro(`${domains.length} domain${domains.length === 1 ? "" : "s"} \u2014 ${summary}`);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/commands/resume.ts
|
|
1733
|
+
import * as p7 from "@clack/prompts";
|
|
1734
|
+
import chalk8 from "chalk";
|
|
1735
|
+
init_state_manager();
|
|
1736
|
+
async function resumeCommand() {
|
|
1737
|
+
p7.intro(chalk8.bgCyan.black(" nodaddy \u2014 resume migration "));
|
|
1738
|
+
const migration = getActiveMigration();
|
|
1739
|
+
if (!migration) {
|
|
1740
|
+
const allMigrations = getAllMigrations();
|
|
1741
|
+
if (allMigrations.length === 0) {
|
|
1742
|
+
p7.log.info("No migrations found. Run `nodaddy migrate` to start one.");
|
|
1743
|
+
p7.outro("");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
p7.log.warn("No active migration. Use `nodaddy status` to review past migrations.");
|
|
1747
|
+
p7.outro("");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const resumable = getResumableDomains(migration.id);
|
|
1751
|
+
const allDomains = Object.values(migration.domains);
|
|
1752
|
+
const completed = allDomains.filter(
|
|
1753
|
+
(d) => d.status === "completed" || d.status === "transfer_initiated"
|
|
1754
|
+
);
|
|
1755
|
+
p7.log.info(
|
|
1756
|
+
`Migration ${chalk8.dim(migration.id.slice(0, 8))}: ${completed.length}/${allDomains.length} done, ${resumable.length} remaining`
|
|
1757
|
+
);
|
|
1758
|
+
if (resumable.length === 0) {
|
|
1759
|
+
p7.log.success("All domains have been processed!");
|
|
1760
|
+
p7.outro("");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
for (const d of resumable) {
|
|
1764
|
+
const statusLabel = d.status === "failed" ? chalk8.red(`failed: ${d.error?.slice(0, 60)}`) : chalk8.yellow(d.status);
|
|
1765
|
+
p7.log.message(` ${d.domain} \u2014 ${statusLabel}`);
|
|
1766
|
+
}
|
|
1767
|
+
const confirmed = await p7.confirm({
|
|
1768
|
+
message: `Resume migration for ${chalk8.bold(resumable.length)} domain${resumable.length === 1 ? "" : "s"}?`,
|
|
1769
|
+
initialValue: true
|
|
1770
|
+
});
|
|
1771
|
+
if (p7.isCancel(confirmed) || !confirmed) {
|
|
1772
|
+
p7.outro("Cancelled.");
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
const config = getConfig();
|
|
1776
|
+
const gd = config.godaddy;
|
|
1777
|
+
const cf = config.cloudflare;
|
|
1778
|
+
if (!gd?.apiKey || !gd?.apiSecret) {
|
|
1779
|
+
p7.log.error("GoDaddy credentials not found. Run `nodaddy migrate` to set them up.");
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
}
|
|
1782
|
+
if (!cf?.accountId) {
|
|
1783
|
+
p7.log.error("Cloudflare credentials not found. Run `nodaddy migrate` to set them up.");
|
|
1784
|
+
process.exit(1);
|
|
1785
|
+
}
|
|
1786
|
+
if (cf.authType === "global-key" && (!cf.apiKey || !cf.email)) {
|
|
1787
|
+
p7.log.error("Cloudflare Global API Key credentials incomplete. Run `nodaddy migrate` to reconfigure.");
|
|
1788
|
+
process.exit(1);
|
|
1789
|
+
}
|
|
1790
|
+
if (cf.authType === "token" && !cf.apiToken) {
|
|
1791
|
+
p7.log.error("Cloudflare API Token not found. Run `nodaddy migrate` to reconfigure.");
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
const cfCreds = cf.authType === "global-key" ? { authType: "global-key", apiKey: cf.apiKey, email: cf.email, accountId: cf.accountId } : { authType: "token", apiToken: cf.apiToken, accountId: cf.accountId };
|
|
1795
|
+
const godaddy = new GoDaddyClient(gd);
|
|
1796
|
+
const cloudflare = new CloudflareClient(cfCreds);
|
|
1797
|
+
let contact;
|
|
1798
|
+
if (cf.authType === "global-key") {
|
|
1799
|
+
contact = await collectRegistrantContact();
|
|
1800
|
+
} else {
|
|
1801
|
+
p7.log.warn(
|
|
1802
|
+
"Scoped API tokens do not support registrar transfers. DNS will be migrated but domains will not be transferred."
|
|
1803
|
+
);
|
|
1804
|
+
contact = void 0;
|
|
1805
|
+
}
|
|
1806
|
+
const domainNames = resumable.map((d) => d.domain);
|
|
1807
|
+
const tasks = createMigrationTasks(
|
|
1808
|
+
domainNames,
|
|
1809
|
+
godaddy,
|
|
1810
|
+
cloudflare,
|
|
1811
|
+
migration.id,
|
|
1812
|
+
{ dryRun: false, migrateRecords: true, proxied: false },
|
|
1813
|
+
contact
|
|
1814
|
+
);
|
|
1815
|
+
const ctx = { results: /* @__PURE__ */ new Map() };
|
|
1816
|
+
try {
|
|
1817
|
+
await tasks.run(ctx);
|
|
1818
|
+
} catch {
|
|
1819
|
+
}
|
|
1820
|
+
const succeeded = domainNames.filter((d) => ctx.results.get(d)?.success);
|
|
1821
|
+
const failed = domainNames.filter((d) => !ctx.results.get(d)?.success);
|
|
1822
|
+
if (failed.length === 0) {
|
|
1823
|
+
p7.log.success("All domains resumed successfully.");
|
|
1824
|
+
} else if (succeeded.length > 0) {
|
|
1825
|
+
p7.log.warn(
|
|
1826
|
+
`${chalk8.green(succeeded.length)} succeeded, ${chalk8.red(failed.length)} still failing`
|
|
1827
|
+
);
|
|
1828
|
+
} else {
|
|
1829
|
+
p7.log.error(`All ${domainNames.length} domain${domainNames.length === 1 ? "" : "s"} failed again`);
|
|
1830
|
+
}
|
|
1831
|
+
if (failed.length > 0) {
|
|
1832
|
+
p7.note(
|
|
1833
|
+
`${failed.length} domain${failed.length === 1 ? "" : "s"} still failing. Progress is saved.
|
|
1834
|
+
|
|
1835
|
+
You can run ${chalk8.cyan("nodaddy resume")} again after fixing the issue.
|
|
1836
|
+
|
|
1837
|
+
Common fixes:
|
|
1838
|
+
\u2022 "Resource is being used" \u2014 wait a few minutes and retry
|
|
1839
|
+
\u2022 Domain Protection \u2014 disable at https://dcc.godaddy.com
|
|
1840
|
+
\u2022 Auth code issues \u2014 check your GoDaddy email inbox`,
|
|
1841
|
+
"Still Failing"
|
|
1842
|
+
);
|
|
1843
|
+
} else {
|
|
1844
|
+
p7.note(
|
|
1845
|
+
`Track transfer progress:
|
|
1846
|
+
${chalk8.cyan("nodaddy status")}
|
|
1847
|
+
|
|
1848
|
+
https://dash.cloudflare.com/?to=/:account/domains/transfer`,
|
|
1849
|
+
"Next Steps"
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
p7.outro(chalk8.green("Done!"));
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// src/commands/cleanup.ts
|
|
1856
|
+
init_state_manager();
|
|
1857
|
+
import * as p8 from "@clack/prompts";
|
|
1858
|
+
import chalk9 from "chalk";
|
|
1859
|
+
async function cleanupCommand() {
|
|
1860
|
+
p8.intro(chalk9.bgCyan.black(" nodaddy \u2014 cleanup "));
|
|
1861
|
+
const config = getConfig();
|
|
1862
|
+
const migrations = getAllMigrations();
|
|
1863
|
+
const storePath = getStorePath();
|
|
1864
|
+
const items = [];
|
|
1865
|
+
if (config.godaddy?.apiKey) {
|
|
1866
|
+
items.push("GoDaddy API credentials");
|
|
1867
|
+
}
|
|
1868
|
+
if (config.cloudflare?.accountId) {
|
|
1869
|
+
items.push("Cloudflare API credentials");
|
|
1870
|
+
}
|
|
1871
|
+
if (config.registrantContact) {
|
|
1872
|
+
const c = config.registrantContact;
|
|
1873
|
+
items.push(`Registrant contact (${c.first_name} ${c.last_name}, ${c.email})`);
|
|
1874
|
+
}
|
|
1875
|
+
if (migrations.length > 0) {
|
|
1876
|
+
const domainCount = migrations.reduce(
|
|
1877
|
+
(sum, m) => sum + Object.keys(m.domains).length,
|
|
1878
|
+
0
|
|
1879
|
+
);
|
|
1880
|
+
items.push(`${migrations.length} migration${migrations.length === 1 ? "" : "s"} (${domainCount} domain${domainCount === 1 ? "" : "s"})`);
|
|
1881
|
+
}
|
|
1882
|
+
if (items.length === 0) {
|
|
1883
|
+
p8.log.info("Nothing stored. Already clean!");
|
|
1884
|
+
p8.outro("");
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
p8.log.info(`Config file: ${chalk9.dim(storePath)}`);
|
|
1888
|
+
p8.log.message("");
|
|
1889
|
+
p8.log.warn("This will permanently delete:");
|
|
1890
|
+
for (const item of items) {
|
|
1891
|
+
p8.log.message(` ${chalk9.red("\u2022")} ${item}`);
|
|
1892
|
+
}
|
|
1893
|
+
const confirmed = await p8.confirm({
|
|
1894
|
+
message: "Delete all stored data?",
|
|
1895
|
+
initialValue: false
|
|
1896
|
+
});
|
|
1897
|
+
if (p8.isCancel(confirmed) || !confirmed) {
|
|
1898
|
+
p8.outro("Nothing deleted.");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
clearAll();
|
|
1902
|
+
p8.log.success("All stored data has been deleted.");
|
|
1903
|
+
p8.outro(chalk9.green("Clean!"));
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/index.ts
|
|
1907
|
+
init_state_manager();
|
|
1908
|
+
|
|
1909
|
+
// src/services/signal-handler.ts
|
|
1910
|
+
init_state_manager();
|
|
1911
|
+
function setupSignalHandlers() {
|
|
1912
|
+
const handler = (signal) => {
|
|
1913
|
+
const migration = getActiveMigration();
|
|
1914
|
+
if (migration) {
|
|
1915
|
+
const domains = Object.values(migration.domains);
|
|
1916
|
+
const completed = domains.filter(
|
|
1917
|
+
(d) => d.status === "completed" || d.status === "transfer_initiated"
|
|
1918
|
+
).length;
|
|
1919
|
+
console.log(
|
|
1920
|
+
`
|
|
1921
|
+
|
|
1922
|
+
Interrupted (${signal}). Migration state saved (${completed}/${domains.length} domains processed).`
|
|
1923
|
+
);
|
|
1924
|
+
console.log("Run `nodaddy resume` to continue.\n");
|
|
1925
|
+
} else {
|
|
1926
|
+
console.log(`
|
|
1927
|
+
|
|
1928
|
+
Interrupted (${signal}).
|
|
1929
|
+
`);
|
|
1930
|
+
}
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
};
|
|
1933
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
1934
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/index.ts
|
|
1938
|
+
var program = new Command();
|
|
1939
|
+
program.name("nodaddy").description(
|
|
1940
|
+
"Bulk domain transfer from GoDaddy to Cloudflare"
|
|
1941
|
+
).version("1.0.0");
|
|
1942
|
+
program.command("migrate").description("Interactive migration wizard").option("--all", "Migrate all domains (skip selection)").option("--dry-run", "Preview without making changes").action(async (opts) => {
|
|
1943
|
+
await migrateCommand(opts);
|
|
1944
|
+
});
|
|
1945
|
+
program.command("list").description("List GoDaddy domains").action(async () => {
|
|
1946
|
+
await listCommand();
|
|
1947
|
+
});
|
|
1948
|
+
program.command("status").description("Check transfer status").action(async () => {
|
|
1949
|
+
await statusCommand();
|
|
1950
|
+
});
|
|
1951
|
+
program.command("resume").description("Resume an interrupted migration").action(async () => {
|
|
1952
|
+
await resumeCommand();
|
|
1953
|
+
});
|
|
1954
|
+
program.command("cleanup").description("Delete all stored credentials, contact info, and migration history").action(async () => {
|
|
1955
|
+
await cleanupCommand();
|
|
1956
|
+
});
|
|
1957
|
+
program.command("config").description("Manage API credentials").option("--reset", "Clear stored credentials").action(async (opts) => {
|
|
1958
|
+
if (opts.reset) {
|
|
1959
|
+
clearConfig();
|
|
1960
|
+
console.log("Credentials cleared.");
|
|
1961
|
+
} else {
|
|
1962
|
+
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_state_manager(), state_manager_exports));
|
|
1963
|
+
const config = getConfig2();
|
|
1964
|
+
const hasGD = config.godaddy?.apiKey ? "configured" : "not set";
|
|
1965
|
+
const hasCF = config.cloudflare?.accountId ? `configured (${config.cloudflare.authType ?? "token"})` : "not set";
|
|
1966
|
+
console.log(`GoDaddy: ${hasGD}`);
|
|
1967
|
+
console.log(`Cloudflare: ${hasCF}`);
|
|
1968
|
+
console.log(
|
|
1969
|
+
"\nRun `nodaddy migrate` to set up credentials, or `nodaddy config --reset` to clear them."
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
setupSignalHandlers();
|
|
1974
|
+
program.action(async () => {
|
|
1975
|
+
await migrateCommand({});
|
|
1976
|
+
});
|
|
1977
|
+
program.parse();
|