@wraps.dev/cli 2.3.5 → 2.4.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 +55 -0
- package/dist/cli.js +1241 -73
- package/dist/cli.js.map +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1989,9 +1989,11 @@ __export(prompts_exports, {
|
|
|
1989
1989
|
getAvailableFeatures: () => getAvailableFeatures,
|
|
1990
1990
|
promptConfigPreset: () => promptConfigPreset,
|
|
1991
1991
|
promptConflictResolution: () => promptConflictResolution,
|
|
1992
|
+
promptContinueManualDNS: () => promptContinueManualDNS,
|
|
1992
1993
|
promptCustomConfig: () => promptCustomConfig,
|
|
1993
1994
|
promptDNSConfirmation: () => promptDNSConfirmation,
|
|
1994
1995
|
promptDNSManagement: () => promptDNSManagement,
|
|
1996
|
+
promptDNSProvider: () => promptDNSProvider,
|
|
1995
1997
|
promptDomain: () => promptDomain,
|
|
1996
1998
|
promptEmailArchiving: () => promptEmailArchiving,
|
|
1997
1999
|
promptEstimatedVolume: () => promptEstimatedVolume,
|
|
@@ -2713,6 +2715,58 @@ async function promptDNSManagement(domain) {
|
|
|
2713
2715
|
}
|
|
2714
2716
|
return manage;
|
|
2715
2717
|
}
|
|
2718
|
+
async function promptDNSProvider(domain, availableProviders) {
|
|
2719
|
+
const options = availableProviders.map((p) => {
|
|
2720
|
+
let label;
|
|
2721
|
+
let hint;
|
|
2722
|
+
switch (p.provider) {
|
|
2723
|
+
case "route53":
|
|
2724
|
+
label = "AWS Route53";
|
|
2725
|
+
hint = p.detected ? "Hosted zone detected" : "Requires Route53 hosted zone";
|
|
2726
|
+
break;
|
|
2727
|
+
case "vercel":
|
|
2728
|
+
label = "Vercel DNS";
|
|
2729
|
+
hint = p.detected ? "Token found" : "Requires VERCEL_TOKEN";
|
|
2730
|
+
break;
|
|
2731
|
+
case "cloudflare":
|
|
2732
|
+
label = "Cloudflare";
|
|
2733
|
+
hint = p.detected ? "Token found" : "Requires CLOUDFLARE_API_TOKEN";
|
|
2734
|
+
break;
|
|
2735
|
+
case "manual":
|
|
2736
|
+
label = "Manual";
|
|
2737
|
+
hint = "I'll add DNS records myself";
|
|
2738
|
+
break;
|
|
2739
|
+
}
|
|
2740
|
+
if (p.detected && p.provider !== "manual") {
|
|
2741
|
+
label = `${label} (Recommended)`;
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
value: p.provider,
|
|
2745
|
+
label,
|
|
2746
|
+
hint: p.hint || hint
|
|
2747
|
+
};
|
|
2748
|
+
});
|
|
2749
|
+
const provider = await clack3.select({
|
|
2750
|
+
message: `Where do you manage DNS for ${pc4.cyan(domain)}?`,
|
|
2751
|
+
options
|
|
2752
|
+
});
|
|
2753
|
+
if (clack3.isCancel(provider)) {
|
|
2754
|
+
clack3.cancel("Operation cancelled.");
|
|
2755
|
+
process.exit(0);
|
|
2756
|
+
}
|
|
2757
|
+
return provider;
|
|
2758
|
+
}
|
|
2759
|
+
async function promptContinueManualDNS() {
|
|
2760
|
+
const continueManual = await clack3.confirm({
|
|
2761
|
+
message: "Continue with manual DNS setup?",
|
|
2762
|
+
initialValue: true
|
|
2763
|
+
});
|
|
2764
|
+
if (clack3.isCancel(continueManual)) {
|
|
2765
|
+
clack3.cancel("Operation cancelled.");
|
|
2766
|
+
process.exit(0);
|
|
2767
|
+
}
|
|
2768
|
+
return continueManual;
|
|
2769
|
+
}
|
|
2716
2770
|
var DNS_CATEGORY_LABELS, DNS_STATUS_SYMBOLS;
|
|
2717
2771
|
var init_prompts = __esm({
|
|
2718
2772
|
"src/utils/shared/prompts.ts"() {
|
|
@@ -4433,6 +4487,929 @@ var init_mail_manager = __esm({
|
|
|
4433
4487
|
}
|
|
4434
4488
|
});
|
|
4435
4489
|
|
|
4490
|
+
// src/utils/dns/cloudflare.ts
|
|
4491
|
+
var CLOUDFLARE_API_BASE, CloudflareDNSClient;
|
|
4492
|
+
var init_cloudflare = __esm({
|
|
4493
|
+
"src/utils/dns/cloudflare.ts"() {
|
|
4494
|
+
"use strict";
|
|
4495
|
+
init_esm_shims();
|
|
4496
|
+
CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
4497
|
+
CloudflareDNSClient = class {
|
|
4498
|
+
zoneId;
|
|
4499
|
+
apiToken;
|
|
4500
|
+
constructor(zoneId, apiToken) {
|
|
4501
|
+
this.zoneId = zoneId;
|
|
4502
|
+
this.apiToken = apiToken;
|
|
4503
|
+
}
|
|
4504
|
+
async request(endpoint, method = "GET", body) {
|
|
4505
|
+
const response = await fetch(
|
|
4506
|
+
`${CLOUDFLARE_API_BASE}/zones/${this.zoneId}${endpoint}`,
|
|
4507
|
+
{
|
|
4508
|
+
method,
|
|
4509
|
+
headers: {
|
|
4510
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
4511
|
+
"Content-Type": "application/json"
|
|
4512
|
+
},
|
|
4513
|
+
body: body ? JSON.stringify(body) : void 0
|
|
4514
|
+
}
|
|
4515
|
+
);
|
|
4516
|
+
return response.json();
|
|
4517
|
+
}
|
|
4518
|
+
async createRecord(name, type, content, priority) {
|
|
4519
|
+
const body = {
|
|
4520
|
+
name,
|
|
4521
|
+
type,
|
|
4522
|
+
content,
|
|
4523
|
+
ttl: 1800,
|
|
4524
|
+
proxied: false
|
|
4525
|
+
// Must not be proxied for email records
|
|
4526
|
+
};
|
|
4527
|
+
if (priority !== void 0) {
|
|
4528
|
+
body.priority = priority;
|
|
4529
|
+
}
|
|
4530
|
+
const result = await this.request(
|
|
4531
|
+
"/dns_records",
|
|
4532
|
+
"POST",
|
|
4533
|
+
body
|
|
4534
|
+
);
|
|
4535
|
+
return result.success;
|
|
4536
|
+
}
|
|
4537
|
+
async findRecord(name, type) {
|
|
4538
|
+
const result = await this.request(
|
|
4539
|
+
`/dns_records?name=${encodeURIComponent(name)}&type=${type}`
|
|
4540
|
+
);
|
|
4541
|
+
if (result.success && result.result.length > 0) {
|
|
4542
|
+
return result.result[0];
|
|
4543
|
+
}
|
|
4544
|
+
return null;
|
|
4545
|
+
}
|
|
4546
|
+
async deleteRecord(recordId) {
|
|
4547
|
+
const result = await this.request(
|
|
4548
|
+
`/dns_records/${recordId}`,
|
|
4549
|
+
"DELETE"
|
|
4550
|
+
);
|
|
4551
|
+
return result.success;
|
|
4552
|
+
}
|
|
4553
|
+
async createEmailRecords(data) {
|
|
4554
|
+
const { domain, dkimTokens, mailFromDomain, region } = data;
|
|
4555
|
+
const errors2 = [];
|
|
4556
|
+
let recordsCreated = 0;
|
|
4557
|
+
try {
|
|
4558
|
+
for (const token of dkimTokens) {
|
|
4559
|
+
const name = `${token}._domainkey.${domain}`;
|
|
4560
|
+
const success = await this.createRecord(
|
|
4561
|
+
name,
|
|
4562
|
+
"CNAME",
|
|
4563
|
+
`${token}.dkim.amazonses.com`
|
|
4564
|
+
);
|
|
4565
|
+
if (success) {
|
|
4566
|
+
recordsCreated++;
|
|
4567
|
+
} else {
|
|
4568
|
+
errors2.push(`Failed to create DKIM record: ${name}`);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
const spfSuccess = await this.createRecord(
|
|
4572
|
+
domain,
|
|
4573
|
+
"TXT",
|
|
4574
|
+
"v=spf1 include:amazonses.com ~all"
|
|
4575
|
+
);
|
|
4576
|
+
if (spfSuccess) {
|
|
4577
|
+
recordsCreated++;
|
|
4578
|
+
} else {
|
|
4579
|
+
errors2.push(`Failed to create SPF record for ${domain}`);
|
|
4580
|
+
}
|
|
4581
|
+
const dmarcSuccess = await this.createRecord(
|
|
4582
|
+
`_dmarc.${domain}`,
|
|
4583
|
+
"TXT",
|
|
4584
|
+
`v=DMARC1; p=quarantine; rua=mailto:postmaster@${mailFromDomain || domain}`
|
|
4585
|
+
);
|
|
4586
|
+
if (dmarcSuccess) {
|
|
4587
|
+
recordsCreated++;
|
|
4588
|
+
} else {
|
|
4589
|
+
errors2.push(`Failed to create DMARC record for ${domain}`);
|
|
4590
|
+
}
|
|
4591
|
+
if (mailFromDomain) {
|
|
4592
|
+
const mxSuccess = await this.createRecord(
|
|
4593
|
+
mailFromDomain,
|
|
4594
|
+
"MX",
|
|
4595
|
+
`feedback-smtp.${region}.amazonses.com`,
|
|
4596
|
+
10
|
|
4597
|
+
);
|
|
4598
|
+
if (mxSuccess) {
|
|
4599
|
+
recordsCreated++;
|
|
4600
|
+
} else {
|
|
4601
|
+
errors2.push(`Failed to create MX record for ${mailFromDomain}`);
|
|
4602
|
+
}
|
|
4603
|
+
const mailFromSpfSuccess = await this.createRecord(
|
|
4604
|
+
mailFromDomain,
|
|
4605
|
+
"TXT",
|
|
4606
|
+
"v=spf1 include:amazonses.com ~all"
|
|
4607
|
+
);
|
|
4608
|
+
if (mailFromSpfSuccess) {
|
|
4609
|
+
recordsCreated++;
|
|
4610
|
+
} else {
|
|
4611
|
+
errors2.push(`Failed to create SPF record for ${mailFromDomain}`);
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
return {
|
|
4615
|
+
success: errors2.length === 0,
|
|
4616
|
+
recordsCreated,
|
|
4617
|
+
errors: errors2.length > 0 ? errors2 : void 0
|
|
4618
|
+
};
|
|
4619
|
+
} catch (error) {
|
|
4620
|
+
return {
|
|
4621
|
+
success: false,
|
|
4622
|
+
recordsCreated,
|
|
4623
|
+
errors: [
|
|
4624
|
+
...errors2,
|
|
4625
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
4626
|
+
]
|
|
4627
|
+
};
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
async deleteEmailRecords(data) {
|
|
4631
|
+
const { domain, dkimTokens, mailFromDomain } = data;
|
|
4632
|
+
const errors2 = [];
|
|
4633
|
+
let recordsCreated = 0;
|
|
4634
|
+
try {
|
|
4635
|
+
for (const token of dkimTokens) {
|
|
4636
|
+
const name = `${token}._domainkey.${domain}`;
|
|
4637
|
+
const record = await this.findRecord(name, "CNAME");
|
|
4638
|
+
if (record) {
|
|
4639
|
+
const success = await this.deleteRecord(record.id);
|
|
4640
|
+
if (success) {
|
|
4641
|
+
recordsCreated++;
|
|
4642
|
+
} else {
|
|
4643
|
+
errors2.push(`Failed to delete DKIM record: ${name}`);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
|
|
4648
|
+
if (dmarcRecord) {
|
|
4649
|
+
const success = await this.deleteRecord(dmarcRecord.id);
|
|
4650
|
+
if (success) {
|
|
4651
|
+
recordsCreated++;
|
|
4652
|
+
} else {
|
|
4653
|
+
errors2.push("Failed to delete DMARC record");
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
if (mailFromDomain) {
|
|
4657
|
+
const mxRecord = await this.findRecord(mailFromDomain, "MX");
|
|
4658
|
+
if (mxRecord) {
|
|
4659
|
+
const success = await this.deleteRecord(mxRecord.id);
|
|
4660
|
+
if (success) {
|
|
4661
|
+
recordsCreated++;
|
|
4662
|
+
} else {
|
|
4663
|
+
errors2.push(`Failed to delete MX record for ${mailFromDomain}`);
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
const spfRecord = await this.findRecord(mailFromDomain, "TXT");
|
|
4667
|
+
if (spfRecord) {
|
|
4668
|
+
const success = await this.deleteRecord(spfRecord.id);
|
|
4669
|
+
if (success) {
|
|
4670
|
+
recordsCreated++;
|
|
4671
|
+
} else {
|
|
4672
|
+
errors2.push(`Failed to delete SPF record for ${mailFromDomain}`);
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
return {
|
|
4677
|
+
success: errors2.length === 0,
|
|
4678
|
+
recordsCreated,
|
|
4679
|
+
errors: errors2.length > 0 ? errors2 : void 0
|
|
4680
|
+
};
|
|
4681
|
+
} catch (error) {
|
|
4682
|
+
return {
|
|
4683
|
+
success: false,
|
|
4684
|
+
recordsCreated,
|
|
4685
|
+
errors: [
|
|
4686
|
+
...errors2,
|
|
4687
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
4688
|
+
]
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
async verifyRecords(data) {
|
|
4693
|
+
const { domain, dkimTokens, mailFromDomain, region } = data;
|
|
4694
|
+
const missing = [];
|
|
4695
|
+
const incorrect = [];
|
|
4696
|
+
for (const token of dkimTokens) {
|
|
4697
|
+
const name = `${token}._domainkey.${domain}`;
|
|
4698
|
+
const expectedValue = `${token}.dkim.amazonses.com`;
|
|
4699
|
+
const record = await this.findRecord(name, "CNAME");
|
|
4700
|
+
if (!record) {
|
|
4701
|
+
missing.push(`DKIM: ${name}`);
|
|
4702
|
+
} else if (record.content !== expectedValue) {
|
|
4703
|
+
incorrect.push(`DKIM: ${name} (expected ${expectedValue})`);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
const spfRecord = await this.findRecord(domain, "TXT");
|
|
4707
|
+
if (!spfRecord) {
|
|
4708
|
+
missing.push(`SPF: ${domain}`);
|
|
4709
|
+
} else if (!spfRecord.content.includes("include:amazonses.com")) {
|
|
4710
|
+
incorrect.push(`SPF: ${domain} (missing amazonses.com include)`);
|
|
4711
|
+
}
|
|
4712
|
+
const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
|
|
4713
|
+
if (!dmarcRecord) {
|
|
4714
|
+
missing.push(`DMARC: _dmarc.${domain}`);
|
|
4715
|
+
}
|
|
4716
|
+
if (mailFromDomain) {
|
|
4717
|
+
const mxRecord = await this.findRecord(mailFromDomain, "MX");
|
|
4718
|
+
if (!mxRecord) {
|
|
4719
|
+
missing.push(`MX: ${mailFromDomain}`);
|
|
4720
|
+
} else if (!mxRecord.content.includes(`feedback-smtp.${region}.amazonses.com`)) {
|
|
4721
|
+
incorrect.push(`MX: ${mailFromDomain}`);
|
|
4722
|
+
}
|
|
4723
|
+
const mailFromSpf = await this.findRecord(mailFromDomain, "TXT");
|
|
4724
|
+
if (!mailFromSpf) {
|
|
4725
|
+
missing.push(`SPF: ${mailFromDomain}`);
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
return {
|
|
4729
|
+
verified: missing.length === 0 && incorrect.length === 0,
|
|
4730
|
+
missing,
|
|
4731
|
+
incorrect
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
});
|
|
4737
|
+
|
|
4738
|
+
// src/utils/dns/vercel.ts
|
|
4739
|
+
var VERCEL_API_BASE, VercelDNSClient;
|
|
4740
|
+
var init_vercel = __esm({
|
|
4741
|
+
"src/utils/dns/vercel.ts"() {
|
|
4742
|
+
"use strict";
|
|
4743
|
+
init_esm_shims();
|
|
4744
|
+
VERCEL_API_BASE = "https://api.vercel.com";
|
|
4745
|
+
VercelDNSClient = class {
|
|
4746
|
+
domain;
|
|
4747
|
+
apiToken;
|
|
4748
|
+
teamId;
|
|
4749
|
+
constructor(domain, apiToken, teamId) {
|
|
4750
|
+
this.domain = domain;
|
|
4751
|
+
this.apiToken = apiToken;
|
|
4752
|
+
this.teamId = teamId;
|
|
4753
|
+
}
|
|
4754
|
+
get teamParam() {
|
|
4755
|
+
return this.teamId ? `&teamId=${this.teamId}` : "";
|
|
4756
|
+
}
|
|
4757
|
+
async request(endpoint, method = "GET", body) {
|
|
4758
|
+
const url = `${VERCEL_API_BASE}${endpoint}${endpoint.includes("?") ? "&" : "?"}${this.teamParam.slice(1)}`;
|
|
4759
|
+
const response = await fetch(url, {
|
|
4760
|
+
method,
|
|
4761
|
+
headers: {
|
|
4762
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
4763
|
+
"Content-Type": "application/json"
|
|
4764
|
+
},
|
|
4765
|
+
body: body ? JSON.stringify(body) : void 0
|
|
4766
|
+
});
|
|
4767
|
+
return response.json();
|
|
4768
|
+
}
|
|
4769
|
+
async createRecord(name, type, value, mxPriority) {
|
|
4770
|
+
const relativeName = name === this.domain ? "@" : name.replace(`.${this.domain}`, "");
|
|
4771
|
+
const body = {
|
|
4772
|
+
name: relativeName,
|
|
4773
|
+
type,
|
|
4774
|
+
value,
|
|
4775
|
+
ttl: 1800
|
|
4776
|
+
};
|
|
4777
|
+
if (mxPriority !== void 0) {
|
|
4778
|
+
body.mxPriority = mxPriority;
|
|
4779
|
+
}
|
|
4780
|
+
const result = await this.request(
|
|
4781
|
+
`/v2/domains/${this.domain}/records`,
|
|
4782
|
+
"POST",
|
|
4783
|
+
body
|
|
4784
|
+
);
|
|
4785
|
+
return !result.error;
|
|
4786
|
+
}
|
|
4787
|
+
async findRecord(name, type) {
|
|
4788
|
+
const result = await this.request(
|
|
4789
|
+
`/v4/domains/${this.domain}/records`
|
|
4790
|
+
);
|
|
4791
|
+
if (result.error || !result.records) {
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
const relativeName = name === this.domain ? "@" : name.replace(`.${this.domain}`, "");
|
|
4795
|
+
return result.records.find(
|
|
4796
|
+
(r) => (r.name === relativeName || r.name === name) && r.type === type
|
|
4797
|
+
) || null;
|
|
4798
|
+
}
|
|
4799
|
+
async deleteRecord(recordId) {
|
|
4800
|
+
const result = await this.request(
|
|
4801
|
+
`/v2/domains/${this.domain}/records/${recordId}`,
|
|
4802
|
+
"DELETE"
|
|
4803
|
+
);
|
|
4804
|
+
return !result.error;
|
|
4805
|
+
}
|
|
4806
|
+
async createEmailRecords(data) {
|
|
4807
|
+
const { domain, dkimTokens, mailFromDomain, region } = data;
|
|
4808
|
+
const errors2 = [];
|
|
4809
|
+
let recordsCreated = 0;
|
|
4810
|
+
try {
|
|
4811
|
+
for (const token of dkimTokens) {
|
|
4812
|
+
const name = `${token}._domainkey.${domain}`;
|
|
4813
|
+
const success = await this.createRecord(
|
|
4814
|
+
name,
|
|
4815
|
+
"CNAME",
|
|
4816
|
+
`${token}.dkim.amazonses.com`
|
|
4817
|
+
);
|
|
4818
|
+
if (success) {
|
|
4819
|
+
recordsCreated++;
|
|
4820
|
+
} else {
|
|
4821
|
+
errors2.push(`Failed to create DKIM record: ${name}`);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
const spfSuccess = await this.createRecord(
|
|
4825
|
+
domain,
|
|
4826
|
+
"TXT",
|
|
4827
|
+
"v=spf1 include:amazonses.com ~all"
|
|
4828
|
+
);
|
|
4829
|
+
if (spfSuccess) {
|
|
4830
|
+
recordsCreated++;
|
|
4831
|
+
} else {
|
|
4832
|
+
errors2.push(`Failed to create SPF record for ${domain}`);
|
|
4833
|
+
}
|
|
4834
|
+
const dmarcSuccess = await this.createRecord(
|
|
4835
|
+
`_dmarc.${domain}`,
|
|
4836
|
+
"TXT",
|
|
4837
|
+
`v=DMARC1; p=quarantine; rua=mailto:postmaster@${mailFromDomain || domain}`
|
|
4838
|
+
);
|
|
4839
|
+
if (dmarcSuccess) {
|
|
4840
|
+
recordsCreated++;
|
|
4841
|
+
} else {
|
|
4842
|
+
errors2.push(`Failed to create DMARC record for ${domain}`);
|
|
4843
|
+
}
|
|
4844
|
+
if (mailFromDomain) {
|
|
4845
|
+
const mxSuccess = await this.createRecord(
|
|
4846
|
+
mailFromDomain,
|
|
4847
|
+
"MX",
|
|
4848
|
+
`feedback-smtp.${region}.amazonses.com`,
|
|
4849
|
+
10
|
|
4850
|
+
);
|
|
4851
|
+
if (mxSuccess) {
|
|
4852
|
+
recordsCreated++;
|
|
4853
|
+
} else {
|
|
4854
|
+
errors2.push(`Failed to create MX record for ${mailFromDomain}`);
|
|
4855
|
+
}
|
|
4856
|
+
const mailFromSpfSuccess = await this.createRecord(
|
|
4857
|
+
mailFromDomain,
|
|
4858
|
+
"TXT",
|
|
4859
|
+
"v=spf1 include:amazonses.com ~all"
|
|
4860
|
+
);
|
|
4861
|
+
if (mailFromSpfSuccess) {
|
|
4862
|
+
recordsCreated++;
|
|
4863
|
+
} else {
|
|
4864
|
+
errors2.push(`Failed to create SPF record for ${mailFromDomain}`);
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
return {
|
|
4868
|
+
success: errors2.length === 0,
|
|
4869
|
+
recordsCreated,
|
|
4870
|
+
errors: errors2.length > 0 ? errors2 : void 0
|
|
4871
|
+
};
|
|
4872
|
+
} catch (error) {
|
|
4873
|
+
return {
|
|
4874
|
+
success: false,
|
|
4875
|
+
recordsCreated,
|
|
4876
|
+
errors: [
|
|
4877
|
+
...errors2,
|
|
4878
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
4879
|
+
]
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
async deleteEmailRecords(data) {
|
|
4884
|
+
const { domain, dkimTokens, mailFromDomain } = data;
|
|
4885
|
+
const errors2 = [];
|
|
4886
|
+
let recordsCreated = 0;
|
|
4887
|
+
try {
|
|
4888
|
+
for (const token of dkimTokens) {
|
|
4889
|
+
const name = `${token}._domainkey.${domain}`;
|
|
4890
|
+
const record = await this.findRecord(name, "CNAME");
|
|
4891
|
+
if (record) {
|
|
4892
|
+
const success = await this.deleteRecord(record.id);
|
|
4893
|
+
if (success) {
|
|
4894
|
+
recordsCreated++;
|
|
4895
|
+
} else {
|
|
4896
|
+
errors2.push(`Failed to delete DKIM record: ${name}`);
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
|
|
4901
|
+
if (dmarcRecord) {
|
|
4902
|
+
const success = await this.deleteRecord(dmarcRecord.id);
|
|
4903
|
+
if (success) {
|
|
4904
|
+
recordsCreated++;
|
|
4905
|
+
} else {
|
|
4906
|
+
errors2.push("Failed to delete DMARC record");
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
if (mailFromDomain) {
|
|
4910
|
+
const mxRecord = await this.findRecord(mailFromDomain, "MX");
|
|
4911
|
+
if (mxRecord) {
|
|
4912
|
+
const success = await this.deleteRecord(mxRecord.id);
|
|
4913
|
+
if (success) {
|
|
4914
|
+
recordsCreated++;
|
|
4915
|
+
} else {
|
|
4916
|
+
errors2.push(`Failed to delete MX record for ${mailFromDomain}`);
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
const spfRecord = await this.findRecord(mailFromDomain, "TXT");
|
|
4920
|
+
if (spfRecord) {
|
|
4921
|
+
const success = await this.deleteRecord(spfRecord.id);
|
|
4922
|
+
if (success) {
|
|
4923
|
+
recordsCreated++;
|
|
4924
|
+
} else {
|
|
4925
|
+
errors2.push(`Failed to delete SPF record for ${mailFromDomain}`);
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
return {
|
|
4930
|
+
success: errors2.length === 0,
|
|
4931
|
+
recordsCreated,
|
|
4932
|
+
errors: errors2.length > 0 ? errors2 : void 0
|
|
4933
|
+
};
|
|
4934
|
+
} catch (error) {
|
|
4935
|
+
return {
|
|
4936
|
+
success: false,
|
|
4937
|
+
recordsCreated,
|
|
4938
|
+
errors: [
|
|
4939
|
+
...errors2,
|
|
4940
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
4941
|
+
]
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
async verifyRecords(data) {
|
|
4946
|
+
const { domain, dkimTokens, mailFromDomain, region } = data;
|
|
4947
|
+
const missing = [];
|
|
4948
|
+
const incorrect = [];
|
|
4949
|
+
for (const token of dkimTokens) {
|
|
4950
|
+
const name = `${token}._domainkey.${domain}`;
|
|
4951
|
+
const expectedValue = `${token}.dkim.amazonses.com`;
|
|
4952
|
+
const record = await this.findRecord(name, "CNAME");
|
|
4953
|
+
if (!record) {
|
|
4954
|
+
missing.push(`DKIM: ${name}`);
|
|
4955
|
+
} else if (record.value !== expectedValue) {
|
|
4956
|
+
incorrect.push(`DKIM: ${name} (expected ${expectedValue})`);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
const spfRecord = await this.findRecord(domain, "TXT");
|
|
4960
|
+
if (!spfRecord) {
|
|
4961
|
+
missing.push(`SPF: ${domain}`);
|
|
4962
|
+
} else if (!spfRecord.value.includes("include:amazonses.com")) {
|
|
4963
|
+
incorrect.push(`SPF: ${domain} (missing amazonses.com include)`);
|
|
4964
|
+
}
|
|
4965
|
+
const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
|
|
4966
|
+
if (!dmarcRecord) {
|
|
4967
|
+
missing.push(`DMARC: _dmarc.${domain}`);
|
|
4968
|
+
}
|
|
4969
|
+
if (mailFromDomain) {
|
|
4970
|
+
const mxRecord = await this.findRecord(mailFromDomain, "MX");
|
|
4971
|
+
if (!mxRecord) {
|
|
4972
|
+
missing.push(`MX: ${mailFromDomain}`);
|
|
4973
|
+
} else if (!mxRecord.value.includes(`feedback-smtp.${region}.amazonses.com`)) {
|
|
4974
|
+
incorrect.push(`MX: ${mailFromDomain}`);
|
|
4975
|
+
}
|
|
4976
|
+
const mailFromSpf = await this.findRecord(mailFromDomain, "TXT");
|
|
4977
|
+
if (!mailFromSpf) {
|
|
4978
|
+
missing.push(`SPF: ${mailFromDomain}`);
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
return {
|
|
4982
|
+
verified: missing.length === 0 && incorrect.length === 0,
|
|
4983
|
+
missing,
|
|
4984
|
+
incorrect
|
|
4985
|
+
};
|
|
4986
|
+
}
|
|
4987
|
+
};
|
|
4988
|
+
}
|
|
4989
|
+
});
|
|
4990
|
+
|
|
4991
|
+
// src/utils/dns/create-records.ts
|
|
4992
|
+
function buildEmailDNSRecords(data) {
|
|
4993
|
+
const { domain, dkimTokens, mailFromDomain, region } = data;
|
|
4994
|
+
const records = [];
|
|
4995
|
+
for (const token of dkimTokens) {
|
|
4996
|
+
records.push({
|
|
4997
|
+
name: `${token}._domainkey.${domain}`,
|
|
4998
|
+
type: "CNAME",
|
|
4999
|
+
value: `${token}.dkim.amazonses.com`,
|
|
5000
|
+
category: "dkim"
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
records.push({
|
|
5004
|
+
name: domain,
|
|
5005
|
+
type: "TXT",
|
|
5006
|
+
value: "v=spf1 include:amazonses.com ~all",
|
|
5007
|
+
category: "spf"
|
|
5008
|
+
});
|
|
5009
|
+
const dmarcRuaDomain = mailFromDomain || domain;
|
|
5010
|
+
records.push({
|
|
5011
|
+
name: `_dmarc.${domain}`,
|
|
5012
|
+
type: "TXT",
|
|
5013
|
+
value: `v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}`,
|
|
5014
|
+
category: "dmarc"
|
|
5015
|
+
});
|
|
5016
|
+
if (mailFromDomain) {
|
|
5017
|
+
records.push({
|
|
5018
|
+
name: mailFromDomain,
|
|
5019
|
+
type: "MX",
|
|
5020
|
+
value: `feedback-smtp.${region}.amazonses.com`,
|
|
5021
|
+
priority: 10,
|
|
5022
|
+
category: "mailfrom_mx"
|
|
5023
|
+
});
|
|
5024
|
+
records.push({
|
|
5025
|
+
name: mailFromDomain,
|
|
5026
|
+
type: "TXT",
|
|
5027
|
+
value: "v=spf1 include:amazonses.com ~all",
|
|
5028
|
+
category: "mailfrom_spf"
|
|
5029
|
+
});
|
|
5030
|
+
}
|
|
5031
|
+
return records;
|
|
5032
|
+
}
|
|
5033
|
+
function formatDNSRecordsForDisplay(records) {
|
|
5034
|
+
return records.map((r) => ({
|
|
5035
|
+
name: r.name,
|
|
5036
|
+
type: r.type,
|
|
5037
|
+
value: r.priority ? `${r.priority} ${r.value}` : r.value
|
|
5038
|
+
}));
|
|
5039
|
+
}
|
|
5040
|
+
async function createDNSRecordsForProvider(credentials, data, selectedCategories) {
|
|
5041
|
+
switch (credentials.provider) {
|
|
5042
|
+
case "route53": {
|
|
5043
|
+
const categories = selectedCategories || /* @__PURE__ */ new Set([
|
|
5044
|
+
"dkim",
|
|
5045
|
+
"spf",
|
|
5046
|
+
"dmarc",
|
|
5047
|
+
"mailfrom_mx",
|
|
5048
|
+
"mailfrom_spf"
|
|
5049
|
+
]);
|
|
5050
|
+
try {
|
|
5051
|
+
await createSelectedDNSRecords(
|
|
5052
|
+
credentials.hostedZoneId,
|
|
5053
|
+
data.domain,
|
|
5054
|
+
data.dkimTokens,
|
|
5055
|
+
data.region,
|
|
5056
|
+
categories,
|
|
5057
|
+
void 0,
|
|
5058
|
+
// customTrackingDomain - not used here
|
|
5059
|
+
data.mailFromDomain
|
|
5060
|
+
);
|
|
5061
|
+
let recordsCreated = 0;
|
|
5062
|
+
if (categories.has("dkim")) recordsCreated += data.dkimTokens.length;
|
|
5063
|
+
if (categories.has("spf")) recordsCreated += 1;
|
|
5064
|
+
if (categories.has("dmarc")) recordsCreated += 1;
|
|
5065
|
+
if (data.mailFromDomain) {
|
|
5066
|
+
if (categories.has("mailfrom_mx")) recordsCreated += 1;
|
|
5067
|
+
if (categories.has("mailfrom_spf")) recordsCreated += 1;
|
|
5068
|
+
}
|
|
5069
|
+
return {
|
|
5070
|
+
success: true,
|
|
5071
|
+
recordsCreated
|
|
5072
|
+
};
|
|
5073
|
+
} catch (error) {
|
|
5074
|
+
return {
|
|
5075
|
+
success: false,
|
|
5076
|
+
recordsCreated: 0,
|
|
5077
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
5078
|
+
};
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
case "vercel": {
|
|
5082
|
+
const client = new VercelDNSClient(
|
|
5083
|
+
data.domain,
|
|
5084
|
+
credentials.token,
|
|
5085
|
+
credentials.teamId
|
|
5086
|
+
);
|
|
5087
|
+
return client.createEmailRecords(data);
|
|
5088
|
+
}
|
|
5089
|
+
case "cloudflare": {
|
|
5090
|
+
const client = new CloudflareDNSClient(
|
|
5091
|
+
credentials.zoneId,
|
|
5092
|
+
credentials.token
|
|
5093
|
+
);
|
|
5094
|
+
return client.createEmailRecords(data);
|
|
5095
|
+
}
|
|
5096
|
+
case "manual": {
|
|
5097
|
+
return {
|
|
5098
|
+
success: true,
|
|
5099
|
+
recordsCreated: 0
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
function getDNSProviderDisplayName(provider) {
|
|
5105
|
+
switch (provider) {
|
|
5106
|
+
case "route53":
|
|
5107
|
+
return "AWS Route53";
|
|
5108
|
+
case "vercel":
|
|
5109
|
+
return "Vercel DNS";
|
|
5110
|
+
case "cloudflare":
|
|
5111
|
+
return "Cloudflare";
|
|
5112
|
+
case "manual":
|
|
5113
|
+
return "Manual";
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
function getDNSProviderTokenUrl(provider) {
|
|
5117
|
+
switch (provider) {
|
|
5118
|
+
case "vercel":
|
|
5119
|
+
return "https://vercel.com/account/tokens";
|
|
5120
|
+
case "cloudflare":
|
|
5121
|
+
return "https://dash.cloudflare.com/profile/api-tokens";
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
var init_create_records = __esm({
|
|
5125
|
+
"src/utils/dns/create-records.ts"() {
|
|
5126
|
+
"use strict";
|
|
5127
|
+
init_esm_shims();
|
|
5128
|
+
init_route53();
|
|
5129
|
+
init_cloudflare();
|
|
5130
|
+
init_vercel();
|
|
5131
|
+
}
|
|
5132
|
+
});
|
|
5133
|
+
|
|
5134
|
+
// src/utils/dns/credentials.ts
|
|
5135
|
+
function getDNSProviderEnvVars(provider) {
|
|
5136
|
+
switch (provider) {
|
|
5137
|
+
case "route53":
|
|
5138
|
+
return [];
|
|
5139
|
+
// Uses AWS credentials from environment/config
|
|
5140
|
+
case "vercel":
|
|
5141
|
+
return ["VERCEL_TOKEN"];
|
|
5142
|
+
case "cloudflare":
|
|
5143
|
+
return ["CLOUDFLARE_API_TOKEN"];
|
|
5144
|
+
case "manual":
|
|
5145
|
+
return [];
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
function getDNSProviderOptionalEnvVars(provider) {
|
|
5149
|
+
switch (provider) {
|
|
5150
|
+
case "vercel":
|
|
5151
|
+
return ["VERCEL_TEAM_ID"];
|
|
5152
|
+
case "cloudflare":
|
|
5153
|
+
return ["CLOUDFLARE_ZONE_ID"];
|
|
5154
|
+
default:
|
|
5155
|
+
return [];
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
function hasVercelToken() {
|
|
5159
|
+
return !!process.env.VERCEL_TOKEN;
|
|
5160
|
+
}
|
|
5161
|
+
function hasCloudflareToken() {
|
|
5162
|
+
return !!process.env.CLOUDFLARE_API_TOKEN;
|
|
5163
|
+
}
|
|
5164
|
+
async function validateVercelCredentials(token) {
|
|
5165
|
+
try {
|
|
5166
|
+
const response = await fetch("https://api.vercel.com/v2/user", {
|
|
5167
|
+
headers: {
|
|
5168
|
+
Authorization: `Bearer ${token}`
|
|
5169
|
+
}
|
|
5170
|
+
});
|
|
5171
|
+
return response.ok;
|
|
5172
|
+
} catch {
|
|
5173
|
+
return false;
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
5176
|
+
async function checkVercelDomain(token, domain, teamId) {
|
|
5177
|
+
try {
|
|
5178
|
+
const teamParam = teamId ? `&teamId=${teamId}` : "";
|
|
5179
|
+
const response = await fetch(
|
|
5180
|
+
`https://api.vercel.com/v5/domains/${domain}?${teamParam}`,
|
|
5181
|
+
{
|
|
5182
|
+
headers: {
|
|
5183
|
+
Authorization: `Bearer ${token}`
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
);
|
|
5187
|
+
return response.ok;
|
|
5188
|
+
} catch {
|
|
5189
|
+
return false;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
async function validateCloudflareCredentials(token) {
|
|
5193
|
+
try {
|
|
5194
|
+
const response = await fetch(
|
|
5195
|
+
"https://api.cloudflare.com/client/v4/user/tokens/verify",
|
|
5196
|
+
{
|
|
5197
|
+
headers: {
|
|
5198
|
+
Authorization: `Bearer ${token}`
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
);
|
|
5202
|
+
const data = await response.json();
|
|
5203
|
+
return data.success === true;
|
|
5204
|
+
} catch {
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
async function findCloudflareZoneId(token, domain) {
|
|
5209
|
+
try {
|
|
5210
|
+
const response = await fetch(
|
|
5211
|
+
`https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(domain)}`,
|
|
5212
|
+
{
|
|
5213
|
+
headers: {
|
|
5214
|
+
Authorization: `Bearer ${token}`,
|
|
5215
|
+
"Content-Type": "application/json"
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
);
|
|
5219
|
+
const data = await response.json();
|
|
5220
|
+
if (data.success && data.result.length > 0) {
|
|
5221
|
+
return data.result[0].id;
|
|
5222
|
+
}
|
|
5223
|
+
const parts = domain.split(".");
|
|
5224
|
+
if (parts.length > 2) {
|
|
5225
|
+
const parentDomain = parts.slice(-2).join(".");
|
|
5226
|
+
const parentResponse = await fetch(
|
|
5227
|
+
`https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(parentDomain)}`,
|
|
5228
|
+
{
|
|
5229
|
+
headers: {
|
|
5230
|
+
Authorization: `Bearer ${token}`,
|
|
5231
|
+
"Content-Type": "application/json"
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
);
|
|
5235
|
+
const parentData = await parentResponse.json();
|
|
5236
|
+
if (parentData.success && parentData.result.length > 0) {
|
|
5237
|
+
return parentData.result[0].id;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
return null;
|
|
5241
|
+
} catch {
|
|
5242
|
+
return null;
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
async function getDNSCredentials(provider, domain, region) {
|
|
5246
|
+
switch (provider) {
|
|
5247
|
+
case "route53": {
|
|
5248
|
+
const hostedZone = await findHostedZone(domain, region);
|
|
5249
|
+
if (hostedZone) {
|
|
5250
|
+
return {
|
|
5251
|
+
valid: true,
|
|
5252
|
+
credentials: { provider: "route53", hostedZoneId: hostedZone.id }
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
return {
|
|
5256
|
+
valid: false,
|
|
5257
|
+
error: `No Route53 hosted zone found for ${domain}`
|
|
5258
|
+
};
|
|
5259
|
+
}
|
|
5260
|
+
case "vercel": {
|
|
5261
|
+
const token = process.env.VERCEL_TOKEN;
|
|
5262
|
+
if (!token) {
|
|
5263
|
+
return {
|
|
5264
|
+
valid: false,
|
|
5265
|
+
error: "VERCEL_TOKEN environment variable is not set"
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5268
|
+
const isValid = await validateVercelCredentials(token);
|
|
5269
|
+
if (!isValid) {
|
|
5270
|
+
return {
|
|
5271
|
+
valid: false,
|
|
5272
|
+
error: "Invalid VERCEL_TOKEN - authentication failed"
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
const teamId = process.env.VERCEL_TEAM_ID;
|
|
5276
|
+
const hasDomain = await checkVercelDomain(token, domain, teamId);
|
|
5277
|
+
if (!hasDomain) {
|
|
5278
|
+
return {
|
|
5279
|
+
valid: false,
|
|
5280
|
+
error: `Domain ${domain} not found in Vercel DNS`
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
return {
|
|
5284
|
+
valid: true,
|
|
5285
|
+
credentials: { provider: "vercel", token, teamId }
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
case "cloudflare": {
|
|
5289
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
5290
|
+
if (!token) {
|
|
5291
|
+
return {
|
|
5292
|
+
valid: false,
|
|
5293
|
+
error: "CLOUDFLARE_API_TOKEN environment variable is not set"
|
|
5294
|
+
};
|
|
5295
|
+
}
|
|
5296
|
+
const isValid = await validateCloudflareCredentials(token);
|
|
5297
|
+
if (!isValid) {
|
|
5298
|
+
return {
|
|
5299
|
+
valid: false,
|
|
5300
|
+
error: "Invalid CLOUDFLARE_API_TOKEN - authentication failed"
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
let zoneId = process.env.CLOUDFLARE_ZONE_ID;
|
|
5304
|
+
if (!zoneId) {
|
|
5305
|
+
const detectedZoneId = await findCloudflareZoneId(token, domain);
|
|
5306
|
+
zoneId = detectedZoneId ?? void 0;
|
|
5307
|
+
}
|
|
5308
|
+
if (!zoneId) {
|
|
5309
|
+
return {
|
|
5310
|
+
valid: false,
|
|
5311
|
+
error: `Could not find Cloudflare zone for ${domain}. Set CLOUDFLARE_ZONE_ID if the domain uses a different zone.`
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
return {
|
|
5315
|
+
valid: true,
|
|
5316
|
+
credentials: { provider: "cloudflare", token, zoneId }
|
|
5317
|
+
};
|
|
5318
|
+
}
|
|
5319
|
+
case "manual":
|
|
5320
|
+
return {
|
|
5321
|
+
valid: true,
|
|
5322
|
+
credentials: { provider: "manual" }
|
|
5323
|
+
};
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
async function detectAvailableDNSProviders(domain, region) {
|
|
5327
|
+
const providers = [];
|
|
5328
|
+
const hostedZone = await findHostedZone(domain, region);
|
|
5329
|
+
providers.push({
|
|
5330
|
+
provider: "route53",
|
|
5331
|
+
detected: !!hostedZone,
|
|
5332
|
+
hint: hostedZone ? "Hosted zone detected" : void 0
|
|
5333
|
+
});
|
|
5334
|
+
const vercelToken = process.env.VERCEL_TOKEN;
|
|
5335
|
+
if (vercelToken) {
|
|
5336
|
+
const teamId = process.env.VERCEL_TEAM_ID;
|
|
5337
|
+
const hasDomain = await checkVercelDomain(vercelToken, domain, teamId);
|
|
5338
|
+
providers.push({
|
|
5339
|
+
provider: "vercel",
|
|
5340
|
+
detected: hasDomain,
|
|
5341
|
+
hint: hasDomain ? "Domain found in Vercel DNS" : "Token found"
|
|
5342
|
+
});
|
|
5343
|
+
} else {
|
|
5344
|
+
providers.push({
|
|
5345
|
+
provider: "vercel",
|
|
5346
|
+
detected: false
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
const cfToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
5350
|
+
if (cfToken) {
|
|
5351
|
+
const zoneId = process.env.CLOUDFLARE_ZONE_ID || await findCloudflareZoneId(cfToken, domain);
|
|
5352
|
+
providers.push({
|
|
5353
|
+
provider: "cloudflare",
|
|
5354
|
+
detected: !!zoneId,
|
|
5355
|
+
hint: zoneId ? "Zone found" : "Token found"
|
|
5356
|
+
});
|
|
5357
|
+
} else {
|
|
5358
|
+
providers.push({
|
|
5359
|
+
provider: "cloudflare",
|
|
5360
|
+
detected: false
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
providers.push({
|
|
5364
|
+
provider: "manual",
|
|
5365
|
+
detected: true,
|
|
5366
|
+
hint: "I'll add DNS records myself"
|
|
5367
|
+
});
|
|
5368
|
+
return providers.sort((a, b) => {
|
|
5369
|
+
if (a.provider === "manual") return 1;
|
|
5370
|
+
if (b.provider === "manual") return -1;
|
|
5371
|
+
if (a.detected && !b.detected) return -1;
|
|
5372
|
+
if (!a.detected && b.detected) return 1;
|
|
5373
|
+
return 0;
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
var init_credentials = __esm({
|
|
5377
|
+
"src/utils/dns/credentials.ts"() {
|
|
5378
|
+
"use strict";
|
|
5379
|
+
init_esm_shims();
|
|
5380
|
+
init_route53();
|
|
5381
|
+
}
|
|
5382
|
+
});
|
|
5383
|
+
|
|
5384
|
+
// src/utils/dns/index.ts
|
|
5385
|
+
var dns_exports = {};
|
|
5386
|
+
__export(dns_exports, {
|
|
5387
|
+
CloudflareDNSClient: () => CloudflareDNSClient,
|
|
5388
|
+
VercelDNSClient: () => VercelDNSClient,
|
|
5389
|
+
buildEmailDNSRecords: () => buildEmailDNSRecords,
|
|
5390
|
+
createDNSRecordsForProvider: () => createDNSRecordsForProvider,
|
|
5391
|
+
detectAvailableDNSProviders: () => detectAvailableDNSProviders,
|
|
5392
|
+
findCloudflareZoneId: () => findCloudflareZoneId,
|
|
5393
|
+
formatDNSRecordsForDisplay: () => formatDNSRecordsForDisplay,
|
|
5394
|
+
getDNSCredentials: () => getDNSCredentials,
|
|
5395
|
+
getDNSProviderDisplayName: () => getDNSProviderDisplayName,
|
|
5396
|
+
getDNSProviderEnvVars: () => getDNSProviderEnvVars,
|
|
5397
|
+
getDNSProviderOptionalEnvVars: () => getDNSProviderOptionalEnvVars,
|
|
5398
|
+
getDNSProviderTokenUrl: () => getDNSProviderTokenUrl,
|
|
5399
|
+
hasCloudflareToken: () => hasCloudflareToken,
|
|
5400
|
+
hasVercelToken: () => hasVercelToken
|
|
5401
|
+
});
|
|
5402
|
+
var init_dns = __esm({
|
|
5403
|
+
"src/utils/dns/index.ts"() {
|
|
5404
|
+
"use strict";
|
|
5405
|
+
init_esm_shims();
|
|
5406
|
+
init_cloudflare();
|
|
5407
|
+
init_create_records();
|
|
5408
|
+
init_credentials();
|
|
5409
|
+
init_vercel();
|
|
5410
|
+
}
|
|
5411
|
+
});
|
|
5412
|
+
|
|
4436
5413
|
// src/utils/shared/assume-role.ts
|
|
4437
5414
|
var assume_role_exports = {};
|
|
4438
5415
|
__export(assume_role_exports, {
|
|
@@ -14978,51 +15955,149 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
|
|
|
14978
15955
|
await saveConnectionMetadata(metadata);
|
|
14979
15956
|
progress.info("Connection metadata saved for upgrade and restore capability");
|
|
14980
15957
|
let dnsAutoCreated = false;
|
|
15958
|
+
let dnsProvider;
|
|
14981
15959
|
if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
|
|
14982
|
-
const {
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15960
|
+
const {
|
|
15961
|
+
detectAvailableDNSProviders: detectAvailableDNSProviders2,
|
|
15962
|
+
getDNSCredentials: getDNSCredentials2,
|
|
15963
|
+
createDNSRecordsForProvider: createDNSRecordsForProvider2,
|
|
15964
|
+
getDNSProviderDisplayName: getDNSProviderDisplayName2,
|
|
15965
|
+
getDNSProviderTokenUrl: getDNSProviderTokenUrl2,
|
|
15966
|
+
buildEmailDNSRecords: buildEmailDNSRecords2
|
|
15967
|
+
} = await Promise.resolve().then(() => (init_dns(), dns_exports));
|
|
15968
|
+
const {
|
|
15969
|
+
promptDNSProvider: promptDNSProvider2,
|
|
15970
|
+
promptDNSConfirmation: promptDNSConfirmation2,
|
|
15971
|
+
promptContinueManualDNS: promptContinueManualDNS2
|
|
15972
|
+
} = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
|
|
15973
|
+
const { previewDNSChanges: previewDNSChanges2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
15974
|
+
progress.start("Detecting DNS providers");
|
|
15975
|
+
const availableProviders = await detectAvailableDNSProviders2(
|
|
15976
|
+
outputs.domain,
|
|
15977
|
+
region
|
|
15978
|
+
);
|
|
15979
|
+
progress.stop();
|
|
15980
|
+
const selectedProvider = await promptDNSProvider2(
|
|
15981
|
+
outputs.domain,
|
|
15982
|
+
availableProviders
|
|
15983
|
+
);
|
|
15984
|
+
dnsProvider = selectedProvider;
|
|
15985
|
+
if (selectedProvider !== "manual") {
|
|
15986
|
+
progress.start(
|
|
15987
|
+
`Validating ${getDNSProviderDisplayName2(selectedProvider)} credentials`
|
|
15988
|
+
);
|
|
15989
|
+
const credentialResult2 = await getDNSCredentials2(
|
|
15990
|
+
selectedProvider,
|
|
15991
|
+
outputs.domain,
|
|
15992
|
+
region
|
|
15993
|
+
);
|
|
15994
|
+
progress.stop();
|
|
15995
|
+
if (credentialResult2.valid && credentialResult2.credentials) {
|
|
15996
|
+
const credentials = credentialResult2.credentials;
|
|
15997
|
+
if (credentials.provider === "route53") {
|
|
15998
|
+
try {
|
|
15999
|
+
progress.start("Checking existing DNS records");
|
|
16000
|
+
const dnsPreview = await previewDNSChanges2(
|
|
16001
|
+
credentials.hostedZoneId,
|
|
15004
16002
|
outputs.domain,
|
|
15005
16003
|
outputs.dkimTokens,
|
|
15006
16004
|
region,
|
|
15007
|
-
selectedCategories,
|
|
15008
16005
|
outputs.customTrackingDomain,
|
|
15009
16006
|
outputs.mailFromDomain
|
|
15010
16007
|
);
|
|
16008
|
+
progress.stop();
|
|
16009
|
+
const { shouldCreate, selectedCategories } = await promptDNSConfirmation2(dnsPreview);
|
|
16010
|
+
if (shouldCreate && selectedCategories.size > 0) {
|
|
16011
|
+
progress.start("Creating selected DNS records in Route53");
|
|
16012
|
+
const result = await createDNSRecordsForProvider2(
|
|
16013
|
+
credentials,
|
|
16014
|
+
{
|
|
16015
|
+
domain: outputs.domain,
|
|
16016
|
+
dkimTokens: outputs.dkimTokens,
|
|
16017
|
+
mailFromDomain: outputs.mailFromDomain,
|
|
16018
|
+
region
|
|
16019
|
+
},
|
|
16020
|
+
selectedCategories
|
|
16021
|
+
);
|
|
16022
|
+
if (result.success) {
|
|
16023
|
+
progress.succeed(
|
|
16024
|
+
`Created ${selectedCategories.size} DNS record group(s) in Route53`
|
|
16025
|
+
);
|
|
16026
|
+
dnsAutoCreated = true;
|
|
16027
|
+
} else {
|
|
16028
|
+
progress.fail("Failed to create some DNS records");
|
|
16029
|
+
if (result.errors) {
|
|
16030
|
+
for (const error of result.errors) {
|
|
16031
|
+
clack17.log.warn(error);
|
|
16032
|
+
}
|
|
16033
|
+
}
|
|
16034
|
+
}
|
|
16035
|
+
} else {
|
|
16036
|
+
clack17.log.info(
|
|
16037
|
+
"Skipping DNS record creation. You can add them manually."
|
|
16038
|
+
);
|
|
16039
|
+
}
|
|
16040
|
+
} catch (error) {
|
|
16041
|
+
progress.stop();
|
|
16042
|
+
clack17.log.warn(`Could not manage DNS records: ${error.message}`);
|
|
16043
|
+
}
|
|
16044
|
+
} else {
|
|
16045
|
+
const recordData = {
|
|
16046
|
+
domain: outputs.domain,
|
|
16047
|
+
dkimTokens: outputs.dkimTokens,
|
|
16048
|
+
mailFromDomain: outputs.mailFromDomain,
|
|
16049
|
+
region
|
|
16050
|
+
};
|
|
16051
|
+
const records = buildEmailDNSRecords2(recordData);
|
|
16052
|
+
clack17.log.info(pc18.bold("DNS records to create:"));
|
|
16053
|
+
for (const record of records) {
|
|
16054
|
+
clack17.log.info(
|
|
16055
|
+
pc18.dim(` ${record.type} ${record.name} \u2192 ${record.value}`)
|
|
16056
|
+
);
|
|
16057
|
+
}
|
|
16058
|
+
progress.start(
|
|
16059
|
+
`Creating DNS records in ${getDNSProviderDisplayName2(credentials.provider)}`
|
|
16060
|
+
);
|
|
16061
|
+
const result = await createDNSRecordsForProvider2(
|
|
16062
|
+
credentials,
|
|
16063
|
+
recordData
|
|
16064
|
+
);
|
|
16065
|
+
if (result.success) {
|
|
15011
16066
|
progress.succeed(
|
|
15012
|
-
`Created ${
|
|
16067
|
+
`Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(credentials.provider)}`
|
|
15013
16068
|
);
|
|
15014
16069
|
dnsAutoCreated = true;
|
|
15015
16070
|
} else {
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
16071
|
+
progress.fail("Failed to create some DNS records");
|
|
16072
|
+
if (result.errors) {
|
|
16073
|
+
for (const error of result.errors) {
|
|
16074
|
+
clack17.log.warn(error);
|
|
16075
|
+
}
|
|
16076
|
+
}
|
|
15019
16077
|
}
|
|
15020
|
-
}
|
|
15021
|
-
|
|
15022
|
-
|
|
16078
|
+
}
|
|
16079
|
+
} else {
|
|
16080
|
+
clack17.log.warn(
|
|
16081
|
+
credentialResult2.error || "Could not validate credentials"
|
|
16082
|
+
);
|
|
16083
|
+
if (selectedProvider === "vercel" || selectedProvider === "cloudflare") {
|
|
16084
|
+
clack17.log.info(
|
|
16085
|
+
`Set the ${selectedProvider === "vercel" ? "VERCEL_TOKEN" : "CLOUDFLARE_API_TOKEN"} environment variable to enable automatic DNS management.`
|
|
16086
|
+
);
|
|
16087
|
+
clack17.log.info(
|
|
16088
|
+
`You can create a token at: ${pc18.cyan(getDNSProviderTokenUrl2(selectedProvider))}`
|
|
16089
|
+
);
|
|
16090
|
+
}
|
|
16091
|
+
const continueManual = await promptContinueManualDNS2();
|
|
16092
|
+
if (continueManual) {
|
|
16093
|
+
dnsProvider = "manual";
|
|
15023
16094
|
}
|
|
15024
16095
|
}
|
|
15025
16096
|
}
|
|
16097
|
+
if (dnsProvider && metadata.services.email) {
|
|
16098
|
+
metadata.services.email.dnsProvider = dnsProvider;
|
|
16099
|
+
await saveConnectionMetadata(metadata);
|
|
16100
|
+
}
|
|
15026
16101
|
}
|
|
15027
16102
|
const dnsRecords = [];
|
|
15028
16103
|
if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0 && !dnsAutoCreated) {
|
|
@@ -15380,6 +16455,8 @@ import * as clack20 from "@clack/prompts";
|
|
|
15380
16455
|
import * as pulumi18 from "@pulumi/pulumi";
|
|
15381
16456
|
import pc21 from "picocolors";
|
|
15382
16457
|
init_events();
|
|
16458
|
+
init_create_records();
|
|
16459
|
+
init_credentials();
|
|
15383
16460
|
init_costs();
|
|
15384
16461
|
init_presets();
|
|
15385
16462
|
init_aws();
|
|
@@ -15820,35 +16897,54 @@ ${pc21.bold("Current Configuration:")}
|
|
|
15820
16897
|
"This ensures all tracking links use secure HTTPS connections."
|
|
15821
16898
|
)
|
|
15822
16899
|
);
|
|
15823
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
15826
|
-
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
clack20.log.info(
|
|
15833
|
-
pc21.dim(
|
|
15834
|
-
"DNS records (SSL certificate validation + CloudFront) will be created automatically."
|
|
15835
|
-
)
|
|
15836
|
-
);
|
|
16900
|
+
let trackingDnsProvider = metadata.services.email?.dnsProvider;
|
|
16901
|
+
let canAutomateDNS = false;
|
|
16902
|
+
if (trackingDnsProvider) {
|
|
16903
|
+
canAutomateDNS = trackingDnsProvider !== "manual";
|
|
16904
|
+
if (canAutomateDNS) {
|
|
16905
|
+
progress.info(
|
|
16906
|
+
`Will use ${pc21.cyan(getDNSProviderDisplayName(trackingDnsProvider))} for DNS records ${pc21.green("\u2713")}`
|
|
16907
|
+
);
|
|
16908
|
+
}
|
|
15837
16909
|
} else {
|
|
15838
|
-
|
|
15839
|
-
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
|
|
15843
|
-
"You'll need to manually create DNS records for SSL certificate validation and CloudFront."
|
|
16910
|
+
const availableProviders = await progress.execute(
|
|
16911
|
+
"Detecting available DNS providers",
|
|
16912
|
+
async () => await detectAvailableDNSProviders(
|
|
16913
|
+
trackingDomain || config2.domain,
|
|
16914
|
+
region
|
|
15844
16915
|
)
|
|
15845
16916
|
);
|
|
15846
|
-
|
|
15847
|
-
|
|
16917
|
+
const detectedProvider = availableProviders.find(
|
|
16918
|
+
(p) => p.detected && p.provider !== "manual"
|
|
15848
16919
|
);
|
|
16920
|
+
if (detectedProvider) {
|
|
16921
|
+
trackingDnsProvider = detectedProvider.provider;
|
|
16922
|
+
canAutomateDNS = true;
|
|
16923
|
+
progress.info(
|
|
16924
|
+
`Found ${pc21.cyan(getDNSProviderDisplayName(detectedProvider.provider))} ${pc21.green("\u2713")}`
|
|
16925
|
+
);
|
|
16926
|
+
clack20.log.info(
|
|
16927
|
+
pc21.dim(
|
|
16928
|
+
"DNS records (SSL certificate validation + CloudFront) will be created automatically."
|
|
16929
|
+
)
|
|
16930
|
+
);
|
|
16931
|
+
} else {
|
|
16932
|
+
canAutomateDNS = false;
|
|
16933
|
+
clack20.log.warn(
|
|
16934
|
+
`No automatic DNS provider detected for ${pc21.cyan(trackingDomain || config2.domain)}`
|
|
16935
|
+
);
|
|
16936
|
+
clack20.log.info(
|
|
16937
|
+
pc21.dim(
|
|
16938
|
+
"You'll need to manually create DNS records for SSL certificate validation and CloudFront."
|
|
16939
|
+
)
|
|
16940
|
+
);
|
|
16941
|
+
clack20.log.info(
|
|
16942
|
+
pc21.dim("DNS record details will be shown after deployment.")
|
|
16943
|
+
);
|
|
16944
|
+
}
|
|
15849
16945
|
}
|
|
15850
16946
|
const confirmHttps = await clack20.confirm({
|
|
15851
|
-
message:
|
|
16947
|
+
message: canAutomateDNS ? "Proceed with automatic HTTPS setup?" : "Proceed with manual HTTPS setup (requires DNS configuration)?",
|
|
15852
16948
|
initialValue: true
|
|
15853
16949
|
});
|
|
15854
16950
|
if (clack20.isCancel(confirmHttps) || !confirmHttps) {
|
|
@@ -16660,31 +17756,103 @@ ${pc21.bold("Cost Impact:")}`);
|
|
|
16660
17756
|
trackError("UPGRADE_FAILED", "email:upgrade", { step: "deploy" });
|
|
16661
17757
|
throw new Error(`Pulumi upgrade failed: ${error.message}`);
|
|
16662
17758
|
}
|
|
17759
|
+
let dnsAutoCreated = false;
|
|
16663
17760
|
if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
17761
|
+
let dnsProvider = metadata.services.email?.dnsProvider;
|
|
17762
|
+
if (!dnsProvider) {
|
|
17763
|
+
const availableProviders = await progress.execute(
|
|
17764
|
+
"Detecting available DNS providers",
|
|
17765
|
+
async () => await detectAvailableDNSProviders(outputs.domain, region)
|
|
17766
|
+
);
|
|
17767
|
+
const detectedProvider = availableProviders.find(
|
|
17768
|
+
(p) => p.detected && p.provider !== "manual"
|
|
17769
|
+
);
|
|
17770
|
+
if (detectedProvider) {
|
|
17771
|
+
dnsProvider = await promptDNSProvider(
|
|
16672
17772
|
outputs.domain,
|
|
16673
|
-
|
|
16674
|
-
region,
|
|
16675
|
-
outputs.customTrackingDomain,
|
|
16676
|
-
mailFromDomain,
|
|
16677
|
-
outputs.cloudFrontDomain
|
|
17773
|
+
availableProviders
|
|
16678
17774
|
);
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
17775
|
+
if (dnsProvider && dnsProvider !== "manual" && metadata.services.email) {
|
|
17776
|
+
metadata.services.email.dnsProvider = dnsProvider;
|
|
17777
|
+
}
|
|
17778
|
+
}
|
|
17779
|
+
}
|
|
17780
|
+
if (dnsProvider && dnsProvider !== "manual") {
|
|
17781
|
+
const credResult = await progress.execute(
|
|
17782
|
+
`Validating ${getDNSProviderDisplayName(dnsProvider)} credentials`,
|
|
17783
|
+
async () => await getDNSCredentials(dnsProvider, outputs.domain, region)
|
|
17784
|
+
);
|
|
17785
|
+
if (credResult.valid && credResult.credentials) {
|
|
17786
|
+
const mailFromDomain = updatedConfig.mailFromDomain || `mail.${outputs.domain}`;
|
|
17787
|
+
const dnsData = {
|
|
17788
|
+
domain: outputs.domain,
|
|
17789
|
+
dkimTokens: outputs.dkimTokens,
|
|
17790
|
+
mailFromDomain,
|
|
17791
|
+
region
|
|
17792
|
+
};
|
|
17793
|
+
try {
|
|
17794
|
+
progress.start(
|
|
17795
|
+
`Creating DNS records in ${getDNSProviderDisplayName(dnsProvider)}`
|
|
17796
|
+
);
|
|
17797
|
+
const result = await createDNSRecordsForProvider(
|
|
17798
|
+
credResult.credentials,
|
|
17799
|
+
dnsData
|
|
17800
|
+
);
|
|
17801
|
+
if (result.success) {
|
|
17802
|
+
progress.succeed(
|
|
17803
|
+
`Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName(dnsProvider)}`
|
|
17804
|
+
);
|
|
17805
|
+
dnsAutoCreated = true;
|
|
17806
|
+
} else {
|
|
17807
|
+
progress.fail(
|
|
17808
|
+
`Failed to create some DNS records: ${result.errors?.join(", ")}`
|
|
17809
|
+
);
|
|
17810
|
+
progress.info(
|
|
17811
|
+
"You can manually add the required DNS records shown below"
|
|
17812
|
+
);
|
|
17813
|
+
}
|
|
17814
|
+
} catch (error) {
|
|
17815
|
+
progress.fail(
|
|
17816
|
+
`Failed to create DNS records automatically: ${error.message}`
|
|
17817
|
+
);
|
|
17818
|
+
progress.info(
|
|
17819
|
+
"You can manually add the required DNS records shown below"
|
|
17820
|
+
);
|
|
17821
|
+
}
|
|
17822
|
+
} else {
|
|
17823
|
+
clack20.log.warn(
|
|
17824
|
+
credResult.error || `Unable to validate ${getDNSProviderDisplayName(dnsProvider)} credentials`
|
|
16686
17825
|
);
|
|
17826
|
+
if (dnsProvider === "vercel" || dnsProvider === "cloudflare") {
|
|
17827
|
+
clack20.log.info(
|
|
17828
|
+
`Set ${dnsProvider === "vercel" ? "VERCEL_TOKEN" : "CLOUDFLARE_API_TOKEN"} to enable automatic DNS management.`
|
|
17829
|
+
);
|
|
17830
|
+
clack20.log.info(
|
|
17831
|
+
`You can create a token at: ${pc21.cyan(getDNSProviderTokenUrl(dnsProvider))}`
|
|
17832
|
+
);
|
|
17833
|
+
}
|
|
17834
|
+
}
|
|
17835
|
+
}
|
|
17836
|
+
if (!dnsAutoCreated) {
|
|
17837
|
+
const mailFromDomain = updatedConfig.mailFromDomain || `mail.${outputs.domain}`;
|
|
17838
|
+
const dnsData = {
|
|
17839
|
+
domain: outputs.domain,
|
|
17840
|
+
dkimTokens: outputs.dkimTokens,
|
|
17841
|
+
mailFromDomain,
|
|
17842
|
+
region
|
|
17843
|
+
};
|
|
17844
|
+
const dnsRecords = buildEmailDNSRecords(dnsData);
|
|
17845
|
+
const displayRecords = formatDNSRecordsForDisplay(dnsRecords);
|
|
17846
|
+
console.log(
|
|
17847
|
+
`
|
|
17848
|
+
${pc21.bold("Add these DNS records to your DNS provider:")}
|
|
17849
|
+
`
|
|
17850
|
+
);
|
|
17851
|
+
for (const record of displayRecords) {
|
|
17852
|
+
console.log(` ${pc21.cyan(record.type)} ${record.name}`);
|
|
17853
|
+
console.log(` ${pc21.dim(record.value)}`);
|
|
16687
17854
|
}
|
|
17855
|
+
console.log("");
|
|
16688
17856
|
}
|
|
16689
17857
|
}
|
|
16690
17858
|
updateEmailConfig(metadata, updatedConfig);
|