@wraps.dev/cli 2.7.0 → 2.8.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
|
@@ -831,6 +831,9 @@ function sanitizeErrorMessage(error) {
|
|
|
831
831
|
/arn:aws:[^:]+:[^:]*:\d{12}:/g,
|
|
832
832
|
"arn:aws:[SERVICE]:[REGION]:[ACCOUNT_ID]:"
|
|
833
833
|
);
|
|
834
|
+
if (message.length > 500) {
|
|
835
|
+
message = `${message.slice(0, 500)}...`;
|
|
836
|
+
}
|
|
834
837
|
return message;
|
|
835
838
|
}
|
|
836
839
|
function handleCLIError(error, command) {
|
|
@@ -2038,14 +2041,17 @@ __export(prompts_exports, {
|
|
|
2038
2041
|
promptDNSManagement: () => promptDNSManagement,
|
|
2039
2042
|
promptDNSProvider: () => promptDNSProvider,
|
|
2040
2043
|
promptDomain: () => promptDomain,
|
|
2044
|
+
promptDomainPurpose: () => promptDomainPurpose,
|
|
2041
2045
|
promptEmailArchiving: () => promptEmailArchiving,
|
|
2042
2046
|
promptEstimatedVolume: () => promptEstimatedVolume,
|
|
2043
2047
|
promptFeatureSelection: () => promptFeatureSelection,
|
|
2044
2048
|
promptInboundSubdomain: () => promptInboundSubdomain,
|
|
2045
2049
|
promptIntegrationLevel: () => promptIntegrationLevel,
|
|
2050
|
+
promptMailFromSubdomain: () => promptMailFromSubdomain,
|
|
2046
2051
|
promptProvider: () => promptProvider,
|
|
2047
2052
|
promptRegion: () => promptRegion,
|
|
2048
2053
|
promptSelectIdentities: () => promptSelectIdentities,
|
|
2054
|
+
promptSubdomainSuggestions: () => promptSubdomainSuggestions,
|
|
2049
2055
|
promptVercelConfig: () => promptVercelConfig,
|
|
2050
2056
|
promptWebhookUrl: () => promptWebhookUrl
|
|
2051
2057
|
});
|
|
@@ -2870,6 +2876,112 @@ async function promptContinueManualDNS() {
|
|
|
2870
2876
|
}
|
|
2871
2877
|
return continueManual;
|
|
2872
2878
|
}
|
|
2879
|
+
async function promptSubdomainSuggestions(primaryDomain) {
|
|
2880
|
+
clack3.log.info(
|
|
2881
|
+
pc4.dim(
|
|
2882
|
+
"Using subdomains isolates sender reputation \u2014 a bounce spike on marketing won't affect transactional mail."
|
|
2883
|
+
)
|
|
2884
|
+
);
|
|
2885
|
+
const choice = await clack3.select({
|
|
2886
|
+
message: `Add a subdomain of ${pc4.cyan(primaryDomain)}?`,
|
|
2887
|
+
options: [
|
|
2888
|
+
{
|
|
2889
|
+
value: `mail.${primaryDomain}`,
|
|
2890
|
+
label: `mail.${primaryDomain}`,
|
|
2891
|
+
hint: "Transactional emails"
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
value: `news.${primaryDomain}`,
|
|
2895
|
+
label: `news.${primaryDomain}`,
|
|
2896
|
+
hint: "Newsletters & marketing"
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
value: `notify.${primaryDomain}`,
|
|
2900
|
+
label: `notify.${primaryDomain}`,
|
|
2901
|
+
hint: "Notifications & alerts"
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
value: "__custom__",
|
|
2905
|
+
label: "Enter a custom domain",
|
|
2906
|
+
hint: "Any domain or subdomain"
|
|
2907
|
+
}
|
|
2908
|
+
]
|
|
2909
|
+
});
|
|
2910
|
+
if (clack3.isCancel(choice)) {
|
|
2911
|
+
clack3.cancel("Operation cancelled.");
|
|
2912
|
+
process.exit(0);
|
|
2913
|
+
}
|
|
2914
|
+
if (choice === "__custom__") {
|
|
2915
|
+
const custom = await clack3.text({
|
|
2916
|
+
message: "Domain to add:",
|
|
2917
|
+
placeholder: `billing.${primaryDomain}`,
|
|
2918
|
+
validate: (value) => {
|
|
2919
|
+
if (!value?.includes(".")) {
|
|
2920
|
+
return "Please enter a valid domain (e.g., billing.myapp.com)";
|
|
2921
|
+
}
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
});
|
|
2925
|
+
if (clack3.isCancel(custom)) {
|
|
2926
|
+
clack3.cancel("Operation cancelled.");
|
|
2927
|
+
process.exit(0);
|
|
2928
|
+
}
|
|
2929
|
+
return custom;
|
|
2930
|
+
}
|
|
2931
|
+
return choice;
|
|
2932
|
+
}
|
|
2933
|
+
async function promptDomainPurpose() {
|
|
2934
|
+
const purpose = await clack3.select({
|
|
2935
|
+
message: "What will this domain be used for?",
|
|
2936
|
+
options: [
|
|
2937
|
+
{
|
|
2938
|
+
value: "transactional",
|
|
2939
|
+
label: "Transactional",
|
|
2940
|
+
hint: "Password resets, receipts, confirmations"
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
value: "marketing",
|
|
2944
|
+
label: "Marketing",
|
|
2945
|
+
hint: "Newsletters, promotions, campaigns"
|
|
2946
|
+
},
|
|
2947
|
+
{
|
|
2948
|
+
value: "notifications",
|
|
2949
|
+
label: "Notifications",
|
|
2950
|
+
hint: "Alerts, digests, system updates"
|
|
2951
|
+
},
|
|
2952
|
+
{
|
|
2953
|
+
value: "other",
|
|
2954
|
+
label: "Other",
|
|
2955
|
+
hint: "General purpose"
|
|
2956
|
+
}
|
|
2957
|
+
]
|
|
2958
|
+
});
|
|
2959
|
+
if (clack3.isCancel(purpose)) {
|
|
2960
|
+
clack3.cancel("Operation cancelled.");
|
|
2961
|
+
process.exit(0);
|
|
2962
|
+
}
|
|
2963
|
+
return purpose;
|
|
2964
|
+
}
|
|
2965
|
+
async function promptMailFromSubdomain(domain) {
|
|
2966
|
+
const subdomain = await clack3.text({
|
|
2967
|
+
message: `MAIL FROM subdomain for ${pc4.cyan(domain)}:`,
|
|
2968
|
+
placeholder: "mail",
|
|
2969
|
+
defaultValue: "mail",
|
|
2970
|
+
validate: (value) => {
|
|
2971
|
+
const v = value || "mail";
|
|
2972
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(v)) {
|
|
2973
|
+
return "Invalid subdomain format";
|
|
2974
|
+
}
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
if (clack3.isCancel(subdomain)) {
|
|
2979
|
+
clack3.cancel("Operation cancelled.");
|
|
2980
|
+
process.exit(0);
|
|
2981
|
+
}
|
|
2982
|
+
const sub = subdomain || "mail";
|
|
2983
|
+
return `${sub}.${domain}`;
|
|
2984
|
+
}
|
|
2873
2985
|
var DNS_CATEGORY_LABELS, DNS_STATUS_SYMBOLS;
|
|
2874
2986
|
var init_prompts = __esm({
|
|
2875
2987
|
"src/utils/shared/prompts.ts"() {
|
|
@@ -3708,6 +3820,7 @@ var init_fs = __esm({
|
|
|
3708
3820
|
// src/utils/shared/metadata.ts
|
|
3709
3821
|
var metadata_exports = {};
|
|
3710
3822
|
__export(metadata_exports, {
|
|
3823
|
+
addDomainToMetadata: () => addDomainToMetadata,
|
|
3711
3824
|
addServiceToConnection: () => addServiceToConnection,
|
|
3712
3825
|
applyConfigUpdates: () => applyConfigUpdates,
|
|
3713
3826
|
buildEmailStackConfig: () => buildEmailStackConfig,
|
|
@@ -3717,10 +3830,13 @@ __export(metadata_exports, {
|
|
|
3717
3830
|
findConnectionsForAccount: () => findConnectionsForAccount,
|
|
3718
3831
|
findConnectionsWithService: () => findConnectionsWithService,
|
|
3719
3832
|
generateWebhookSecret: () => generateWebhookSecret,
|
|
3833
|
+
getAllTrackedDomains: () => getAllTrackedDomains,
|
|
3720
3834
|
getConfiguredServices: () => getConfiguredServices,
|
|
3835
|
+
getDomainFromMetadata: () => getDomainFromMetadata,
|
|
3721
3836
|
hasService: () => hasService,
|
|
3722
3837
|
listConnections: () => listConnections,
|
|
3723
3838
|
loadConnectionMetadata: () => loadConnectionMetadata,
|
|
3839
|
+
removeDomainFromMetadata: () => removeDomainFromMetadata,
|
|
3724
3840
|
removeServiceFromConnection: () => removeServiceFromConnection,
|
|
3725
3841
|
saveConnectionMetadata: () => saveConnectionMetadata,
|
|
3726
3842
|
updateEmailConfig: () => updateEmailConfig,
|
|
@@ -4110,6 +4226,74 @@ function buildEmailStackConfig(metadata, region, overrides) {
|
|
|
4110
4226
|
function generateWebhookSecret() {
|
|
4111
4227
|
return randomBytes(32).toString("hex");
|
|
4112
4228
|
}
|
|
4229
|
+
function addDomainToMetadata(metadata, entry) {
|
|
4230
|
+
if (!metadata.services.email) {
|
|
4231
|
+
throw new Error("Email service not configured in metadata");
|
|
4232
|
+
}
|
|
4233
|
+
const config2 = metadata.services.email.config;
|
|
4234
|
+
const existing = config2.additionalDomains ?? [];
|
|
4235
|
+
const idx = existing.findIndex((d) => d.domain === entry.domain);
|
|
4236
|
+
if (idx >= 0) {
|
|
4237
|
+
existing[idx] = entry;
|
|
4238
|
+
} else {
|
|
4239
|
+
existing.push(entry);
|
|
4240
|
+
}
|
|
4241
|
+
config2.additionalDomains = existing;
|
|
4242
|
+
metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4243
|
+
}
|
|
4244
|
+
function removeDomainFromMetadata(metadata, domain) {
|
|
4245
|
+
if (!metadata.services.email) {
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
const config2 = metadata.services.email.config;
|
|
4249
|
+
if (!config2.additionalDomains) {
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
config2.additionalDomains = config2.additionalDomains.filter(
|
|
4253
|
+
(d) => d.domain !== domain
|
|
4254
|
+
);
|
|
4255
|
+
metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4256
|
+
}
|
|
4257
|
+
function getDomainFromMetadata(metadata, domain) {
|
|
4258
|
+
if (!metadata.services.email) {
|
|
4259
|
+
return null;
|
|
4260
|
+
}
|
|
4261
|
+
const config2 = metadata.services.email.config;
|
|
4262
|
+
if (config2.domain === domain) {
|
|
4263
|
+
return { isPrimary: true };
|
|
4264
|
+
}
|
|
4265
|
+
const entry = config2.additionalDomains?.find((d) => d.domain === domain);
|
|
4266
|
+
if (entry) {
|
|
4267
|
+
return { isPrimary: false, entry };
|
|
4268
|
+
}
|
|
4269
|
+
return null;
|
|
4270
|
+
}
|
|
4271
|
+
function getAllTrackedDomains(metadata) {
|
|
4272
|
+
if (!metadata.services.email) {
|
|
4273
|
+
return [];
|
|
4274
|
+
}
|
|
4275
|
+
const config2 = metadata.services.email.config;
|
|
4276
|
+
const result = [];
|
|
4277
|
+
if (config2.domain) {
|
|
4278
|
+
result.push({
|
|
4279
|
+
domain: config2.domain,
|
|
4280
|
+
isPrimary: true,
|
|
4281
|
+
managed: true,
|
|
4282
|
+
mailFromDomain: config2.mailFromDomain
|
|
4283
|
+
});
|
|
4284
|
+
}
|
|
4285
|
+
for (const d of config2.additionalDomains ?? []) {
|
|
4286
|
+
result.push({
|
|
4287
|
+
domain: d.domain,
|
|
4288
|
+
isPrimary: false,
|
|
4289
|
+
managed: true,
|
|
4290
|
+
purpose: d.purpose,
|
|
4291
|
+
mailFromDomain: d.mailFromDomain,
|
|
4292
|
+
addedAt: d.addedAt
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
return result;
|
|
4296
|
+
}
|
|
4113
4297
|
var init_metadata = __esm({
|
|
4114
4298
|
"src/utils/shared/metadata.ts"() {
|
|
4115
4299
|
"use strict";
|
|
@@ -7804,10 +7988,22 @@ function displayStatus(status2) {
|
|
|
7804
7988
|
`${pc6.bold("Region:")} ${pc6.cyan(status2.region)}`
|
|
7805
7989
|
];
|
|
7806
7990
|
if (status2.domains.length > 0) {
|
|
7991
|
+
const PURPOSE_DISPLAY = {
|
|
7992
|
+
transactional: "Transactional",
|
|
7993
|
+
marketing: "Marketing",
|
|
7994
|
+
notifications: "Notifications",
|
|
7995
|
+
other: "General"
|
|
7996
|
+
};
|
|
7807
7997
|
const domainStrings = status2.domains.map((d) => {
|
|
7808
7998
|
const statusIcon = d.status === "verified" ? "\u2713" : d.status === "pending" ? "\u23F1" : "\u2717";
|
|
7809
7999
|
const statusColor = d.status === "verified" ? pc6.green : d.status === "pending" ? pc6.yellow : pc6.red;
|
|
7810
|
-
let
|
|
8000
|
+
let label = "";
|
|
8001
|
+
if (d.isPrimary) {
|
|
8002
|
+
label = pc6.dim(" (Primary)");
|
|
8003
|
+
} else if (d.managed && d.purpose) {
|
|
8004
|
+
label = pc6.dim(` (${PURPOSE_DISPLAY[d.purpose] || d.purpose})`);
|
|
8005
|
+
}
|
|
8006
|
+
let domainLine = ` ${d.domain}${label} ${statusColor(`${statusIcon} ${d.status}`)}`;
|
|
7811
8007
|
if (d.mailFromDomain) {
|
|
7812
8008
|
const mailFromStatusIcon = d.mailFromStatus === "SUCCESS" ? "\u2713" : "\u23F1";
|
|
7813
8009
|
const mailFromColor = d.mailFromStatus === "SUCCESS" ? pc6.green : pc6.yellow;
|
|
@@ -16058,11 +16254,14 @@ Run ${pc16.cyan("wraps email init")} to deploy infrastructure again.
|
|
|
16058
16254
|
init_esm_shims();
|
|
16059
16255
|
init_client();
|
|
16060
16256
|
init_events();
|
|
16257
|
+
init_dns();
|
|
16061
16258
|
init_aws();
|
|
16259
|
+
init_metadata();
|
|
16062
16260
|
import { Resolver as Resolver2 } from "dns/promises";
|
|
16063
16261
|
import { GetEmailIdentityCommand as GetEmailIdentityCommand2, SESv2Client as SESv2Client3 } from "@aws-sdk/client-sesv2";
|
|
16064
16262
|
import * as clack16 from "@clack/prompts";
|
|
16065
16263
|
import pc17 from "picocolors";
|
|
16264
|
+
init_prompts();
|
|
16066
16265
|
async function verifyDomain(options) {
|
|
16067
16266
|
clack16.intro(pc17.bold(`Verifying ${options.domain}`));
|
|
16068
16267
|
const progress = new DeploymentProgress();
|
|
@@ -16255,20 +16454,74 @@ Run ${pc17.cyan("wraps email status")} to see the correct DNS records.
|
|
|
16255
16454
|
});
|
|
16256
16455
|
}
|
|
16257
16456
|
async function addDomain(options) {
|
|
16258
|
-
clack16.intro(pc17.bold(
|
|
16457
|
+
clack16.intro(pc17.bold("Add Email Domain"));
|
|
16259
16458
|
const progress = new DeploymentProgress();
|
|
16260
|
-
const region = await getAWSRegion();
|
|
16261
|
-
const sesClient = new SESv2Client3({ region });
|
|
16262
16459
|
try {
|
|
16460
|
+
const identity = await progress.execute(
|
|
16461
|
+
"Validating AWS credentials",
|
|
16462
|
+
async () => validateAWSCredentials()
|
|
16463
|
+
);
|
|
16464
|
+
let region = options.region || await getAWSRegion();
|
|
16465
|
+
const emailConnections = await findConnectionsWithService(
|
|
16466
|
+
identity.accountId,
|
|
16467
|
+
"email"
|
|
16468
|
+
);
|
|
16469
|
+
if (emailConnections.length === 0) {
|
|
16470
|
+
progress.stop();
|
|
16471
|
+
clack16.log.error("No email infrastructure found");
|
|
16472
|
+
console.log(
|
|
16473
|
+
`
|
|
16474
|
+
Run ${pc17.cyan("wraps email init")} first to deploy email infrastructure.
|
|
16475
|
+
`
|
|
16476
|
+
);
|
|
16477
|
+
process.exit(1);
|
|
16478
|
+
return;
|
|
16479
|
+
}
|
|
16480
|
+
if (emailConnections.length === 1) {
|
|
16481
|
+
region = emailConnections[0].region;
|
|
16482
|
+
}
|
|
16483
|
+
const metadata = await loadConnectionMetadata(identity.accountId, region);
|
|
16484
|
+
if (!metadata?.services.email) {
|
|
16485
|
+
progress.stop();
|
|
16486
|
+
clack16.log.error(`No email service found in ${region}`);
|
|
16487
|
+
process.exit(1);
|
|
16488
|
+
return;
|
|
16489
|
+
}
|
|
16490
|
+
const primaryDomain = metadata.services.email.config.domain;
|
|
16491
|
+
let domain = options.domain;
|
|
16492
|
+
if (!domain) {
|
|
16493
|
+
progress.stop();
|
|
16494
|
+
if (primaryDomain) {
|
|
16495
|
+
domain = await promptSubdomainSuggestions(primaryDomain);
|
|
16496
|
+
} else {
|
|
16497
|
+
const entered = await clack16.text({
|
|
16498
|
+
message: "Domain to add:",
|
|
16499
|
+
placeholder: "myapp.com",
|
|
16500
|
+
validate: (value) => {
|
|
16501
|
+
if (!value?.includes(".")) {
|
|
16502
|
+
return "Please enter a valid domain (e.g., myapp.com)";
|
|
16503
|
+
}
|
|
16504
|
+
return;
|
|
16505
|
+
}
|
|
16506
|
+
});
|
|
16507
|
+
if (clack16.isCancel(entered)) {
|
|
16508
|
+
clack16.cancel("Operation cancelled.");
|
|
16509
|
+
process.exit(0);
|
|
16510
|
+
}
|
|
16511
|
+
domain = entered;
|
|
16512
|
+
}
|
|
16513
|
+
}
|
|
16514
|
+
clack16.log.step(`Adding ${pc17.cyan(domain)}`);
|
|
16515
|
+
const sesClient = new SESv2Client3({ region });
|
|
16263
16516
|
try {
|
|
16264
16517
|
await sesClient.send(
|
|
16265
|
-
new GetEmailIdentityCommand2({ EmailIdentity:
|
|
16518
|
+
new GetEmailIdentityCommand2({ EmailIdentity: domain })
|
|
16266
16519
|
);
|
|
16267
16520
|
progress.stop();
|
|
16268
|
-
clack16.log.warn(`Domain ${
|
|
16521
|
+
clack16.log.warn(`Domain ${domain} already exists in SES`);
|
|
16269
16522
|
console.log(
|
|
16270
16523
|
`
|
|
16271
|
-
Run ${pc17.cyan(`wraps email domains verify --domain ${
|
|
16524
|
+
Run ${pc17.cyan(`wraps email domains verify --domain ${domain}`)} to check verification status.
|
|
16272
16525
|
`
|
|
16273
16526
|
);
|
|
16274
16527
|
return;
|
|
@@ -16277,51 +16530,168 @@ Run ${pc17.cyan(`wraps email domains verify --domain ${options.domain}`)} to che
|
|
|
16277
16530
|
throw error;
|
|
16278
16531
|
}
|
|
16279
16532
|
}
|
|
16533
|
+
let purpose = "other";
|
|
16534
|
+
if (!options.yes) {
|
|
16535
|
+
purpose = await promptDomainPurpose();
|
|
16536
|
+
}
|
|
16280
16537
|
const { CreateEmailIdentityCommand } = await import("@aws-sdk/client-sesv2");
|
|
16281
|
-
await progress.execute("
|
|
16538
|
+
await progress.execute("Creating SES identity", async () => {
|
|
16282
16539
|
await sesClient.send(
|
|
16283
16540
|
new CreateEmailIdentityCommand({
|
|
16284
|
-
EmailIdentity:
|
|
16541
|
+
EmailIdentity: domain,
|
|
16542
|
+
ConfigurationSetName: "wraps-email-tracking",
|
|
16285
16543
|
DkimSigningAttributes: {
|
|
16286
16544
|
NextSigningKeyLength: "RSA_2048_BIT"
|
|
16287
16545
|
}
|
|
16288
16546
|
})
|
|
16289
16547
|
);
|
|
16290
16548
|
});
|
|
16291
|
-
const
|
|
16292
|
-
new GetEmailIdentityCommand2({ EmailIdentity:
|
|
16549
|
+
const sesIdentity = await sesClient.send(
|
|
16550
|
+
new GetEmailIdentityCommand2({ EmailIdentity: domain })
|
|
16293
16551
|
);
|
|
16294
|
-
const dkimTokens =
|
|
16552
|
+
const dkimTokens = sesIdentity.DkimAttributes?.Tokens || [];
|
|
16553
|
+
let mailFromDomain;
|
|
16554
|
+
if (options.yes) {
|
|
16555
|
+
mailFromDomain = `mail.${domain}`;
|
|
16556
|
+
} else {
|
|
16557
|
+
const wantsMailFrom = await clack16.confirm({
|
|
16558
|
+
message: `Configure MAIL FROM for ${pc17.cyan(domain)}? ${pc17.dim("(improves DMARC alignment)")}`,
|
|
16559
|
+
initialValue: true
|
|
16560
|
+
});
|
|
16561
|
+
if (clack16.isCancel(wantsMailFrom)) {
|
|
16562
|
+
clack16.cancel("Operation cancelled.");
|
|
16563
|
+
process.exit(0);
|
|
16564
|
+
}
|
|
16565
|
+
if (wantsMailFrom) {
|
|
16566
|
+
mailFromDomain = await promptMailFromSubdomain(domain);
|
|
16567
|
+
}
|
|
16568
|
+
}
|
|
16569
|
+
if (mailFromDomain) {
|
|
16570
|
+
const { PutEmailIdentityMailFromAttributesCommand } = await import("@aws-sdk/client-sesv2");
|
|
16571
|
+
await progress.execute("Setting up MAIL FROM", async () => {
|
|
16572
|
+
await sesClient.send(
|
|
16573
|
+
new PutEmailIdentityMailFromAttributesCommand({
|
|
16574
|
+
EmailIdentity: domain,
|
|
16575
|
+
MailFromDomain: mailFromDomain,
|
|
16576
|
+
BehaviorOnMxFailure: "USE_DEFAULT_VALUE"
|
|
16577
|
+
})
|
|
16578
|
+
);
|
|
16579
|
+
});
|
|
16580
|
+
}
|
|
16581
|
+
const cachedDnsProvider = metadata.services.email.dnsProvider;
|
|
16582
|
+
let dnsAutoCreated = false;
|
|
16583
|
+
const domainParts = domain.split(".");
|
|
16584
|
+
const rootDomain = domainParts.length > 2 ? domainParts.slice(-2).join(".") : domain;
|
|
16585
|
+
if (!options.yes || cachedDnsProvider) {
|
|
16586
|
+
let dnsProvider = cachedDnsProvider;
|
|
16587
|
+
if (!dnsProvider) {
|
|
16588
|
+
progress.stop();
|
|
16589
|
+
const availableProviders = await detectAvailableDNSProviders(
|
|
16590
|
+
rootDomain,
|
|
16591
|
+
region
|
|
16592
|
+
);
|
|
16593
|
+
dnsProvider = await promptDNSProvider(rootDomain, availableProviders);
|
|
16594
|
+
}
|
|
16595
|
+
if (dnsProvider && dnsProvider !== "manual") {
|
|
16596
|
+
const credResult = await getDNSCredentials(
|
|
16597
|
+
dnsProvider,
|
|
16598
|
+
rootDomain,
|
|
16599
|
+
region
|
|
16600
|
+
);
|
|
16601
|
+
if (credResult.valid && credResult.credentials) {
|
|
16602
|
+
const dnsData = {
|
|
16603
|
+
domain,
|
|
16604
|
+
dkimTokens,
|
|
16605
|
+
mailFromDomain,
|
|
16606
|
+
region
|
|
16607
|
+
};
|
|
16608
|
+
const result = await progress.execute(
|
|
16609
|
+
`Creating DNS records via ${getDNSProviderDisplayName(dnsProvider)}`,
|
|
16610
|
+
async () => createDNSRecordsForProvider(credResult.credentials, dnsData)
|
|
16611
|
+
);
|
|
16612
|
+
if (result.success && result.recordsCreated > 0) {
|
|
16613
|
+
dnsAutoCreated = true;
|
|
16614
|
+
clack16.log.success(
|
|
16615
|
+
`${result.recordsCreated} DNS records created via ${getDNSProviderDisplayName(dnsProvider)}`
|
|
16616
|
+
);
|
|
16617
|
+
} else if (!result.success) {
|
|
16618
|
+
clack16.log.warn(
|
|
16619
|
+
`DNS auto-creation failed: ${result.errors?.join(", ") || "unknown error"}`
|
|
16620
|
+
);
|
|
16621
|
+
}
|
|
16622
|
+
if (!metadata.services.email.dnsProvider) {
|
|
16623
|
+
metadata.services.email.dnsProvider = dnsProvider;
|
|
16624
|
+
}
|
|
16625
|
+
} else {
|
|
16626
|
+
clack16.log.warn(`DNS credentials not available: ${credResult.error}`);
|
|
16627
|
+
}
|
|
16628
|
+
}
|
|
16629
|
+
}
|
|
16630
|
+
if (!dnsAutoCreated) {
|
|
16631
|
+
const dnsRecords = buildEmailDNSRecords({
|
|
16632
|
+
domain,
|
|
16633
|
+
dkimTokens,
|
|
16634
|
+
mailFromDomain,
|
|
16635
|
+
region
|
|
16636
|
+
});
|
|
16637
|
+
const displayRecords = formatDNSRecordsForDisplay(dnsRecords);
|
|
16638
|
+
progress.stop();
|
|
16639
|
+
console.log();
|
|
16640
|
+
clack16.log.info(pc17.bold("Add these DNS records:"));
|
|
16641
|
+
console.log();
|
|
16642
|
+
for (const record of displayRecords) {
|
|
16643
|
+
console.log(` ${pc17.cyan(record.name)}`);
|
|
16644
|
+
console.log(
|
|
16645
|
+
` ${pc17.dim("Type:")} ${record.type} ${pc17.dim("Value:")} ${record.value}`
|
|
16646
|
+
);
|
|
16647
|
+
console.log();
|
|
16648
|
+
}
|
|
16649
|
+
}
|
|
16650
|
+
const entry = {
|
|
16651
|
+
domain,
|
|
16652
|
+
mailFromDomain,
|
|
16653
|
+
purpose,
|
|
16654
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16655
|
+
};
|
|
16656
|
+
addDomainToMetadata(metadata, entry);
|
|
16657
|
+
await saveConnectionMetadata(metadata);
|
|
16295
16658
|
progress.stop();
|
|
16296
|
-
clack16.outro(pc17.green(`\u2713 Domain ${
|
|
16297
|
-
|
|
16298
|
-
${pc17.bold("Next steps:")}
|
|
16299
|
-
`);
|
|
16300
|
-
console.log("1. Add the following DKIM records to your DNS:\n");
|
|
16301
|
-
for (const token of dkimTokens) {
|
|
16302
|
-
console.log(` ${pc17.cyan(`${token}._domainkey.${options.domain}`)}`);
|
|
16659
|
+
clack16.outro(pc17.green(`\u2713 Domain ${domain} added successfully!`));
|
|
16660
|
+
if (dnsAutoCreated) {
|
|
16303
16661
|
console.log(
|
|
16304
|
-
`
|
|
16305
|
-
`
|
|
16662
|
+
`
|
|
16663
|
+
${pc17.dim("DNS records were created automatically. Verification should complete within a few minutes.")}`
|
|
16306
16664
|
);
|
|
16307
16665
|
}
|
|
16666
|
+
console.log(`
|
|
16667
|
+
${pc17.bold("Next steps:")}`);
|
|
16308
16668
|
console.log(
|
|
16309
|
-
`
|
|
16669
|
+
` Verify: ${pc17.cyan(`wraps email domains verify --domain ${domain}`)}`
|
|
16310
16670
|
);
|
|
16311
|
-
console.log(`
|
|
16671
|
+
console.log(` Status: ${pc17.cyan("wraps email status")}
|
|
16312
16672
|
`);
|
|
16313
16673
|
trackCommand("email:domains:add", {
|
|
16314
|
-
success: true
|
|
16674
|
+
success: true,
|
|
16675
|
+
dns_auto_created: dnsAutoCreated,
|
|
16676
|
+
has_mail_from: !!mailFromDomain,
|
|
16677
|
+
purpose
|
|
16678
|
+
});
|
|
16679
|
+
trackFeature("domain_added", {
|
|
16680
|
+
purpose,
|
|
16681
|
+
subdomain: domain !== primaryDomain
|
|
16315
16682
|
});
|
|
16316
|
-
trackFeature("domain_added", {});
|
|
16317
16683
|
} catch (error) {
|
|
16318
16684
|
progress.stop();
|
|
16319
|
-
trackCommand("email:domains:add", {
|
|
16320
|
-
success: false
|
|
16321
|
-
});
|
|
16685
|
+
trackCommand("email:domains:add", { success: false });
|
|
16322
16686
|
throw error;
|
|
16323
16687
|
}
|
|
16324
16688
|
}
|
|
16689
|
+
var PURPOSE_LABELS = {
|
|
16690
|
+
transactional: "Transactional",
|
|
16691
|
+
marketing: "Marketing",
|
|
16692
|
+
notifications: "Notifications",
|
|
16693
|
+
other: "General"
|
|
16694
|
+
};
|
|
16325
16695
|
async function listDomains() {
|
|
16326
16696
|
clack16.intro(pc17.bold("SES Email Domains"));
|
|
16327
16697
|
const progress = new DeploymentProgress();
|
|
@@ -16338,21 +16708,34 @@ async function listDomains() {
|
|
|
16338
16708
|
return response.EmailIdentities || [];
|
|
16339
16709
|
}
|
|
16340
16710
|
);
|
|
16341
|
-
const
|
|
16711
|
+
const sesDomains = identities.filter(
|
|
16342
16712
|
(identity) => identity.IdentityType === "DOMAIN" || identity.IdentityName && !identity.IdentityName.includes("@")
|
|
16343
16713
|
);
|
|
16714
|
+
let trackedDomains = [];
|
|
16715
|
+
try {
|
|
16716
|
+
const awsIdentity = await validateAWSCredentials();
|
|
16717
|
+
const metadata = await loadConnectionMetadata(
|
|
16718
|
+
awsIdentity.accountId,
|
|
16719
|
+
region
|
|
16720
|
+
);
|
|
16721
|
+
if (metadata) {
|
|
16722
|
+
trackedDomains = getAllTrackedDomains(metadata);
|
|
16723
|
+
}
|
|
16724
|
+
} catch {
|
|
16725
|
+
}
|
|
16726
|
+
const trackedSet = new Map(trackedDomains.map((d) => [d.domain, d]));
|
|
16344
16727
|
progress.stop();
|
|
16345
|
-
if (
|
|
16728
|
+
if (sesDomains.length === 0) {
|
|
16346
16729
|
clack16.outro("No domains found in SES");
|
|
16347
16730
|
console.log(
|
|
16348
16731
|
`
|
|
16349
|
-
Run ${pc17.cyan("wraps email domains add
|
|
16732
|
+
Run ${pc17.cyan("wraps email domains add")} to add a domain.
|
|
16350
16733
|
`
|
|
16351
16734
|
);
|
|
16352
16735
|
return;
|
|
16353
16736
|
}
|
|
16354
16737
|
const domainDetails = await Promise.all(
|
|
16355
|
-
|
|
16738
|
+
sesDomains.map(async (domain) => {
|
|
16356
16739
|
try {
|
|
16357
16740
|
const details = await sesClient.send(
|
|
16358
16741
|
new GetEmailIdentityCommand2({
|
|
@@ -16373,15 +16756,26 @@ Run ${pc17.cyan("wraps email domains add <domain>")} to add a domain.
|
|
|
16373
16756
|
}
|
|
16374
16757
|
})
|
|
16375
16758
|
);
|
|
16376
|
-
const
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16759
|
+
const managed = domainDetails.filter((d) => trackedSet.has(d.name));
|
|
16760
|
+
const unmanaged = domainDetails.filter((d) => !trackedSet.has(d.name));
|
|
16761
|
+
if (managed.length > 0) {
|
|
16762
|
+
const managedLines = managed.map((d) => {
|
|
16763
|
+
const tracked = trackedSet.get(d.name);
|
|
16764
|
+
const statusIcon = d.verified ? pc17.green("\u2713") : pc17.yellow("\u23F1");
|
|
16765
|
+
const dkimIcon = d.dkimStatus === "SUCCESS" ? pc17.green("\u2713 SUCCESS") : pc17.yellow(`\u23F1 ${d.dkimStatus}`);
|
|
16766
|
+
const label = tracked.isPrimary ? pc17.dim("Primary") : pc17.dim(PURPOSE_LABELS[tracked.purpose || "other"] || "General");
|
|
16767
|
+
return ` ${statusIcon} ${pc17.bold(d.name.padEnd(30))} ${label.padEnd(24)} DKIM: ${dkimIcon}`;
|
|
16768
|
+
});
|
|
16769
|
+
clack16.note(managedLines.join("\n"), "Managed by Wraps");
|
|
16770
|
+
}
|
|
16771
|
+
if (unmanaged.length > 0) {
|
|
16772
|
+
const unmanagedLines = unmanaged.map((d) => {
|
|
16773
|
+
const statusIcon = d.verified ? pc17.green("\u2713") : pc17.yellow("\u23F1");
|
|
16774
|
+
const dkimIcon = d.dkimStatus === "SUCCESS" ? pc17.green("\u2713 SUCCESS") : pc17.yellow(`\u23F1 ${d.dkimStatus}`);
|
|
16775
|
+
return ` ${statusIcon} ${pc17.bold(d.name.padEnd(30))} ${pc17.dim("".padEnd(16))} DKIM: ${dkimIcon}`;
|
|
16776
|
+
});
|
|
16777
|
+
clack16.note(unmanagedLines.join("\n"), "Other SES domains");
|
|
16778
|
+
}
|
|
16385
16779
|
clack16.outro(
|
|
16386
16780
|
pc17.dim(
|
|
16387
16781
|
`Run ${pc17.cyan("wraps email domains verify --domain <domain>")} for details`
|
|
@@ -16389,7 +16783,8 @@ Run ${pc17.cyan("wraps email domains add <domain>")} to add a domain.
|
|
|
16389
16783
|
);
|
|
16390
16784
|
trackCommand("email:domains:list", {
|
|
16391
16785
|
success: true,
|
|
16392
|
-
domain_count:
|
|
16786
|
+
domain_count: sesDomains.length,
|
|
16787
|
+
managed_count: managed.length
|
|
16393
16788
|
});
|
|
16394
16789
|
getTelemetryClient().showFooterOnce();
|
|
16395
16790
|
} catch (error) {
|
|
@@ -16468,6 +16863,31 @@ async function removeDomain(options) {
|
|
|
16468
16863
|
new GetEmailIdentityCommand2({ EmailIdentity: options.domain })
|
|
16469
16864
|
);
|
|
16470
16865
|
});
|
|
16866
|
+
let metadata = null;
|
|
16867
|
+
try {
|
|
16868
|
+
const awsIdentity = await validateAWSCredentials();
|
|
16869
|
+
metadata = await loadConnectionMetadata(awsIdentity.accountId, region);
|
|
16870
|
+
} catch {
|
|
16871
|
+
}
|
|
16872
|
+
if (metadata) {
|
|
16873
|
+
const domainInfo = getDomainFromMetadata(metadata, options.domain);
|
|
16874
|
+
if (domainInfo?.isPrimary && !options.force) {
|
|
16875
|
+
progress.stop();
|
|
16876
|
+
clack16.log.error(
|
|
16877
|
+
`${options.domain} is the primary domain (managed by Pulumi).`
|
|
16878
|
+
);
|
|
16879
|
+
console.log(
|
|
16880
|
+
`
|
|
16881
|
+
Use ${pc17.cyan(`wraps email domains remove --domain ${options.domain} --force`)} to remove it,`
|
|
16882
|
+
);
|
|
16883
|
+
console.log(
|
|
16884
|
+
`or ${pc17.cyan("wraps email destroy")} to remove all email infrastructure.
|
|
16885
|
+
`
|
|
16886
|
+
);
|
|
16887
|
+
process.exit(1);
|
|
16888
|
+
return;
|
|
16889
|
+
}
|
|
16890
|
+
}
|
|
16471
16891
|
progress.stop();
|
|
16472
16892
|
if (!options.force) {
|
|
16473
16893
|
const shouldContinue = await clack16.confirm({
|
|
@@ -16487,6 +16907,10 @@ async function removeDomain(options) {
|
|
|
16487
16907
|
})
|
|
16488
16908
|
);
|
|
16489
16909
|
});
|
|
16910
|
+
if (metadata) {
|
|
16911
|
+
removeDomainFromMetadata(metadata, options.domain);
|
|
16912
|
+
await saveConnectionMetadata(metadata);
|
|
16913
|
+
}
|
|
16490
16914
|
progress.stop();
|
|
16491
16915
|
clack16.outro(pc17.green(`\u2713 Domain ${options.domain} removed successfully`));
|
|
16492
16916
|
trackCommand("email:domains:remove", {
|
|
@@ -18158,18 +18582,25 @@ Run ${pc21.cyan("wraps email init")} to deploy email infrastructure.
|
|
|
18158
18582
|
const domains = await listSESDomains(region);
|
|
18159
18583
|
const { SESv2Client: SESv2Client6, GetEmailIdentityCommand: GetEmailIdentityCommand5 } = await import("@aws-sdk/client-sesv2");
|
|
18160
18584
|
const sesv2Client = new SESv2Client6({ region });
|
|
18585
|
+
const metadata = await loadConnectionMetadata(identity.accountId, region);
|
|
18586
|
+
const trackedDomains = metadata ? getAllTrackedDomains(metadata) : [];
|
|
18587
|
+
const trackedMap = new Map(trackedDomains.map((d) => [d.domain, d]));
|
|
18161
18588
|
const domainsWithTokens = await Promise.all(
|
|
18162
18589
|
domains.map(async (d) => {
|
|
18590
|
+
const tracked = trackedMap.get(d.domain);
|
|
18163
18591
|
try {
|
|
18164
|
-
const
|
|
18592
|
+
const sesIdentity = await sesv2Client.send(
|
|
18165
18593
|
new GetEmailIdentityCommand5({ EmailIdentity: d.domain })
|
|
18166
18594
|
);
|
|
18167
18595
|
return {
|
|
18168
18596
|
domain: d.domain,
|
|
18169
18597
|
status: d.verified ? "verified" : "pending",
|
|
18170
|
-
dkimTokens:
|
|
18171
|
-
mailFromDomain:
|
|
18172
|
-
mailFromStatus:
|
|
18598
|
+
dkimTokens: sesIdentity.DkimAttributes?.Tokens || [],
|
|
18599
|
+
mailFromDomain: sesIdentity.MailFromAttributes?.MailFromDomain,
|
|
18600
|
+
mailFromStatus: sesIdentity.MailFromAttributes?.MailFromDomainStatus,
|
|
18601
|
+
managed: tracked?.managed,
|
|
18602
|
+
isPrimary: tracked?.isPrimary,
|
|
18603
|
+
purpose: tracked?.purpose
|
|
18173
18604
|
};
|
|
18174
18605
|
} catch (_error) {
|
|
18175
18606
|
return {
|
|
@@ -18177,7 +18608,10 @@ Run ${pc21.cyan("wraps email init")} to deploy email infrastructure.
|
|
|
18177
18608
|
status: d.verified ? "verified" : "pending",
|
|
18178
18609
|
dkimTokens: void 0,
|
|
18179
18610
|
mailFromDomain: void 0,
|
|
18180
|
-
mailFromStatus: void 0
|
|
18611
|
+
mailFromStatus: void 0,
|
|
18612
|
+
managed: tracked?.managed,
|
|
18613
|
+
isPrimary: tracked?.isPrimary,
|
|
18614
|
+
purpose: tracked?.purpose
|
|
18181
18615
|
};
|
|
18182
18616
|
}
|
|
18183
18617
|
})
|
|
@@ -20861,6 +21295,7 @@ ${pc27.bold("Updated Permissions:")}`);
|
|
|
20861
21295
|
` ${pc27.green("\u2713")} SES metrics and identity verification (always enabled)`
|
|
20862
21296
|
);
|
|
20863
21297
|
console.log(` ${pc27.green("\u2713")} SES template management (always enabled)`);
|
|
21298
|
+
console.log(` ${pc27.green("\u2713")} Inbound bucket detection (always enabled)`);
|
|
20864
21299
|
if (sendingEnabled) {
|
|
20865
21300
|
console.log(` ${pc27.green("\u2713")} Email sending via SES`);
|
|
20866
21301
|
}
|
|
@@ -20875,6 +21310,10 @@ ${pc27.bold("Updated Permissions:")}`);
|
|
|
20875
21310
|
if (emailArchiving?.enabled) {
|
|
20876
21311
|
console.log(` ${pc27.green("\u2713")} Mail Manager Archive access`);
|
|
20877
21312
|
}
|
|
21313
|
+
const inbound = emailConfig?.inbound;
|
|
21314
|
+
if (inbound?.enabled) {
|
|
21315
|
+
console.log(` ${pc27.green("\u2713")} S3 access for inbound email`);
|
|
21316
|
+
}
|
|
20878
21317
|
if (smsEnabled) {
|
|
20879
21318
|
console.log(`
|
|
20880
21319
|
${pc27.bold(pc27.cyan("SMS:"))}`);
|
|
@@ -20914,10 +21353,17 @@ function buildConsolePolicyDocument2(emailConfig, smsConfig) {
|
|
|
20914
21353
|
"ses:GetConfigurationSet",
|
|
20915
21354
|
"ses:GetConfigurationSetEventDestinations",
|
|
20916
21355
|
"cloudwatch:GetMetricData",
|
|
20917
|
-
"cloudwatch:GetMetricStatistics"
|
|
21356
|
+
"cloudwatch:GetMetricStatistics",
|
|
21357
|
+
// SES dedicated IP scanning
|
|
21358
|
+
"ses:GetDedicatedIps"
|
|
20918
21359
|
],
|
|
20919
21360
|
Resource: "*"
|
|
20920
21361
|
});
|
|
21362
|
+
statements.push({
|
|
21363
|
+
Effect: "Allow",
|
|
21364
|
+
Action: ["s3:HeadBucket"],
|
|
21365
|
+
Resource: "arn:aws:s3:::wraps-inbound-*"
|
|
21366
|
+
});
|
|
20921
21367
|
statements.push({
|
|
20922
21368
|
Effect: "Allow",
|
|
20923
21369
|
Action: [
|
|
@@ -20999,6 +21445,22 @@ function buildConsolePolicyDocument2(emailConfig, smsConfig) {
|
|
|
20999
21445
|
Resource: "arn:aws:ses:*:*:mailmanager-archive/*"
|
|
21000
21446
|
});
|
|
21001
21447
|
}
|
|
21448
|
+
const inbound = emailConfig?.inbound;
|
|
21449
|
+
if (inbound?.enabled) {
|
|
21450
|
+
statements.push({
|
|
21451
|
+
Effect: "Allow",
|
|
21452
|
+
Action: [
|
|
21453
|
+
"s3:HeadBucket",
|
|
21454
|
+
"s3:ListBucket",
|
|
21455
|
+
"s3:GetObject",
|
|
21456
|
+
"s3:GetObjectTagging"
|
|
21457
|
+
],
|
|
21458
|
+
Resource: [
|
|
21459
|
+
"arn:aws:s3:::wraps-inbound-*",
|
|
21460
|
+
"arn:aws:s3:::wraps-inbound-*/*"
|
|
21461
|
+
]
|
|
21462
|
+
});
|
|
21463
|
+
}
|
|
21002
21464
|
if (smsConfig) {
|
|
21003
21465
|
statements.push({
|
|
21004
21466
|
Effect: "Allow",
|
|
@@ -27942,10 +28404,8 @@ function printCompletionScript() {
|
|
|
27942
28404
|
console.log("# wraps console [--port <port>] [--no-open]");
|
|
27943
28405
|
console.log("# wraps completion");
|
|
27944
28406
|
console.log("# wraps telemetry [enable|disable|status]\n");
|
|
27945
|
-
console.log("#
|
|
27946
|
-
console.log(
|
|
27947
|
-
"# wraps dashboard update-role [--region <region>] [--force]\n"
|
|
27948
|
-
);
|
|
28407
|
+
console.log("# Platform Commands:");
|
|
28408
|
+
console.log("# wraps platform update-role [--region <region>] [--force]\n");
|
|
27949
28409
|
console.log("# Flags:");
|
|
27950
28410
|
console.log("# -p, --provider : vercel, aws, railway, other");
|
|
27951
28411
|
console.log(
|
|
@@ -28532,16 +28992,11 @@ Available commands: ${pc41.cyan("init")}, ${pc41.cyan("destroy")}, ${pc41.cyan("
|
|
|
28532
28992
|
const domainsSubCommand = args.sub[2];
|
|
28533
28993
|
switch (domainsSubCommand) {
|
|
28534
28994
|
case "add": {
|
|
28535
|
-
|
|
28536
|
-
|
|
28537
|
-
|
|
28538
|
-
|
|
28539
|
-
|
|
28540
|
-
`
|
|
28541
|
-
);
|
|
28542
|
-
process.exit(1);
|
|
28543
|
-
}
|
|
28544
|
-
await addDomain({ domain: flags.domain });
|
|
28995
|
+
await addDomain({
|
|
28996
|
+
domain: flags.domain,
|
|
28997
|
+
region: flags.region,
|
|
28998
|
+
yes: flags.yes
|
|
28999
|
+
});
|
|
28545
29000
|
break;
|
|
28546
29001
|
}
|
|
28547
29002
|
case "list":
|