@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/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 { findHostedZone: findHostedZone2, previewDNSChanges: previewDNSChanges2, createSelectedDNSRecords: createSelectedDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
14983
- const { promptDNSManagement: promptDNSManagement2, promptDNSConfirmation: promptDNSConfirmation2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
14984
- const hostedZone = await findHostedZone2(outputs.domain, region);
14985
- if (hostedZone) {
14986
- const manageDNS = await promptDNSManagement2(outputs.domain);
14987
- if (manageDNS) {
14988
- try {
14989
- progress.start("Checking existing DNS records");
14990
- const dnsPreview = await previewDNSChanges2(
14991
- hostedZone.id,
14992
- outputs.domain,
14993
- outputs.dkimTokens,
14994
- region,
14995
- outputs.customTrackingDomain,
14996
- outputs.mailFromDomain
14997
- );
14998
- progress.stop();
14999
- const { shouldCreate, selectedCategories } = await promptDNSConfirmation2(dnsPreview);
15000
- if (shouldCreate && selectedCategories.size > 0) {
15001
- progress.start("Creating selected DNS records in Route53");
15002
- await createSelectedDNSRecords2(
15003
- hostedZone.id,
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 ${selectedCategories.size} DNS record group(s) in Route53`
16067
+ `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(credentials.provider)}`
15013
16068
  );
15014
16069
  dnsAutoCreated = true;
15015
16070
  } else {
15016
- clack17.log.info(
15017
- "Skipping DNS record creation. You can add them manually."
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
- } catch (error) {
15021
- progress.stop();
15022
- clack17.log.warn(`Could not manage DNS records: ${error.message}`);
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
- const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
15824
- const hostedZone = await progress.execute(
15825
- "Checking for Route53 hosted zone",
15826
- async () => await findHostedZone2(trackingDomain || config2.domain, region)
15827
- );
15828
- if (hostedZone) {
15829
- progress.info(
15830
- `Found Route53 hosted zone: ${pc21.cyan(hostedZone.name)} ${pc21.green("\u2713")}`
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
- clack20.log.warn(
15839
- `No Route53 hosted zone found for ${pc21.cyan(trackingDomain || config2.domain)}`
15840
- );
15841
- clack20.log.info(
15842
- pc21.dim(
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
- clack20.log.info(
15847
- pc21.dim("DNS record details will be shown after deployment.")
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: hostedZone ? "Proceed with automatic HTTPS setup?" : "Proceed with manual HTTPS setup (requires DNS configuration)?",
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
- const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
16665
- const hostedZone = await findHostedZone2(outputs.domain, region);
16666
- if (hostedZone) {
16667
- try {
16668
- progress.start("Creating DNS records in Route53");
16669
- const mailFromDomain = updatedConfig.mailFromDomain || `mail.${outputs.domain}`;
16670
- await createDNSRecords2(
16671
- hostedZone.id,
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
- outputs.dkimTokens,
16674
- region,
16675
- outputs.customTrackingDomain,
16676
- mailFromDomain,
16677
- outputs.cloudFrontDomain
17773
+ availableProviders
16678
17774
  );
16679
- progress.succeed("DNS records created in Route53");
16680
- } catch (error) {
16681
- progress.fail(
16682
- `Failed to create DNS records automatically: ${error.message}`
16683
- );
16684
- progress.info(
16685
- "You can manually add the required DNS records shown below"
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);