@wraps.dev/cli 2.18.3 → 2.18.5

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
@@ -1855,6 +1855,7 @@ You may need to merge your existing rules into the wraps rule set.`,
1855
1855
  // src/utils/shared/aws.ts
1856
1856
  var aws_exports = {};
1857
1857
  __export(aws_exports, {
1858
+ SES_REGIONS: () => SES_REGIONS,
1858
1859
  checkRegion: () => checkRegion,
1859
1860
  getACMCertificateStatus: () => getACMCertificateStatus,
1860
1861
  getAWSRegion: () => getAWSRegion,
@@ -1933,31 +1934,7 @@ async function validateAWSCredentialsWithDetails() {
1933
1934
  }
1934
1935
  }
1935
1936
  async function checkRegion(region) {
1936
- const validRegions = [
1937
- "us-east-1",
1938
- "us-east-2",
1939
- "us-west-1",
1940
- "us-west-2",
1941
- "af-south-1",
1942
- "ap-east-1",
1943
- "ap-south-1",
1944
- "ap-northeast-1",
1945
- "ap-northeast-2",
1946
- "ap-northeast-3",
1947
- "ap-southeast-1",
1948
- "ap-southeast-2",
1949
- "ap-southeast-3",
1950
- "ca-central-1",
1951
- "eu-central-1",
1952
- "eu-west-1",
1953
- "eu-west-2",
1954
- "eu-west-3",
1955
- "eu-south-1",
1956
- "eu-north-1",
1957
- "me-south-1",
1958
- "sa-east-1"
1959
- ];
1960
- return validRegions.includes(region);
1937
+ return SES_REGIONS.includes(region);
1961
1938
  }
1962
1939
  async function getAWSRegion() {
1963
1940
  if (process.env.AWS_REGION) {
@@ -2042,12 +2019,37 @@ async function getACMCertificateStatus(certificateArn) {
2042
2019
  return null;
2043
2020
  }
2044
2021
  }
2022
+ var SES_REGIONS;
2045
2023
  var init_aws = __esm({
2046
2024
  "src/utils/shared/aws.ts"() {
2047
2025
  "use strict";
2048
2026
  init_esm_shims();
2049
2027
  init_aws_detection();
2050
2028
  init_errors();
2029
+ SES_REGIONS = [
2030
+ "us-east-1",
2031
+ "us-east-2",
2032
+ "us-west-1",
2033
+ "us-west-2",
2034
+ "af-south-1",
2035
+ "ap-east-1",
2036
+ "ap-south-1",
2037
+ "ap-northeast-1",
2038
+ "ap-northeast-2",
2039
+ "ap-northeast-3",
2040
+ "ap-southeast-1",
2041
+ "ap-southeast-2",
2042
+ "ap-southeast-3",
2043
+ "ca-central-1",
2044
+ "eu-central-1",
2045
+ "eu-west-1",
2046
+ "eu-west-2",
2047
+ "eu-west-3",
2048
+ "eu-south-1",
2049
+ "eu-north-1",
2050
+ "me-south-1",
2051
+ "sa-east-1"
2052
+ ];
2051
2053
  }
2052
2054
  });
2053
2055
 
@@ -8123,6 +8125,9 @@ async function scanSESIdentities(region) {
8123
8125
  }
8124
8126
  return identities;
8125
8127
  } catch (error) {
8128
+ if (error instanceof Error && (error.name === "AccessDeniedException" || error.name === "AccessDenied")) {
8129
+ throw error;
8130
+ }
8126
8131
  console.error(
8127
8132
  "Error scanning SES identities:",
8128
8133
  error instanceof Error ? error.message : error
@@ -8303,27 +8308,37 @@ async function scanIAMRoles(region) {
8303
8308
  }
8304
8309
  async function scanAWSResources(region) {
8305
8310
  const [
8306
- identities,
8311
+ identityResult,
8307
8312
  configurationSets,
8308
8313
  snsTopics,
8309
8314
  dynamoTables,
8310
8315
  lambdaFunctions,
8311
8316
  iamRoles
8312
8317
  ] = await Promise.all([
8313
- scanSESIdentities(region),
8318
+ scanSESIdentities(region).catch(
8319
+ (error) => error instanceof Error ? error : new Error(String(error))
8320
+ ),
8314
8321
  scanSESConfigurationSets(region),
8315
8322
  scanSNSTopics(region),
8316
8323
  scanDynamoTables(region),
8317
8324
  scanLambdaFunctions(region),
8318
8325
  scanIAMRoles(region)
8319
8326
  ]);
8327
+ let identities = [];
8328
+ let scanErrors;
8329
+ if (identityResult instanceof Error) {
8330
+ scanErrors = { identities: identityResult.name };
8331
+ } else {
8332
+ identities = identityResult;
8333
+ }
8320
8334
  return {
8321
8335
  identities,
8322
8336
  configurationSets,
8323
8337
  snsTopics,
8324
8338
  dynamoTables,
8325
8339
  lambdaFunctions,
8326
- iamRoles
8340
+ iamRoles,
8341
+ scanErrors
8327
8342
  };
8328
8343
  }
8329
8344
  function filterWrapsResources(scan) {
@@ -8978,9 +8993,8 @@ Run ${pc27.cyan("wraps email init")} to deploy email infrastructure.
8978
8993
  process.exit(1);
8979
8994
  return;
8980
8995
  }
8981
- const emailConfig = metadata.services.email;
8982
- const domain = emailConfig.config.domain;
8983
- if (!domain) {
8996
+ const trackedDomains = getAllTrackedDomains(metadata);
8997
+ if (trackedDomains.length === 0) {
8984
8998
  progress.stop();
8985
8999
  clack25.log.error("No domain configured in email infrastructure");
8986
9000
  console.log(`
@@ -8989,6 +9003,24 @@ Run ${pc27.cyan("wraps email init")} to set up a domain.
8989
9003
  process.exit(1);
8990
9004
  return;
8991
9005
  }
9006
+ let domain;
9007
+ if (trackedDomains.length === 1) {
9008
+ domain = trackedDomains[0].domain;
9009
+ } else {
9010
+ const selected = await clack25.select({
9011
+ message: "Which domain do you want to send from?",
9012
+ options: trackedDomains.map((d) => ({
9013
+ value: d.domain,
9014
+ label: d.domain,
9015
+ hint: d.isPrimary ? "primary" : d.purpose
9016
+ }))
9017
+ });
9018
+ if (clack25.isCancel(selected)) {
9019
+ clack25.cancel("Operation cancelled.");
9020
+ process.exit(0);
9021
+ }
9022
+ domain = selected;
9023
+ }
8992
9024
  let toEmail = options.to;
8993
9025
  if (!toEmail) {
8994
9026
  if (isJsonMode() && !options.scenario) {
@@ -17714,12 +17746,7 @@ async function createSMTPCredentials(config2) {
17714
17746
  {
17715
17747
  Effect: "Allow",
17716
17748
  Action: "ses:SendRawEmail",
17717
- Resource: "*",
17718
- Condition: {
17719
- StringEquals: {
17720
- "ses:ConfigurationSetName": config2.configSetName
17721
- }
17722
- }
17749
+ Resource: "*"
17723
17750
  }
17724
17751
  ]
17725
17752
  })
@@ -18366,11 +18393,40 @@ async function connect2(options) {
18366
18393
  progress.info(
18367
18394
  `Found: ${scan.identities.length} identities, ${scan.configurationSets.length} config sets`
18368
18395
  );
18396
+ if (scan.scanErrors?.identities) {
18397
+ throw errors.sesPermissionDenied("ListIdentities");
18398
+ }
18369
18399
  if (scan.identities.length === 0) {
18370
- clack17.log.warn("No SES identities found in this region.");
18371
- clack17.log.info(
18372
- `Use ${pc19.cyan("wraps email init")} to create new email infrastructure instead.`
18400
+ const otherRegions = SES_REGIONS.filter((r) => r !== region);
18401
+ const regionHits = await progress.execute(
18402
+ "Checking other regions for SES identities",
18403
+ async () => {
18404
+ const results = await Promise.all(
18405
+ otherRegions.map(async (r) => {
18406
+ try {
18407
+ const ids = await scanSESIdentities(r);
18408
+ return ids.length > 0 ? r : null;
18409
+ } catch {
18410
+ return null;
18411
+ }
18412
+ })
18413
+ );
18414
+ return results.filter((r) => r !== null);
18415
+ }
18373
18416
  );
18417
+ if (regionHits.length > 0) {
18418
+ clack17.log.warn(
18419
+ `No SES identities found in ${pc19.cyan(region)}, but found identities in: ${regionHits.map((r) => pc19.cyan(r)).join(", ")}`
18420
+ );
18421
+ clack17.log.info(
18422
+ `Run ${pc19.cyan(`wraps email connect --region ${regionHits[0]}`)} to connect to your existing infrastructure.`
18423
+ );
18424
+ } else {
18425
+ clack17.log.warn("No SES identities found in any region.");
18426
+ clack17.log.info(
18427
+ `Use ${pc19.cyan("wraps email init")} to create new email infrastructure instead.`
18428
+ );
18429
+ }
18374
18430
  process.exit(0);
18375
18431
  }
18376
18432
  const verifiedIdentities = scan.identities.filter((id) => id.verified);
@@ -18397,6 +18453,15 @@ async function connect2(options) {
18397
18453
  clack17.log.warn("No identities selected. Nothing to connect.");
18398
18454
  process.exit(0);
18399
18455
  }
18456
+ const unverifiedSelected = selectedIdentities.filter((name) => {
18457
+ const identity2 = scan.identities.find((id) => id.name === name);
18458
+ return identity2 && !identity2.verified;
18459
+ });
18460
+ if (unverifiedSelected.length > 0) {
18461
+ clack17.log.warn(
18462
+ `${unverifiedSelected.map((id) => pc19.cyan(id)).join(", ")} not yet verified \u2014 SES will reject sends from unverified identities until verification is complete.`
18463
+ );
18464
+ }
18400
18465
  const preset = await promptConfigPreset();
18401
18466
  const emailConfig = preset === "custom" ? await Promise.resolve().then(() => (init_prompts(), prompts_exports)).then(
18402
18467
  (m) => m.promptCustomConfig()
@@ -19749,18 +19814,17 @@ Run ${pc23.cyan("wraps email init")} first to deploy email infrastructure.
19749
19814
  }
19750
19815
  clack21.log.step(`Adding ${pc23.cyan(domain)}`);
19751
19816
  const sesClient = new SESv2Client4({ region });
19817
+ let domainAlreadyExists = false;
19818
+ let dkimTokens = [];
19752
19819
  try {
19753
- await sesClient.send(
19820
+ const existing = await sesClient.send(
19754
19821
  new GetEmailIdentityCommand2({ EmailIdentity: domain })
19755
19822
  );
19756
- progress.stop();
19757
- clack21.log.warn(`Domain ${domain} already exists in SES`);
19758
- console.log(
19759
- `
19760
- Run ${pc23.cyan(`wraps email domains verify --domain ${domain}`)} to check verification status.
19761
- `
19823
+ domainAlreadyExists = true;
19824
+ dkimTokens = existing.DkimAttributes?.Tokens || [];
19825
+ clack21.log.info(
19826
+ `Domain ${pc23.cyan(domain)} already exists in SES \u2014 adopting into Wraps`
19762
19827
  );
19763
- return;
19764
19828
  } catch (error) {
19765
19829
  if (!isAWSNotFoundError(error)) {
19766
19830
  throw error;
@@ -19770,22 +19834,34 @@ Run ${pc23.cyan(`wraps email domains verify --domain ${domain}`)} to check verif
19770
19834
  if (!options.yes) {
19771
19835
  purpose = await promptDomainPurpose();
19772
19836
  }
19773
- const { CreateEmailIdentityCommand } = await import("@aws-sdk/client-sesv2");
19774
- await progress.execute("Creating SES identity", async () => {
19775
- await sesClient.send(
19776
- new CreateEmailIdentityCommand({
19777
- EmailIdentity: domain,
19778
- ConfigurationSetName: "wraps-email-tracking",
19779
- DkimSigningAttributes: {
19780
- NextSigningKeyLength: "RSA_2048_BIT"
19781
- }
19782
- })
19837
+ if (domainAlreadyExists) {
19838
+ const { PutEmailIdentityConfigurationSetAttributesCommand } = await import("@aws-sdk/client-sesv2");
19839
+ await progress.execute("Associating tracking configuration", async () => {
19840
+ await sesClient.send(
19841
+ new PutEmailIdentityConfigurationSetAttributesCommand({
19842
+ EmailIdentity: domain,
19843
+ ConfigurationSetName: "wraps-email-tracking"
19844
+ })
19845
+ );
19846
+ });
19847
+ } else {
19848
+ const { CreateEmailIdentityCommand } = await import("@aws-sdk/client-sesv2");
19849
+ await progress.execute("Creating SES identity", async () => {
19850
+ await sesClient.send(
19851
+ new CreateEmailIdentityCommand({
19852
+ EmailIdentity: domain,
19853
+ ConfigurationSetName: "wraps-email-tracking",
19854
+ DkimSigningAttributes: {
19855
+ NextSigningKeyLength: "RSA_2048_BIT"
19856
+ }
19857
+ })
19858
+ );
19859
+ });
19860
+ const sesIdentity = await sesClient.send(
19861
+ new GetEmailIdentityCommand2({ EmailIdentity: domain })
19783
19862
  );
19784
- });
19785
- const sesIdentity = await sesClient.send(
19786
- new GetEmailIdentityCommand2({ EmailIdentity: domain })
19787
- );
19788
- const dkimTokens = sesIdentity.DkimAttributes?.Tokens || [];
19863
+ dkimTokens = sesIdentity.DkimAttributes?.Tokens || [];
19864
+ }
19789
19865
  let mailFromDomain;
19790
19866
  if (options.yes) {
19791
19867
  mailFromDomain = `mail.${domain}`;
@@ -20477,12 +20553,30 @@ async function inboundInit(options) {
20477
20553
  if (!metadata?.services?.email) {
20478
20554
  throw errors.inboundRequiresOutbound();
20479
20555
  }
20480
- const emailService = metadata.services.email;
20481
- const emailConfig = emailService.config;
20482
- const domain = emailConfig.domain;
20483
- if (!domain) {
20556
+ const trackedDomains = getAllTrackedDomains(metadata);
20557
+ if (trackedDomains.length === 0) {
20484
20558
  throw errors.inboundRequiresOutbound();
20485
20559
  }
20560
+ let domain;
20561
+ if (trackedDomains.length === 1) {
20562
+ domain = trackedDomains[0].domain;
20563
+ } else {
20564
+ const selected = await clack22.select({
20565
+ message: "Which domain do you want to receive email on?",
20566
+ options: trackedDomains.map((d) => ({
20567
+ value: d.domain,
20568
+ label: d.domain,
20569
+ hint: d.isPrimary ? "primary" : d.purpose
20570
+ }))
20571
+ });
20572
+ if (clack22.isCancel(selected)) {
20573
+ clack22.cancel("Operation cancelled.");
20574
+ process.exit(0);
20575
+ }
20576
+ domain = selected;
20577
+ }
20578
+ const emailService = metadata.services.email;
20579
+ const emailConfig = emailService.config;
20486
20580
  const subdomain = options.root ? "" : options.subdomain ?? (options.yes ? "inbound" : await promptInboundSubdomain(domain));
20487
20581
  const receivingDomain = subdomain ? `${subdomain}.${domain}` : domain;
20488
20582
  clack22.log.info(`Receiving domain: ${pc24.cyan(receivingDomain)}`);
@@ -21658,6 +21752,21 @@ async function init2(options) {
21658
21752
  if (domain) {
21659
21753
  emailConfig.domain = domain;
21660
21754
  }
21755
+ if (domain && !options.quick && preset !== "custom") {
21756
+ const wantsMailFrom = await clack26.confirm({
21757
+ message: `Configure MAIL FROM for ${pc28.cyan(domain)}? ${pc28.dim("(improves DMARC alignment)")}`,
21758
+ initialValue: true
21759
+ });
21760
+ if (clack26.isCancel(wantsMailFrom)) {
21761
+ clack26.cancel("Operation cancelled.");
21762
+ process.exit(0);
21763
+ }
21764
+ if (wantsMailFrom) {
21765
+ const mailFromFull = await promptMailFromSubdomain(domain);
21766
+ const suffix = `.${domain}`;
21767
+ emailConfig.mailFromSubdomain = mailFromFull.endsWith(suffix) ? mailFromFull.slice(0, -suffix.length) || "mail" : "mail";
21768
+ }
21769
+ }
21661
21770
  let costSummary;
21662
21771
  if (!options.quick) {
21663
21772
  const estimatedVolume = await promptEstimatedVolume();