@startanaicompany/dns 1.3.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 +435 -0
- package/bin/saac_dns.js +1340 -0
- package/index.js +675 -0
- package/lib/client.js +134 -0
- package/lib/namesilo.js +754 -0
- package/package.json +27 -0
package/bin/saac_dns.js
ADDED
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const Table = require('cli-table3');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const dns = require('../index');
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('saac_dns')
|
|
13
|
+
.description('SAAC DNS — Programmatic Domain Registration & DNS Management CLI')
|
|
14
|
+
.version('1.0.0')
|
|
15
|
+
.option('--json', 'Output raw JSON')
|
|
16
|
+
.option('--quiet', 'Minimal output')
|
|
17
|
+
.option('--verbose', 'Verbose output');
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function isJson() { return program.opts().json; }
|
|
22
|
+
function isQuiet() { return program.opts().quiet; }
|
|
23
|
+
function isVerbose() { return program.opts().verbose; }
|
|
24
|
+
|
|
25
|
+
/** Interactive yes/no prompt using readline (Node.js-safe replacement for browser confirm()) */
|
|
26
|
+
function rlConfirm(question) {
|
|
27
|
+
// In non-TTY mode (pipes, --json, CI) auto-confirm to avoid hanging
|
|
28
|
+
if (!process.stdin.isTTY || isJson() || isQuiet()) return Promise.resolve(true);
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
31
|
+
rl.question(`${question} [y/N] `, (answer) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function output(data, tableRenderer) {
|
|
39
|
+
if (isJson()) {
|
|
40
|
+
console.log(JSON.stringify(data, null, 2));
|
|
41
|
+
} else if (isQuiet()) {
|
|
42
|
+
// --quiet: suppress all table/formatted output; only tick() messages are shown
|
|
43
|
+
return;
|
|
44
|
+
} else if (isVerbose()) {
|
|
45
|
+
// --verbose: show table AND raw JSON response
|
|
46
|
+
if (tableRenderer) tableRenderer(data);
|
|
47
|
+
console.error('\n[verbose] Raw response:');
|
|
48
|
+
console.error(JSON.stringify(data, null, 2));
|
|
49
|
+
} else if (tableRenderer) {
|
|
50
|
+
tableRenderer(data);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(JSON.stringify(data, null, 2));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleError(err) {
|
|
57
|
+
if (isJson()) {
|
|
58
|
+
console.error(JSON.stringify({ error: err.message, code: err.code }));
|
|
59
|
+
} else {
|
|
60
|
+
console.error(`✗ ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tick(msg) { if (!isQuiet() && !isJson()) console.log(`✓ ${msg}`); }
|
|
66
|
+
|
|
67
|
+
// ─── search ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('search <domains...>')
|
|
71
|
+
.description('Check domain availability and pricing')
|
|
72
|
+
.option('--buy', 'Register the domain if available')
|
|
73
|
+
.action(async (domains, opts) => {
|
|
74
|
+
try {
|
|
75
|
+
const results = await dns.search(domains);
|
|
76
|
+
|
|
77
|
+
output(results, (data) => {
|
|
78
|
+
const table = new Table({
|
|
79
|
+
head: ['Domain', 'Status', 'Price', 'TLD'],
|
|
80
|
+
style: { head: ['cyan'] }
|
|
81
|
+
});
|
|
82
|
+
data.forEach(r => {
|
|
83
|
+
table.push([
|
|
84
|
+
r.fqdn,
|
|
85
|
+
r.available ? '✓ Available' : '✗ Taken',
|
|
86
|
+
r.price ? `$${r.price}/yr` : '—',
|
|
87
|
+
r.tld
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
console.log(table.toString());
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (opts.buy) {
|
|
94
|
+
const available = results.filter(r => r.available);
|
|
95
|
+
for (const r of available) {
|
|
96
|
+
const confirmed = await rlConfirm(`Register ${r.fqdn} for $${r.price}/yr?`);
|
|
97
|
+
if (confirmed) {
|
|
98
|
+
const res = await dns.buy(r.fqdn);
|
|
99
|
+
tick(`Domain ${r.fqdn} registered!`);
|
|
100
|
+
if (isJson()) console.log(JSON.stringify(res, null, 2));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (err) { handleError(err); }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── buy ─────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command('buy <domain>')
|
|
111
|
+
.description('Register a domain')
|
|
112
|
+
.option('--years <n>', 'Registration years', '1')
|
|
113
|
+
.option('--privacy', 'Enable WHOIS privacy', false)
|
|
114
|
+
.option('--no-auto-renew', 'Disable auto-renewal')
|
|
115
|
+
.action(async (domain, opts) => {
|
|
116
|
+
try {
|
|
117
|
+
const result = await dns.buy(domain, {
|
|
118
|
+
years: parseInt(opts.years),
|
|
119
|
+
privacy: opts.privacy,
|
|
120
|
+
autoRenew: opts.autoRenew !== false
|
|
121
|
+
});
|
|
122
|
+
if (result.status === 'pending_confirmation') {
|
|
123
|
+
tick(`Purchase initiated for ${domain} — awaiting human approval`);
|
|
124
|
+
output(result, (r) => {
|
|
125
|
+
console.log(`\n A confirmation code has been sent to the account owner by email.`);
|
|
126
|
+
console.log(` The code was NOT included here — only the account owner can see it.`);
|
|
127
|
+
console.log(` Expires in ${Math.floor(r.expiresIn / 60)} min.`);
|
|
128
|
+
console.log(`\n Once the account owner provides the code, run:`);
|
|
129
|
+
console.log(` saac_dns buy-confirm <code>`);
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
tick(`Domain ${domain} registered successfully!`);
|
|
133
|
+
output(result, (d) => {
|
|
134
|
+
console.log(` Domain: ${d.domain?.fqdn || domain}`);
|
|
135
|
+
console.log(` Status: ${d.domain?.status || 'active'}`);
|
|
136
|
+
console.log(` Expires: ${d.domain?.expires_at ? new Date(d.domain.expires_at).toDateString() : 'in 1 year'}`);
|
|
137
|
+
console.log(` Price: $${d.price}/yr`);
|
|
138
|
+
console.log(` Auto-renew: ${d.domain?.auto_renew ? 'on' : 'off'}`);
|
|
139
|
+
console.log(` Nameservers: ${(d.domain?.nameservers || ['ns1.saac.dns','ns2.saac.dns']).join(', ')}`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} catch (err) { handleError(err); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ─── buy-confirm ─────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command('buy-confirm <token>')
|
|
149
|
+
.description('Confirm a pending domain purchase using the token from buy')
|
|
150
|
+
.action(async (token) => {
|
|
151
|
+
try {
|
|
152
|
+
const result = await dns.confirmPurchase(token);
|
|
153
|
+
tick(`Domain ${result.domain?.fqdn || result.domain} registered successfully`);
|
|
154
|
+
output(result, (r) => {
|
|
155
|
+
const d = r.domain;
|
|
156
|
+
if (d) {
|
|
157
|
+
console.log(` Domain: ${d.fqdn}`);
|
|
158
|
+
console.log(` Status: ${d.status}`);
|
|
159
|
+
console.log(` Expires: ${d.expires_at}`);
|
|
160
|
+
console.log(` Price: $${r.price}/yr`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
} catch (err) { handleError(err); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ─── buy-drop ─────────────────────────────────────────────────────────────────
|
|
167
|
+
program
|
|
168
|
+
.command('buy-drop <domain>')
|
|
169
|
+
.description('Register an expiring/dropped domain (drop-catching)')
|
|
170
|
+
.option('--years <n>', 'Registration years', '1')
|
|
171
|
+
.option('--privacy', 'Enable WHOIS privacy', false)
|
|
172
|
+
.option('--no-auto-renew', 'Disable auto-renewal')
|
|
173
|
+
.action(async (domain, opts) => {
|
|
174
|
+
try {
|
|
175
|
+
const result = await dns.buyDrop(domain, {
|
|
176
|
+
years: parseInt(opts.years),
|
|
177
|
+
privacy: opts.privacy,
|
|
178
|
+
autoRenew: opts.autoRenew !== false
|
|
179
|
+
});
|
|
180
|
+
tick(`Drop registration initiated for ${domain}`);
|
|
181
|
+
output(result, (r) => {
|
|
182
|
+
if (r.order_amount) console.log(` Order amount: $${r.order_amount}`);
|
|
183
|
+
});
|
|
184
|
+
} catch (err) { handleError(err); }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─── list ─────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
program
|
|
190
|
+
.command('list')
|
|
191
|
+
.description('List all your domains')
|
|
192
|
+
.action(async () => {
|
|
193
|
+
try {
|
|
194
|
+
const domains = await dns.list();
|
|
195
|
+
output(domains, (data) => {
|
|
196
|
+
if (!data.length) { console.log('No domains found.'); return; }
|
|
197
|
+
const table = new Table({
|
|
198
|
+
head: ['Domain', 'Status', 'Expires', 'Auto-Renew', 'Locked'],
|
|
199
|
+
style: { head: ['cyan'] }
|
|
200
|
+
});
|
|
201
|
+
data.forEach(d => {
|
|
202
|
+
table.push([
|
|
203
|
+
d.fqdn,
|
|
204
|
+
d.status || 'active',
|
|
205
|
+
d.expires_at ? new Date(d.expires_at).toDateString() : '—',
|
|
206
|
+
d.auto_renew ? 'on' : 'off',
|
|
207
|
+
d.locked ? 'yes' : 'no'
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
console.log(table.toString());
|
|
211
|
+
console.log(`\nTotal: ${data.length} domain(s)`);
|
|
212
|
+
});
|
|
213
|
+
} catch (err) { handleError(err); }
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── info ─────────────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
program
|
|
219
|
+
.command('info <domain>')
|
|
220
|
+
.description('Get full domain details')
|
|
221
|
+
.action(async (domain) => {
|
|
222
|
+
try {
|
|
223
|
+
const info = await dns.info(domain);
|
|
224
|
+
output(info, (d) => {
|
|
225
|
+
console.log(`\nDomain: ${d.fqdn}`);
|
|
226
|
+
console.log(` Status: ${d.status}`);
|
|
227
|
+
console.log(` Registered: ${d.registered_at ? new Date(d.registered_at).toDateString() : '—'}`);
|
|
228
|
+
console.log(` Expires: ${d.expires_at ? new Date(d.expires_at).toDateString() : '—'}`);
|
|
229
|
+
console.log(` Auto-Renew: ${d.auto_renew ? 'on' : 'off'}`);
|
|
230
|
+
console.log(` Nameservers: ${(d.nameservers || []).join(', ')}`);
|
|
231
|
+
});
|
|
232
|
+
} catch (err) { handleError(err); }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ─── records ─────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
const recordsCmd = program.command('records').description('DNS record management');
|
|
238
|
+
|
|
239
|
+
recordsCmd
|
|
240
|
+
.command('list <domain>')
|
|
241
|
+
.description('List DNS records for a domain')
|
|
242
|
+
.option('--type <type>', 'Filter by record type (A, CNAME, MX, TXT...)')
|
|
243
|
+
.action(async (domain, opts) => {
|
|
244
|
+
try {
|
|
245
|
+
let recs = await dns.records.list(domain);
|
|
246
|
+
if (opts.type) recs = recs.filter(r => r.type === opts.type.toUpperCase());
|
|
247
|
+
output(recs, (data) => {
|
|
248
|
+
if (!data.length) { console.log('No records found.'); return; }
|
|
249
|
+
const table = new Table({
|
|
250
|
+
head: ['ID', 'Type', 'Name', 'Value', 'TTL', 'Priority'],
|
|
251
|
+
style: { head: ['cyan'] }
|
|
252
|
+
});
|
|
253
|
+
data.forEach(r => {
|
|
254
|
+
table.push([
|
|
255
|
+
r.id.slice(0, 8) + '...',
|
|
256
|
+
r.type,
|
|
257
|
+
r.name,
|
|
258
|
+
r.value.length > 40 ? r.value.slice(0, 37) + '...' : r.value,
|
|
259
|
+
r.ttl,
|
|
260
|
+
r.priority || '—'
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
console.log(table.toString());
|
|
264
|
+
console.log(`\nTotal: ${data.length} record(s)`);
|
|
265
|
+
});
|
|
266
|
+
} catch (err) { handleError(err); }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
recordsCmd
|
|
270
|
+
.command('add <domain> <type> <name> <value>')
|
|
271
|
+
.description('Add a DNS record (e.g.: records add cool.io A @ 1.2.3.4)')
|
|
272
|
+
.option('--ttl <seconds>', 'TTL in seconds', '300')
|
|
273
|
+
.option('--priority <n>', 'Priority (for MX/SRV records)')
|
|
274
|
+
.option('--file <path>', 'Bulk import from JSON file')
|
|
275
|
+
.action(async (domain, type, name, value, opts) => {
|
|
276
|
+
try {
|
|
277
|
+
if (opts.file) {
|
|
278
|
+
const fs = require('fs');
|
|
279
|
+
const records = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
|
|
280
|
+
const results = await dns.importRecords(domain, records);
|
|
281
|
+
tick(`Imported ${results.length} records to ${domain}`);
|
|
282
|
+
if (isJson()) console.log(JSON.stringify(results, null, 2));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const record = await dns.records.add(domain, {
|
|
286
|
+
type, name, value,
|
|
287
|
+
ttl: parseInt(opts.ttl),
|
|
288
|
+
priority: opts.priority ? parseInt(opts.priority) : undefined
|
|
289
|
+
});
|
|
290
|
+
tick(`${type} record added to ${domain}`);
|
|
291
|
+
output(record, (r) => {
|
|
292
|
+
console.log(` ID: ${r.id}`);
|
|
293
|
+
console.log(` Type: ${r.type}`);
|
|
294
|
+
console.log(` Name: ${r.name}`);
|
|
295
|
+
console.log(` Value: ${r.value}`);
|
|
296
|
+
console.log(` TTL: ${r.ttl}s`);
|
|
297
|
+
});
|
|
298
|
+
} catch (err) { handleError(err); }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
recordsCmd
|
|
302
|
+
.command('update <domain> <id>')
|
|
303
|
+
.description('Update a DNS record by ID')
|
|
304
|
+
.option('--type <type>', 'Record type')
|
|
305
|
+
.option('--name <name>', 'Record name/host')
|
|
306
|
+
.option('--value <value>', 'Record value')
|
|
307
|
+
.option('--ttl <seconds>', 'TTL in seconds')
|
|
308
|
+
.option('--priority <n>', 'Priority')
|
|
309
|
+
.action(async (domain, id, opts) => {
|
|
310
|
+
try {
|
|
311
|
+
const record = await dns.records.update(domain, id, {
|
|
312
|
+
type: opts.type,
|
|
313
|
+
name: opts.name,
|
|
314
|
+
value: opts.value,
|
|
315
|
+
ttl: opts.ttl ? parseInt(opts.ttl) : undefined,
|
|
316
|
+
priority: opts.priority ? parseInt(opts.priority) : undefined
|
|
317
|
+
});
|
|
318
|
+
tick(`Record ${id.slice(0,8)}... updated`);
|
|
319
|
+
output(record, (r) => {
|
|
320
|
+
console.log(` ID: ${r.id}`);
|
|
321
|
+
console.log(` Type: ${r.type}`);
|
|
322
|
+
console.log(` Name: ${r.name}`);
|
|
323
|
+
console.log(` Value: ${r.value}`);
|
|
324
|
+
console.log(` TTL: ${r.ttl}s`);
|
|
325
|
+
});
|
|
326
|
+
} catch (err) { handleError(err); }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
recordsCmd
|
|
330
|
+
.command('delete <domain> <id>')
|
|
331
|
+
.description('Delete a DNS record by ID')
|
|
332
|
+
.action(async (domain, id) => {
|
|
333
|
+
try {
|
|
334
|
+
await dns.records.delete(domain, id);
|
|
335
|
+
tick(`Record ${id.slice(0,8)}... deleted from ${domain}`);
|
|
336
|
+
} catch (err) { handleError(err); }
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
recordsCmd
|
|
340
|
+
.command('export <domain>')
|
|
341
|
+
.description('Export all DNS records as JSON (pipe to file: saac_dns records export cool.io > records.json)')
|
|
342
|
+
.option('--format <fmt>', 'Output format: json (default)', 'json')
|
|
343
|
+
.option('--out <file>', 'Write output to file instead of stdout')
|
|
344
|
+
.action(async (domain, opts) => {
|
|
345
|
+
try {
|
|
346
|
+
const records = await dns.exportRecords(domain);
|
|
347
|
+
const json = JSON.stringify(records, null, 2);
|
|
348
|
+
if (opts.out) {
|
|
349
|
+
const fs = require('fs');
|
|
350
|
+
fs.writeFileSync(opts.out, json, 'utf8');
|
|
351
|
+
tick(`${records.length} record(s) exported to ${opts.out}`);
|
|
352
|
+
} else {
|
|
353
|
+
// Always output raw JSON for export (suitable for piping)
|
|
354
|
+
console.log(json);
|
|
355
|
+
if (!isJson()) console.error(` ✓ ${records.length} record(s) from ${domain}`);
|
|
356
|
+
}
|
|
357
|
+
} catch (err) { handleError(err); }
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ─── ns (nameservers) ─────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
const nsCmd = program.command('ns').description('Nameserver management');
|
|
363
|
+
|
|
364
|
+
nsCmd
|
|
365
|
+
.command('set <domain> <nameservers...>')
|
|
366
|
+
.description('Set nameservers for a domain')
|
|
367
|
+
.action(async (domain, nameserverList) => {
|
|
368
|
+
try {
|
|
369
|
+
const result = await dns.nameservers.set(domain, nameserverList);
|
|
370
|
+
tick(`Nameservers updated for ${domain}`);
|
|
371
|
+
output(result, () => {
|
|
372
|
+
console.log(` Nameservers: ${nameserverList.join(', ')}`);
|
|
373
|
+
});
|
|
374
|
+
} catch (err) { handleError(err); }
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
nsCmd
|
|
378
|
+
.command('list <domain>')
|
|
379
|
+
.description('List nameservers for a domain')
|
|
380
|
+
.action(async (domain) => {
|
|
381
|
+
try {
|
|
382
|
+
const ns = await dns.nameservers.list(domain);
|
|
383
|
+
output(ns, (data) => {
|
|
384
|
+
console.log(`Nameservers for ${domain}:`);
|
|
385
|
+
data.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
|
|
386
|
+
});
|
|
387
|
+
} catch (err) { handleError(err); }
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
nsCmd
|
|
391
|
+
.command('register <domain> <host> <ips...>')
|
|
392
|
+
.description('Register a private nameserver (e.g. ns register example.com ns1.example.com 1.2.3.4)')
|
|
393
|
+
.action(async (domain, host, ips) => {
|
|
394
|
+
try {
|
|
395
|
+
const result = await dns.nameservers.register(domain, host, ips);
|
|
396
|
+
tick(`Nameserver ${host} registered for ${domain}`);
|
|
397
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
398
|
+
} catch (err) { handleError(err); }
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
nsCmd
|
|
402
|
+
.command('modify <domain> <host> <ips...>')
|
|
403
|
+
.description('Modify a registered private nameserver IPs')
|
|
404
|
+
.action(async (domain, host, ips) => {
|
|
405
|
+
try {
|
|
406
|
+
const result = await dns.nameservers.modify(domain, host, ips);
|
|
407
|
+
tick(`Nameserver ${host} updated`);
|
|
408
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
409
|
+
} catch (err) { handleError(err); }
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
nsCmd
|
|
413
|
+
.command('unregister <domain> <host>')
|
|
414
|
+
.description('Unregister/delete a private nameserver')
|
|
415
|
+
.action(async (domain, host) => {
|
|
416
|
+
try {
|
|
417
|
+
await dns.nameservers.unregister(domain, host);
|
|
418
|
+
tick(`Nameserver ${host} unregistered from ${domain}`);
|
|
419
|
+
} catch (err) { handleError(err); }
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
nsCmd
|
|
423
|
+
.command('list-registered <domain>')
|
|
424
|
+
.description('List registered private nameservers for a domain')
|
|
425
|
+
.action(async (domain) => {
|
|
426
|
+
try {
|
|
427
|
+
const hosts = await dns.nameservers.listRegistered(domain);
|
|
428
|
+
output(hosts, (data) => {
|
|
429
|
+
if (!data.length) { console.log('No registered nameservers.'); return; }
|
|
430
|
+
data.forEach((h, i) => console.log(` ${i + 1}. ${h.host || h} — ${h.ip || ''}`));
|
|
431
|
+
});
|
|
432
|
+
} catch (err) { handleError(err); }
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ─── whois ────────────────────────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
program
|
|
438
|
+
.command('whois <domain>')
|
|
439
|
+
.description('WHOIS lookup for a domain')
|
|
440
|
+
.action(async (domain) => {
|
|
441
|
+
try {
|
|
442
|
+
const info = await dns.whois(domain);
|
|
443
|
+
output(info, (d) => {
|
|
444
|
+
console.log(`\nWHOIS: ${domain}`);
|
|
445
|
+
console.log(` Registrar: ${d.registrar || '—'}`);
|
|
446
|
+
console.log(` Status: ${d.status || '—'}`);
|
|
447
|
+
console.log(` Created: ${d.created || d.registered_at || '—'}`);
|
|
448
|
+
console.log(` Updated: ${d.updated || '—'}`);
|
|
449
|
+
console.log(` Expires: ${d.expires || d.expires_at || '—'}`);
|
|
450
|
+
console.log(` Nameservers:`);
|
|
451
|
+
const ns = d.nameservers ? (d.nameservers.nameserver || d.nameservers) : [];
|
|
452
|
+
(Array.isArray(ns) ? ns : [ns]).forEach(n => console.log(` - ${n}`));
|
|
453
|
+
});
|
|
454
|
+
} catch (err) { handleError(err); }
|
|
455
|
+
});
|
|
456
|
+
// ─── prices ──────────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
program
|
|
459
|
+
.command('prices')
|
|
460
|
+
.description('Show domain registration prices')
|
|
461
|
+
.option('--tld <tlds>', 'Comma-separated TLDs (e.g. com,io,dev)')
|
|
462
|
+
.action(async (opts) => {
|
|
463
|
+
try {
|
|
464
|
+
const tlds = opts.tld ? opts.tld.split(',').map(t => t.startsWith('.') ? t : `.${t}`) : null;
|
|
465
|
+
const priceData = await dns.prices(tlds);
|
|
466
|
+
output(priceData, (data) => {
|
|
467
|
+
const table = new Table({
|
|
468
|
+
head: ['TLD', 'Price/yr'],
|
|
469
|
+
style: { head: ['cyan'] }
|
|
470
|
+
});
|
|
471
|
+
Object.entries(data).forEach(([tld, price]) => {
|
|
472
|
+
table.push([tld, `$${price}`]);
|
|
473
|
+
});
|
|
474
|
+
console.log(table.toString());
|
|
475
|
+
});
|
|
476
|
+
} catch (err) { handleError(err); }
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ─── lock ────────────────────────────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
program
|
|
482
|
+
.command('lock <domain>')
|
|
483
|
+
.description('Lock a domain to prevent unauthorized transfers')
|
|
484
|
+
.action(async (domain) => {
|
|
485
|
+
try {
|
|
486
|
+
const result = await dns.lock(domain);
|
|
487
|
+
tick(`Domain ${domain} locked`);
|
|
488
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
489
|
+
} catch (err) { handleError(err); }
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// ─── unlock ──────────────────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
program
|
|
495
|
+
.command('unlock <domain>')
|
|
496
|
+
.description('Unlock a domain (e.g. before initiating a transfer)')
|
|
497
|
+
.action(async (domain) => {
|
|
498
|
+
try {
|
|
499
|
+
const result = await dns.unlock(domain);
|
|
500
|
+
tick(`Domain ${domain} unlocked`);
|
|
501
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
502
|
+
} catch (err) { handleError(err); }
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ─── auto-renew ──────────────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
program
|
|
508
|
+
.command('auto-renew <domain> <on|off>')
|
|
509
|
+
.description('Toggle auto-renewal for a domain')
|
|
510
|
+
.action(async (domain, toggle) => {
|
|
511
|
+
try {
|
|
512
|
+
const enabled = toggle === 'on';
|
|
513
|
+
const result = await dns.autoRenew(domain, enabled);
|
|
514
|
+
tick(`Auto-renewal ${enabled ? 'enabled' : 'disabled'} for ${domain}`);
|
|
515
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
516
|
+
} catch (err) { handleError(err); }
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ─── privacy ─────────────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
program
|
|
522
|
+
.command('privacy <domain> <on|off>')
|
|
523
|
+
.description('Toggle WHOIS privacy for a domain')
|
|
524
|
+
.action(async (domain, toggle) => {
|
|
525
|
+
try {
|
|
526
|
+
const enabled = toggle === 'on';
|
|
527
|
+
const result = await dns.privacy(domain, enabled);
|
|
528
|
+
tick(`WHOIS privacy ${enabled ? 'enabled' : 'disabled'} for ${domain}`);
|
|
529
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
530
|
+
} catch (err) { handleError(err); }
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ─── renew ───────────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
program
|
|
536
|
+
.command('renew <domain>')
|
|
537
|
+
.description('Renew a domain')
|
|
538
|
+
.option('--years <n>', 'Renewal years', '1')
|
|
539
|
+
.action(async (domain, opts) => {
|
|
540
|
+
try {
|
|
541
|
+
const result = await dns.renew(domain, parseInt(opts.years));
|
|
542
|
+
tick(`Domain ${domain} renewed for ${opts.years} year(s)`);
|
|
543
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
544
|
+
} catch (err) { handleError(err); }
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// ─── account ─────────────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
const accountCmd = program.command('account').description('Account management');
|
|
550
|
+
|
|
551
|
+
accountCmd
|
|
552
|
+
.command('balance')
|
|
553
|
+
.description('Check account balance')
|
|
554
|
+
.option('--warn-below <amount>', 'Show warning if balance is below this amount (default: 20)', '20')
|
|
555
|
+
.action(async (opts) => {
|
|
556
|
+
try {
|
|
557
|
+
const data = await dns.account.balance();
|
|
558
|
+
output(data, (d) => {
|
|
559
|
+
const bal = parseFloat(d.balance || '0');
|
|
560
|
+
const warnThreshold = parseFloat(opts.warnBelow) || 20;
|
|
561
|
+
console.log(`\nAccount Balance: $${bal.toFixed(2)} USD`);
|
|
562
|
+
if (bal < warnThreshold) {
|
|
563
|
+
console.warn(`\nWARNING: Balance ($${bal.toFixed(2)}) is below warning threshold ($${warnThreshold.toFixed(2)}). Please top up your account.`);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
} catch (err) { handleError(err); }
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
accountCmd
|
|
570
|
+
.command('fund <amount>')
|
|
571
|
+
.description('Add funds to your account (requires a saved payment method ID)')
|
|
572
|
+
.option('--payment-id <id>', 'Saved payment method ID')
|
|
573
|
+
.action(async (amount, opts) => {
|
|
574
|
+
try {
|
|
575
|
+
if (!opts.paymentId) {
|
|
576
|
+
console.error('✗ --payment-id is required. Run: saac_dns account payment-methods');
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
const result = await dns.account.addFunds(parseFloat(amount), opts.paymentId);
|
|
580
|
+
tick(`$${amount} added to account`);
|
|
581
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
582
|
+
} catch (err) { handleError(err); }
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
accountCmd
|
|
586
|
+
.command('payment-methods')
|
|
587
|
+
.description('List saved payment methods')
|
|
588
|
+
.action(async () => {
|
|
589
|
+
try {
|
|
590
|
+
const methods = await dns.account.listPaymentMethods();
|
|
591
|
+
output(methods, (data) => {
|
|
592
|
+
if (!data.length) { console.log('No saved payment methods.'); return; }
|
|
593
|
+
const table = new Table({
|
|
594
|
+
head: ['ID', 'Type', 'Last 4', 'Expiry'],
|
|
595
|
+
style: { head: ['cyan'] }
|
|
596
|
+
});
|
|
597
|
+
data.forEach(m => table.push([m.id || m.payment_id || '—', m.type || '—', m.last4 || '—', m.expiry || '—']));
|
|
598
|
+
console.log(table.toString());
|
|
599
|
+
});
|
|
600
|
+
} catch (err) { handleError(err); }
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ─── config ──────────────────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
program
|
|
606
|
+
.command('config')
|
|
607
|
+
.description('View or set default preferences')
|
|
608
|
+
.option('--privacy <on|off>', 'Default WHOIS privacy')
|
|
609
|
+
.option('--auto-renew <on|off>', 'Default auto-renewal')
|
|
610
|
+
.option('--ttl <seconds>', 'Default TTL for DNS records')
|
|
611
|
+
.option('--require-purchase-confirmation <on|off>', 'Require email confirmation before domain purchase (set off for agent/automation use)')
|
|
612
|
+
.action(async (opts) => {
|
|
613
|
+
try {
|
|
614
|
+
const me = await dns.account.me();
|
|
615
|
+
if (!opts.privacy && !opts.autoRenew && !opts.ttl && !opts.requirePurchaseConfirmation) {
|
|
616
|
+
output(me, (d) => {
|
|
617
|
+
console.log('\nCurrent config:');
|
|
618
|
+
console.log(` Email: ${d.email}`);
|
|
619
|
+
console.log(` API Key: ${d.api_key ? d.api_key.slice(0,10) + '...' : '—'}`);
|
|
620
|
+
console.log(` Domains: ${d.stats?.domains || 0}`);
|
|
621
|
+
console.log('\nEnvironment:');
|
|
622
|
+
console.log(` SAAC_USER_API_KEY: ${process.env.SAAC_USER_API_KEY ? '✓ Set' : '✗ Not set'}`);
|
|
623
|
+
console.log(` SAAC_USER_EMAIL: ${process.env.SAAC_USER_EMAIL ? '✓ Set' : '✗ Not set'}`);
|
|
624
|
+
});
|
|
625
|
+
} else {
|
|
626
|
+
const prefs = {};
|
|
627
|
+
if (opts.privacy) prefs.default_privacy = opts.privacy === 'on';
|
|
628
|
+
if (opts.autoRenew) prefs.default_auto_renew = opts.autoRenew === 'on';
|
|
629
|
+
if (opts.ttl) prefs.default_ttl = parseInt(opts.ttl);
|
|
630
|
+
if (opts.requirePurchaseConfirmation) prefs.require_purchase_confirmation = opts.requirePurchaseConfirmation === 'on';
|
|
631
|
+
await dns.account.preferences(prefs);
|
|
632
|
+
tick('Preferences updated');
|
|
633
|
+
}
|
|
634
|
+
} catch (err) { handleError(err); }
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// ─── forward ─────────────────────────────────────────────────────────────────
|
|
638
|
+
|
|
639
|
+
program
|
|
640
|
+
.command('forward <domain> <url>')
|
|
641
|
+
.description('Set up domain forwarding')
|
|
642
|
+
.action(async (domain, url) => {
|
|
643
|
+
try {
|
|
644
|
+
const result = await dns.forwarding.set(domain, url);
|
|
645
|
+
output(result, () => tick(`${domain} now forwards to ${url}`));
|
|
646
|
+
} catch (err) { handleError(err); }
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
program
|
|
650
|
+
.command('forward-sub <subdomain> <url>')
|
|
651
|
+
.description('Set up subdomain forwarding (e.g. forward-sub blog.cool.io https://myblog.com)')
|
|
652
|
+
.action(async (subdomain, url) => {
|
|
653
|
+
try {
|
|
654
|
+
const parts = subdomain.split('.');
|
|
655
|
+
const sub = parts[0];
|
|
656
|
+
const domain = parts.slice(1).join('.');
|
|
657
|
+
const result = await dns.forwarding.setSub(domain, sub, url);
|
|
658
|
+
output(result, () => tick(`${subdomain} now forwards to ${url}`));
|
|
659
|
+
} catch (err) { handleError(err); }
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ─── contacts ────────────────────────────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
const contactsCmd = program.command('contacts').description('WHOIS contact management');
|
|
665
|
+
|
|
666
|
+
contactsCmd
|
|
667
|
+
.command('list')
|
|
668
|
+
.description('List contact profiles')
|
|
669
|
+
.action(async () => {
|
|
670
|
+
try {
|
|
671
|
+
const contacts = await dns.contacts.list();
|
|
672
|
+
output(contacts, (data) => {
|
|
673
|
+
if (!data.length) { console.log('No contacts found.'); return; }
|
|
674
|
+
const table = new Table({
|
|
675
|
+
head: ['ID', 'Name', 'Email', 'Country'],
|
|
676
|
+
style: { head: ['cyan'] }
|
|
677
|
+
});
|
|
678
|
+
data.forEach(c => {
|
|
679
|
+
table.push([c.id.slice(0,8)+'...', `${c.first_name} ${c.last_name}`, c.email || '—', c.country || '—']);
|
|
680
|
+
});
|
|
681
|
+
console.log(table.toString());
|
|
682
|
+
});
|
|
683
|
+
} catch (err) { handleError(err); }
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
contactsCmd
|
|
687
|
+
.command('add')
|
|
688
|
+
.description('Add a new WHOIS contact')
|
|
689
|
+
.option('--interactive', 'Interactive guided mode', false)
|
|
690
|
+
.option('--first-name <n>', 'First name')
|
|
691
|
+
.option('--last-name <n>', 'Last name')
|
|
692
|
+
.option('--email <e>', 'Email address')
|
|
693
|
+
.option('--org <o>', 'Organization')
|
|
694
|
+
.option('--address <a>', 'Street address')
|
|
695
|
+
.option('--city <c>', 'City')
|
|
696
|
+
.option('--state <s>', 'State/Province')
|
|
697
|
+
.option('--zip <z>', 'ZIP/Postal code')
|
|
698
|
+
.option('--country <cc>', 'Country code (e.g. US)')
|
|
699
|
+
.option('--phone <p>', 'Phone number (e.g. +1.3125551234)')
|
|
700
|
+
.action(async (opts) => {
|
|
701
|
+
try {
|
|
702
|
+
let contact = {};
|
|
703
|
+
|
|
704
|
+
if (opts.interactive) {
|
|
705
|
+
const readline = require('readline');
|
|
706
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
707
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
708
|
+
|
|
709
|
+
console.log('\nAdd Contact — Interactive Mode\n');
|
|
710
|
+
contact.first_name = await ask('First name: ');
|
|
711
|
+
contact.last_name = await ask('Last name: ');
|
|
712
|
+
contact.email = await ask('Email: ');
|
|
713
|
+
contact.org = await ask('Organization (optional): ');
|
|
714
|
+
contact.address = await ask('Street address: ');
|
|
715
|
+
contact.city = await ask('City: ');
|
|
716
|
+
contact.state = await ask('State/Province: ');
|
|
717
|
+
contact.zip = await ask('ZIP/Postal code: ');
|
|
718
|
+
contact.country = await ask('Country code (e.g. US): ');
|
|
719
|
+
contact.phone = await ask('Phone (e.g. +1.3125551234): ');
|
|
720
|
+
rl.close();
|
|
721
|
+
console.log('');
|
|
722
|
+
} else {
|
|
723
|
+
contact = {
|
|
724
|
+
first_name: opts.firstName,
|
|
725
|
+
last_name: opts.lastName,
|
|
726
|
+
email: opts.email,
|
|
727
|
+
org: opts.org,
|
|
728
|
+
address: opts.address,
|
|
729
|
+
city: opts.city,
|
|
730
|
+
state: opts.state,
|
|
731
|
+
zip: opts.zip,
|
|
732
|
+
country: opts.country,
|
|
733
|
+
phone: opts.phone
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const result = await dns.contacts.add(contact);
|
|
738
|
+
tick(`Contact added (ID: ${result.id || result.contact_id || '—'})`);
|
|
739
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
740
|
+
} catch (err) { handleError(err); }
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
contactsCmd
|
|
744
|
+
.command('set <domain>')
|
|
745
|
+
.description('Assign a contact to a domain')
|
|
746
|
+
.option('--contact <id>', 'Contact ID')
|
|
747
|
+
.action(async (domain, opts) => {
|
|
748
|
+
try {
|
|
749
|
+
if (!opts.contact) { handleError(new Error('--contact <id> is required')); return; }
|
|
750
|
+
await dns.contacts.associate(opts.contact, domain);
|
|
751
|
+
tick(`Contact ${opts.contact.slice(0,8)}... assigned to ${domain}`);
|
|
752
|
+
} catch (err) { handleError(err); }
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
contactsCmd
|
|
757
|
+
.command('update <id>')
|
|
758
|
+
.description('Update a contact profile')
|
|
759
|
+
.option('--first-name <name>', 'First name')
|
|
760
|
+
.option('--last-name <name>', 'Last name')
|
|
761
|
+
.option('--email <email>', 'Email address')
|
|
762
|
+
.option('--org <org>', 'Organization')
|
|
763
|
+
.option('--address <addr>', 'Street address')
|
|
764
|
+
.option('--city <city>', 'City')
|
|
765
|
+
.option('--state <state>', 'State/Province')
|
|
766
|
+
.option('--zip <zip>', 'ZIP/Postal code')
|
|
767
|
+
.option('--country <cc>', 'Country code (e.g. US)')
|
|
768
|
+
.option('--phone <phone>', 'Phone number')
|
|
769
|
+
.action(async (id, opts) => {
|
|
770
|
+
try {
|
|
771
|
+
const contact = await dns.contacts.update(id, {
|
|
772
|
+
first_name: opts.firstName, last_name: opts.lastName,
|
|
773
|
+
email: opts.email, org: opts.org, address: opts.address,
|
|
774
|
+
city: opts.city, state: opts.state, zip: opts.zip,
|
|
775
|
+
country: opts.country, phone: opts.phone
|
|
776
|
+
});
|
|
777
|
+
tick(`Contact ${id.slice(0,8)}... updated`);
|
|
778
|
+
output(contact, (c) => { console.log(` ID: ${c.id}`); });
|
|
779
|
+
} catch (err) { handleError(err); }
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
contactsCmd
|
|
783
|
+
.command('delete <id>')
|
|
784
|
+
.description('Delete a contact profile')
|
|
785
|
+
.action(async (id) => {
|
|
786
|
+
try {
|
|
787
|
+
await dns.contacts.delete(id);
|
|
788
|
+
tick(`Contact ${id.slice(0,8)}... deleted`);
|
|
789
|
+
} catch (err) { handleError(err); }
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// ─── Domain Transfer Suite ───────────────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
program
|
|
795
|
+
.command('transfer <domain>')
|
|
796
|
+
.description('Initiate a domain transfer in')
|
|
797
|
+
.option('--auth <code>', 'EPP/auth code from current registrar')
|
|
798
|
+
.option('--years <n>', 'Transfer period in years', '1')
|
|
799
|
+
.action(async (domain, opts) => {
|
|
800
|
+
try {
|
|
801
|
+
if (!opts.auth) { handleError(new Error('--auth <code> is required')); return; }
|
|
802
|
+
const result = await dns.transfer(domain, opts.auth, { years: parseInt(opts.years) });
|
|
803
|
+
tick(`Transfer initiated for ${domain}`);
|
|
804
|
+
output(result, () => console.log(` Status: Transfer in progress`));
|
|
805
|
+
} catch (err) { handleError(err); }
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
program
|
|
809
|
+
.command('transfer-check <domain>')
|
|
810
|
+
.description('Check if a domain is transferable')
|
|
811
|
+
.action(async (domain) => {
|
|
812
|
+
try {
|
|
813
|
+
const result = await dns.transferCheck(domain);
|
|
814
|
+
output(result, (d) => {
|
|
815
|
+
console.log(`Domain: ${domain}`);
|
|
816
|
+
console.log(`Transferable: ${d.transferable || d.code === 300 ? 'Yes' : 'No'}`);
|
|
817
|
+
});
|
|
818
|
+
} catch (err) { handleError(err); }
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
program
|
|
822
|
+
.command('transfer-status <domain>')
|
|
823
|
+
.description('Check the status of an in-progress domain transfer')
|
|
824
|
+
.action(async (domain) => {
|
|
825
|
+
try {
|
|
826
|
+
const result = await dns.transferStatus(domain);
|
|
827
|
+
output(result, (d) => {
|
|
828
|
+
console.log(`Domain: ${domain}`);
|
|
829
|
+
console.log(`Status: ${d.status || '(see JSON output)'}`);
|
|
830
|
+
if (d.step) console.log(`Step: ${d.step}`);
|
|
831
|
+
if (d.message) console.log(`Message: ${d.message}`);
|
|
832
|
+
});
|
|
833
|
+
} catch (err) { handleError(err); }
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// ─── transfer-update-epp ─────────────────────────────────────────────────────
|
|
837
|
+
program
|
|
838
|
+
.command('transfer-update-epp <domain>')
|
|
839
|
+
.description('Update EPP/auth code for an in-progress transfer')
|
|
840
|
+
.option('--auth <code>', 'New EPP/auth code')
|
|
841
|
+
.action(async (domain, opts) => {
|
|
842
|
+
try {
|
|
843
|
+
if (!opts.auth) { console.error('✗ --auth <code> is required'); process.exit(1); }
|
|
844
|
+
const result = await dns.transferUpdateEpp(domain, opts.auth);
|
|
845
|
+
tick(`EPP code updated for ${domain} transfer`);
|
|
846
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
847
|
+
} catch (err) { handleError(err); }
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// ─── transfer-resend ──────────────────────────────────────────────────────────
|
|
851
|
+
program
|
|
852
|
+
.command('transfer-resend <domain>')
|
|
853
|
+
.description('Resend admin approval email for an in-progress transfer')
|
|
854
|
+
.action(async (domain) => {
|
|
855
|
+
try {
|
|
856
|
+
const result = await dns.transferResendEmail(domain);
|
|
857
|
+
tick(`Admin approval email resent for ${domain}`);
|
|
858
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
859
|
+
} catch (err) { handleError(err); }
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// ─── transfer-resubmit ────────────────────────────────────────────────────────
|
|
863
|
+
program
|
|
864
|
+
.command('transfer-resubmit <domain>')
|
|
865
|
+
.description('Resubmit a transfer to the registry')
|
|
866
|
+
.action(async (domain) => {
|
|
867
|
+
try {
|
|
868
|
+
const result = await dns.transferResubmit(domain);
|
|
869
|
+
tick(`Transfer resubmitted for ${domain}`);
|
|
870
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
871
|
+
} catch (err) { handleError(err); }
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
program
|
|
875
|
+
.command('auth-code <domain>')
|
|
876
|
+
.description('Retrieve EPP/auth code for outbound transfer')
|
|
877
|
+
.action(async (domain) => {
|
|
878
|
+
try {
|
|
879
|
+
const result = await dns.authCode(domain);
|
|
880
|
+
output(result, (d) => {
|
|
881
|
+
console.log(`Domain: ${domain}`);
|
|
882
|
+
console.log(`Auth Code: ${d.auth_code || d.code || '(see JSON output)'}`);
|
|
883
|
+
});
|
|
884
|
+
} catch (err) { handleError(err); }
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
// ─── expiring ─────────────────────────────────────────────────────────────────
|
|
889
|
+
|
|
890
|
+
program
|
|
891
|
+
.command('expiring')
|
|
892
|
+
.description('List domains expiring soon')
|
|
893
|
+
.option('--days <n>', 'Days ahead to check', '30')
|
|
894
|
+
.action(async (opts) => {
|
|
895
|
+
try {
|
|
896
|
+
const domains = await dns.expiring(parseInt(opts.days));
|
|
897
|
+
output(domains, (data) => {
|
|
898
|
+
if (!data.length) { console.log(`No domains expiring in the next ${opts.days} days.`); return; }
|
|
899
|
+
const table = new Table({
|
|
900
|
+
head: ['Domain', 'Expires At', 'Auto Renew'],
|
|
901
|
+
style: { head: ['cyan'] }
|
|
902
|
+
});
|
|
903
|
+
data.forEach(d => {
|
|
904
|
+
table.push([d.fqdn, d.expires_at ? new Date(d.expires_at).toLocaleDateString() : '—', d.auto_renew ? 'Yes' : 'No']);
|
|
905
|
+
});
|
|
906
|
+
console.log(table.toString());
|
|
907
|
+
});
|
|
908
|
+
} catch (err) { handleError(err); }
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// ─── key management ───────────────────────────────────────────────────────────────────────────
|
|
912
|
+
|
|
913
|
+
const keyCmd = program.command('key').description('API key and security management');
|
|
914
|
+
|
|
915
|
+
keyCmd
|
|
916
|
+
.command('rotate')
|
|
917
|
+
.description('Rotate your SAAC API key')
|
|
918
|
+
.action(async () => {
|
|
919
|
+
try {
|
|
920
|
+
const result = await dns.account.rotateKey();
|
|
921
|
+
tick('API key rotated');
|
|
922
|
+
output(result, () => {
|
|
923
|
+
console.log(` New API key: ${result.api_key}`);
|
|
924
|
+
console.log(` Update SAAC_USER_API_KEY in your environment.`);
|
|
925
|
+
});
|
|
926
|
+
} catch (err) { handleError(err); }
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const allowlistCmd = keyCmd.command('allowlist').description('IP allowlist management');
|
|
930
|
+
|
|
931
|
+
allowlistCmd
|
|
932
|
+
.command('list')
|
|
933
|
+
.description('List allowed IPs')
|
|
934
|
+
.action(async () => {
|
|
935
|
+
try {
|
|
936
|
+
const result = await dns.account.allowlist.list();
|
|
937
|
+
output(result, (data) => {
|
|
938
|
+
if (!data.ip_allowlist || !data.ip_allowlist.length) {
|
|
939
|
+
console.log('No IP restrictions set (all IPs allowed)');
|
|
940
|
+
} else {
|
|
941
|
+
data.ip_allowlist.forEach((ip, i) => console.log(` ${i + 1}. ${ip}`));
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
} catch (err) { handleError(err); }
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
allowlistCmd
|
|
948
|
+
.command('add <ip>')
|
|
949
|
+
.description('Add IP or CIDR to allowlist')
|
|
950
|
+
.action(async (ip) => {
|
|
951
|
+
try {
|
|
952
|
+
const result = await dns.account.allowlist.add(ip);
|
|
953
|
+
tick(`${ip} added to allowlist`);
|
|
954
|
+
output(result, (data) => console.log(` Total IPs: ${data.ip_allowlist.length}`));
|
|
955
|
+
} catch (err) { handleError(err); }
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
allowlistCmd
|
|
959
|
+
.command('remove <ip>')
|
|
960
|
+
.description('Remove IP or CIDR from allowlist')
|
|
961
|
+
.action(async (ip) => {
|
|
962
|
+
try {
|
|
963
|
+
const result = await dns.account.allowlist.remove(ip);
|
|
964
|
+
tick(`${ip} removed from allowlist`);
|
|
965
|
+
output(result, (data) => console.log(` Remaining IPs: ${data.ip_allowlist.length}`));
|
|
966
|
+
} catch (err) { handleError(err); }
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// ─── setup-web ───────────────────────────────────────────────────────────────
|
|
970
|
+
|
|
971
|
+
program
|
|
972
|
+
.command('setup-web <domain>')
|
|
973
|
+
.description('Set up web DNS records (A + CNAME). Use --hosting for provider templates.')
|
|
974
|
+
.option('--ip <address>', 'IP address for A record')
|
|
975
|
+
.option('--hosting <provider>', 'Hosting provider: vercel, netlify, cloudflare, railway, render')
|
|
976
|
+
.action(async (domain, opts) => {
|
|
977
|
+
try {
|
|
978
|
+
const target = opts.hosting || opts.ip;
|
|
979
|
+
if (!target) {
|
|
980
|
+
console.error('✗ Provide --hosting <provider> or --ip <address>');
|
|
981
|
+
console.error(' Providers: vercel, netlify, cloudflare, railway, render');
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
const result = await dns.setupWeb(domain, target);
|
|
985
|
+
tick(result.message);
|
|
986
|
+
if (result.note) console.log(` Note: ${result.note}`);
|
|
987
|
+
output(result, (r) => {
|
|
988
|
+
console.log(` Records added: ${r.records ? r.records.length : 0}`);
|
|
989
|
+
});
|
|
990
|
+
} catch (err) { handleError(err); }
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// ─── setup-email ─────────────────────────────────────────────────────────────
|
|
994
|
+
|
|
995
|
+
program
|
|
996
|
+
.command('setup-email <domain> [provider]')
|
|
997
|
+
.description('Set up email DNS records (MX + SPF + DMARC) for an email provider')
|
|
998
|
+
.option('--provider <name>', 'Email provider: gmail, office365, microsoft365, zoho, titan', 'gmail')
|
|
999
|
+
.action(async (domain, providerArg, opts) => {
|
|
1000
|
+
try {
|
|
1001
|
+
// Support both positional arg (setup-email domain.com titan) and --provider flag
|
|
1002
|
+
const provider = providerArg || opts.provider || 'gmail';
|
|
1003
|
+
const result = await dns.setupEmail(domain, provider);
|
|
1004
|
+
tick(result.message);
|
|
1005
|
+
if (result.note && !isJson()) console.log(` Next step: ${result.note}`);
|
|
1006
|
+
output(result, (r) => {
|
|
1007
|
+
console.log(` Records added: ${r.records ? r.records.length : 0}`);
|
|
1008
|
+
if (r.note) console.log(` Note: ${r.note}`);
|
|
1009
|
+
});
|
|
1010
|
+
} catch (err) { handleError(err); }
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// ─── clone-dns ───────────────────────────────────────────────────────────────
|
|
1014
|
+
|
|
1015
|
+
program
|
|
1016
|
+
.command('clone-dns <source> <target>')
|
|
1017
|
+
.description('Clone all DNS records from one domain to another')
|
|
1018
|
+
.action(async (source, target) => {
|
|
1019
|
+
try {
|
|
1020
|
+
const result = await dns.cloneDns(source, target);
|
|
1021
|
+
tick(result.message);
|
|
1022
|
+
output(result, (r) => {
|
|
1023
|
+
console.log(` Cloned: ${r.cloned}`);
|
|
1024
|
+
if (r.failed) console.log(` Failed: ${r.failed}`);
|
|
1025
|
+
});
|
|
1026
|
+
} catch (err) { handleError(err); }
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// ─── email-fwd ───────────────────────────────────────────────────────────────
|
|
1030
|
+
|
|
1031
|
+
const emailFwdCmd = program.command('email-fwd').description('Email forwarding management');
|
|
1032
|
+
|
|
1033
|
+
emailFwdCmd
|
|
1034
|
+
.command('list <domain>')
|
|
1035
|
+
.description('List email forwards for a domain')
|
|
1036
|
+
.action(async (domain) => {
|
|
1037
|
+
try {
|
|
1038
|
+
const forwards = await dns.email.list(domain);
|
|
1039
|
+
output(forwards, (data) => {
|
|
1040
|
+
if (!data.length) { console.log('No email forwards configured.'); return; }
|
|
1041
|
+
const table = new Table({
|
|
1042
|
+
head: ['From', 'To'],
|
|
1043
|
+
style: { head: ['cyan'] }
|
|
1044
|
+
});
|
|
1045
|
+
data.forEach(f => table.push([f.from_email || f.email2 || '—', f.to_email || f.forward2 || '—']));
|
|
1046
|
+
console.log(table.toString());
|
|
1047
|
+
});
|
|
1048
|
+
} catch (err) { handleError(err); }
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
emailFwdCmd
|
|
1052
|
+
.command('add <domain> <from> <to>')
|
|
1053
|
+
.description('Add email forward: <from>@domain → <to>')
|
|
1054
|
+
.action(async (domain, from, to) => {
|
|
1055
|
+
try {
|
|
1056
|
+
const result = await dns.email.forward(domain, from, to);
|
|
1057
|
+
tick(`Email forward ${from}@${domain} → ${to} configured`);
|
|
1058
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1059
|
+
} catch (err) { handleError(err); }
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
emailFwdCmd
|
|
1063
|
+
.command('delete <domain> <from>')
|
|
1064
|
+
.description('Delete email forward for <from>@domain')
|
|
1065
|
+
.action(async (domain, from) => {
|
|
1066
|
+
try {
|
|
1067
|
+
await dns.email.deleteForward(domain, from);
|
|
1068
|
+
tick(`Email forward ${from}@${domain} deleted`);
|
|
1069
|
+
} catch (err) { handleError(err); }
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// ─── dnssec ──────────────────────────────────────────────────────────────────
|
|
1073
|
+
|
|
1074
|
+
const dnssecCmd = program.command('dnssec').description('DNSSEC management');
|
|
1075
|
+
|
|
1076
|
+
dnssecCmd
|
|
1077
|
+
.command('list <domain>')
|
|
1078
|
+
.description('List DNSSEC records for a domain')
|
|
1079
|
+
.action(async (domain) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const records = await dns.dnssec.list(domain);
|
|
1082
|
+
output(records, (data) => {
|
|
1083
|
+
if (!data.length) { console.log('No DNSSEC records configured.'); return; }
|
|
1084
|
+
const table = new Table({
|
|
1085
|
+
head: ['Algorithm', 'Key Tag', 'Digest Type', 'Digest'],
|
|
1086
|
+
style: { head: ['cyan'] }
|
|
1087
|
+
});
|
|
1088
|
+
data.forEach(r => table.push([r.algorithm || '—', r.key_tag || '—', r.digest_type || '—', (r.digest || '—').slice(0, 20) + '...']));
|
|
1089
|
+
console.log(table.toString());
|
|
1090
|
+
});
|
|
1091
|
+
} catch (err) { handleError(err); }
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
dnssecCmd
|
|
1095
|
+
.command('add <domain>')
|
|
1096
|
+
.description('Add a DNSSEC DS record')
|
|
1097
|
+
.option('--algorithm <n>', 'Algorithm number')
|
|
1098
|
+
.option('--key-tag <n>', 'Key tag number')
|
|
1099
|
+
.option('--digest-type <n>', 'Digest type number')
|
|
1100
|
+
.option('--digest <hash>', 'Digest hash value')
|
|
1101
|
+
.action(async (domain, opts) => {
|
|
1102
|
+
try {
|
|
1103
|
+
if (!opts.algorithm || !opts.keyTag || !opts.digestType || !opts.digest) {
|
|
1104
|
+
console.error('✗ All of --algorithm, --key-tag, --digest-type, --digest are required');
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
const result = await dns.dnssec.add(domain, {
|
|
1108
|
+
algorithm: opts.algorithm,
|
|
1109
|
+
key_tag: opts.keyTag,
|
|
1110
|
+
digest_type: opts.digestType,
|
|
1111
|
+
digest: opts.digest
|
|
1112
|
+
});
|
|
1113
|
+
tick(`DNSSEC record added to ${domain}`);
|
|
1114
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1115
|
+
} catch (err) { handleError(err); }
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
dnssecCmd
|
|
1119
|
+
.command('delete <domain>')
|
|
1120
|
+
.description('Delete a DNSSEC DS record')
|
|
1121
|
+
.option('--algorithm <n>', 'Algorithm number')
|
|
1122
|
+
.option('--key-tag <n>', 'Key tag number')
|
|
1123
|
+
.option('--digest-type <n>', 'Digest type number')
|
|
1124
|
+
.option('--digest <hash>', 'Digest hash value')
|
|
1125
|
+
.action(async (domain, opts) => {
|
|
1126
|
+
try {
|
|
1127
|
+
await dns.dnssec.delete(domain, {
|
|
1128
|
+
algorithm: opts.algorithm,
|
|
1129
|
+
key_tag: opts.keyTag,
|
|
1130
|
+
digest_type: opts.digestType,
|
|
1131
|
+
digest: opts.digest
|
|
1132
|
+
});
|
|
1133
|
+
tick(`DNSSEC record deleted from ${domain}`);
|
|
1134
|
+
} catch (err) { handleError(err); }
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// ─── portfolio ────────────────────────────────────────────────────────────────
|
|
1138
|
+
const portfolioCmd = program.command('portfolio').description('Domain portfolio management');
|
|
1139
|
+
|
|
1140
|
+
portfolioCmd
|
|
1141
|
+
.command('list')
|
|
1142
|
+
.description('List all portfolios')
|
|
1143
|
+
.action(async () => {
|
|
1144
|
+
try {
|
|
1145
|
+
const portfolios = await dns.portfolio.list();
|
|
1146
|
+
output(portfolios, (data) => {
|
|
1147
|
+
if (!data.length) { console.log('No portfolios found.'); return; }
|
|
1148
|
+
const table = new Table({ head: ['Name', 'Domains'], style: { head: ['cyan'] } });
|
|
1149
|
+
data.forEach(p => table.push([p.name || '—', p.domains || 0]));
|
|
1150
|
+
console.log(table.toString());
|
|
1151
|
+
});
|
|
1152
|
+
} catch (err) { handleError(err); }
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
portfolioCmd
|
|
1156
|
+
.command('create <name>')
|
|
1157
|
+
.description('Create a new portfolio')
|
|
1158
|
+
.action(async (name) => {
|
|
1159
|
+
try {
|
|
1160
|
+
const result = await dns.portfolio.create(name);
|
|
1161
|
+
tick(`Portfolio '${name}' created`);
|
|
1162
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1163
|
+
} catch (err) { handleError(err); }
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
portfolioCmd
|
|
1167
|
+
.command('delete <name>')
|
|
1168
|
+
.description('Delete a portfolio')
|
|
1169
|
+
.action(async (name) => {
|
|
1170
|
+
try {
|
|
1171
|
+
await dns.portfolio.delete(name);
|
|
1172
|
+
tick(`Portfolio '${name}' deleted`);
|
|
1173
|
+
} catch (err) { handleError(err); }
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
portfolioCmd
|
|
1177
|
+
.command('assign <name> <domains...>')
|
|
1178
|
+
.description('Assign domains to a portfolio')
|
|
1179
|
+
.action(async (name, domains) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const result = await dns.portfolio.assign(name, domains);
|
|
1182
|
+
tick(`${domains.length} domain(s) assigned to '${name}'`);
|
|
1183
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1184
|
+
} catch (err) { handleError(err); }
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// ─── orders ──────────────────────────────────────────────────────────────────
|
|
1188
|
+
const ordersCmd = program.command('orders').description('Order management');
|
|
1189
|
+
|
|
1190
|
+
ordersCmd
|
|
1191
|
+
.command('list')
|
|
1192
|
+
.description('List all orders')
|
|
1193
|
+
.action(async () => {
|
|
1194
|
+
try {
|
|
1195
|
+
const orderList = await dns.orders.list();
|
|
1196
|
+
output(orderList, (data) => {
|
|
1197
|
+
if (!data.length) { console.log('No orders found.'); return; }
|
|
1198
|
+
const table = new Table({ head: ['Order #', 'Domain', 'Amount', 'Date', 'Status'], style: { head: ['cyan'] } });
|
|
1199
|
+
data.forEach(o => table.push([o.order_number || '—', o.domain || '—', o.amount ? `$${o.amount}` : '—', o.date || '—', o.status || '—']));
|
|
1200
|
+
console.log(table.toString());
|
|
1201
|
+
});
|
|
1202
|
+
} catch (err) { handleError(err); }
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
ordersCmd
|
|
1206
|
+
.command('details <orderNumber>')
|
|
1207
|
+
.description('Get details for a specific order')
|
|
1208
|
+
.action(async (orderNumber) => {
|
|
1209
|
+
try {
|
|
1210
|
+
const order = await dns.orders.details(orderNumber);
|
|
1211
|
+
output(order, (o) => {
|
|
1212
|
+
console.log(`Order: ${o.order_number || orderNumber}`);
|
|
1213
|
+
console.log(` Domain: ${o.domain || '—'}`);
|
|
1214
|
+
console.log(` Amount: $${o.amount || '—'}`);
|
|
1215
|
+
console.log(` Date: ${o.date || '—'}`);
|
|
1216
|
+
console.log(` Status: ${o.status || '—'}`);
|
|
1217
|
+
});
|
|
1218
|
+
} catch (err) { handleError(err); }
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// ─── push ─────────────────────────────────────────────────────────────────────
|
|
1222
|
+
program
|
|
1223
|
+
.command('push <domain>')
|
|
1224
|
+
.description('Push a domain to another NameSilo account')
|
|
1225
|
+
.option('--to <email>', 'Target account email')
|
|
1226
|
+
.action(async (domain, opts) => {
|
|
1227
|
+
try {
|
|
1228
|
+
if (!opts.to) { console.error('✗ --to <email> is required'); process.exit(1); }
|
|
1229
|
+
const result = await dns.push(domain, opts.to);
|
|
1230
|
+
tick(`Domain ${domain} pushed to ${opts.to}`);
|
|
1231
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1232
|
+
} catch (err) { handleError(err); }
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// ─── verify-status ───────────────────────────────────────────────────────────
|
|
1236
|
+
program
|
|
1237
|
+
.command('verify-status')
|
|
1238
|
+
.description('Check registrant verification status')
|
|
1239
|
+
.action(async () => {
|
|
1240
|
+
try {
|
|
1241
|
+
const result = await dns.verificationStatus();
|
|
1242
|
+
output(result, (r) => {
|
|
1243
|
+
console.log(`Verified: ${r.verified ? '✓ Yes' : '✗ No'}`);
|
|
1244
|
+
if (r.email) console.log(`Email: ${r.email}`);
|
|
1245
|
+
});
|
|
1246
|
+
} catch (err) { handleError(err); }
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// ─── verify-email ─────────────────────────────────────────────────────────────
|
|
1250
|
+
program
|
|
1251
|
+
.command('verify-email <email>')
|
|
1252
|
+
.description('Send email verification for registrant')
|
|
1253
|
+
.action(async (email) => {
|
|
1254
|
+
try {
|
|
1255
|
+
const result = await dns.emailVerify(email);
|
|
1256
|
+
tick(`Verification email sent to ${email}`);
|
|
1257
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1258
|
+
} catch (err) { handleError(err); }
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// ─── webhooks ─────────────────────────────────────────────────────────────────
|
|
1262
|
+
|
|
1263
|
+
const webhooksCmd = program.command('webhooks').description('Webhook notification management');
|
|
1264
|
+
|
|
1265
|
+
webhooksCmd
|
|
1266
|
+
.command('list')
|
|
1267
|
+
.description('List registered webhooks')
|
|
1268
|
+
.action(async () => {
|
|
1269
|
+
try {
|
|
1270
|
+
const hooks = await dns.webhooks.list();
|
|
1271
|
+
output(hooks, (data) => {
|
|
1272
|
+
if (!data.length) { console.log('No webhooks registered.'); return; }
|
|
1273
|
+
const table = new Table({ head: ['ID', 'URL', 'Events', 'Active', 'Last Status'], style: { head: ['cyan'] } });
|
|
1274
|
+
data.forEach(h => table.push([h.id.slice(0, 8) + '...', h.url.slice(0, 40), (h.events || []).join(', '), h.active ? '✓' : '✗', h.last_status_code || '—']));
|
|
1275
|
+
console.log(table.toString());
|
|
1276
|
+
});
|
|
1277
|
+
} catch (err) { handleError(err); }
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
webhooksCmd
|
|
1281
|
+
.command('add <url>')
|
|
1282
|
+
.description('Register a webhook URL')
|
|
1283
|
+
.option('--events <events>', 'Comma-separated events (default: domain.expiring,purchase.confirmed)', 'domain.expiring,purchase.confirmed')
|
|
1284
|
+
.option('--secret <secret>', 'Signing secret for HMAC verification')
|
|
1285
|
+
.action(async (url, opts) => {
|
|
1286
|
+
try {
|
|
1287
|
+
const events = opts.events.split(',').map(e => e.trim());
|
|
1288
|
+
const result = await dns.webhooks.add(url, events, opts.secret);
|
|
1289
|
+
tick(`Webhook registered (ID: ${result.id})`);
|
|
1290
|
+
output(result, (r) => {
|
|
1291
|
+
console.log(` URL: ${r.url}`);
|
|
1292
|
+
console.log(` Events: ${(r.events || []).join(', ')}`);
|
|
1293
|
+
console.log(` ID: ${r.id}`);
|
|
1294
|
+
});
|
|
1295
|
+
} catch (err) { handleError(err); }
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
webhooksCmd
|
|
1299
|
+
.command('delete <id>')
|
|
1300
|
+
.description('Delete a webhook by ID')
|
|
1301
|
+
.action(async (id) => {
|
|
1302
|
+
try {
|
|
1303
|
+
await dns.webhooks.delete(id);
|
|
1304
|
+
tick(`Webhook ${id} deleted`);
|
|
1305
|
+
} catch (err) { handleError(err); }
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
webhooksCmd
|
|
1309
|
+
.command('test <id>')
|
|
1310
|
+
.description('Send a test event to a webhook')
|
|
1311
|
+
.action(async (id) => {
|
|
1312
|
+
try {
|
|
1313
|
+
const result = await dns.webhooks.test(id);
|
|
1314
|
+
if (isJson()) console.log(JSON.stringify(result, null, 2));
|
|
1315
|
+
tick(`Test event sent to ${result.url}`);
|
|
1316
|
+
} catch (err) { handleError(err); }
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// ─── Parse & run ─────────────────────────────────────────────────────────────
|
|
1320
|
+
|
|
1321
|
+
// Verbose timing interceptor: check --verbose directly in argv so it runs before parse
|
|
1322
|
+
const _verboseMode = process.argv.includes('--verbose');
|
|
1323
|
+
if (_verboseMode && typeof fetch !== 'undefined') {
|
|
1324
|
+
const _origFetch = fetch;
|
|
1325
|
+
global.fetch = async function(url, opts) {
|
|
1326
|
+
const _t0 = Date.now();
|
|
1327
|
+
const _res = await _origFetch(url, opts);
|
|
1328
|
+
const _elapsed = Date.now() - _t0;
|
|
1329
|
+
const _urlStr = String(url).replace(/^https?:\/\/[^/]+/, '');
|
|
1330
|
+
console.error(`[verbose] ${(opts && opts.method) || 'GET'} ${_urlStr} → ${_res.status} ${_res.statusText} (${_elapsed}ms)`);
|
|
1331
|
+
return _res;
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
program.parse(process.argv);
|
|
1336
|
+
|
|
1337
|
+
// Show help if no command given
|
|
1338
|
+
if (!process.argv.slice(2).length) {
|
|
1339
|
+
program.outputHelp();
|
|
1340
|
+
}
|