@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/index.js ADDED
@@ -0,0 +1,675 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @startanaicompany/saac_dns
5
+ * Programmatic Domain Registration, DNS Management & CLI Toolkit
6
+ *
7
+ * Usage:
8
+ * const dns = require('@startanaicompany/saac_dns');
9
+ * // Set env vars: SAAC_USER_API_KEY and SAAC_USER_EMAIL
10
+ * const results = await dns.search(['mycompany.com', 'mycompany.io']);
11
+ * await dns.buy('mycompany.io', { years: 1, privacy: true, autoRenew: true });
12
+ * await dns.records.add('mycompany.io', { type: 'A', host: '@', value: '1.2.3.4' });
13
+ */
14
+
15
+ const { request, ensureRegistered } = require('./lib/client');
16
+
17
+ // ─── Domain Search & Registration ──────────────────────────────────────────
18
+
19
+ /**
20
+ * Search domain availability and pricing
21
+ * @param {string[]} domains - Array of domain names to search
22
+ * @returns {Promise<Array<{fqdn, available, price, currency, tld}>>}
23
+ */
24
+ async function search(domains) {
25
+ if (!Array.isArray(domains)) domains = [domains];
26
+ // Validate domain format
27
+ const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
28
+ for (const domain of domains) {
29
+ if (!domain || typeof domain !== 'string') {
30
+ const err = new Error(`Invalid domain: "${domain}". Domain must be a non-empty string.`);
31
+ err.name = 'ValidationError';
32
+ err.code = 108;
33
+ throw err;
34
+ }
35
+ if (!DOMAIN_REGEX.test(domain.trim())) {
36
+ const err = new Error(`Invalid domain format: "${domain}". Must be a valid FQDN like "example.com".`);
37
+ err.name = 'ValidationError';
38
+ err.code = 108;
39
+ throw err;
40
+ }
41
+ }
42
+ const results = [];
43
+ for (const domain of domains) {
44
+ const data = await request('GET', `/v1/domains/search?q=${encodeURIComponent(domain.trim())}`);
45
+ results.push(...(data.results || []));
46
+ }
47
+ return results;
48
+ }
49
+
50
+ /**
51
+ * Register a domain
52
+ * @param {string} domain - FQDN to register (e.g. 'mycompany.io')
53
+ * @param {Object} opts - Options: years, privacy, autoRenew
54
+ */
55
+ async function buy(domain, opts = {}) {
56
+ const { years = 1, privacy = false, autoRenew = true } = opts;
57
+ return request('POST', '/v1/domains/register', {
58
+ fqdn: domain,
59
+ years,
60
+ privacy,
61
+ auto_renew: autoRenew
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Confirm a pending domain purchase using the token from buy()
67
+ * @param {string} token - Confirmation token received from buy()
68
+ */
69
+ async function confirmPurchase(token) {
70
+ if (!token) throw new Error('token is required');
71
+ return request('POST', '/v1/domains/confirm', { token });
72
+ }
73
+
74
+ /**
75
+ * Register an expiring/dropped domain (drop-catching)
76
+ * @param {string} domain - FQDN to catch
77
+ * @param {Object} opts - years, privacy, autoRenew
78
+ */
79
+ async function buyDrop(domain, opts = {}) {
80
+ const { years = 1, privacy = false, autoRenew = true } = opts;
81
+ return request('POST', '/v1/domains/drop-register', { fqdn: domain, years, privacy, auto_renew: autoRenew });
82
+ }
83
+
84
+ /**
85
+ * List all domains for the authenticated user
86
+ */
87
+ async function list() {
88
+ const data = await request('GET', '/v1/users/me/domains');
89
+ return data.domains || [];
90
+ }
91
+
92
+ /**
93
+ * Get domain info
94
+ * @param {string} domain
95
+ */
96
+ async function info(domain) {
97
+ return request('GET', `/v1/domains/${domain}`);
98
+ }
99
+
100
+ /**
101
+ * Get domain prices for given TLDs
102
+ * @param {string[]} tlds - e.g. ['.com', '.io']
103
+ */
104
+ async function prices(tlds) {
105
+ const data = await request('GET', '/v1/domains/prices');
106
+ if (!tlds || !tlds.length) return data;
107
+ return Object.fromEntries(Object.entries(data).filter(([k]) => tlds.includes(k)));
108
+ }
109
+
110
+ /**
111
+ * Renew a domain
112
+ * @param {string} domain
113
+ * @param {number} years
114
+ */
115
+ async function renew(domain, years = 1) {
116
+ return request('POST', `/v1/domains/${domain}/renew`, { years });
117
+ }
118
+
119
+ // ─── DNS Records ─────────────────────────────────────────────────────────────
120
+
121
+ const records = {
122
+ /**
123
+ * List DNS records for a domain
124
+ * @param {string} domain
125
+ */
126
+ list(domain) {
127
+ return request('GET', `/v1/dns/records?domain=${encodeURIComponent(domain)}`).then(d => d.records || []);
128
+ },
129
+
130
+ /**
131
+ * Add a DNS record
132
+ * @param {string} domain
133
+ * @param {Object} record - {type, name/host, value, ttl, priority}
134
+ */
135
+ add(domain, record) {
136
+ return request('POST', '/v1/dns/records', {
137
+ domain,
138
+ type: record.type,
139
+ name: record.host || record.name || '@',
140
+ value: record.value,
141
+ ttl: record.ttl || 300,
142
+ priority: record.priority
143
+ });
144
+ },
145
+
146
+ /**
147
+ * Update a DNS record
148
+ * @param {string} domain
149
+ * @param {string} id - Record ID
150
+ * @param {Object} record - Fields to update
151
+ */
152
+ update(domain, id, record) {
153
+ return request('PUT', `/v1/dns/records/${id}`, {
154
+ domain,
155
+ type: record.type,
156
+ name: record.host || record.name,
157
+ value: record.value,
158
+ ttl: record.ttl,
159
+ priority: record.priority
160
+ });
161
+ },
162
+
163
+ /**
164
+ * Delete a DNS record
165
+ * @param {string} domain
166
+ * @param {string} id - Record ID
167
+ */
168
+ delete(domain, id) {
169
+ return request('DELETE', `/v1/dns/records/${id}`);
170
+ }
171
+ };
172
+
173
+ // ─── Nameservers ─────────────────────────────────────────────────────────────
174
+
175
+ const nameservers = {
176
+ set(domain, ns) {
177
+ return request('PUT', `/v1/domains/${domain}/nameservers`, { nameservers: ns });
178
+ },
179
+ list(domain) {
180
+ return request('GET', `/v1/domains/${domain}/nameservers`).then(d => d.nameservers || []);
181
+ },
182
+ register(domain, host, ips) {
183
+ return request('POST', `/v1/domains/${domain}/nameservers/registered`, { host, ips: Array.isArray(ips) ? ips : [ips] });
184
+ },
185
+ modify(domain, host, ips) {
186
+ return request('PUT', `/v1/domains/${domain}/nameservers/registered/${encodeURIComponent(host)}`, { ips: Array.isArray(ips) ? ips : [ips] });
187
+ },
188
+ unregister(domain, host) {
189
+ return request('DELETE', `/v1/domains/${domain}/nameservers/registered/${encodeURIComponent(host)}`);
190
+ },
191
+ listRegistered(domain) {
192
+ return request('GET', `/v1/domains/${domain}/nameservers/registered`).then(d => d.hosts || []);
193
+ }
194
+ };
195
+
196
+ // ─── Contacts ─────────────────────────────────────────────────────────────────
197
+
198
+ const contacts = {
199
+ list() {
200
+ return request('GET', '/v1/contacts').then(d => d.contacts || []);
201
+ },
202
+ add(contact) {
203
+ return request('POST', '/v1/contacts', contact);
204
+ },
205
+ update(id, contact) {
206
+ return request('PUT', `/v1/contacts/${id}`, contact);
207
+ },
208
+ delete(id) {
209
+ return request('DELETE', `/v1/contacts/${id}`);
210
+ },
211
+ associate(id, domain) {
212
+ return request('POST', `/v1/domains/${domain}/contact`, { contact_id: id });
213
+ }
214
+ };
215
+
216
+ // ─── User Account ─────────────────────────────────────────────────────────────
217
+
218
+ const account = {
219
+ me() {
220
+ return request('GET', '/v1/users/me');
221
+ },
222
+ preferences(prefs) {
223
+ return request('PUT', '/v1/users/me/preferences', prefs);
224
+ },
225
+ audit(page = 1, limit = 50) {
226
+ return request('GET', `/v1/users/me/audit?page=${page}&limit=${limit}`);
227
+ },
228
+ balance() {
229
+ return request('GET', '/v1/account/balance');
230
+ },
231
+ addFunds(amount, paymentId) {
232
+ return request('POST', '/v1/account/funds', { amount, payment_id: paymentId });
233
+ },
234
+ listPaymentMethods() {
235
+ return request('GET', '/v1/account/payment-methods').then(d => d.payment_methods || []);
236
+ },
237
+ rotateKey() {
238
+ return request('POST', '/v1/auth/rotate-key');
239
+ },
240
+ allowlist: {
241
+ list() { return request('GET', '/v1/auth/allowlist'); },
242
+ add(ip) { return request('POST', '/v1/auth/allowlist', { ip }); },
243
+ remove(ip) { return request('DELETE', `/v1/auth/allowlist/${encodeURIComponent(ip)}`); }
244
+ }
245
+ };
246
+
247
+ // ─── High-level Convenience Methods ──────────────────────────────────────────
248
+
249
+ /**
250
+ * Search + buy in one flow (if available)
251
+ * @param {string} domain
252
+ * @param {Object} opts
253
+ */
254
+ async function quickBuy(domain, opts = {}) {
255
+ const results = await search([domain]);
256
+ const result = results.find(r => r.fqdn === domain);
257
+ if (!result) throw new Error(`Domain ${domain} not found in search results`);
258
+ if (!result.available) throw new Error(`Domain ${domain} is not available`);
259
+ return buy(domain, { autoRenew: true, privacy: true, ...opts });
260
+ }
261
+
262
+ // Hosting provider DNS templates
263
+ const HOSTING_TEMPLATES = {
264
+ vercel: { ips: ['76.76.21.21'], cname: 'cname.vercel-dns.com' },
265
+ netlify: { ips: ['75.2.60.5', '99.83.190.102'], cname: 'apex-loadbalancer.netlify.com' },
266
+ cloudflare: { ips: ['192.0.2.1'], cname: null, note: 'Point @ A record to your Cloudflare assigned IP, then proxy via CF dashboard' },
267
+ railway: { ips: [], cname: 'proxy.railway.app' },
268
+ render: { ips: ['216.24.57.1'], cname: 'oregon-render.global.ssl.fastly.net' },
269
+ 'github-pages': { ips: ['185.199.108.153', '185.199.109.153', '185.199.110.153', '185.199.111.153'], cname: null, note: 'Add CNAME for www pointing to your-username.github.io' }
270
+ };
271
+
272
+ /**
273
+ * Set up web DNS records for a domain (A + CNAME for www)
274
+ * @param {string} domain
275
+ * @param {string} ipOrProvider - IP address or hosting provider name (vercel, netlify, cloudflare, railway, render)
276
+ */
277
+ async function setupWeb(domain, ipOrProvider) {
278
+ const template = typeof ipOrProvider === 'string' ? HOSTING_TEMPLATES[ipOrProvider.toLowerCase()] : null;
279
+ const added = [];
280
+
281
+ if (template) {
282
+ // Use hosting provider template
283
+ for (const ip of (template.ips || [])) {
284
+ added.push(await records.add(domain, { type: 'A', host: '@', value: ip, ttl: 300 }));
285
+ }
286
+ if (template.cname) {
287
+ added.push(await records.add(domain, { type: 'CNAME', host: 'www', value: template.cname, ttl: 300 }));
288
+ }
289
+ return { message: `Web DNS configured for ${domain} (${ipOrProvider})`, records: added, note: template.note };
290
+ } else {
291
+ // Use raw IP
292
+ const ip = ipOrProvider;
293
+ if (!ip) throw new Error('IP address or hosting provider name required');
294
+ added.push(await records.add(domain, { type: 'A', host: '@', value: ip, ttl: 300 }));
295
+ added.push(await records.add(domain, { type: 'CNAME', host: 'www', value: domain, ttl: 300 }));
296
+ return { message: `Web DNS configured for ${domain}`, records: added };
297
+ }
298
+ }
299
+
300
+ // Email provider DNS templates
301
+ const EMAIL_TEMPLATES = {
302
+ gmail: {
303
+ mx: [
304
+ { priority: 1, value: 'aspmx.l.google.com' },
305
+ { priority: 5, value: 'alt1.aspmx.l.google.com' },
306
+ { priority: 5, value: 'alt2.aspmx.l.google.com' },
307
+ { priority: 10, value: 'alt3.aspmx.l.google.com' },
308
+ { priority: 10, value: 'alt4.aspmx.l.google.com' }
309
+ ],
310
+ spf: 'v=spf1 include:_spf.google.com ~all',
311
+ dkimNote: 'Add DKIM TXT record from Google Workspace admin after setup'
312
+ },
313
+ office365: {
314
+ mx: [{ priority: 0, value: (domain) => `${domain.replace(/\./g, '-')}.mail.protection.outlook.com` }],
315
+ spf: 'v=spf1 include:spf.protection.outlook.com ~all',
316
+ dkimNote: 'Add DKIM CNAME records from Microsoft 365 admin after setup'
317
+ },
318
+ // microsoft365 is an alias for office365
319
+ microsoft365: {
320
+ mx: [{ priority: 0, value: (domain) => `${domain.replace(/\./g, '-')}.mail.protection.outlook.com` }],
321
+ spf: 'v=spf1 include:spf.protection.outlook.com ~all',
322
+ dkimNote: 'Add DKIM CNAME records from Microsoft 365 admin after setup'
323
+ },
324
+ zoho: {
325
+ mx: [
326
+ { priority: 10, value: 'mx.zoho.com' },
327
+ { priority: 20, value: 'mx2.zoho.com' },
328
+ { priority: 50, value: 'mx3.zoho.com' }
329
+ ],
330
+ spf: 'v=spf1 include:zoho.com ~all',
331
+ dkimNote: 'Add DKIM TXT record from Zoho Mail admin after setup'
332
+ },
333
+ titan: {
334
+ mx: [{ priority: 10, value: 'smtp.titan.email' }],
335
+ spf: 'v=spf1 include:spf.titan.email ~all',
336
+ dkimNote: 'Add DKIM TXT record from Titan admin after setup'
337
+ }
338
+ };
339
+
340
+ /**
341
+ * Set up email DNS records (MX + SPF + DMARC) for a domain
342
+ * @param {string} domain
343
+ * @param {string} provider - Email provider: gmail, office365, zoho, titan
344
+ */
345
+ async function setupEmail(domain, provider = 'gmail') {
346
+ if (!domain || typeof domain !== 'string') {
347
+ throw new Error('domain must be a non-empty string');
348
+ }
349
+ const tmpl = EMAIL_TEMPLATES[provider.toLowerCase()];
350
+ if (!tmpl) {
351
+ throw new Error(`Unknown email provider: ${provider}. Supported: ${Object.keys(EMAIL_TEMPLATES).join(', ')}`);
352
+ }
353
+
354
+ const added = [];
355
+
356
+ // Add MX records
357
+ for (const mx of tmpl.mx) {
358
+ const value = typeof mx.value === 'function' ? mx.value(domain) : mx.value;
359
+ added.push(await records.add(domain, { type: 'MX', host: '@', value, ttl: 300, priority: mx.priority }));
360
+ }
361
+
362
+ // Add SPF TXT record
363
+ added.push(await records.add(domain, { type: 'TXT', host: '@', value: tmpl.spf, ttl: 300 }));
364
+
365
+ // Add DMARC TXT record
366
+ added.push(await records.add(domain, {
367
+ type: 'TXT',
368
+ host: '_dmarc',
369
+ value: `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`,
370
+ ttl: 300
371
+ }));
372
+
373
+ return {
374
+ message: `Email DNS configured for ${domain} (${provider})`,
375
+ records: added,
376
+ note: tmpl.dkimNote
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Clone all DNS records from one domain to another
382
+ * @param {string} source - Source domain FQDN
383
+ * @param {string} target - Target domain FQDN
384
+ */
385
+ async function cloneDns(source, target) {
386
+ const sourceRecords = await records.list(source);
387
+ if (!sourceRecords.length) {
388
+ return { cloned: 0, message: `No DNS records found on ${source}` };
389
+ }
390
+
391
+ const results = [];
392
+ for (const record of sourceRecords) {
393
+ try {
394
+ results.push(await records.add(target, {
395
+ type: record.type,
396
+ host: record.name || '@',
397
+ value: record.value,
398
+ ttl: record.ttl || 300,
399
+ priority: record.priority
400
+ }));
401
+ } catch (e) {
402
+ // Skip records that fail (e.g. CNAME conflicts)
403
+ results.push({ error: e.message, record });
404
+ }
405
+ }
406
+
407
+ return {
408
+ cloned: results.filter(r => !r.error).length,
409
+ failed: results.filter(r => r.error).length,
410
+ message: `Cloned ${results.filter(r => !r.error).length} of ${sourceRecords.length} records from ${source} to ${target}`,
411
+ results
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Export all DNS records for a domain as JSON
417
+ * @param {string} domain
418
+ */
419
+ async function exportRecords(domain) {
420
+ return records.list(domain);
421
+ }
422
+
423
+ /**
424
+ * Import DNS records for a domain from an array
425
+ * @param {string} domain
426
+ * @param {Array} recordsArr
427
+ */
428
+ async function importRecords(domain, recordsArr) {
429
+ const results = [];
430
+ for (const record of recordsArr) {
431
+ results.push(await records.add(domain, record));
432
+ }
433
+ return results;
434
+ }
435
+
436
+ // ─── Domain Lock / Unlock / Privacy / Auto-Renew ─────────────────────────
437
+
438
+ /**
439
+ * Lock a domain to prevent unauthorized transfers
440
+ * @param {string} domain
441
+ */
442
+ async function lock(domain) {
443
+ return request('POST', `/v1/domains/${domain}/lock`);
444
+ }
445
+
446
+ /**
447
+ * Unlock a domain (e.g. before initiating a transfer)
448
+ * @param {string} domain
449
+ */
450
+ async function unlock(domain) {
451
+ return request('POST', `/v1/domains/${domain}/unlock`);
452
+ }
453
+
454
+ /**
455
+ * Toggle WHOIS privacy for a domain
456
+ * @param {string} domain
457
+ * @param {boolean} enabled
458
+ */
459
+ async function privacy(domain, enabled) {
460
+ return request('POST', `/v1/domains/${domain}/privacy`, { enabled });
461
+ }
462
+
463
+ /**
464
+ * Toggle auto-renewal for a domain
465
+ * @param {string} domain
466
+ * @param {boolean} enabled
467
+ */
468
+ async function autoRenew(domain, enabled) {
469
+ return request('POST', `/v1/domains/${domain}/auto-renew`, { enabled });
470
+ }
471
+
472
+ async function transfer(domain, authCode, opts = {}) {
473
+ return request('POST', `/v1/domains/${domain}/transfer`, { auth_code: authCode, years: opts.years || 1 });
474
+ }
475
+ async function transferCheck(domain) {
476
+ return request('GET', `/v1/domains/${domain}/transfer-check`);
477
+ }
478
+ async function transferStatus(domain) {
479
+ return request('GET', `/v1/domains/${domain}/transfer-status`);
480
+ }
481
+ async function transferUpdateEpp(domain, auth) {
482
+ return request('POST', `/v1/domains/${domain}/transfer-update-epp`, { auth });
483
+ }
484
+ async function transferResendEmail(domain) {
485
+ return request('POST', `/v1/domains/${domain}/transfer-resend`);
486
+ }
487
+ async function transferResubmit(domain) {
488
+ return request('POST', `/v1/domains/${domain}/transfer-resubmit`);
489
+ }
490
+ async function expiring(days = 30) {
491
+ const data = await request('GET', `/v1/domains/expiring?days=${days}`);
492
+ return data.domains || [];
493
+ }
494
+
495
+ async function authCode(domain) {
496
+ return request('GET', `/v1/domains/${domain}/auth-code`);
497
+ }
498
+
499
+
500
+ // ─── Forwarding ──────────────────────────────────────────────────────────────
501
+ const forwarding = {
502
+ set(domain, url) {
503
+ return request('POST', `/v1/domains/${domain}/forward`, { url });
504
+ },
505
+ setSub(domain, subdomain, url) {
506
+ return request('POST', `/v1/domains/${domain}/forward-sub`, { subdomain, url });
507
+ },
508
+ deleteSub(domain, subdomain) {
509
+ return request('DELETE', `/v1/domains/${domain}/forward-sub/${subdomain}`);
510
+ }
511
+ };
512
+
513
+ // ─── Email Forwarding ─────────────────────────────────────────────────────
514
+ const email = {
515
+ list(domain) {
516
+ return request('GET', `/v1/domains/${domain}/email-fwd`).then(d => d.forwards || []);
517
+ },
518
+ forward(domain, fromEmail, toEmail) {
519
+ return request('POST', `/v1/domains/${domain}/email-fwd`, { from_email: fromEmail, to_email: toEmail });
520
+ },
521
+ deleteForward(domain, fromEmail) {
522
+ return request('DELETE', `/v1/domains/${domain}/email-fwd/${encodeURIComponent(fromEmail)}`);
523
+ }
524
+ };
525
+
526
+
527
+ // ─── Webhooks ────────────────────────────────────────────────────────────────
528
+ const webhooks = {
529
+ list() {
530
+ return request('GET', '/v1/webhooks').then(d => d.webhooks || []);
531
+ },
532
+ add(url, events, secret) {
533
+ return request('POST', '/v1/webhooks', { url, events, secret });
534
+ },
535
+ delete(id) {
536
+ return request('DELETE', `/v1/webhooks/${id}`);
537
+ },
538
+ update(id, opts) {
539
+ return request('PUT', `/v1/webhooks/${id}`, opts);
540
+ },
541
+ test(id) {
542
+ return request('POST', `/v1/webhooks/${id}/test`);
543
+ }
544
+ };
545
+
546
+ // ─── Portfolio ────────────────────────────────────────────────────────────────
547
+ const portfolio = {
548
+ list() {
549
+ return request('GET', '/v1/portfolio').then(d => d.portfolios || []);
550
+ },
551
+ create(name) {
552
+ return request('POST', '/v1/portfolio', { name });
553
+ },
554
+ delete(name) {
555
+ return request('DELETE', `/v1/portfolio/${encodeURIComponent(name)}`);
556
+ },
557
+ assign(name, domains) {
558
+ return request('POST', `/v1/portfolio/${encodeURIComponent(name)}/assign`, { domains: Array.isArray(domains) ? domains : [domains] });
559
+ }
560
+ };
561
+
562
+ // ─── Orders ──────────────────────────────────────────────────────────────────
563
+ const orders = {
564
+ list() {
565
+ return request('GET', '/v1/orders').then(d => d.orders || []);
566
+ },
567
+ details(orderNumber) {
568
+ return request('GET', `/v1/orders/${encodeURIComponent(orderNumber)}`);
569
+ }
570
+ };
571
+
572
+ // ─── DNSSEC ──────────────────────────────────────────────────────────────────
573
+ const dnssec = {
574
+ list(domain) {
575
+ return request('GET', `/v1/domains/${domain}/dnssec`).then(d => d.records || []);
576
+ },
577
+ add(domain, record) {
578
+ // Normalize camelCase → snake_case so both styles work
579
+ const normalized = {
580
+ key_tag: record.key_tag ?? record.keyTag,
581
+ algorithm: record.algorithm,
582
+ digest_type: record.digest_type ?? record.digestType,
583
+ digest: record.digest
584
+ };
585
+ return request('POST', `/v1/domains/${domain}/dnssec`, normalized);
586
+ },
587
+ delete(domain, record) {
588
+ const normalized = {
589
+ key_tag: record.key_tag ?? record.keyTag,
590
+ algorithm: record.algorithm,
591
+ digest_type: record.digest_type ?? record.digestType,
592
+ digest: record.digest
593
+ };
594
+ return request('DELETE', `/v1/domains/${domain}/dnssec`, normalized);
595
+ }
596
+ };
597
+
598
+ // ─── WHOIS ────────────────────────────────────────────────────────────────────
599
+ async function whois(domain) {
600
+ return request('GET', `/v1/domains/${domain}/whois`);
601
+ }
602
+
603
+
604
+ // ─── Domain Push & Registrant Verification ────────────────────────────────────
605
+ async function push(domain, toEmail) {
606
+ return request('POST', `/v1/domains/${domain}/push`, { to_email: toEmail });
607
+ }
608
+ async function verificationStatus() {
609
+ return request('GET', '/v1/verification/status');
610
+ }
611
+ async function emailVerify(email) {
612
+ return request('POST', '/v1/verification/email', { email });
613
+ }
614
+
615
+ module.exports = {
616
+ // Domain ops
617
+ search,
618
+ buy,
619
+ confirmPurchase,
620
+ list,
621
+ info,
622
+ prices,
623
+ renew,
624
+ lock,
625
+ unlock,
626
+ privacy,
627
+ autoRenew,
628
+ quickBuy,
629
+ transfer,
630
+ transferCheck,
631
+ transferStatus,
632
+ transferUpdateEpp,
633
+ transferResendEmail,
634
+ transferResubmit,
635
+ buyDrop,
636
+ authCode,
637
+ expiring,
638
+ // DNS records
639
+ records,
640
+ // Nameservers
641
+ nameservers,
642
+ // Forwarding
643
+ forwarding,
644
+ // Email forwarding
645
+ email,
646
+ // Webhooks
647
+ webhooks,
648
+ // DNSSEC
649
+ dnssec,
650
+ // WHOIS
651
+ whois,
652
+ // Contacts
653
+ contacts,
654
+ // Account
655
+ account,
656
+ // Convenience
657
+ setupWeb,
658
+ setupEmail,
659
+ exportRecords,
660
+ importRecords,
661
+ cloneDns,
662
+ // Provider templates (exported for reference)
663
+ HOSTING_TEMPLATES,
664
+ EMAIL_TEMPLATES,
665
+ // Portfolio
666
+ portfolio,
667
+ // Orders
668
+ orders,
669
+ // Domain push & verification
670
+ push,
671
+ verificationStatus,
672
+ emailVerify,
673
+ // Errors
674
+ ...require('./lib/client')
675
+ };