@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 domainLine = ` ${d.domain} ${statusColor(`${statusIcon} ${d.status}`)}`;
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(`Adding domain ${options.domain} to SES`));
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: options.domain })
16518
+ new GetEmailIdentityCommand2({ EmailIdentity: domain })
16266
16519
  );
16267
16520
  progress.stop();
16268
- clack16.log.warn(`Domain ${options.domain} already exists in SES`);
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 ${options.domain}`)} to check verification status.
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("Adding domain to SES", async () => {
16538
+ await progress.execute("Creating SES identity", async () => {
16282
16539
  await sesClient.send(
16283
16540
  new CreateEmailIdentityCommand({
16284
- EmailIdentity: options.domain,
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 identity = await sesClient.send(
16292
- new GetEmailIdentityCommand2({ EmailIdentity: options.domain })
16549
+ const sesIdentity = await sesClient.send(
16550
+ new GetEmailIdentityCommand2({ EmailIdentity: domain })
16293
16551
  );
16294
- const dkimTokens = identity.DkimAttributes?.Tokens || [];
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 ${options.domain} added successfully!`));
16297
- console.log(`
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
- ` ${pc17.dim("Type:")} CNAME ${pc17.dim("Value:")} ${token}.dkim.amazonses.com
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
- `2. Verify DNS propagation: ${pc17.cyan(`wraps email domains verify --domain ${options.domain}`)}`
16669
+ ` Verify: ${pc17.cyan(`wraps email domains verify --domain ${domain}`)}`
16310
16670
  );
16311
- console.log(`3. Check status: ${pc17.cyan("wraps email status")}
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 domains = identities.filter(
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 (domains.length === 0) {
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 <domain>")} to add a domain.
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
- domains.map(async (domain) => {
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 domainLines = domainDetails.map((domain) => {
16377
- const statusIcon = domain.verified ? pc17.green("\u2713") : pc17.yellow("\u23F1");
16378
- const dkimIcon = domain.dkimStatus === "SUCCESS" ? pc17.green("\u2713") : pc17.yellow("\u23F1");
16379
- return ` ${statusIcon} ${pc17.bold(domain.name)} DKIM: ${dkimIcon} ${domain.dkimStatus}`;
16380
- });
16381
- clack16.note(
16382
- domainLines.join("\n"),
16383
- `${domains.length} domain(s) in ${region}`
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: domains.length
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 identity2 = await sesv2Client.send(
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: identity2.DkimAttributes?.Tokens || [],
18171
- mailFromDomain: identity2.MailFromAttributes?.MailFromDomain,
18172
- mailFromStatus: identity2.MailFromAttributes?.MailFromDomainStatus
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("# Dashboard Commands:");
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
- if (!flags.domain) {
28536
- clack38.log.error("--domain flag is required");
28537
- console.log(
28538
- `
28539
- Usage: ${pc41.cyan("wraps email domains add --domain yourapp.com")}
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":