@wraps.dev/cli 2.3.4 → 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
@@ -1023,7 +1023,7 @@ To remove: wraps destroy --stack ${stackName}`,
1023
1023
  "The Pulumi stack is locked from a previous run",
1024
1024
  "STACK_LOCKED",
1025
1025
  "This happens when a previous deployment was interrupted.\n\nTo unlock, run:\n rm -rf ~/.wraps/pulumi/.pulumi/locks\n\nThen try your command again.",
1026
- "https://wraps.dev/docs/guides/aws-setup/troubleshooting"
1026
+ "https://wraps.dev/docs/guides/aws-setup/permissions/troubleshooting"
1027
1027
  ),
1028
1028
  // SMS-specific errors
1029
1029
  smsNotConfigured: () => new WrapsError(
@@ -1080,7 +1080,7 @@ To remove: wraps destroy --stack ${stackName}`,
1080
1080
  `AWS SSO session has expired${profile ? ` for profile "${profile}"` : ""}`,
1081
1081
  "SSO_SESSION_EXPIRED",
1082
1082
  profile ? `Run: aws sso login --profile ${profile}` : "Run: aws sso login",
1083
- "https://wraps.dev/docs/guides/aws-setup"
1083
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1084
1084
  ),
1085
1085
  profileNotFound: (profile, availableProfiles) => new WrapsError(
1086
1086
  `AWS profile "${profile}" not found`,
@@ -1092,25 +1092,25 @@ Set a valid profile:
1092
1092
 
1093
1093
  Or configure a new profile:
1094
1094
  aws configure --profile ${profile}` : "No AWS profiles configured.\n\nConfigure AWS credentials:\n aws configure\n\nOr set up SSO:\n aws configure sso",
1095
- "https://wraps.dev/docs/guides/aws-setup"
1095
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1096
1096
  ),
1097
1097
  credentialsFileMissing: () => new WrapsError(
1098
1098
  "AWS credentials file not found",
1099
1099
  "CREDENTIALS_FILE_MISSING",
1100
1100
  "Configure AWS credentials:\n aws configure\n\nOr set environment variables:\n export AWS_ACCESS_KEY_ID=<your-key>\n export AWS_SECRET_ACCESS_KEY=<your-secret>",
1101
- "https://wraps.dev/docs/guides/aws-setup"
1101
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1102
1102
  ),
1103
1103
  accessKeyInvalid: () => new WrapsError(
1104
1104
  "AWS access key is invalid or has been deactivated",
1105
1105
  "ACCESS_KEY_INVALID",
1106
1106
  "Check your AWS access keys in the IAM console.\n\nReconfigure credentials:\n aws configure\n\nOr generate new access keys in AWS IAM.",
1107
- "https://wraps.dev/docs/guides/aws-setup"
1107
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1108
1108
  ),
1109
1109
  sessionTokenExpired: () => new WrapsError(
1110
1110
  "AWS session token has expired",
1111
1111
  "SESSION_TOKEN_EXPIRED",
1112
1112
  "Your temporary credentials have expired.\n\nFor SSO users:\n aws sso login\n\nFor assumed roles:\n Re-run your assume-role command",
1113
- "https://wraps.dev/docs/guides/aws-setup"
1113
+ "https://wraps.dev/docs/guides/aws-setup/permissions"
1114
1114
  ),
1115
1115
  // IAM permission errors
1116
1116
  iamPermissionDenied: (action, resource, suggestion) => new WrapsError(
@@ -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"() {
@@ -4407,29 +4461,952 @@ async function createMailManagerArchive(config2) {
4407
4461
  const accountId = identity.Account;
4408
4462
  archiveArn = `arn:aws:ses:${region}:${accountId}:mailmanager-archive/${archiveId}`;
4409
4463
  }
4410
- const configSetName = await new Promise((resolve) => {
4411
- config2.configSetName.apply((name) => {
4412
- resolve(name);
4464
+ const configSetName = await new Promise((resolve) => {
4465
+ config2.configSetName.apply((name) => {
4466
+ resolve(name);
4467
+ });
4468
+ });
4469
+ const putArchivingOptionsCommand = new PutConfigurationSetArchivingOptionsCommand({
4470
+ ConfigurationSetName: configSetName,
4471
+ ArchiveArn: archiveArn
4472
+ });
4473
+ await sesClient.send(putArchivingOptionsCommand);
4474
+ if (!(archiveId && archiveArn)) {
4475
+ throw new Error("Failed to get archive ID or ARN");
4476
+ }
4477
+ return {
4478
+ archiveId,
4479
+ archiveArn,
4480
+ kmsKeyArn
4481
+ };
4482
+ }
4483
+ var init_mail_manager = __esm({
4484
+ "src/infrastructure/resources/mail-manager.ts"() {
4485
+ "use strict";
4486
+ init_esm_shims();
4487
+ }
4488
+ });
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
4413
5361
  });
5362
+ }
5363
+ providers.push({
5364
+ provider: "manual",
5365
+ detected: true,
5366
+ hint: "I'll add DNS records myself"
4414
5367
  });
4415
- const putArchivingOptionsCommand = new PutConfigurationSetArchivingOptionsCommand({
4416
- ConfigurationSetName: configSetName,
4417
- ArchiveArn: archiveArn
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;
4418
5374
  });
4419
- await sesClient.send(putArchivingOptionsCommand);
4420
- if (!(archiveId && archiveArn)) {
4421
- throw new Error("Failed to get archive ID or ARN");
4422
- }
4423
- return {
4424
- archiveId,
4425
- archiveArn,
4426
- kmsKeyArn
4427
- };
4428
5375
  }
4429
- var init_mail_manager = __esm({
4430
- "src/infrastructure/resources/mail-manager.ts"() {
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"() {
4431
5404
  "use strict";
4432
5405
  init_esm_shims();
5406
+ init_cloudflare();
5407
+ init_create_records();
5408
+ init_credentials();
5409
+ init_vercel();
4433
5410
  }
4434
5411
  });
4435
5412
 
@@ -9210,7 +10187,7 @@ function createNodeDnsProvider(options = {}) {
9210
10187
  if (options.servers && options.servers.length > 0) {
9211
10188
  resolver.setServers(options.servers);
9212
10189
  }
9213
- async function withTimeout(promise, operation) {
10190
+ async function withTimeout2(promise, operation) {
9214
10191
  const timeoutPromise = new Promise((_, reject) => {
9215
10192
  setTimeout(() => {
9216
10193
  reject(new Error(`DNS ${operation} timed out after ${timeout}ms`));
@@ -9220,7 +10197,7 @@ function createNodeDnsProvider(options = {}) {
9220
10197
  }
9221
10198
  async function resolveTxt(domain) {
9222
10199
  try {
9223
- const records = await withTimeout(
10200
+ const records = await withTimeout2(
9224
10201
  dns.resolveTxt(domain),
9225
10202
  `TXT lookup for ${domain}`
9226
10203
  );
@@ -9234,7 +10211,7 @@ function createNodeDnsProvider(options = {}) {
9234
10211
  }
9235
10212
  async function resolveMx(domain) {
9236
10213
  try {
9237
- const records = await withTimeout(
10214
+ const records = await withTimeout2(
9238
10215
  dns.resolveMx(domain),
9239
10216
  `MX lookup for ${domain}`
9240
10217
  );
@@ -9248,7 +10225,7 @@ function createNodeDnsProvider(options = {}) {
9248
10225
  }
9249
10226
  async function resolveA(domain) {
9250
10227
  try {
9251
- const records = await withTimeout(
10228
+ const records = await withTimeout2(
9252
10229
  dns.resolve4(domain),
9253
10230
  `A lookup for ${domain}`
9254
10231
  );
@@ -9262,7 +10239,7 @@ function createNodeDnsProvider(options = {}) {
9262
10239
  }
9263
10240
  async function resolveAaaa(domain) {
9264
10241
  try {
9265
- const records = await withTimeout(
10242
+ const records = await withTimeout2(
9266
10243
  dns.resolve6(domain),
9267
10244
  `AAAA lookup for ${domain}`
9268
10245
  );
@@ -9276,7 +10253,7 @@ function createNodeDnsProvider(options = {}) {
9276
10253
  }
9277
10254
  async function resolvePtr(ip) {
9278
10255
  try {
9279
- const records = await withTimeout(
10256
+ const records = await withTimeout2(
9280
10257
  dns.reverse(ip),
9281
10258
  `PTR lookup for ${ip}`
9282
10259
  );
@@ -9290,7 +10267,7 @@ function createNodeDnsProvider(options = {}) {
9290
10267
  }
9291
10268
  async function resolveCaa(domain) {
9292
10269
  try {
9293
- const records = await withTimeout(
10270
+ const records = await withTimeout2(
9294
10271
  dns.resolveCaa(domain),
9295
10272
  `CAA lookup for ${domain}`
9296
10273
  );
@@ -9322,7 +10299,7 @@ function createNodeDnsProvider(options = {}) {
9322
10299
  }
9323
10300
  async function resolveCname(domain) {
9324
10301
  try {
9325
- const records = await withTimeout(
10302
+ const records = await withTimeout2(
9326
10303
  dns.resolveCname(domain),
9327
10304
  `CNAME lookup for ${domain}`
9328
10305
  );
@@ -11885,14 +12862,13 @@ async function tryGetSesDkimTokens(domain) {
11885
12862
  // src/commands/email/config.ts
11886
12863
  init_esm_shims();
11887
12864
  import * as clack13 from "@clack/prompts";
11888
- import * as pulumi13 from "@pulumi/pulumi";
12865
+ import * as pulumi12 from "@pulumi/pulumi";
11889
12866
  import pc14 from "picocolors";
11890
12867
 
11891
12868
  // src/infrastructure/email-stack.ts
11892
12869
  init_esm_shims();
11893
12870
  init_dist();
11894
12871
  import * as aws14 from "@pulumi/aws";
11895
- import * as pulumi12 from "@pulumi/pulumi";
11896
12872
 
11897
12873
  // src/infrastructure/resources/alerting.ts
11898
12874
  init_esm_shims();
@@ -13066,7 +14042,7 @@ ${pc14.bold("Current Configuration:")}
13066
14042
  "Generating update preview",
13067
14043
  async () => {
13068
14044
  await ensurePulumiWorkDir();
13069
- const stack = await pulumi13.automation.LocalWorkspace.createOrSelectStack(
14045
+ const stack = await pulumi12.automation.LocalWorkspace.createOrSelectStack(
13070
14046
  {
13071
14047
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
13072
14048
  projectName: "wraps-email",
@@ -13130,7 +14106,7 @@ ${pc14.bold("Current Configuration:")}
13130
14106
  "Updating Wraps infrastructure (this may take 2-3 minutes)",
13131
14107
  async () => {
13132
14108
  await ensurePulumiWorkDir();
13133
- const stack = await pulumi13.automation.LocalWorkspace.createOrSelectStack(
14109
+ const stack = await pulumi12.automation.LocalWorkspace.createOrSelectStack(
13134
14110
  {
13135
14111
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
13136
14112
  projectName: "wraps-email",
@@ -13227,7 +14203,7 @@ ${pc14.green("\u2713")} ${pc14.bold("Update complete!")}
13227
14203
  // src/commands/email/connect.ts
13228
14204
  init_esm_shims();
13229
14205
  import * as clack14 from "@clack/prompts";
13230
- import * as pulumi14 from "@pulumi/pulumi";
14206
+ import * as pulumi13 from "@pulumi/pulumi";
13231
14207
  import pc15 from "picocolors";
13232
14208
  init_events();
13233
14209
  init_presets();
@@ -13567,7 +14543,7 @@ async function connect2(options) {
13567
14543
  "Generating infrastructure preview",
13568
14544
  async () => {
13569
14545
  await ensurePulumiWorkDir();
13570
- const stack = await pulumi14.automation.LocalWorkspace.createOrSelectStack(
14546
+ const stack = await pulumi13.automation.LocalWorkspace.createOrSelectStack(
13571
14547
  {
13572
14548
  stackName: `wraps-${identity.accountId}-${region}`,
13573
14549
  projectName: "wraps-email",
@@ -13631,7 +14607,7 @@ async function connect2(options) {
13631
14607
  "Deploying Wraps infrastructure (this may take 2-3 minutes)",
13632
14608
  async () => {
13633
14609
  await ensurePulumiWorkDir();
13634
- const stack = await pulumi14.automation.LocalWorkspace.createOrSelectStack(
14610
+ const stack = await pulumi13.automation.LocalWorkspace.createOrSelectStack(
13635
14611
  {
13636
14612
  stackName: `wraps-${identity.accountId}-${region}`,
13637
14613
  projectName: "wraps-email",
@@ -13791,8 +14767,42 @@ init_errors();
13791
14767
  init_fs();
13792
14768
  init_metadata();
13793
14769
  import * as clack15 from "@clack/prompts";
13794
- import * as pulumi15 from "@pulumi/pulumi";
14770
+ import * as pulumi14 from "@pulumi/pulumi";
13795
14771
  import pc16 from "picocolors";
14772
+
14773
+ // src/utils/shared/timeout.ts
14774
+ init_esm_shims();
14775
+ init_errors();
14776
+ var DEFAULT_PULUMI_TIMEOUT_MS = 10 * 60 * 1e3;
14777
+ var TimeoutError = class extends WrapsError {
14778
+ constructor(operation, timeoutMs) {
14779
+ const timeoutMinutes = Math.round(timeoutMs / 6e4);
14780
+ super(
14781
+ `Operation "${operation}" timed out after ${timeoutMinutes} minute${timeoutMinutes === 1 ? "" : "s"}`,
14782
+ "OPERATION_TIMEOUT",
14783
+ "The operation took longer than expected. This can happen due to:\n - Slow network connection\n - AWS API throttling\n - Large number of resources\n\nYou can try:\n 1. Check AWS CloudFormation/Pulumi state for partial deployments\n 2. Run the command again (it will resume where it left off)\n 3. Check your AWS console for any stuck resources",
14784
+ "https://wraps.dev/docs/guides/aws-setup/troubleshooting"
14785
+ );
14786
+ }
14787
+ };
14788
+ async function withTimeout(promise, timeoutMs, operation) {
14789
+ let timeoutId;
14790
+ const timeoutPromise = new Promise((_, reject) => {
14791
+ timeoutId = setTimeout(() => {
14792
+ reject(new TimeoutError(operation, timeoutMs));
14793
+ }, timeoutMs);
14794
+ });
14795
+ try {
14796
+ const result = await Promise.race([promise, timeoutPromise]);
14797
+ return result;
14798
+ } finally {
14799
+ if (timeoutId) {
14800
+ clearTimeout(timeoutId);
14801
+ }
14802
+ }
14803
+ }
14804
+
14805
+ // src/commands/email/destroy.ts
13796
14806
  async function getEmailIdentityInfo(domain, region) {
13797
14807
  try {
13798
14808
  const { SESv2Client: SESv2Client6, GetEmailIdentityCommand: GetEmailIdentityCommand5 } = await import("@aws-sdk/client-sesv2");
@@ -13893,10 +14903,10 @@ async function emailDestroy(options) {
13893
14903
  "Generating destruction preview",
13894
14904
  async () => {
13895
14905
  await ensurePulumiWorkDir();
13896
- const stackName = storedStackName || `wraps-email-${identity.accountId}-${region}`;
14906
+ const stackName = storedStackName || `wraps-${identity.accountId}-${region}`;
13897
14907
  let stack;
13898
14908
  try {
13899
- stack = await pulumi15.automation.LocalWorkspace.selectStack({
14909
+ stack = await pulumi14.automation.LocalWorkspace.selectStack({
13900
14910
  stackName,
13901
14911
  workDir: getPulumiWorkDir()
13902
14912
  });
@@ -13964,18 +14974,23 @@ async function emailDestroy(options) {
13964
14974
  "Destroying email infrastructure (this may take 2-3 minutes)",
13965
14975
  async () => {
13966
14976
  await ensurePulumiWorkDir();
13967
- const stackName = storedStackName || `wraps-email-${identity.accountId}-${region}`;
14977
+ const stackName = storedStackName || `wraps-${identity.accountId}-${region}`;
13968
14978
  let stack;
13969
14979
  try {
13970
- stack = await pulumi15.automation.LocalWorkspace.selectStack({
14980
+ stack = await pulumi14.automation.LocalWorkspace.selectStack({
13971
14981
  stackName,
13972
14982
  workDir: getPulumiWorkDir()
13973
14983
  });
13974
14984
  } catch (_error) {
13975
14985
  throw new Error("No email infrastructure found to destroy");
13976
14986
  }
13977
- await stack.destroy({ onOutput: () => {
13978
- } });
14987
+ await withTimeout(
14988
+ stack.destroy({ onOutput: () => {
14989
+ } }),
14990
+ // Suppress Pulumi output
14991
+ DEFAULT_PULUMI_TIMEOUT_MS,
14992
+ "Pulumi destroy"
14993
+ );
13979
14994
  await stack.workspace.removeStack(stackName);
13980
14995
  }
13981
14996
  );
@@ -14479,7 +15494,7 @@ async function removeDomain(options) {
14479
15494
  // src/commands/email/init.ts
14480
15495
  init_esm_shims();
14481
15496
  import * as clack17 from "@clack/prompts";
14482
- import * as pulumi16 from "@pulumi/pulumi";
15497
+ import * as pulumi15 from "@pulumi/pulumi";
14483
15498
  import pc18 from "picocolors";
14484
15499
  init_events();
14485
15500
  init_costs();
@@ -14487,6 +15502,141 @@ init_presets();
14487
15502
  init_aws();
14488
15503
  init_errors();
14489
15504
  init_fs();
15505
+
15506
+ // src/utils/shared/iam-check.ts
15507
+ init_esm_shims();
15508
+ var CORE_IAM_ACTIONS = [
15509
+ "iam:CreateRole",
15510
+ "iam:GetRole",
15511
+ "iam:PutRolePolicy",
15512
+ "iam:DeleteRole",
15513
+ "iam:DeleteRolePolicy",
15514
+ "iam:TagRole",
15515
+ "ses:CreateConfigurationSet",
15516
+ "ses:DeleteConfigurationSet",
15517
+ "ses:CreateEmailIdentity",
15518
+ "ses:DeleteEmailIdentity",
15519
+ "ses:GetEmailIdentity",
15520
+ "ses:PutEmailIdentityDkimAttributes"
15521
+ ];
15522
+ var EVENT_TRACKING_ACTIONS = [
15523
+ "events:CreateEventBus",
15524
+ "events:DeleteEventBus",
15525
+ "events:PutRule",
15526
+ "events:DeleteRule",
15527
+ "events:PutTargets",
15528
+ "events:RemoveTargets",
15529
+ "sqs:CreateQueue",
15530
+ "sqs:DeleteQueue",
15531
+ "sqs:SetQueueAttributes",
15532
+ "sqs:GetQueueAttributes"
15533
+ ];
15534
+ var DYNAMODB_ACTIONS = [
15535
+ "dynamodb:CreateTable",
15536
+ "dynamodb:DeleteTable",
15537
+ "dynamodb:DescribeTable",
15538
+ "dynamodb:UpdateTable",
15539
+ "dynamodb:TagResource"
15540
+ ];
15541
+ var LAMBDA_ACTIONS = [
15542
+ "lambda:CreateFunction",
15543
+ "lambda:DeleteFunction",
15544
+ "lambda:UpdateFunctionCode",
15545
+ "lambda:UpdateFunctionConfiguration",
15546
+ "lambda:GetFunction",
15547
+ "lambda:AddPermission",
15548
+ "lambda:RemovePermission",
15549
+ "lambda:CreateEventSourceMapping",
15550
+ "lambda:DeleteEventSourceMapping"
15551
+ ];
15552
+ function getRequiredActions(config2) {
15553
+ const actions = [...CORE_IAM_ACTIONS];
15554
+ if (config2.eventTracking?.enabled) {
15555
+ actions.push(...EVENT_TRACKING_ACTIONS);
15556
+ }
15557
+ if (config2.eventTracking?.dynamoDBHistory) {
15558
+ actions.push(...DYNAMODB_ACTIONS);
15559
+ actions.push(...LAMBDA_ACTIONS);
15560
+ }
15561
+ return [...new Set(actions)];
15562
+ }
15563
+ async function checkIAMPermissions(userArn, actions, region) {
15564
+ try {
15565
+ const { IAMClient: IAMClient3, SimulatePrincipalPolicyCommand } = await import("@aws-sdk/client-iam");
15566
+ const client = new IAMClient3({ region });
15567
+ const batchSize = 100;
15568
+ const batches = [];
15569
+ for (let i = 0; i < actions.length; i += batchSize) {
15570
+ batches.push(actions.slice(i, i + batchSize));
15571
+ }
15572
+ const allowedActions = [];
15573
+ const deniedActions = [];
15574
+ for (const batch of batches) {
15575
+ const command = new SimulatePrincipalPolicyCommand({
15576
+ PolicySourceArn: userArn,
15577
+ ActionNames: batch,
15578
+ // Use a wildcard resource since we're checking general permissions
15579
+ // More specific resource-level checks could be added later
15580
+ ResourceArns: ["*"]
15581
+ });
15582
+ const response = await client.send(command);
15583
+ for (const result of response.EvaluationResults || []) {
15584
+ const actionName = result.EvalActionName;
15585
+ const decision = result.EvalDecision;
15586
+ if (decision === "allowed") {
15587
+ if (actionName) allowedActions.push(actionName);
15588
+ } else if (actionName) deniedActions.push(actionName);
15589
+ }
15590
+ }
15591
+ return {
15592
+ success: deniedActions.length === 0,
15593
+ deniedActions,
15594
+ allowedActions,
15595
+ skipped: false
15596
+ };
15597
+ } catch (error) {
15598
+ if (error instanceof Error && (error.name === "AccessDenied" || error.name === "AccessDeniedException" || error.message?.includes("AccessDenied") || error.message?.includes("iam:SimulatePrincipalPolicy"))) {
15599
+ return {
15600
+ success: true,
15601
+ // Don't block on this
15602
+ deniedActions: [],
15603
+ allowedActions: [],
15604
+ skipped: true,
15605
+ skipReason: "Unable to verify permissions (iam:SimulatePrincipalPolicy not allowed). Deployment will proceed, but may fail if permissions are missing."
15606
+ };
15607
+ }
15608
+ return {
15609
+ success: true,
15610
+ deniedActions: [],
15611
+ allowedActions: [],
15612
+ skipped: true,
15613
+ skipReason: `Permission check failed: ${error instanceof Error ? error.message : "Unknown error"}. Proceeding with deployment.`
15614
+ };
15615
+ }
15616
+ }
15617
+ function formatDeniedActions(actions) {
15618
+ if (actions.length === 0) return "";
15619
+ const byService = {};
15620
+ for (const action of actions) {
15621
+ const [service, actionName] = action.split(":");
15622
+ if (!byService[service]) {
15623
+ byService[service] = [];
15624
+ }
15625
+ byService[service].push(actionName);
15626
+ }
15627
+ const lines = ["Missing permissions:"];
15628
+ for (const [service, serviceActions] of Object.entries(byService)) {
15629
+ lines.push(` ${service.toUpperCase()}:`);
15630
+ for (const action of serviceActions) {
15631
+ lines.push(` - ${service}:${action}`);
15632
+ }
15633
+ }
15634
+ lines.push("");
15635
+ lines.push("Run `wraps permissions --json` to see the full policy document.");
15636
+ return lines.join("\n");
15637
+ }
15638
+
15639
+ // src/commands/email/init.ts
14490
15640
  init_metadata();
14491
15641
  init_prompts();
14492
15642
  async function init2(options) {
@@ -14594,6 +15744,23 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
14594
15744
  process.exit(0);
14595
15745
  }
14596
15746
  }
15747
+ if (!options.preview) {
15748
+ const iamCheckResult = await progress.execute(
15749
+ "Checking IAM permissions",
15750
+ async () => {
15751
+ const requiredActions = getRequiredActions(emailConfig);
15752
+ return checkIAMPermissions(identity.arn, requiredActions, region);
15753
+ }
15754
+ );
15755
+ if (iamCheckResult.skipped && iamCheckResult.skipReason) {
15756
+ progress.info(pc18.dim(iamCheckResult.skipReason));
15757
+ } else if (!iamCheckResult.success) {
15758
+ clack17.log.warn(
15759
+ pc18.yellow("Some IAM permissions may be missing. Deployment may fail.")
15760
+ );
15761
+ clack17.log.info(formatDeniedActions(iamCheckResult.deniedActions));
15762
+ }
15763
+ }
14597
15764
  const stackConfig = {
14598
15765
  provider,
14599
15766
  region,
@@ -14606,7 +15773,7 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
14606
15773
  "Generating infrastructure preview",
14607
15774
  async () => {
14608
15775
  await ensurePulumiWorkDir();
14609
- const stack = await pulumi16.automation.LocalWorkspace.createOrSelectStack(
15776
+ const stack = await pulumi15.automation.LocalWorkspace.createOrSelectStack(
14610
15777
  {
14611
15778
  stackName: `wraps-${identity.accountId}-${region}`,
14612
15779
  projectName: "wraps-email",
@@ -14675,7 +15842,7 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
14675
15842
  "Deploying infrastructure (this may take 2-3 minutes)",
14676
15843
  async () => {
14677
15844
  await ensurePulumiWorkDir();
14678
- const stack = await pulumi16.automation.LocalWorkspace.createOrSelectStack(
15845
+ const stack = await pulumi15.automation.LocalWorkspace.createOrSelectStack(
14679
15846
  {
14680
15847
  stackName: `wraps-${identity.accountId}-${region}`,
14681
15848
  projectName: "wraps-email",
@@ -14712,8 +15879,13 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
14712
15879
  `wraps-${identity.accountId}-${region}`
14713
15880
  );
14714
15881
  await stack.setConfig("aws:region", { value: region });
14715
- const upResult = await stack.up({ onOutput: () => {
14716
- } });
15882
+ const upResult = await withTimeout(
15883
+ stack.up({ onOutput: () => {
15884
+ } }),
15885
+ // Suppress Pulumi output
15886
+ DEFAULT_PULUMI_TIMEOUT_MS,
15887
+ "Pulumi deployment"
15888
+ );
14717
15889
  const pulumiOutputs = upResult.outputs;
14718
15890
  return {
14719
15891
  roleArn: pulumiOutputs.roleArn?.value,
@@ -14783,51 +15955,149 @@ ${pc18.yellow(pc18.bold("Configuration Warnings:"))}`);
14783
15955
  await saveConnectionMetadata(metadata);
14784
15956
  progress.info("Connection metadata saved for upgrade and restore capability");
14785
15957
  let dnsAutoCreated = false;
15958
+ let dnsProvider;
14786
15959
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
14787
- const { findHostedZone: findHostedZone2, previewDNSChanges: previewDNSChanges2, createSelectedDNSRecords: createSelectedDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
14788
- const { promptDNSManagement: promptDNSManagement2, promptDNSConfirmation: promptDNSConfirmation2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
14789
- const hostedZone = await findHostedZone2(outputs.domain, region);
14790
- if (hostedZone) {
14791
- const manageDNS = await promptDNSManagement2(outputs.domain);
14792
- if (manageDNS) {
14793
- try {
14794
- progress.start("Checking existing DNS records");
14795
- const dnsPreview = await previewDNSChanges2(
14796
- hostedZone.id,
14797
- outputs.domain,
14798
- outputs.dkimTokens,
14799
- region,
14800
- outputs.customTrackingDomain,
14801
- outputs.mailFromDomain
14802
- );
14803
- progress.stop();
14804
- const { shouldCreate, selectedCategories } = await promptDNSConfirmation2(dnsPreview);
14805
- if (shouldCreate && selectedCategories.size > 0) {
14806
- progress.start("Creating selected DNS records in Route53");
14807
- await createSelectedDNSRecords2(
14808
- 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,
14809
16002
  outputs.domain,
14810
16003
  outputs.dkimTokens,
14811
16004
  region,
14812
- selectedCategories,
14813
16005
  outputs.customTrackingDomain,
14814
16006
  outputs.mailFromDomain
14815
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) {
14816
16066
  progress.succeed(
14817
- `Created ${selectedCategories.size} DNS record group(s) in Route53`
16067
+ `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(credentials.provider)}`
14818
16068
  );
14819
16069
  dnsAutoCreated = true;
14820
16070
  } else {
14821
- clack17.log.info(
14822
- "Skipping DNS record creation. You can add them manually."
14823
- );
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
+ }
14824
16077
  }
14825
- } catch (error) {
14826
- progress.stop();
14827
- 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";
14828
16094
  }
14829
16095
  }
14830
16096
  }
16097
+ if (dnsProvider && metadata.services.email) {
16098
+ metadata.services.email.dnsProvider = dnsProvider;
16099
+ await saveConnectionMetadata(metadata);
16100
+ }
14831
16101
  }
14832
16102
  const dnsRecords = [];
14833
16103
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0 && !dnsAutoCreated) {
@@ -14891,7 +16161,7 @@ init_aws();
14891
16161
  init_fs();
14892
16162
  init_metadata();
14893
16163
  import * as clack18 from "@clack/prompts";
14894
- import * as pulumi17 from "@pulumi/pulumi";
16164
+ import * as pulumi16 from "@pulumi/pulumi";
14895
16165
  import pc19 from "picocolors";
14896
16166
  async function restore(options) {
14897
16167
  const startTime = Date.now();
@@ -14963,7 +16233,7 @@ ${pc19.bold("The following Wraps resources will be removed:")}
14963
16233
  const previewResult = await progress.execute(
14964
16234
  "Generating removal preview",
14965
16235
  async () => {
14966
- const stack = await pulumi17.automation.LocalWorkspace.selectStack(
16236
+ const stack = await pulumi16.automation.LocalWorkspace.selectStack(
14967
16237
  {
14968
16238
  stackName: pulumiStackName,
14969
16239
  projectName: "wraps-email",
@@ -15015,7 +16285,7 @@ ${pc19.bold("The following Wraps resources will be removed:")}
15015
16285
  if (!metadata.services.email?.pulumiStackName) {
15016
16286
  throw new Error("No Pulumi stack name found in metadata");
15017
16287
  }
15018
- const stack = await pulumi17.automation.LocalWorkspace.selectStack(
16288
+ const stack = await pulumi16.automation.LocalWorkspace.selectStack(
15019
16289
  {
15020
16290
  stackName: metadata.services.email.pulumiStackName,
15021
16291
  projectName: "wraps-email",
@@ -15069,7 +16339,7 @@ init_aws();
15069
16339
  init_fs();
15070
16340
  init_metadata();
15071
16341
  import * as clack19 from "@clack/prompts";
15072
- import * as pulumi18 from "@pulumi/pulumi";
16342
+ import * as pulumi17 from "@pulumi/pulumi";
15073
16343
  import pc20 from "picocolors";
15074
16344
  async function emailStatus(options) {
15075
16345
  const startTime = Date.now();
@@ -15105,7 +16375,7 @@ async function emailStatus(options) {
15105
16375
  let stackOutputs = {};
15106
16376
  try {
15107
16377
  await ensurePulumiWorkDir();
15108
- const stack = await pulumi18.automation.LocalWorkspace.selectStack({
16378
+ const stack = await pulumi17.automation.LocalWorkspace.selectStack({
15109
16379
  stackName: `wraps-${identity.accountId}-${region}`,
15110
16380
  workDir: getPulumiWorkDir()
15111
16381
  });
@@ -15182,9 +16452,11 @@ Run ${pc20.cyan("wraps email init")} to deploy email infrastructure.
15182
16452
  // src/commands/email/upgrade.ts
15183
16453
  init_esm_shims();
15184
16454
  import * as clack20 from "@clack/prompts";
15185
- import * as pulumi19 from "@pulumi/pulumi";
16455
+ import * as pulumi18 from "@pulumi/pulumi";
15186
16456
  import pc21 from "picocolors";
15187
16457
  init_events();
16458
+ init_create_records();
16459
+ init_credentials();
15188
16460
  init_costs();
15189
16461
  init_presets();
15190
16462
  init_aws();
@@ -15625,35 +16897,54 @@ ${pc21.bold("Current Configuration:")}
15625
16897
  "This ensures all tracking links use secure HTTPS connections."
15626
16898
  )
15627
16899
  );
15628
- const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
15629
- const hostedZone = await progress.execute(
15630
- "Checking for Route53 hosted zone",
15631
- async () => await findHostedZone2(trackingDomain || config2.domain, region)
15632
- );
15633
- if (hostedZone) {
15634
- progress.info(
15635
- `Found Route53 hosted zone: ${pc21.cyan(hostedZone.name)} ${pc21.green("\u2713")}`
15636
- );
15637
- clack20.log.info(
15638
- pc21.dim(
15639
- "DNS records (SSL certificate validation + CloudFront) will be created automatically."
15640
- )
15641
- );
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
+ }
15642
16909
  } else {
15643
- clack20.log.warn(
15644
- `No Route53 hosted zone found for ${pc21.cyan(trackingDomain || config2.domain)}`
15645
- );
15646
- clack20.log.info(
15647
- pc21.dim(
15648
- "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
15649
16915
  )
15650
16916
  );
15651
- clack20.log.info(
15652
- pc21.dim("DNS record details will be shown after deployment.")
16917
+ const detectedProvider = availableProviders.find(
16918
+ (p) => p.detected && p.provider !== "manual"
15653
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
+ }
15654
16945
  }
15655
16946
  const confirmHttps = await clack20.confirm({
15656
- 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)?",
15657
16948
  initialValue: true
15658
16949
  });
15659
16950
  if (clack20.isCancel(confirmHttps) || !confirmHttps) {
@@ -16303,7 +17594,7 @@ ${pc21.bold("Cost Impact:")}`);
16303
17594
  "Generating upgrade preview",
16304
17595
  async () => {
16305
17596
  await ensurePulumiWorkDir();
16306
- const stack = await pulumi19.automation.LocalWorkspace.createOrSelectStack(
17597
+ const stack = await pulumi18.automation.LocalWorkspace.createOrSelectStack(
16307
17598
  {
16308
17599
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
16309
17600
  projectName: "wraps-email",
@@ -16381,7 +17672,7 @@ ${pc21.bold("Cost Impact:")}`);
16381
17672
  "Updating Wraps infrastructure (this may take 2-3 minutes)",
16382
17673
  async () => {
16383
17674
  await ensurePulumiWorkDir();
16384
- const stack = await pulumi19.automation.LocalWorkspace.createOrSelectStack(
17675
+ const stack = await pulumi18.automation.LocalWorkspace.createOrSelectStack(
16385
17676
  {
16386
17677
  stackName: metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`,
16387
17678
  projectName: "wraps-email",
@@ -16465,31 +17756,103 @@ ${pc21.bold("Cost Impact:")}`);
16465
17756
  trackError("UPGRADE_FAILED", "email:upgrade", { step: "deploy" });
16466
17757
  throw new Error(`Pulumi upgrade failed: ${error.message}`);
16467
17758
  }
17759
+ let dnsAutoCreated = false;
16468
17760
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
16469
- const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
16470
- const hostedZone = await findHostedZone2(outputs.domain, region);
16471
- if (hostedZone) {
16472
- try {
16473
- progress.start("Creating DNS records in Route53");
16474
- const mailFromDomain = updatedConfig.mailFromDomain || `mail.${outputs.domain}`;
16475
- await createDNSRecords2(
16476
- 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(
16477
17772
  outputs.domain,
16478
- outputs.dkimTokens,
16479
- region,
16480
- outputs.customTrackingDomain,
16481
- mailFromDomain,
16482
- outputs.cloudFrontDomain
16483
- );
16484
- progress.succeed("DNS records created in Route53");
16485
- } catch (error) {
16486
- progress.fail(
16487
- `Failed to create DNS records automatically: ${error.message}`
17773
+ availableProviders
16488
17774
  );
16489
- progress.info(
16490
- "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`
16491
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)}`);
16492
17854
  }
17855
+ console.log("");
16493
17856
  }
16494
17857
  }
16495
17858
  updateEmailConfig(metadata, updatedConfig);
@@ -17478,7 +18841,7 @@ function buildConsolePolicyDocument(emailConfig, smsConfig) {
17478
18841
  // src/commands/shared/dashboard.ts
17479
18842
  init_esm_shims();
17480
18843
  import * as clack24 from "@clack/prompts";
17481
- import * as pulumi20 from "@pulumi/pulumi";
18844
+ import * as pulumi19 from "@pulumi/pulumi";
17482
18845
  import getPort from "get-port";
17483
18846
  import open from "open";
17484
18847
  import pc26 from "picocolors";
@@ -20014,7 +21377,7 @@ async function dashboard(options) {
20014
21377
  try {
20015
21378
  await ensurePulumiWorkDir();
20016
21379
  try {
20017
- const emailStack = await pulumi20.automation.LocalWorkspace.selectStack({
21380
+ const emailStack = await pulumi19.automation.LocalWorkspace.selectStack({
20018
21381
  stackName: `wraps-${identity.accountId}-${region}`,
20019
21382
  workDir: getPulumiWorkDir()
20020
21383
  });
@@ -20022,7 +21385,7 @@ async function dashboard(options) {
20022
21385
  } catch (_emailError) {
20023
21386
  }
20024
21387
  try {
20025
- const smsStack = await pulumi20.automation.LocalWorkspace.selectStack({
21388
+ const smsStack = await pulumi19.automation.LocalWorkspace.selectStack({
20026
21389
  stackName: `wraps-sms-${identity.accountId}-${region}`,
20027
21390
  workDir: getPulumiWorkDir()
20028
21391
  });
@@ -20030,7 +21393,7 @@ async function dashboard(options) {
20030
21393
  } catch (_smsError) {
20031
21394
  }
20032
21395
  try {
20033
- const cdnStack = await pulumi20.automation.LocalWorkspace.selectStack({
21396
+ const cdnStack = await pulumi19.automation.LocalWorkspace.selectStack({
20034
21397
  stackName: `wraps-cdn-${identity.accountId}-${region}`,
20035
21398
  workDir: getPulumiWorkDir()
20036
21399
  });
@@ -20207,7 +21570,7 @@ init_events();
20207
21570
  init_aws();
20208
21571
  init_fs();
20209
21572
  import * as clack26 from "@clack/prompts";
20210
- import * as pulumi21 from "@pulumi/pulumi";
21573
+ import * as pulumi20 from "@pulumi/pulumi";
20211
21574
  import pc28 from "picocolors";
20212
21575
  async function status(_options) {
20213
21576
  const startTime = Date.now();
@@ -20223,7 +21586,7 @@ async function status(_options) {
20223
21586
  const services = [];
20224
21587
  try {
20225
21588
  await ensurePulumiWorkDir();
20226
- const emailStack = await pulumi21.automation.LocalWorkspace.selectStack({
21589
+ const emailStack = await pulumi20.automation.LocalWorkspace.selectStack({
20227
21590
  stackName: `wraps-${identity.accountId}-${region}`,
20228
21591
  workDir: getPulumiWorkDir()
20229
21592
  });
@@ -20242,7 +21605,7 @@ async function status(_options) {
20242
21605
  services.push({ name: "Email", status: "not_deployed" });
20243
21606
  }
20244
21607
  try {
20245
- const smsStack = await pulumi21.automation.LocalWorkspace.selectStack({
21608
+ const smsStack = await pulumi20.automation.LocalWorkspace.selectStack({
20246
21609
  stackName: `wraps-sms-${identity.accountId}-${region}`,
20247
21610
  workDir: getPulumiWorkDir()
20248
21611
  });
@@ -20302,13 +21665,13 @@ ${pc28.bold("Dashboard:")} ${pc28.blue("https://app.wraps.dev")}`);
20302
21665
  // src/commands/sms/destroy.ts
20303
21666
  init_esm_shims();
20304
21667
  import * as clack27 from "@clack/prompts";
20305
- import * as pulumi23 from "@pulumi/pulumi";
21668
+ import * as pulumi22 from "@pulumi/pulumi";
20306
21669
  import pc29 from "picocolors";
20307
21670
 
20308
21671
  // src/infrastructure/sms-stack.ts
20309
21672
  init_esm_shims();
20310
21673
  import * as aws15 from "@pulumi/aws";
20311
- import * as pulumi22 from "@pulumi/pulumi";
21674
+ import * as pulumi21 from "@pulumi/pulumi";
20312
21675
  async function roleExists3(roleName) {
20313
21676
  try {
20314
21677
  const { IAMClient: IAMClient3, GetRoleCommand: GetRoleCommand2 } = await import("@aws-sdk/client-iam");
@@ -20342,7 +21705,7 @@ async function tableExists2(tableName) {
20342
21705
  async function createSMSIAMRole(config2) {
20343
21706
  let assumeRolePolicy;
20344
21707
  if (config2.provider === "vercel" && config2.oidcProvider) {
20345
- assumeRolePolicy = pulumi22.interpolate`{
21708
+ assumeRolePolicy = pulumi21.interpolate`{
20346
21709
  "Version": "2012-10-17",
20347
21710
  "Statement": [
20348
21711
  {
@@ -20370,7 +21733,7 @@ async function createSMSIAMRole(config2) {
20370
21733
  ]
20371
21734
  }`;
20372
21735
  } else if (config2.provider === "aws") {
20373
- assumeRolePolicy = pulumi22.output(`{
21736
+ assumeRolePolicy = pulumi21.output(`{
20374
21737
  "Version": "2012-10-17",
20375
21738
  "Statement": [{
20376
21739
  "Effect": "Allow",
@@ -20725,7 +22088,7 @@ async function createSMSSNSResources(config2) {
20725
22088
  });
20726
22089
  new aws15.sqs.QueuePolicy("wraps-sms-events-queue-policy", {
20727
22090
  queueUrl: config2.queueUrl,
20728
- policy: pulumi22.all([config2.queueArn, topic.arn]).apply(
22091
+ policy: pulumi21.all([config2.queueArn, topic.arn]).apply(
20729
22092
  ([queueArn, topicArn2]) => JSON.stringify({
20730
22093
  Version: "2012-10-17",
20731
22094
  Statement: [
@@ -20824,7 +22187,7 @@ async function deploySMSLambdaFunction(config2) {
20824
22187
  });
20825
22188
  new aws15.iam.RolePolicy("wraps-sms-lambda-policy", {
20826
22189
  role: lambdaRole.name,
20827
- policy: pulumi22.all([config2.tableName, config2.queueArn]).apply(
22190
+ policy: pulumi21.all([config2.tableName, config2.queueArn]).apply(
20828
22191
  ([tableName, queueArn]) => JSON.stringify({
20829
22192
  Version: "2012-10-17",
20830
22193
  Statement: [
@@ -20861,7 +22224,7 @@ async function deploySMSLambdaFunction(config2) {
20861
22224
  runtime: "nodejs20.x",
20862
22225
  handler: "index.handler",
20863
22226
  role: lambdaRole.arn,
20864
- code: new pulumi22.asset.FileArchive(codeDir),
22227
+ code: new pulumi21.asset.FileArchive(codeDir),
20865
22228
  timeout: 300,
20866
22229
  // 5 minutes
20867
22230
  memorySize: 512,
@@ -21272,7 +22635,7 @@ async function smsDestroy(options) {
21272
22635
  const stackName = storedStackName || `wraps-sms-${identity.accountId}-${region}`;
21273
22636
  let stack;
21274
22637
  try {
21275
- stack = await pulumi23.automation.LocalWorkspace.selectStack({
22638
+ stack = await pulumi22.automation.LocalWorkspace.selectStack({
21276
22639
  stackName,
21277
22640
  workDir: getPulumiWorkDir()
21278
22641
  });
@@ -21329,7 +22692,7 @@ async function smsDestroy(options) {
21329
22692
  const stackName = storedStackName || `wraps-sms-${identity.accountId}-${region}`;
21330
22693
  let stack;
21331
22694
  try {
21332
- stack = await pulumi23.automation.LocalWorkspace.selectStack({
22695
+ stack = await pulumi22.automation.LocalWorkspace.selectStack({
21333
22696
  stackName,
21334
22697
  workDir: getPulumiWorkDir()
21335
22698
  });
@@ -21386,7 +22749,7 @@ Run ${pc29.cyan("wraps sms init")} to deploy infrastructure again.
21386
22749
  // src/commands/sms/init.ts
21387
22750
  init_esm_shims();
21388
22751
  import * as clack28 from "@clack/prompts";
21389
- import * as pulumi24 from "@pulumi/pulumi";
22752
+ import * as pulumi23 from "@pulumi/pulumi";
21390
22753
  import pc30 from "picocolors";
21391
22754
  init_events();
21392
22755
  init_aws();
@@ -22033,7 +23396,7 @@ ${pc30.yellow(pc30.bold("Important Notes:"))}`);
22033
23396
  "Deploying SMS infrastructure (this may take 2-3 minutes)",
22034
23397
  async () => {
22035
23398
  await ensurePulumiWorkDir();
22036
- const stack = await pulumi24.automation.LocalWorkspace.createOrSelectStack(
23399
+ const stack = await pulumi23.automation.LocalWorkspace.createOrSelectStack(
22037
23400
  {
22038
23401
  stackName: `wraps-sms-${identity.accountId}-${region}`,
22039
23402
  projectName: "wraps-sms",
@@ -22379,7 +23742,7 @@ init_aws();
22379
23742
  init_fs();
22380
23743
  init_metadata();
22381
23744
  import * as clack30 from "@clack/prompts";
22382
- import * as pulumi25 from "@pulumi/pulumi";
23745
+ import * as pulumi24 from "@pulumi/pulumi";
22383
23746
  import pc32 from "picocolors";
22384
23747
  function displaySMSStatus(options) {
22385
23748
  const lines = [];
@@ -22444,7 +23807,7 @@ Run ${pc32.cyan("wraps sms init")} to deploy SMS infrastructure.
22444
23807
  try {
22445
23808
  await ensurePulumiWorkDir();
22446
23809
  const stackName = metadata.services.sms.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`;
22447
- const stack = await pulumi25.automation.LocalWorkspace.selectStack({
23810
+ const stack = await pulumi24.automation.LocalWorkspace.selectStack({
22448
23811
  stackName,
22449
23812
  workDir: getPulumiWorkDir()
22450
23813
  });
@@ -22483,7 +23846,7 @@ Run ${pc32.cyan("wraps sms init")} to deploy SMS infrastructure.
22483
23846
  // src/commands/sms/sync.ts
22484
23847
  init_esm_shims();
22485
23848
  import * as clack31 from "@clack/prompts";
22486
- import * as pulumi26 from "@pulumi/pulumi";
23849
+ import * as pulumi25 from "@pulumi/pulumi";
22487
23850
  import pc33 from "picocolors";
22488
23851
  init_events();
22489
23852
  init_aws();
@@ -22541,7 +23904,7 @@ Run ${pc33.cyan("wraps sms init")} to deploy SMS infrastructure first.
22541
23904
  outputs = await progress.execute("Syncing SMS infrastructure", async () => {
22542
23905
  await ensurePulumiWorkDir();
22543
23906
  const stackName = storedStackName || `wraps-sms-${identity.accountId}-${region}`;
22544
- const stack = await pulumi26.automation.LocalWorkspace.createOrSelectStack(
23907
+ const stack = await pulumi25.automation.LocalWorkspace.createOrSelectStack(
22545
23908
  {
22546
23909
  stackName,
22547
23910
  projectName: "wraps-sms",
@@ -22862,7 +24225,7 @@ Run ${pc34.cyan("wraps sms register")} to complete registration.
22862
24225
  // src/commands/sms/upgrade.ts
22863
24226
  init_esm_shims();
22864
24227
  import * as clack33 from "@clack/prompts";
22865
- import * as pulumi27 from "@pulumi/pulumi";
24228
+ import * as pulumi26 from "@pulumi/pulumi";
22866
24229
  import pc35 from "picocolors";
22867
24230
  init_events();
22868
24231
  init_aws();
@@ -23605,7 +24968,7 @@ ${pc35.bold("Cost Impact:")}`);
23605
24968
  "Updating SMS infrastructure (this may take 2-3 minutes)",
23606
24969
  async () => {
23607
24970
  await ensurePulumiWorkDir();
23608
- const stack = await pulumi27.automation.LocalWorkspace.createOrSelectStack(
24971
+ const stack = await pulumi26.automation.LocalWorkspace.createOrSelectStack(
23609
24972
  {
23610
24973
  stackName: metadata.services.sms?.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`,
23611
24974
  projectName: "wraps-sms",
@@ -24270,6 +25633,20 @@ function printCompletionScript() {
24270
25633
 
24271
25634
  // src/cli.ts
24272
25635
  init_errors();
25636
+ var [nodeMajorVersion] = process.versions.node.split(".").map(Number);
25637
+ if (nodeMajorVersion < 20) {
25638
+ console.error(
25639
+ "\x1B[31mError: Wraps CLI requires Node.js 20 or higher.\x1B[0m"
25640
+ );
25641
+ console.error(`Current version: ${process.versions.node}`);
25642
+ console.error("");
25643
+ console.error("To upgrade Node.js:");
25644
+ console.error(" macOS/Linux (nvm): nvm install 20 && nvm use 20");
25645
+ console.error(" macOS (Homebrew): brew install node@20");
25646
+ console.error(" Windows: Download from https://nodejs.org/");
25647
+ console.error("");
25648
+ process.exit(1);
25649
+ }
24273
25650
  var __filename2 = fileURLToPath4(import.meta.url);
24274
25651
  var __dirname3 = dirname2(__filename2);
24275
25652
  var packageJson = JSON.parse(