@wraps.dev/cli 1.1.3 → 1.1.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
@@ -77,6 +77,7 @@ async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, custom
77
77
  ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
78
78
  }
79
79
  });
80
+ const dmarcReportEmail = mailFromDomain ? `postmaster@${mailFromDomain}` : `postmaster@${domain}`;
80
81
  changes.push({
81
82
  Action: "UPSERT",
82
83
  ResourceRecordSet: {
@@ -84,7 +85,7 @@ async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, custom
84
85
  Type: "TXT",
85
86
  TTL: 1800,
86
87
  ResourceRecords: [
87
- { Value: `"v=DMARC1; p=quarantine; rua=mailto:postmaster@${domain}"` }
88
+ { Value: `"v=DMARC1; p=quarantine; rua=mailto:${dmarcReportEmail}"` }
88
89
  ]
89
90
  }
90
91
  });
@@ -755,6 +756,312 @@ var init_aws = __esm({
755
756
  }
756
757
  });
757
758
 
759
+ // src/utils/shared/fs.ts
760
+ import { existsSync as existsSync2 } from "fs";
761
+ import { mkdir } from "fs/promises";
762
+ import { homedir } from "os";
763
+ import { join as join2 } from "path";
764
+ function getWrapsDir() {
765
+ return join2(homedir(), ".wraps");
766
+ }
767
+ function getPulumiWorkDir() {
768
+ return join2(getWrapsDir(), "pulumi");
769
+ }
770
+ async function ensureWrapsDir() {
771
+ const wrapsDir = getWrapsDir();
772
+ if (!existsSync2(wrapsDir)) {
773
+ await mkdir(wrapsDir, { recursive: true });
774
+ }
775
+ }
776
+ async function ensurePulumiWorkDir() {
777
+ await ensureWrapsDir();
778
+ const pulumiDir = getPulumiWorkDir();
779
+ if (!existsSync2(pulumiDir)) {
780
+ await mkdir(pulumiDir, { recursive: true });
781
+ }
782
+ process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
783
+ process.env.PULUMI_CONFIG_PASSPHRASE = "";
784
+ }
785
+ var init_fs = __esm({
786
+ "src/utils/shared/fs.ts"() {
787
+ "use strict";
788
+ init_esm_shims();
789
+ }
790
+ });
791
+
792
+ // src/utils/shared/metadata.ts
793
+ var metadata_exports = {};
794
+ __export(metadata_exports, {
795
+ addServiceToConnection: () => addServiceToConnection,
796
+ applyConfigUpdates: () => applyConfigUpdates,
797
+ connectionExists: () => connectionExists,
798
+ createConnectionMetadata: () => createConnectionMetadata,
799
+ deleteConnectionMetadata: () => deleteConnectionMetadata,
800
+ getConfiguredServices: () => getConfiguredServices,
801
+ hasService: () => hasService,
802
+ listConnections: () => listConnections,
803
+ loadConnectionMetadata: () => loadConnectionMetadata,
804
+ removeServiceFromConnection: () => removeServiceFromConnection,
805
+ saveConnectionMetadata: () => saveConnectionMetadata,
806
+ updateEmailConfig: () => updateEmailConfig,
807
+ updateServiceConfig: () => updateServiceConfig
808
+ });
809
+ import { existsSync as existsSync3 } from "fs";
810
+ import { readFile, writeFile } from "fs/promises";
811
+ import { join as join3 } from "path";
812
+ function getConnectionsDir() {
813
+ return join3(getWrapsDir(), "connections");
814
+ }
815
+ function getMetadataPath(accountId, region) {
816
+ return join3(getConnectionsDir(), `${accountId}-${region}.json`);
817
+ }
818
+ async function ensureConnectionsDir() {
819
+ await ensureWrapsDir();
820
+ const connectionsDir = getConnectionsDir();
821
+ if (!existsSync3(connectionsDir)) {
822
+ const { mkdir: mkdir2 } = await import("fs/promises");
823
+ await mkdir2(connectionsDir, { recursive: true });
824
+ }
825
+ }
826
+ function migrateLegacyMetadata(legacy) {
827
+ return {
828
+ version: "1.0.0",
829
+ accountId: legacy.accountId,
830
+ region: legacy.region,
831
+ provider: legacy.provider,
832
+ timestamp: legacy.timestamp,
833
+ vercel: legacy.vercel,
834
+ services: {
835
+ email: {
836
+ preset: legacy.preset,
837
+ config: legacy.emailConfig,
838
+ pulumiStackName: legacy.pulumiStackName,
839
+ deployedAt: legacy.timestamp
840
+ }
841
+ }
842
+ };
843
+ }
844
+ function isLegacyMetadata(data) {
845
+ return "emailConfig" in data && !("services" in data) && typeof data.emailConfig === "object";
846
+ }
847
+ async function loadConnectionMetadata(accountId, region) {
848
+ const metadataPath = getMetadataPath(accountId, region);
849
+ if (!existsSync3(metadataPath)) {
850
+ return null;
851
+ }
852
+ try {
853
+ const content = await readFile(metadataPath, "utf-8");
854
+ const data = JSON.parse(content);
855
+ if (isLegacyMetadata(data)) {
856
+ const migrated = migrateLegacyMetadata(data);
857
+ await saveConnectionMetadata(migrated);
858
+ return migrated;
859
+ }
860
+ if (!data.version) {
861
+ data.version = "1.0.0";
862
+ await saveConnectionMetadata(data);
863
+ }
864
+ return data;
865
+ } catch (error) {
866
+ console.error("Error loading connection metadata:", error.message);
867
+ return null;
868
+ }
869
+ }
870
+ async function saveConnectionMetadata(metadata) {
871
+ await ensureConnectionsDir();
872
+ const metadataPath = getMetadataPath(metadata.accountId, metadata.region);
873
+ try {
874
+ const content = JSON.stringify(metadata, null, 2);
875
+ await writeFile(metadataPath, content, "utf-8");
876
+ } catch (error) {
877
+ console.error("Error saving connection metadata:", error.message);
878
+ throw error;
879
+ }
880
+ }
881
+ async function deleteConnectionMetadata(accountId, region) {
882
+ const metadataPath = getMetadataPath(accountId, region);
883
+ if (existsSync3(metadataPath)) {
884
+ const { unlink } = await import("fs/promises");
885
+ await unlink(metadataPath);
886
+ }
887
+ }
888
+ async function listConnections() {
889
+ const connectionsDir = getConnectionsDir();
890
+ if (!existsSync3(connectionsDir)) {
891
+ return [];
892
+ }
893
+ try {
894
+ const { readdir } = await import("fs/promises");
895
+ const files = await readdir(connectionsDir);
896
+ const connections = [];
897
+ for (const file of files) {
898
+ if (file.endsWith(".json")) {
899
+ const content = await readFile(join3(connectionsDir, file), "utf-8");
900
+ try {
901
+ const metadata = JSON.parse(content);
902
+ connections.push(metadata);
903
+ } catch (error) {
904
+ console.error(`Error parsing ${file}:`, error);
905
+ }
906
+ }
907
+ }
908
+ return connections;
909
+ } catch (error) {
910
+ console.error("Error listing connections:", error.message);
911
+ return [];
912
+ }
913
+ }
914
+ async function connectionExists(accountId, region) {
915
+ const metadataPath = getMetadataPath(accountId, region);
916
+ return existsSync3(metadataPath);
917
+ }
918
+ function createConnectionMetadata(accountId, region, provider, emailConfig, preset) {
919
+ return {
920
+ version: "1.0.0",
921
+ accountId,
922
+ region,
923
+ provider,
924
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
925
+ services: {
926
+ email: {
927
+ preset,
928
+ config: emailConfig,
929
+ deployedAt: (/* @__PURE__ */ new Date()).toISOString()
930
+ }
931
+ }
932
+ };
933
+ }
934
+ function applyConfigUpdates(existingConfig, updates) {
935
+ const result = { ...existingConfig };
936
+ for (const [key, value] of Object.entries(updates)) {
937
+ if (value === void 0) continue;
938
+ if (key === "tracking" && typeof value === "object") {
939
+ const trackingUpdate = value;
940
+ result.tracking = {
941
+ ...result.tracking,
942
+ ...trackingUpdate,
943
+ // Always preserve these if they exist in original
944
+ customRedirectDomain: result.tracking?.customRedirectDomain || trackingUpdate.customRedirectDomain,
945
+ httpsEnabled: result.tracking?.httpsEnabled ?? trackingUpdate.httpsEnabled
946
+ };
947
+ } else if (key === "eventTracking" && typeof value === "object") {
948
+ result.eventTracking = {
949
+ ...result.eventTracking,
950
+ ...value
951
+ };
952
+ } else if (key === "suppressionList" && typeof value === "object") {
953
+ result.suppressionList = {
954
+ ...result.suppressionList,
955
+ ...value
956
+ };
957
+ } else if (key === "emailArchiving" && typeof value === "object") {
958
+ result.emailArchiving = {
959
+ ...result.emailArchiving,
960
+ ...value
961
+ };
962
+ } else {
963
+ result[key] = value;
964
+ }
965
+ }
966
+ return result;
967
+ }
968
+ function updateEmailConfig(metadata, emailConfig) {
969
+ if (!metadata.services.email) {
970
+ throw new Error("Email service not configured in metadata");
971
+ }
972
+ metadata.services.email.config = applyConfigUpdates(
973
+ metadata.services.email.config,
974
+ emailConfig
975
+ );
976
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
977
+ }
978
+ function addServiceToConnection(accountId, region, provider, service, config2, preset, existingMetadata) {
979
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
980
+ if (existingMetadata) {
981
+ if (service === "email") {
982
+ existingMetadata.services.email = {
983
+ preset,
984
+ config: config2,
985
+ deployedAt: timestamp
986
+ };
987
+ } else if (service === "sms") {
988
+ existingMetadata.services.sms = {
989
+ preset,
990
+ config: config2,
991
+ deployedAt: timestamp
992
+ };
993
+ }
994
+ existingMetadata.timestamp = timestamp;
995
+ return existingMetadata;
996
+ }
997
+ const metadata = {
998
+ version: "1.0.0",
999
+ accountId,
1000
+ region,
1001
+ provider,
1002
+ timestamp,
1003
+ services: {}
1004
+ };
1005
+ if (service === "email") {
1006
+ metadata.services.email = {
1007
+ preset,
1008
+ config: config2,
1009
+ deployedAt: timestamp
1010
+ };
1011
+ } else if (service === "sms") {
1012
+ metadata.services.sms = {
1013
+ preset,
1014
+ config: config2,
1015
+ deployedAt: timestamp
1016
+ };
1017
+ }
1018
+ return metadata;
1019
+ }
1020
+ function updateServiceConfig(metadata, service, config2) {
1021
+ if (service === "email" && metadata.services.email) {
1022
+ metadata.services.email.config = {
1023
+ ...metadata.services.email.config,
1024
+ ...config2
1025
+ };
1026
+ } else if (service === "sms" && metadata.services.sms) {
1027
+ metadata.services.sms.config = {
1028
+ ...metadata.services.sms.config,
1029
+ ...config2
1030
+ };
1031
+ } else {
1032
+ throw new Error(`${service} service not configured in metadata`);
1033
+ }
1034
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
1035
+ }
1036
+ function removeServiceFromConnection(metadata, service) {
1037
+ if (service === "email") {
1038
+ const { email, ...rest } = metadata.services;
1039
+ metadata.services = rest;
1040
+ } else if (service === "sms") {
1041
+ const { sms, ...rest } = metadata.services;
1042
+ metadata.services = rest;
1043
+ }
1044
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
1045
+ }
1046
+ function hasService(metadata, service) {
1047
+ if (service === "email") return metadata.services.email !== void 0;
1048
+ if (service === "sms") return metadata.services.sms !== void 0;
1049
+ return false;
1050
+ }
1051
+ function getConfiguredServices(metadata) {
1052
+ const services = [];
1053
+ if (metadata.services.email) services.push("email");
1054
+ if (metadata.services.sms) services.push("sms");
1055
+ return services;
1056
+ }
1057
+ var init_metadata = __esm({
1058
+ "src/utils/shared/metadata.ts"() {
1059
+ "use strict";
1060
+ init_esm_shims();
1061
+ init_fs();
1062
+ }
1063
+ });
1064
+
758
1065
  // src/utils/email/costs.ts
759
1066
  function estimateStorageSize(emailsPerMonth, retention, numEventTypes = 8) {
760
1067
  const avgRecordSizeKB = 2;
@@ -1712,12 +2019,12 @@ async function promptEmailArchiving() {
1712
2019
  retention
1713
2020
  };
1714
2021
  }
1715
- async function promptCustomConfig() {
2022
+ async function promptCustomConfig(existingConfig) {
1716
2023
  clack4.log.info("Custom configuration builder");
1717
2024
  clack4.log.info("Configure each feature individually");
1718
2025
  const trackingEnabled = await clack4.confirm({
1719
2026
  message: "Enable open & click tracking?",
1720
- initialValue: true
2027
+ initialValue: existingConfig?.tracking?.enabled ?? true
1721
2028
  });
1722
2029
  if (clack4.isCancel(trackingEnabled)) {
1723
2030
  clack4.cancel("Operation cancelled.");
@@ -1725,7 +2032,7 @@ async function promptCustomConfig() {
1725
2032
  }
1726
2033
  const eventTrackingEnabled = await clack4.confirm({
1727
2034
  message: "Enable real-time event tracking (EventBridge)?",
1728
- initialValue: true
2035
+ initialValue: existingConfig?.eventTracking?.enabled ?? true
1729
2036
  });
1730
2037
  if (clack4.isCancel(eventTrackingEnabled)) {
1731
2038
  clack4.cancel("Operation cancelled.");
@@ -1736,7 +2043,7 @@ async function promptCustomConfig() {
1736
2043
  if (eventTrackingEnabled) {
1737
2044
  dynamoDBHistory = await clack4.confirm({
1738
2045
  message: "Store email history in DynamoDB?",
1739
- initialValue: true
2046
+ initialValue: existingConfig?.eventTracking?.dynamoDBHistory ?? true
1740
2047
  });
1741
2048
  if (clack4.isCancel(dynamoDBHistory)) {
1742
2049
  clack4.cancel("Operation cancelled.");
@@ -1759,7 +2066,8 @@ async function promptCustomConfig() {
1759
2066
  label: "Indefinite",
1760
2067
  hint: "Higher storage cost"
1761
2068
  }
1762
- ]
2069
+ ],
2070
+ initialValue: existingConfig?.eventTracking?.archiveRetention || "90days"
1763
2071
  });
1764
2072
  if (clack4.isCancel(archiveRetention)) {
1765
2073
  clack4.cancel("Operation cancelled.");
@@ -1769,7 +2077,7 @@ async function promptCustomConfig() {
1769
2077
  }
1770
2078
  const tlsRequired = await clack4.confirm({
1771
2079
  message: "Require TLS encryption for all emails?",
1772
- initialValue: true
2080
+ initialValue: existingConfig?.tlsRequired ?? true
1773
2081
  });
1774
2082
  if (clack4.isCancel(tlsRequired)) {
1775
2083
  clack4.cancel("Operation cancelled.");
@@ -1777,7 +2085,7 @@ async function promptCustomConfig() {
1777
2085
  }
1778
2086
  const reputationMetrics = await clack4.confirm({
1779
2087
  message: "Enable reputation metrics dashboard?",
1780
- initialValue: true
2088
+ initialValue: existingConfig?.reputationMetrics ?? true
1781
2089
  });
1782
2090
  if (clack4.isCancel(reputationMetrics)) {
1783
2091
  clack4.cancel("Operation cancelled.");
@@ -1785,7 +2093,7 @@ async function promptCustomConfig() {
1785
2093
  }
1786
2094
  const dedicatedIp = await clack4.confirm({
1787
2095
  message: "Request dedicated IP address? (requires 100k+ emails/day)",
1788
- initialValue: false
2096
+ initialValue: existingConfig?.dedicatedIp ?? false
1789
2097
  });
1790
2098
  if (clack4.isCancel(dedicatedIp)) {
1791
2099
  clack4.cancel("Operation cancelled.");
@@ -1793,7 +2101,7 @@ async function promptCustomConfig() {
1793
2101
  }
1794
2102
  const emailArchivingEnabled = await clack4.confirm({
1795
2103
  message: "Enable email archiving? (Store full email content with HTML for viewing)",
1796
- initialValue: false
2104
+ initialValue: existingConfig?.emailArchiving?.enabled ?? false
1797
2105
  });
1798
2106
  if (clack4.isCancel(emailArchivingEnabled)) {
1799
2107
  clack4.cancel("Operation cancelled.");
@@ -1823,7 +2131,7 @@ async function promptCustomConfig() {
1823
2131
  hint: "~$35-60/mo for 10k emails"
1824
2132
  }
1825
2133
  ],
1826
- initialValue: "90days"
2134
+ initialValue: existingConfig?.emailArchiving?.retention || "90days"
1827
2135
  });
1828
2136
  if (clack4.isCancel(emailArchiveRetention)) {
1829
2137
  clack4.cancel("Operation cancelled.");
@@ -2225,7 +2533,9 @@ import * as aws from "@pulumi/aws";
2225
2533
  async function tableExists(tableName) {
2226
2534
  try {
2227
2535
  const { DynamoDBClient: DynamoDBClient4, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
2228
- const dynamodb2 = new DynamoDBClient4({});
2536
+ const dynamodb2 = new DynamoDBClient4({
2537
+ region: process.env.AWS_REGION || "us-east-1"
2538
+ });
2229
2539
  await dynamodb2.send(new DescribeTableCommand2({ TableName: tableName }));
2230
2540
  return true;
2231
2541
  } catch (error) {
@@ -2362,7 +2672,9 @@ import * as pulumi2 from "@pulumi/pulumi";
2362
2672
  async function roleExists(roleName) {
2363
2673
  try {
2364
2674
  const { IAMClient: IAMClient2, GetRoleCommand } = await import("@aws-sdk/client-iam");
2365
- const iam4 = new IAMClient2({});
2675
+ const iam4 = new IAMClient2({
2676
+ region: process.env.AWS_REGION || "us-east-1"
2677
+ });
2366
2678
  await iam4.send(new GetRoleCommand({ RoleName: roleName }));
2367
2679
  return true;
2368
2680
  } catch (error) {
@@ -2545,7 +2857,9 @@ function getPackageRoot() {
2545
2857
  async function lambdaFunctionExists(functionName) {
2546
2858
  try {
2547
2859
  const { LambdaClient: LambdaClient2, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
2548
- const lambda2 = new LambdaClient2({});
2860
+ const lambda2 = new LambdaClient2({
2861
+ region: process.env.AWS_REGION || "us-east-1"
2862
+ });
2549
2863
  await lambda2.send(new GetFunctionCommand({ FunctionName: functionName }));
2550
2864
  return true;
2551
2865
  } catch (error) {
@@ -2559,7 +2873,9 @@ async function lambdaFunctionExists(functionName) {
2559
2873
  async function findEventSourceMapping(functionName, queueArn) {
2560
2874
  try {
2561
2875
  const { LambdaClient: LambdaClient2, ListEventSourceMappingsCommand } = await import("@aws-sdk/client-lambda");
2562
- const lambda2 = new LambdaClient2({});
2876
+ const lambda2 = new LambdaClient2({
2877
+ region: process.env.AWS_REGION || "us-east-1"
2878
+ });
2563
2879
  const response = await lambda2.send(
2564
2880
  new ListEventSourceMappingsCommand({
2565
2881
  FunctionName: functionName,
@@ -2782,27 +3098,13 @@ async function emailIdentityExists(emailIdentity, region) {
2782
3098
  return false;
2783
3099
  }
2784
3100
  }
2785
- async function eventDestinationExists(configSetName, eventDestName, region) {
2786
- try {
2787
- const { SESv2Client: SESv2Client5, GetConfigurationSetEventDestinationsCommand } = await import("@aws-sdk/client-sesv2");
2788
- const ses = new SESv2Client5({ region });
2789
- const response = await ses.send(
2790
- new GetConfigurationSetEventDestinationsCommand({
2791
- ConfigurationSetName: configSetName
2792
- })
2793
- );
2794
- return response.EventDestinations?.some((dest) => dest.Name === eventDestName) ?? false;
2795
- } catch (error) {
2796
- if (error.name === "NotFoundException") {
2797
- return false;
2798
- }
2799
- console.error("Error checking for existing event destination:", error);
2800
- return false;
2801
- }
2802
- }
2803
3101
  async function createSESResources(config2) {
2804
3102
  const configSetOptions = {
2805
3103
  configurationSetName: "wraps-email-tracking",
3104
+ deliveryOptions: config2.tlsRequired ? {
3105
+ tlsPolicy: "REQUIRE"
3106
+ // Require TLS 1.2+ for all emails
3107
+ } : void 0,
2806
3108
  tags: {
2807
3109
  ManagedBy: "wraps-cli",
2808
3110
  Description: "Wraps email tracking configuration set"
@@ -2826,13 +3128,8 @@ async function createSESResources(config2) {
2826
3128
  const defaultEventBus = aws5.cloudwatch.getEventBusOutput({
2827
3129
  name: "default"
2828
3130
  });
2829
- const eventDestName = "wraps-email-eventbridge";
2830
- const eventDestExists = await eventDestinationExists(
2831
- configSetName,
2832
- eventDestName,
2833
- config2.region
2834
- );
2835
- if (!eventDestExists) {
3131
+ if (config2.eventTrackingEnabled) {
3132
+ const eventDestName = "wraps-email-eventbridge";
2836
3133
  new aws5.sesv2.ConfigurationSetEventDestination("wraps-email-all-events", {
2837
3134
  configurationSetName: configSet.configurationSetName,
2838
3135
  eventDestinationName: eventDestName,
@@ -2969,7 +3266,9 @@ import * as aws7 from "@pulumi/aws";
2969
3266
  async function getExistingOIDCProviderArn(url) {
2970
3267
  try {
2971
3268
  const { IAMClient: IAMClient2, ListOpenIDConnectProvidersCommand } = await import("@aws-sdk/client-iam");
2972
- const iam4 = new IAMClient2({});
3269
+ const iam4 = new IAMClient2({
3270
+ region: process.env.AWS_REGION || "us-east-1"
3271
+ });
2973
3272
  const response = await iam4.send(new ListOpenIDConnectProvidersCommand({}));
2974
3273
  const expectedArnSuffix = url.replace("https://", "");
2975
3274
  const provider = response.OpenIDConnectProviderList?.find(
@@ -3058,7 +3357,9 @@ async function deployEmailStack(config2) {
3058
3357
  cloudFrontResources = await createCloudFrontTracking2({
3059
3358
  customTrackingDomain: emailConfig.tracking.customRedirectDomain,
3060
3359
  region: config2.region,
3061
- certificateArn
3360
+ certificateArn,
3361
+ hostedZoneId: hostedZone?.id
3362
+ // Pass hosted zone ID for automatic DNS record creation
3062
3363
  });
3063
3364
  }
3064
3365
  let sesResources;
@@ -3068,7 +3369,11 @@ async function deployEmailStack(config2) {
3068
3369
  mailFromDomain: emailConfig.mailFromDomain,
3069
3370
  region: config2.region,
3070
3371
  trackingConfig: emailConfig.tracking,
3071
- eventTypes: emailConfig.eventTracking?.events
3372
+ eventTypes: emailConfig.eventTracking?.events,
3373
+ eventTrackingEnabled: emailConfig.eventTracking?.enabled,
3374
+ // Pass flag to create EventBridge destination
3375
+ tlsRequired: emailConfig.tlsRequired
3376
+ // Require TLS encryption for all emails
3072
3377
  });
3073
3378
  }
3074
3379
  let dynamoTables;
@@ -3133,142 +3438,8 @@ async function deployEmailStack(config2) {
3133
3438
 
3134
3439
  // src/commands/email/config.ts
3135
3440
  init_aws();
3136
-
3137
- // src/utils/shared/fs.ts
3138
- init_esm_shims();
3139
- import { existsSync as existsSync2 } from "fs";
3140
- import { mkdir } from "fs/promises";
3141
- import { homedir } from "os";
3142
- import { join as join2 } from "path";
3143
- function getWrapsDir() {
3144
- return join2(homedir(), ".wraps");
3145
- }
3146
- function getPulumiWorkDir() {
3147
- return join2(getWrapsDir(), "pulumi");
3148
- }
3149
- async function ensureWrapsDir() {
3150
- const wrapsDir = getWrapsDir();
3151
- if (!existsSync2(wrapsDir)) {
3152
- await mkdir(wrapsDir, { recursive: true });
3153
- }
3154
- }
3155
- async function ensurePulumiWorkDir() {
3156
- await ensureWrapsDir();
3157
- const pulumiDir = getPulumiWorkDir();
3158
- if (!existsSync2(pulumiDir)) {
3159
- await mkdir(pulumiDir, { recursive: true });
3160
- }
3161
- process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
3162
- process.env.PULUMI_CONFIG_PASSPHRASE = "";
3163
- }
3164
-
3165
- // src/utils/shared/metadata.ts
3166
- init_esm_shims();
3167
- import { existsSync as existsSync3 } from "fs";
3168
- import { readFile, writeFile } from "fs/promises";
3169
- import { join as join3 } from "path";
3170
- function getConnectionsDir() {
3171
- return join3(getWrapsDir(), "connections");
3172
- }
3173
- function getMetadataPath(accountId, region) {
3174
- return join3(getConnectionsDir(), `${accountId}-${region}.json`);
3175
- }
3176
- async function ensureConnectionsDir() {
3177
- await ensureWrapsDir();
3178
- const connectionsDir = getConnectionsDir();
3179
- if (!existsSync3(connectionsDir)) {
3180
- const { mkdir: mkdir2 } = await import("fs/promises");
3181
- await mkdir2(connectionsDir, { recursive: true });
3182
- }
3183
- }
3184
- function migrateLegacyMetadata(legacy) {
3185
- return {
3186
- version: "1.0.0",
3187
- accountId: legacy.accountId,
3188
- region: legacy.region,
3189
- provider: legacy.provider,
3190
- timestamp: legacy.timestamp,
3191
- vercel: legacy.vercel,
3192
- services: {
3193
- email: {
3194
- preset: legacy.preset,
3195
- config: legacy.emailConfig,
3196
- pulumiStackName: legacy.pulumiStackName,
3197
- deployedAt: legacy.timestamp
3198
- }
3199
- }
3200
- };
3201
- }
3202
- function isLegacyMetadata(data) {
3203
- return "emailConfig" in data && !("services" in data) && typeof data.emailConfig === "object";
3204
- }
3205
- async function loadConnectionMetadata(accountId, region) {
3206
- const metadataPath = getMetadataPath(accountId, region);
3207
- if (!existsSync3(metadataPath)) {
3208
- return null;
3209
- }
3210
- try {
3211
- const content = await readFile(metadataPath, "utf-8");
3212
- const data = JSON.parse(content);
3213
- if (isLegacyMetadata(data)) {
3214
- const migrated = migrateLegacyMetadata(data);
3215
- await saveConnectionMetadata(migrated);
3216
- return migrated;
3217
- }
3218
- if (!data.version) {
3219
- data.version = "1.0.0";
3220
- await saveConnectionMetadata(data);
3221
- }
3222
- return data;
3223
- } catch (error) {
3224
- console.error("Error loading connection metadata:", error.message);
3225
- return null;
3226
- }
3227
- }
3228
- async function saveConnectionMetadata(metadata) {
3229
- await ensureConnectionsDir();
3230
- const metadataPath = getMetadataPath(metadata.accountId, metadata.region);
3231
- try {
3232
- const content = JSON.stringify(metadata, null, 2);
3233
- await writeFile(metadataPath, content, "utf-8");
3234
- } catch (error) {
3235
- console.error("Error saving connection metadata:", error.message);
3236
- throw error;
3237
- }
3238
- }
3239
- async function deleteConnectionMetadata(accountId, region) {
3240
- const metadataPath = getMetadataPath(accountId, region);
3241
- if (existsSync3(metadataPath)) {
3242
- const { unlink } = await import("fs/promises");
3243
- await unlink(metadataPath);
3244
- }
3245
- }
3246
- function createConnectionMetadata(accountId, region, provider, emailConfig, preset) {
3247
- return {
3248
- version: "1.0.0",
3249
- accountId,
3250
- region,
3251
- provider,
3252
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3253
- services: {
3254
- email: {
3255
- preset,
3256
- config: emailConfig,
3257
- deployedAt: (/* @__PURE__ */ new Date()).toISOString()
3258
- }
3259
- }
3260
- };
3261
- }
3262
- function updateEmailConfig(metadata, emailConfig) {
3263
- if (!metadata.services.email) {
3264
- throw new Error("Email service not configured in metadata");
3265
- }
3266
- metadata.services.email.config = {
3267
- ...metadata.services.email.config,
3268
- ...emailConfig
3269
- };
3270
- metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
3271
- }
3441
+ init_fs();
3442
+ init_metadata();
3272
3443
 
3273
3444
  // src/utils/shared/output.ts
3274
3445
  init_esm_shims();
@@ -3771,7 +3942,8 @@ ${pc3.bold("Current Configuration:")}
3771
3942
  {
3772
3943
  workDir: getPulumiWorkDir(),
3773
3944
  envVars: {
3774
- PULUMI_CONFIG_PASSPHRASE: ""
3945
+ PULUMI_CONFIG_PASSPHRASE: "",
3946
+ AWS_REGION: region
3775
3947
  },
3776
3948
  secretsProvider: "passphrase"
3777
3949
  }
@@ -3842,6 +4014,8 @@ import * as pulumi6 from "@pulumi/pulumi";
3842
4014
  import pc5 from "picocolors";
3843
4015
  init_presets();
3844
4016
  init_aws();
4017
+ init_fs();
4018
+ init_metadata();
3845
4019
  init_prompts();
3846
4020
 
3847
4021
  // src/utils/shared/scanner.ts
@@ -4146,6 +4320,10 @@ async function connect(options) {
4146
4320
  const emailConfig = preset === "custom" ? await Promise.resolve().then(() => (init_prompts(), prompts_exports)).then(
4147
4321
  (m) => m.promptCustomConfig()
4148
4322
  ) : getPreset(preset);
4323
+ const domainIdentities = selectedIdentities.filter((id) => !id.includes("@"));
4324
+ if (domainIdentities.length > 0) {
4325
+ emailConfig.domain = domainIdentities[0];
4326
+ }
4149
4327
  if (!options.yes) {
4150
4328
  const confirmed = await confirmConnect();
4151
4329
  if (!confirmed) {
@@ -4186,7 +4364,8 @@ async function connect(options) {
4186
4364
  {
4187
4365
  workDir: getPulumiWorkDir(),
4188
4366
  envVars: {
4189
- PULUMI_CONFIG_PASSPHRASE: ""
4367
+ PULUMI_CONFIG_PASSPHRASE: "",
4368
+ AWS_REGION: region
4190
4369
  },
4191
4370
  secretsProvider: "passphrase"
4192
4371
  }
@@ -4220,6 +4399,32 @@ async function connect(options) {
4220
4399
  }
4221
4400
  throw new Error(`Pulumi deployment failed: ${error.message}`);
4222
4401
  }
4402
+ if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
4403
+ const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
4404
+ const hostedZone = await findHostedZone2(outputs.domain, region);
4405
+ if (hostedZone) {
4406
+ try {
4407
+ progress.start("Creating DNS records in Route53");
4408
+ const mailFromDomain = emailConfig.mailFromDomain || `mail.${outputs.domain}`;
4409
+ await createDNSRecords2(
4410
+ hostedZone.id,
4411
+ outputs.domain,
4412
+ outputs.dkimTokens,
4413
+ region,
4414
+ outputs.customTrackingDomain,
4415
+ mailFromDomain
4416
+ );
4417
+ progress.succeed("DNS records created in Route53");
4418
+ } catch (error) {
4419
+ progress.fail(
4420
+ `Failed to create DNS records automatically: ${error.message}`
4421
+ );
4422
+ progress.info(
4423
+ "You can manually add the required DNS records shown below"
4424
+ );
4425
+ }
4426
+ }
4427
+ }
4223
4428
  const metadata = createConnectionMetadata(
4224
4429
  identity.accountId,
4225
4430
  region,
@@ -4688,6 +4893,8 @@ import pc7 from "picocolors";
4688
4893
  init_costs();
4689
4894
  init_presets();
4690
4895
  init_aws();
4896
+ init_fs();
4897
+ init_metadata();
4691
4898
  init_prompts();
4692
4899
  async function init(options) {
4693
4900
  clack7.intro(pc7.bold("Wraps Email Infrastructure Setup"));
@@ -4818,8 +5025,9 @@ ${pc7.yellow(pc7.bold("Configuration Warnings:"))}`);
4818
5025
  workDir: getPulumiWorkDir(),
4819
5026
  // Use local file-based backend (no Pulumi Cloud login required)
4820
5027
  envVars: {
4821
- PULUMI_CONFIG_PASSPHRASE: ""
5028
+ PULUMI_CONFIG_PASSPHRASE: "",
4822
5029
  // Use empty passphrase for local state
5030
+ AWS_REGION: region
4823
5031
  },
4824
5032
  secretsProvider: "passphrase"
4825
5033
  }
@@ -4910,6 +5118,8 @@ ${pc7.yellow(pc7.bold("Configuration Warnings:"))}`);
4910
5118
  // src/commands/email/restore.ts
4911
5119
  init_esm_shims();
4912
5120
  init_aws();
5121
+ init_fs();
5122
+ init_metadata();
4913
5123
  import * as clack8 from "@clack/prompts";
4914
5124
  import * as pulumi8 from "@pulumi/pulumi";
4915
5125
  import pc8 from "picocolors";
@@ -4988,7 +5198,8 @@ ${pc8.bold("The following Wraps resources will be removed:")}
4988
5198
  {
4989
5199
  workDir: getPulumiWorkDir(),
4990
5200
  envVars: {
4991
- PULUMI_CONFIG_PASSPHRASE: ""
5201
+ PULUMI_CONFIG_PASSPHRASE: "",
5202
+ AWS_REGION: region
4992
5203
  },
4993
5204
  secretsProvider: "passphrase"
4994
5205
  }
@@ -5025,6 +5236,8 @@ import pc9 from "picocolors";
5025
5236
  init_costs();
5026
5237
  init_presets();
5027
5238
  init_aws();
5239
+ init_fs();
5240
+ init_metadata();
5028
5241
  init_prompts();
5029
5242
  async function upgrade(options) {
5030
5243
  clack9.intro(pc9.bold("Wraps Upgrade - Enhance Your Email Infrastructure"));
@@ -5192,12 +5405,9 @@ ${pc9.bold("Current Configuration:")}
5192
5405
  clack9.cancel("Upgrade cancelled.");
5193
5406
  process.exit(0);
5194
5407
  }
5408
+ const { applyConfigUpdates: applyConfigUpdates2 } = await Promise.resolve().then(() => (init_metadata(), metadata_exports));
5195
5409
  const presetConfig = getPreset(selectedPreset);
5196
- updatedConfig = {
5197
- ...presetConfig,
5198
- domain: config2.domain
5199
- // Preserve original domain
5200
- };
5410
+ updatedConfig = applyConfigUpdates2(config2, presetConfig);
5201
5411
  newPreset = selectedPreset;
5202
5412
  break;
5203
5413
  }
@@ -5630,11 +5840,9 @@ ${pc9.bold("Current Configuration:")}
5630
5840
  }
5631
5841
  case "custom": {
5632
5842
  const { promptCustomConfig: promptCustomConfig2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
5633
- const customConfig = await promptCustomConfig2();
5634
- updatedConfig = {
5635
- ...customConfig,
5636
- domain: config2.domain
5637
- };
5843
+ const { applyConfigUpdates: applyConfigUpdates2 } = await Promise.resolve().then(() => (init_metadata(), metadata_exports));
5844
+ const customConfig = await promptCustomConfig2(config2);
5845
+ updatedConfig = applyConfigUpdates2(config2, customConfig);
5638
5846
  newPreset = void 0;
5639
5847
  break;
5640
5848
  }
@@ -5712,7 +5920,8 @@ ${pc9.bold("Cost Impact:")}`);
5712
5920
  {
5713
5921
  workDir: getPulumiWorkDir(),
5714
5922
  envVars: {
5715
- PULUMI_CONFIG_PASSPHRASE: ""
5923
+ PULUMI_CONFIG_PASSPHRASE: "",
5924
+ AWS_REGION: region
5716
5925
  },
5717
5926
  secretsProvider: "passphrase"
5718
5927
  }
@@ -5754,6 +5963,33 @@ ${pc9.bold("Cost Impact:")}`);
5754
5963
  }
5755
5964
  throw new Error(`Pulumi upgrade failed: ${error.message}`);
5756
5965
  }
5966
+ if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
5967
+ const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
5968
+ const hostedZone = await findHostedZone2(outputs.domain, region);
5969
+ if (hostedZone) {
5970
+ try {
5971
+ progress.start("Creating DNS records in Route53");
5972
+ const mailFromDomain = updatedConfig.mailFromDomain || `mail.${outputs.domain}`;
5973
+ await createDNSRecords2(
5974
+ hostedZone.id,
5975
+ outputs.domain,
5976
+ outputs.dkimTokens,
5977
+ region,
5978
+ outputs.customTrackingDomain,
5979
+ mailFromDomain,
5980
+ outputs.cloudFrontDomain
5981
+ );
5982
+ progress.succeed("DNS records created in Route53");
5983
+ } catch (error) {
5984
+ progress.fail(
5985
+ `Failed to create DNS records automatically: ${error.message}`
5986
+ );
5987
+ progress.info(
5988
+ "You can manually add the required DNS records shown below"
5989
+ );
5990
+ }
5991
+ }
5992
+ }
5757
5993
  updateEmailConfig(metadata, updatedConfig);
5758
5994
  if (metadata.services.email) {
5759
5995
  metadata.services.email.preset = newPreset;
@@ -6573,6 +6809,7 @@ function createMetricsRouter(config2) {
6573
6809
 
6574
6810
  // src/console/routes/settings.ts
6575
6811
  init_esm_shims();
6812
+ init_metadata();
6576
6813
  import dns from "dns/promises";
6577
6814
  import { Router as createRouter4 } from "express";
6578
6815
 
@@ -6915,6 +7152,7 @@ function createSettingsRouter(config2) {
6915
7152
 
6916
7153
  // src/console/routes/user.ts
6917
7154
  init_esm_shims();
7155
+ init_metadata();
6918
7156
  import { Router as createRouter5 } from "express";
6919
7157
  function createUserRouter(config2) {
6920
7158
  const router = createRouter5();
@@ -7060,6 +7298,7 @@ async function startConsoleServer(config2) {
7060
7298
 
7061
7299
  // src/commands/shared/dashboard.ts
7062
7300
  init_aws();
7301
+ init_fs();
7063
7302
  async function dashboard(options) {
7064
7303
  clack10.intro(pc10.bold("Wraps Dashboard"));
7065
7304
  const progress = new DeploymentProgress();
@@ -7116,6 +7355,8 @@ async function dashboard(options) {
7116
7355
  // src/commands/shared/destroy.ts
7117
7356
  init_esm_shims();
7118
7357
  init_aws();
7358
+ init_fs();
7359
+ init_metadata();
7119
7360
  import * as clack11 from "@clack/prompts";
7120
7361
  import * as pulumi11 from "@pulumi/pulumi";
7121
7362
  import pc11 from "picocolors";
@@ -7182,6 +7423,7 @@ Run ${pc11.cyan("wraps email init")} to deploy infrastructure again.
7182
7423
  // src/commands/shared/status.ts
7183
7424
  init_esm_shims();
7184
7425
  init_aws();
7426
+ init_fs();
7185
7427
  import * as clack12 from "@clack/prompts";
7186
7428
  import * as pulumi12 from "@pulumi/pulumi";
7187
7429
  import pc12 from "picocolors";