@wraps.dev/cli 2.14.7 → 2.15.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
@@ -4102,6 +4102,7 @@ var init_route53 = __esm({
4102
4102
  var metadata_exports = {};
4103
4103
  __export(metadata_exports, {
4104
4104
  addDomainToMetadata: () => addDomainToMetadata,
4105
+ addInboundDomainToMetadata: () => addInboundDomainToMetadata,
4105
4106
  addServiceToConnection: () => addServiceToConnection,
4106
4107
  applyConfigUpdates: () => applyConfigUpdates,
4107
4108
  buildEmailStackConfig: () => buildEmailStackConfig,
@@ -4117,7 +4118,9 @@ __export(metadata_exports, {
4117
4118
  hasService: () => hasService,
4118
4119
  listConnections: () => listConnections,
4119
4120
  loadConnectionMetadata: () => loadConnectionMetadata,
4121
+ migrateInboundToMultiDomain: () => migrateInboundToMultiDomain,
4120
4122
  removeDomainFromMetadata: () => removeDomainFromMetadata,
4123
+ removeInboundDomainFromMetadata: () => removeInboundDomainFromMetadata,
4121
4124
  removeServiceFromConnection: () => removeServiceFromConnection,
4122
4125
  saveConnectionMetadata: () => saveConnectionMetadata,
4123
4126
  updateEmailConfig: () => updateEmailConfig,
@@ -4180,6 +4183,9 @@ async function loadConnectionMetadata(accountId, region) {
4180
4183
  }
4181
4184
  localData = data;
4182
4185
  }
4186
+ if (localData && migrateInboundToMultiDomain(localData)) {
4187
+ await saveConnectionMetadataLocal(localData);
4188
+ }
4183
4189
  } catch (error) {
4184
4190
  console.error(
4185
4191
  "Error loading connection metadata:",
@@ -4599,6 +4605,61 @@ function getAllTrackedDomains(metadata) {
4599
4605
  }
4600
4606
  return result;
4601
4607
  }
4608
+ function migrateInboundToMultiDomain(metadata) {
4609
+ const emailConfig = metadata.services.email?.config;
4610
+ if (!emailConfig?.inbound?.enabled) {
4611
+ return false;
4612
+ }
4613
+ if (emailConfig.inboundDomains && emailConfig.inboundDomains.length > 0) {
4614
+ return false;
4615
+ }
4616
+ const inbound = emailConfig.inbound;
4617
+ const receivingDomain = inbound.receivingDomain || (inbound.subdomain && emailConfig.domain ? `${inbound.subdomain}.${emailConfig.domain}` : null);
4618
+ if (!receivingDomain) {
4619
+ return false;
4620
+ }
4621
+ const parentDomain = emailConfig.domain || "";
4622
+ const subdomain = inbound.subdomain || "inbound";
4623
+ emailConfig.inboundDomains = [
4624
+ {
4625
+ subdomain,
4626
+ receivingDomain,
4627
+ parentDomain,
4628
+ addedAt: metadata.services.email?.deployedAt || (/* @__PURE__ */ new Date()).toISOString()
4629
+ }
4630
+ ];
4631
+ return true;
4632
+ }
4633
+ function addInboundDomainToMetadata(metadata, entry) {
4634
+ if (!metadata.services.email) {
4635
+ throw new Error("Email service not configured in metadata");
4636
+ }
4637
+ const config2 = metadata.services.email.config;
4638
+ const existing = config2.inboundDomains ?? [];
4639
+ const idx = existing.findIndex(
4640
+ (d) => d.receivingDomain === entry.receivingDomain
4641
+ );
4642
+ if (idx >= 0) {
4643
+ existing[idx] = entry;
4644
+ } else {
4645
+ existing.push(entry);
4646
+ }
4647
+ config2.inboundDomains = existing;
4648
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
4649
+ }
4650
+ function removeInboundDomainFromMetadata(metadata, receivingDomain) {
4651
+ if (!metadata.services.email) {
4652
+ return;
4653
+ }
4654
+ const config2 = metadata.services.email.config;
4655
+ if (!config2.inboundDomains) {
4656
+ return;
4657
+ }
4658
+ config2.inboundDomains = config2.inboundDomains.filter(
4659
+ (d) => d.receivingDomain !== receivingDomain
4660
+ );
4661
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
4662
+ }
4602
4663
  var init_metadata = __esm({
4603
4664
  "src/utils/shared/metadata.ts"() {
4604
4665
  "use strict";
@@ -5560,19 +5621,19 @@ import { randomBytes as randomBytes2 } from "crypto";
5560
5621
  import { existsSync as existsSync6, mkdirSync } from "fs";
5561
5622
  import { builtinModules } from "module";
5562
5623
  import { tmpdir } from "os";
5563
- import { dirname as dirname2, join as join7 } from "path";
5624
+ import { dirname as dirname3, join as join7 } from "path";
5564
5625
  import { fileURLToPath as fileURLToPath3 } from "url";
5565
5626
  import * as aws8 from "@pulumi/aws";
5566
5627
  import * as pulumi11 from "@pulumi/pulumi";
5567
5628
  import { build } from "esbuild";
5568
5629
  function getPackageRoot() {
5569
5630
  const currentFile = fileURLToPath3(import.meta.url);
5570
- let dir = dirname2(currentFile);
5571
- while (dir !== dirname2(dir)) {
5631
+ let dir = dirname3(currentFile);
5632
+ while (dir !== dirname3(dir)) {
5572
5633
  if (existsSync6(join7(dir, "package.json"))) {
5573
5634
  return dir;
5574
5635
  }
5575
- dir = dirname2(dir);
5636
+ dir = dirname3(dir);
5576
5637
  }
5577
5638
  throw new Error("Could not find package.json");
5578
5639
  }
@@ -5781,14 +5842,18 @@ var init_lambda = __esm({
5781
5842
  var acm_exports = {};
5782
5843
  __export(acm_exports, {
5783
5844
  checkCertificateValidation: () => checkCertificateValidation,
5784
- createACMCertificate: () => createACMCertificate
5845
+ createACMCertificate: () => createACMCertificate,
5846
+ getCertificateValidationRecords: () => getCertificateValidationRecords
5785
5847
  });
5786
- import { ACMClient as ACMClient2, DescribeCertificateCommand as DescribeCertificateCommand2 } from "@aws-sdk/client-acm";
5848
+ import {
5849
+ ACMClient as ACMClient2,
5850
+ DescribeCertificateCommand as DescribeCertificateCommand2,
5851
+ ListCertificatesCommand
5852
+ } from "@aws-sdk/client-acm";
5787
5853
  import * as aws12 from "@pulumi/aws";
5788
5854
  async function checkCertificateValidation(domain) {
5789
5855
  try {
5790
5856
  const acm3 = new ACMClient2({ region: "us-east-1" });
5791
- const { ListCertificatesCommand } = await import("@aws-sdk/client-acm");
5792
5857
  const listResponse = await acm3.send(
5793
5858
  new ListCertificatesCommand({
5794
5859
  CertificateStatuses: ["ISSUED"]
@@ -5864,6 +5929,31 @@ async function createACMCertificate(config2) {
5864
5929
  validationRecords
5865
5930
  };
5866
5931
  }
5932
+ async function getCertificateValidationRecords(domain) {
5933
+ const acm3 = new ACMClient2({ region: "us-east-1" });
5934
+ const listResponse = await acm3.send(
5935
+ new ListCertificatesCommand({
5936
+ CertificateStatuses: ["PENDING_VALIDATION", "ISSUED"]
5937
+ })
5938
+ );
5939
+ const cert = listResponse.CertificateSummaryList?.find(
5940
+ (c) => c.DomainName === domain
5941
+ );
5942
+ if (!cert?.CertificateArn) {
5943
+ return [];
5944
+ }
5945
+ const describeResponse = await acm3.send(
5946
+ new DescribeCertificateCommand2({
5947
+ CertificateArn: cert.CertificateArn
5948
+ })
5949
+ );
5950
+ const options = describeResponse.Certificate?.DomainValidationOptions ?? [];
5951
+ return options.filter((opt) => opt.ResourceRecord?.Name && opt.ResourceRecord?.Value).map((opt) => ({
5952
+ name: opt.ResourceRecord.Name,
5953
+ type: opt.ResourceRecord.Type ?? "CNAME",
5954
+ value: opt.ResourceRecord.Value
5955
+ }));
5956
+ }
5867
5957
  var init_acm = __esm({
5868
5958
  "src/infrastructure/resources/acm.ts"() {
5869
5959
  "use strict";
@@ -8703,7 +8793,7 @@ var init_dynamodb_metrics = __esm({
8703
8793
  // src/cli.ts
8704
8794
  init_esm_shims();
8705
8795
  import { readFileSync as readFileSync3 } from "fs";
8706
- import { dirname as dirname3, join as join19 } from "path";
8796
+ import { dirname as dirname4, join as join19 } from "path";
8707
8797
  import { fileURLToPath as fileURLToPath5 } from "url";
8708
8798
  import * as clack50 from "@clack/prompts";
8709
8799
  import args from "args";
@@ -9902,6 +9992,7 @@ import pc10 from "picocolors";
9902
9992
  init_esm_shims();
9903
9993
  init_errors();
9904
9994
  import { exec } from "child_process";
9995
+ import { dirname as dirname2 } from "path";
9905
9996
  import { promisify } from "util";
9906
9997
  import { PulumiCommand } from "@pulumi/pulumi/automation/index.js";
9907
9998
  var execAsync = promisify(exec);
@@ -9917,7 +10008,9 @@ async function ensurePulumiInstalled() {
9917
10008
  const isInstalled = await checkPulumiInstalled();
9918
10009
  if (!isInstalled) {
9919
10010
  try {
9920
- await PulumiCommand.install();
10011
+ const cmd = await PulumiCommand.install();
10012
+ const binDir = dirname2(cmd.command);
10013
+ process.env.PATH = `${binDir}:${process.env.PATH}`;
9921
10014
  return true;
9922
10015
  } catch (_error) {
9923
10016
  throw errors.pulumiNotInstalled();
@@ -19095,7 +19188,8 @@ import {
19095
19188
  DescribeActiveReceiptRuleSetCommand,
19096
19189
  DescribeReceiptRuleCommand,
19097
19190
  SESClient as SESClient3,
19098
- SetActiveReceiptRuleSetCommand
19191
+ SetActiveReceiptRuleSetCommand,
19192
+ UpdateReceiptRuleCommand
19099
19193
  } from "@aws-sdk/client-ses";
19100
19194
  var RULE_SET_NAME = "wraps-inbound-rules";
19101
19195
  var RULE_NAME = "wraps-inbound-catch-all";
@@ -19218,6 +19312,77 @@ async function deleteReceiptRuleSet(region) {
19218
19312
  throw error;
19219
19313
  }
19220
19314
  }
19315
+ async function addDomainToReceiptRule(region, domain, s3BucketName) {
19316
+ const ses = createSESClient(region);
19317
+ try {
19318
+ const response = await ses.send(
19319
+ new DescribeReceiptRuleCommand({
19320
+ RuleSetName: RULE_SET_NAME,
19321
+ RuleName: RULE_NAME
19322
+ })
19323
+ );
19324
+ const existingRecipients = response.Rule?.Recipients ?? [];
19325
+ if (existingRecipients.includes(domain)) {
19326
+ return;
19327
+ }
19328
+ await ses.send(
19329
+ new UpdateReceiptRuleCommand({
19330
+ RuleSetName: RULE_SET_NAME,
19331
+ Rule: {
19332
+ Name: RULE_NAME,
19333
+ Enabled: response.Rule?.Enabled,
19334
+ ScanEnabled: response.Rule?.ScanEnabled,
19335
+ TlsPolicy: response.Rule?.TlsPolicy,
19336
+ Actions: response.Rule?.Actions,
19337
+ Recipients: [...existingRecipients, domain]
19338
+ }
19339
+ })
19340
+ );
19341
+ } catch (error) {
19342
+ if (error instanceof Error && (error.name === "RuleDoesNotExistException" || error.name === "RuleSetDoesNotExistException")) {
19343
+ await createReceiptRuleSet(region);
19344
+ await createReceiptRule(region, domain, s3BucketName);
19345
+ await setActiveReceiptRuleSet(region, RULE_SET_NAME);
19346
+ return;
19347
+ }
19348
+ throw error;
19349
+ }
19350
+ }
19351
+ async function removeDomainFromReceiptRule(region, domain) {
19352
+ const ses = createSESClient(region);
19353
+ try {
19354
+ const response = await ses.send(
19355
+ new DescribeReceiptRuleCommand({
19356
+ RuleSetName: RULE_SET_NAME,
19357
+ RuleName: RULE_NAME
19358
+ })
19359
+ );
19360
+ const existingRecipients = response.Rule?.Recipients ?? [];
19361
+ const updated = existingRecipients.filter((r) => r !== domain);
19362
+ if (updated.length === 0) {
19363
+ await deleteReceiptRule(region);
19364
+ return;
19365
+ }
19366
+ await ses.send(
19367
+ new UpdateReceiptRuleCommand({
19368
+ RuleSetName: RULE_SET_NAME,
19369
+ Rule: {
19370
+ Name: RULE_NAME,
19371
+ Enabled: response.Rule?.Enabled,
19372
+ ScanEnabled: response.Rule?.ScanEnabled,
19373
+ TlsPolicy: response.Rule?.TlsPolicy,
19374
+ Actions: response.Rule?.Actions,
19375
+ Recipients: updated
19376
+ }
19377
+ })
19378
+ );
19379
+ } catch (error) {
19380
+ if (error instanceof Error && (error.name === "RuleDoesNotExistException" || error.name === "RuleSetDoesNotExistException")) {
19381
+ return;
19382
+ }
19383
+ throw error;
19384
+ }
19385
+ }
19221
19386
 
19222
19387
  // src/commands/email/inbound.ts
19223
19388
  init_aws();
@@ -19296,7 +19461,15 @@ async function inboundInit(options) {
19296
19461
  bucketName: `wraps-inbound-${identity.accountId}-${region}`,
19297
19462
  webhookUrl,
19298
19463
  webhookSecret
19299
- }
19464
+ },
19465
+ inboundDomains: [
19466
+ {
19467
+ subdomain,
19468
+ receivingDomain,
19469
+ parentDomain: domain,
19470
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
19471
+ }
19472
+ ]
19300
19473
  };
19301
19474
  const stackConfig = buildEmailStackConfig(metadata, region, {
19302
19475
  emailConfig: updatedEmailConfig
@@ -19537,7 +19710,8 @@ Deploy first: ${pc21.cyan("wraps email inbound init")}
19537
19710
  const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
19538
19711
  const updatedEmailConfig = {
19539
19712
  ...emailService.config,
19540
- inbound: void 0
19713
+ inbound: void 0,
19714
+ inboundDomains: void 0
19541
19715
  };
19542
19716
  const stackConfig = buildEmailStackConfig(metadata, region, {
19543
19717
  emailConfig: updatedEmailConfig
@@ -19606,13 +19780,18 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19606
19780
  `);
19607
19781
  return;
19608
19782
  }
19609
- const inbound = metadata.services.email.config.inbound;
19783
+ const emailConfig = metadata.services.email.config;
19784
+ const inbound = emailConfig.inbound;
19785
+ const inboundDomains = emailConfig.inboundDomains ?? [];
19610
19786
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19611
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${metadata.services.email.config.domain}`;
19787
+ const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19788
+ inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19789
+ ];
19612
19790
  if (isJsonMode()) {
19613
19791
  jsonSuccess("email.inbound.status", {
19614
19792
  enabled: true,
19615
- receivingDomain,
19793
+ receivingDomains: domainList,
19794
+ receivingDomain: domainList[0],
19616
19795
  bucketName: inbound.bucketName || "",
19617
19796
  region,
19618
19797
  webhookUrl: inbound.webhookUrl || null,
@@ -19624,7 +19803,16 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19624
19803
  console.log();
19625
19804
  console.log(pc21.bold(" Inbound Email Configuration"));
19626
19805
  console.log();
19627
- console.log(` ${pc21.dim("Receiving domain:")} ${pc21.cyan(receivingDomain)}`);
19806
+ if (domainList.length === 1) {
19807
+ console.log(
19808
+ ` ${pc21.dim("Receiving domain:")} ${pc21.cyan(domainList[0])}`
19809
+ );
19810
+ } else {
19811
+ console.log(` ${pc21.dim("Receiving domains:")}`);
19812
+ for (const d of domainList) {
19813
+ console.log(` ${pc21.cyan(d)}`);
19814
+ }
19815
+ }
19628
19816
  console.log(
19629
19817
  ` ${pc21.dim("S3 bucket:")} ${pc21.cyan(inbound.bucketName || "")}`
19630
19818
  );
@@ -19658,61 +19846,77 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19658
19846
  `);
19659
19847
  process.exit(1);
19660
19848
  }
19661
- const inbound = metadata.services.email.config.inbound;
19662
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${metadata.services.email.config.domain}`;
19849
+ const emailConfig = metadata.services.email.config;
19850
+ const inbound = emailConfig.inbound;
19851
+ const inboundDomains = emailConfig.inboundDomains ?? [];
19852
+ const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19853
+ inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19854
+ ];
19663
19855
  let allPassed = true;
19856
+ const domainChecks = {};
19664
19857
  console.log();
19665
- const mxResult = await progress.execute(
19666
- `Checking MX record for ${receivingDomain}`,
19667
- async () => {
19668
- try {
19669
- const records = await dns2.resolveMx(receivingDomain);
19670
- const hasSES = records.some((r) => r.exchange.includes("inbound-smtp"));
19671
- return { found: true, hasSES, records };
19672
- } catch {
19673
- return { found: false, hasSES: false, records: [] };
19674
- }
19858
+ for (const receivingDomain of domainList) {
19859
+ if (domainList.length > 1) {
19860
+ clack20.log.info(pc21.bold(`Checking ${pc21.cyan(receivingDomain)}`));
19675
19861
  }
19676
- );
19677
- if (mxResult.hasSES) {
19678
- clack20.log.success(
19679
- `MX record: ${pc21.green("verified")} \u2192 inbound-smtp.${region}.amazonaws.com`
19680
- );
19681
- } else if (mxResult.found) {
19682
- clack20.log.warn(
19683
- `MX record found but not pointing to SES. Expected: ${pc21.cyan(`10 inbound-smtp.${region}.amazonaws.com`)}`
19684
- );
19685
- allPassed = false;
19686
- } else {
19687
- clack20.log.error(
19688
- `MX record: ${pc21.red("not found")}. Add: ${pc21.cyan(`${receivingDomain} MX 10 inbound-smtp.${region}.amazonaws.com`)}`
19689
- );
19690
- allPassed = false;
19691
- }
19692
- const spfResult = await progress.execute(
19693
- `Checking SPF record for ${receivingDomain}`,
19694
- async () => {
19695
- try {
19696
- const records = await dns2.resolveTxt(receivingDomain);
19697
- const flat = records.map((r) => r.join(""));
19698
- const spf = flat.find((r) => r.startsWith("v=spf1"));
19699
- const hasSES = spf?.includes("amazonses.com") ?? false;
19700
- return { found: !!spf, hasSES, value: spf };
19701
- } catch {
19702
- return { found: false, hasSES: false, value: null };
19862
+ const mxResult = await progress.execute(
19863
+ `Checking MX record for ${receivingDomain}`,
19864
+ async () => {
19865
+ try {
19866
+ const records = await dns2.resolveMx(receivingDomain);
19867
+ const hasSES = records.some(
19868
+ (r) => r.exchange.includes("inbound-smtp")
19869
+ );
19870
+ return { found: true, hasSES, records };
19871
+ } catch {
19872
+ return { found: false, hasSES: false, records: [] };
19873
+ }
19703
19874
  }
19875
+ );
19876
+ if (mxResult.hasSES) {
19877
+ clack20.log.success(
19878
+ `MX record: ${pc21.green("verified")} \u2192 inbound-smtp.${region}.amazonaws.com`
19879
+ );
19880
+ } else if (mxResult.found) {
19881
+ clack20.log.warn(
19882
+ `MX record found but not pointing to SES. Expected: ${pc21.cyan(`10 inbound-smtp.${region}.amazonaws.com`)}`
19883
+ );
19884
+ allPassed = false;
19885
+ } else {
19886
+ clack20.log.error(
19887
+ `MX record: ${pc21.red("not found")}. Add: ${pc21.cyan(`${receivingDomain} MX 10 inbound-smtp.${region}.amazonaws.com`)}`
19888
+ );
19889
+ allPassed = false;
19704
19890
  }
19705
- );
19706
- if (spfResult.hasSES) {
19707
- clack20.log.success(`SPF record: ${pc21.green("verified")}`);
19708
- } else if (spfResult.found) {
19709
- clack20.log.warn("SPF record exists but missing amazonses.com include");
19710
- allPassed = false;
19711
- } else {
19712
- clack20.log.error(
19713
- `SPF record: ${pc21.red("not found")}. Add TXT: ${pc21.cyan("v=spf1 include:amazonses.com ~all")}`
19891
+ const spfResult = await progress.execute(
19892
+ `Checking SPF record for ${receivingDomain}`,
19893
+ async () => {
19894
+ try {
19895
+ const records = await dns2.resolveTxt(receivingDomain);
19896
+ const flat = records.map((r) => r.join(""));
19897
+ const spf = flat.find((r) => r.startsWith("v=spf1"));
19898
+ const hasSES = spf?.includes("amazonses.com") ?? false;
19899
+ return { found: !!spf, hasSES, value: spf };
19900
+ } catch {
19901
+ return { found: false, hasSES: false, value: null };
19902
+ }
19903
+ }
19714
19904
  );
19715
- allPassed = false;
19905
+ if (spfResult.hasSES) {
19906
+ clack20.log.success(`SPF record: ${pc21.green("verified")}`);
19907
+ } else if (spfResult.found) {
19908
+ clack20.log.warn("SPF record exists but missing amazonses.com include");
19909
+ allPassed = false;
19910
+ } else {
19911
+ clack20.log.error(
19912
+ `SPF record: ${pc21.red("not found")}. Add TXT: ${pc21.cyan("v=spf1 include:amazonses.com ~all")}`
19913
+ );
19914
+ allPassed = false;
19915
+ }
19916
+ domainChecks[receivingDomain] = {
19917
+ mx: { found: mxResult.found, verified: mxResult.hasSES },
19918
+ spf: { found: spfResult.found, verified: spfResult.hasSES }
19919
+ };
19716
19920
  }
19717
19921
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19718
19922
  if (activeRuleSet === RULE_SET_NAME) {
@@ -19725,13 +19929,11 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19725
19929
  }
19726
19930
  if (isJsonMode()) {
19727
19931
  jsonSuccess("email.inbound.verify", {
19728
- receivingDomain,
19932
+ receivingDomains: domainList,
19933
+ receivingDomain: domainList[0],
19729
19934
  allPassed,
19730
- checks: {
19731
- mx: { found: mxResult.found, verified: mxResult.hasSES },
19732
- spf: { found: spfResult.found, verified: spfResult.hasSES },
19733
- receiptRuleSet: { active: activeRuleSet === RULE_SET_NAME }
19734
- }
19935
+ domainChecks,
19936
+ receiptRuleSet: { active: activeRuleSet === RULE_SET_NAME }
19735
19937
  });
19736
19938
  return;
19737
19939
  }
@@ -19888,6 +20090,304 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19888
20090
  clack20.log.success(pc21.bold("Inbound email is working correctly!"));
19889
20091
  console.log();
19890
20092
  }
20093
+ async function inboundAdd(options) {
20094
+ if (!isJsonMode()) {
20095
+ clack20.intro(pc21.bold("Add Inbound Receiving Domain"));
20096
+ }
20097
+ const progress = new DeploymentProgress();
20098
+ const identity = await progress.execute(
20099
+ "Validating AWS credentials",
20100
+ async () => validateAWSCredentials()
20101
+ );
20102
+ const region = options.region || await getAWSRegion();
20103
+ if (!SES_RECEIVING_REGIONS.includes(
20104
+ region
20105
+ )) {
20106
+ throw errors.inboundRegionNotSupported(region);
20107
+ }
20108
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
20109
+ if (!metadata?.services?.email?.config?.inbound?.enabled) {
20110
+ clack20.log.error("Inbound email infrastructure is not deployed.");
20111
+ console.log(
20112
+ `
20113
+ Deploy first: ${pc21.cyan("wraps email inbound init")}
20114
+ `
20115
+ );
20116
+ process.exit(1);
20117
+ }
20118
+ const emailConfig = metadata.services.email.config;
20119
+ const primaryDomain = emailConfig.domain || "";
20120
+ const allDomains = [primaryDomain];
20121
+ for (const d of emailConfig.additionalDomains ?? []) {
20122
+ if (!allDomains.includes(d.domain)) {
20123
+ allDomains.push(d.domain);
20124
+ }
20125
+ }
20126
+ let parentDomain = options.domain;
20127
+ if (!parentDomain) {
20128
+ if (options.yes) {
20129
+ parentDomain = primaryDomain;
20130
+ } else if (allDomains.length === 1) {
20131
+ parentDomain = allDomains[0];
20132
+ } else {
20133
+ const selected = await clack20.select({
20134
+ message: "Which domain should the inbound subdomain be under?",
20135
+ options: allDomains.map((d) => ({
20136
+ value: d,
20137
+ label: d,
20138
+ hint: d === primaryDomain ? "primary" : void 0
20139
+ }))
20140
+ });
20141
+ if (clack20.isCancel(selected)) {
20142
+ clack20.cancel("Operation cancelled.");
20143
+ process.exit(0);
20144
+ }
20145
+ parentDomain = selected;
20146
+ }
20147
+ }
20148
+ const subdomain = options.subdomain || (options.yes ? "inbound" : await promptInboundSubdomain(parentDomain));
20149
+ const receivingDomain = `${subdomain}.${parentDomain}`;
20150
+ const existingDomains = emailConfig.inboundDomains ?? [];
20151
+ if (existingDomains.some((d) => d.receivingDomain === receivingDomain)) {
20152
+ clack20.log.warn(
20153
+ `${pc21.cyan(receivingDomain)} is already configured as an inbound domain.`
20154
+ );
20155
+ return;
20156
+ }
20157
+ clack20.log.info(`Adding receiving domain: ${pc21.cyan(receivingDomain)}`);
20158
+ const bucketName = emailConfig.inbound?.bucketName || `wraps-inbound-${identity.accountId}-${region}`;
20159
+ await progress.execute("Updating SES receipt rule", async () => {
20160
+ await addDomainToReceiptRule(region, receivingDomain, bucketName);
20161
+ });
20162
+ let dnsAutoCreated = false;
20163
+ const {
20164
+ detectAvailableDNSProviders: detectAvailableDNSProviders2,
20165
+ getDNSCredentials: getDNSCredentials2,
20166
+ createInboundDNSRecordsForProvider: createInboundDNSRecordsForProvider2,
20167
+ getDNSProviderDisplayName: getDNSProviderDisplayName2,
20168
+ getDNSProviderTokenUrl: getDNSProviderTokenUrl2,
20169
+ buildInboundDNSRecords: buildRecords,
20170
+ formatDNSRecordsForDisplay: formatRecords
20171
+ } = await Promise.resolve().then(() => (init_dns(), dns_exports));
20172
+ const { promptDNSProvider: promptDNSProvider2, promptContinueManualDNS: promptContinueManualDNS2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
20173
+ const existingDnsProvider = metadata.services.email.dnsProvider;
20174
+ let dnsProvider = existingDnsProvider;
20175
+ if (!dnsProvider || dnsProvider === "manual") {
20176
+ progress.start("Detecting DNS providers");
20177
+ const availableProviders = await detectAvailableDNSProviders2(
20178
+ parentDomain,
20179
+ region
20180
+ );
20181
+ progress.stop();
20182
+ dnsProvider = options.yes ? "manual" : await promptDNSProvider2(parentDomain, availableProviders);
20183
+ }
20184
+ if (dnsProvider !== "manual") {
20185
+ progress.start(
20186
+ `Validating ${getDNSProviderDisplayName2(dnsProvider)} credentials`
20187
+ );
20188
+ const credentialResult = await getDNSCredentials2(
20189
+ dnsProvider,
20190
+ parentDomain,
20191
+ region
20192
+ );
20193
+ progress.stop();
20194
+ if (credentialResult.valid && credentialResult.credentials) {
20195
+ const records = buildRecords(receivingDomain, region);
20196
+ clack20.log.info(pc21.bold("DNS records to create:"));
20197
+ for (const record of records) {
20198
+ const value = record.priority ? `${record.priority} ${record.value}` : record.value;
20199
+ clack20.log.info(pc21.dim(` ${record.type} ${record.name} \u2192 ${value}`));
20200
+ }
20201
+ progress.start(
20202
+ `Creating DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
20203
+ );
20204
+ const result = await createInboundDNSRecordsForProvider2(
20205
+ credentialResult.credentials,
20206
+ receivingDomain,
20207
+ region,
20208
+ parentDomain
20209
+ );
20210
+ if (result.success && result.recordsCreated > 0) {
20211
+ progress.succeed(
20212
+ `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
20213
+ );
20214
+ dnsAutoCreated = true;
20215
+ } else {
20216
+ progress.fail("Failed to create some DNS records");
20217
+ if (result.errors) {
20218
+ for (const err of result.errors) {
20219
+ clack20.log.warn(err);
20220
+ }
20221
+ }
20222
+ }
20223
+ } else {
20224
+ clack20.log.warn(
20225
+ credentialResult.error || "Could not validate credentials"
20226
+ );
20227
+ if (dnsProvider === "vercel" || dnsProvider === "cloudflare") {
20228
+ clack20.log.info(
20229
+ `Set the ${dnsProvider === "vercel" ? "VERCEL_TOKEN" : "CLOUDFLARE_API_TOKEN"} environment variable to enable automatic DNS management.`
20230
+ );
20231
+ clack20.log.info(
20232
+ `You can create a token at: ${pc21.cyan(getDNSProviderTokenUrl2(dnsProvider))}`
20233
+ );
20234
+ }
20235
+ if (!options.yes) {
20236
+ const continueManual = await promptContinueManualDNS2();
20237
+ if (continueManual) {
20238
+ dnsProvider = "manual";
20239
+ }
20240
+ }
20241
+ }
20242
+ }
20243
+ if (!dnsAutoCreated) {
20244
+ const dnsRecords = buildRecords(receivingDomain, region);
20245
+ const formattedRecords = formatRecords(dnsRecords);
20246
+ console.log();
20247
+ clack20.log.info(pc21.bold("DNS Records Required"));
20248
+ console.log(
20249
+ ` Add these records to your DNS provider for ${pc21.cyan(receivingDomain)}:
20250
+ `
20251
+ );
20252
+ for (const record of formattedRecords) {
20253
+ console.log(` ${pc21.dim(record.type.padEnd(6))} ${record.name}`);
20254
+ console.log(` ${pc21.dim("\u2192")} ${pc21.green(record.value)}
20255
+ `);
20256
+ }
20257
+ }
20258
+ await progress.execute("Saving configuration", async () => {
20259
+ addInboundDomainToMetadata(metadata, {
20260
+ subdomain,
20261
+ receivingDomain,
20262
+ parentDomain,
20263
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
20264
+ });
20265
+ await saveConnectionMetadata(metadata);
20266
+ });
20267
+ if (isJsonMode()) {
20268
+ jsonSuccess("email.inbound.add", {
20269
+ receivingDomain,
20270
+ subdomain,
20271
+ parentDomain,
20272
+ dnsAutoCreated,
20273
+ region
20274
+ });
20275
+ return;
20276
+ }
20277
+ console.log();
20278
+ clack20.log.success(
20279
+ `${pc21.bold("Added inbound domain:")} ${pc21.cyan(receivingDomain)}`
20280
+ );
20281
+ console.log();
20282
+ if (!dnsAutoCreated) {
20283
+ console.log(
20284
+ ` ${pc21.dim("1.")} Add DNS records above to your DNS provider`
20285
+ );
20286
+ console.log(
20287
+ ` ${pc21.dim("2.")} Verify: ${pc21.cyan("wraps email inbound verify")}`
20288
+ );
20289
+ } else {
20290
+ console.log(
20291
+ ` Verify: ${pc21.cyan("wraps email inbound verify")}`
20292
+ );
20293
+ }
20294
+ console.log();
20295
+ }
20296
+ async function inboundRemove(options) {
20297
+ if (!isJsonMode()) {
20298
+ clack20.intro(pc21.bold("Remove Inbound Receiving Domain"));
20299
+ }
20300
+ const progress = new DeploymentProgress();
20301
+ const identity = await progress.execute(
20302
+ "Validating AWS credentials",
20303
+ async () => validateAWSCredentials()
20304
+ );
20305
+ const region = options.region || await getAWSRegion();
20306
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
20307
+ if (!metadata?.services?.email?.config?.inbound?.enabled) {
20308
+ clack20.log.error("Inbound email infrastructure is not deployed.");
20309
+ console.log(
20310
+ `
20311
+ Deploy first: ${pc21.cyan("wraps email inbound init")}
20312
+ `
20313
+ );
20314
+ process.exit(1);
20315
+ }
20316
+ const emailConfig = metadata.services.email.config;
20317
+ const inboundDomains = emailConfig.inboundDomains ?? [];
20318
+ if (inboundDomains.length === 0) {
20319
+ clack20.log.warn("No inbound domains configured.");
20320
+ return;
20321
+ }
20322
+ let domainToRemove = options.domain;
20323
+ if (!domainToRemove) {
20324
+ if (inboundDomains.length === 1) {
20325
+ domainToRemove = inboundDomains[0].receivingDomain;
20326
+ } else {
20327
+ const selected = await clack20.select({
20328
+ message: "Which inbound domain do you want to remove?",
20329
+ options: inboundDomains.map((d) => ({
20330
+ value: d.receivingDomain,
20331
+ label: d.receivingDomain,
20332
+ hint: `added ${d.addedAt.split("T")[0]}`
20333
+ }))
20334
+ });
20335
+ if (clack20.isCancel(selected)) {
20336
+ clack20.cancel("Operation cancelled.");
20337
+ process.exit(0);
20338
+ }
20339
+ domainToRemove = selected;
20340
+ }
20341
+ }
20342
+ if (!inboundDomains.some((d) => d.receivingDomain === domainToRemove)) {
20343
+ clack20.log.error(
20344
+ `${pc21.cyan(domainToRemove)} is not in the inbound domains list.`
20345
+ );
20346
+ return;
20347
+ }
20348
+ if (inboundDomains.length === 1) {
20349
+ clack20.log.error(
20350
+ "Cannot remove the last inbound domain. Use " + pc21.cyan("wraps email inbound destroy") + " to remove all inbound infrastructure."
20351
+ );
20352
+ return;
20353
+ }
20354
+ if (!options.yes) {
20355
+ const confirmed = await clack20.confirm({
20356
+ message: `Remove inbound domain ${pc21.cyan(domainToRemove)}?`,
20357
+ initialValue: false
20358
+ });
20359
+ if (clack20.isCancel(confirmed) || !confirmed) {
20360
+ clack20.cancel("Operation cancelled.");
20361
+ process.exit(0);
20362
+ }
20363
+ }
20364
+ await progress.execute("Updating SES receipt rule", async () => {
20365
+ await removeDomainFromReceiptRule(region, domainToRemove);
20366
+ });
20367
+ await progress.execute("Saving configuration", async () => {
20368
+ removeInboundDomainFromMetadata(metadata, domainToRemove);
20369
+ await saveConnectionMetadata(metadata);
20370
+ });
20371
+ if (isJsonMode()) {
20372
+ jsonSuccess("email.inbound.remove", {
20373
+ removedDomain: domainToRemove,
20374
+ remainingDomains: (emailConfig.inboundDomains ?? []).map(
20375
+ (d) => d.receivingDomain
20376
+ ),
20377
+ region
20378
+ });
20379
+ return;
20380
+ }
20381
+ console.log();
20382
+ clack20.log.success(
20383
+ `${pc21.bold("Removed inbound domain:")} ${pc21.cyan(domainToRemove)}`
20384
+ );
20385
+ console.log();
20386
+ console.log(
20387
+ ` ${pc21.dim("Remember to remove the MX and SPF DNS records for")} ${pc21.cyan(domainToRemove)}`
20388
+ );
20389
+ console.log();
20390
+ }
19891
20391
 
19892
20392
  // src/commands/email/init.ts
19893
20393
  init_esm_shims();
@@ -21870,7 +22370,7 @@ async function findCliNodeModules() {
21870
22370
  const paths = [];
21871
22371
  try {
21872
22372
  const { createRequire } = await import("module");
21873
- const { dirname: dirname4 } = await import("path");
22373
+ const { dirname: dirname5 } = await import("path");
21874
22374
  for (const base of [
21875
22375
  // The current file's location (works when running from source)
21876
22376
  import.meta.url,
@@ -21880,7 +22380,7 @@ async function findCliNodeModules() {
21880
22380
  try {
21881
22381
  const req = createRequire(base);
21882
22382
  const reactPkg = req.resolve("react/package.json");
21883
- const reactNodeModules = join10(dirname4(reactPkg), "..");
22383
+ const reactNodeModules = join10(dirname5(reactPkg), "..");
21884
22384
  if (existsSync9(join10(reactNodeModules, "react"))) {
21885
22385
  paths.push(reactNodeModules);
21886
22386
  break;
@@ -24458,6 +24958,13 @@ ${pc30.bold("Add these DNS records to your DNS provider:")}
24458
24958
  if (outputs.httpsTrackingEnabled && outputs.acmCertificateValidationRecords) {
24459
24959
  acmValidationRecords.push(...outputs.acmCertificateValidationRecords);
24460
24960
  }
24961
+ if (acmValidationRecords.length === 0 && outputs.httpsTrackingPending && outputs.customTrackingDomain) {
24962
+ const { getCertificateValidationRecords: getCertificateValidationRecords2 } = await Promise.resolve().then(() => (init_acm(), acm_exports));
24963
+ const directRecords = await getCertificateValidationRecords2(
24964
+ outputs.customTrackingDomain
24965
+ );
24966
+ acmValidationRecords.push(...directRecords);
24967
+ }
24461
24968
  let acmDnsAutoCreated = false;
24462
24969
  if (outputs.httpsTrackingPending && acmValidationRecords.length > 0 && outputs.customTrackingDomain) {
24463
24970
  const trackingDnsProvider = metadata.services.email?.dnsProvider;
@@ -27137,7 +27644,7 @@ import {
27137
27644
  IAMClient as IAMClient2,
27138
27645
  PutRolePolicyCommand
27139
27646
  } from "@aws-sdk/client-iam";
27140
- import { confirm as confirm14, intro as intro32, isCancel as isCancel20, log as log32, outro as outro19, select as select14 } from "@clack/prompts";
27647
+ import { confirm as confirm14, intro as intro32, isCancel as isCancel20, log as log32, outro as outro19, select as select15 } from "@clack/prompts";
27141
27648
  import * as pulumi21 from "@pulumi/pulumi";
27142
27649
  import pc36 from "picocolors";
27143
27650
  init_events();
@@ -27480,7 +27987,7 @@ async function resolveOrganization() {
27480
27987
  if (orgs.length === 1) {
27481
27988
  return orgs[0];
27482
27989
  }
27483
- const selected = await select14({
27990
+ const selected = await select15({
27484
27991
  message: "Which organization should this AWS account connect to?",
27485
27992
  options: orgs.map((org) => ({
27486
27993
  value: org.id,
@@ -27776,7 +28283,7 @@ Run ${pc36.cyan("wraps email init")} or ${pc36.cyan("wraps sms init")} first.
27776
28283
  log32.info(
27777
28284
  `Already connected to Wraps Platform (AWS Account: ${pc36.cyan(metadata.accountId)})`
27778
28285
  );
27779
- const action = await select14({
28286
+ const action = await select15({
27780
28287
  message: "What would you like to do?",
27781
28288
  options: [
27782
28289
  {
@@ -35746,7 +36253,7 @@ if (nodeMajorVersion < 20) {
35746
36253
  process.exit(1);
35747
36254
  }
35748
36255
  var __filename2 = fileURLToPath5(import.meta.url);
35749
- var __dirname3 = dirname3(__filename2);
36256
+ var __dirname3 = dirname4(__filename2);
35750
36257
  var packageJson = JSON.parse(
35751
36258
  readFileSync3(join19(__dirname3, "../package.json"), "utf-8")
35752
36259
  );
@@ -36136,6 +36643,11 @@ args.options([
36136
36643
  name: "org",
36137
36644
  description: "Organization slug",
36138
36645
  defaultValue: void 0
36646
+ },
36647
+ {
36648
+ name: "subdomain",
36649
+ description: "Subdomain for inbound email (e.g., inbound, support)",
36650
+ defaultValue: void 0
36139
36651
  }
36140
36652
  ]);
36141
36653
  var flags = args.parse(process.argv);
@@ -36450,13 +36962,30 @@ Usage: ${pc53.cyan("wraps email verify --domain yourapp.com")}
36450
36962
  json: flags.json
36451
36963
  });
36452
36964
  break;
36965
+ case "add":
36966
+ await inboundAdd({
36967
+ region: flags.region,
36968
+ subdomain: flags.subdomain,
36969
+ domain: flags.domain,
36970
+ yes: flags.yes,
36971
+ json: flags.json
36972
+ });
36973
+ break;
36974
+ case "remove":
36975
+ await inboundRemove({
36976
+ region: flags.region,
36977
+ domain: flags.domain,
36978
+ yes: flags.yes,
36979
+ json: flags.json
36980
+ });
36981
+ break;
36453
36982
  default:
36454
36983
  clack50.log.error(
36455
36984
  `Unknown inbound command: ${inboundSubCommand || "(none)"}`
36456
36985
  );
36457
36986
  console.log(
36458
36987
  `
36459
- Available commands: ${pc53.cyan("init")}, ${pc53.cyan("destroy")}, ${pc53.cyan("status")}, ${pc53.cyan("verify")}, ${pc53.cyan("test")}
36988
+ Available commands: ${pc53.cyan("init")}, ${pc53.cyan("destroy")}, ${pc53.cyan("status")}, ${pc53.cyan("verify")}, ${pc53.cyan("test")}, ${pc53.cyan("add")}, ${pc53.cyan("remove")}
36460
36989
  `
36461
36990
  );
36462
36991
  throw new Error(