@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/README.md +55 -0
- package/dist/cli.js +1532 -155
- package/dist/cli.js.map +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +1 -1
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
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
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
|
|
4430
|
-
"src/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14906
|
+
const stackName = storedStackName || `wraps-${identity.accountId}-${region}`;
|
|
13897
14907
|
let stack;
|
|
13898
14908
|
try {
|
|
13899
|
-
stack = await
|
|
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
|
|
14977
|
+
const stackName = storedStackName || `wraps-${identity.accountId}-${region}`;
|
|
13968
14978
|
let stack;
|
|
13969
14979
|
try {
|
|
13970
|
-
stack = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
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 ${
|
|
16067
|
+
`Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(credentials.provider)}`
|
|
14818
16068
|
);
|
|
14819
16069
|
dnsAutoCreated = true;
|
|
14820
16070
|
} else {
|
|
14821
|
-
|
|
14822
|
-
|
|
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
|
-
}
|
|
14826
|
-
|
|
14827
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
|
|
15632
|
-
|
|
15633
|
-
|
|
15634
|
-
|
|
15635
|
-
|
|
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
|
-
|
|
15644
|
-
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
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
|
-
|
|
15652
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16490
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|