@wraps.dev/cli 1.1.4 → 1.1.6

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.");
@@ -2790,27 +3098,18 @@ async function emailIdentityExists(emailIdentity, region) {
2790
3098
  return false;
2791
3099
  }
2792
3100
  }
2793
- async function eventDestinationExists(configSetName, eventDestName, region) {
2794
- try {
2795
- const { SESv2Client: SESv2Client5, GetConfigurationSetEventDestinationsCommand } = await import("@aws-sdk/client-sesv2");
2796
- const ses = new SESv2Client5({ region });
2797
- const response = await ses.send(
2798
- new GetConfigurationSetEventDestinationsCommand({
2799
- ConfigurationSetName: configSetName
2800
- })
2801
- );
2802
- return response.EventDestinations?.some((dest) => dest.Name === eventDestName) ?? false;
2803
- } catch (error) {
2804
- if (error.name === "NotFoundException") {
2805
- return false;
2806
- }
2807
- console.error("Error checking for existing event destination:", error);
2808
- return false;
2809
- }
2810
- }
2811
3101
  async function createSESResources(config2) {
2812
3102
  const configSetOptions = {
2813
3103
  configurationSetName: "wraps-email-tracking",
3104
+ deliveryOptions: config2.tlsRequired ? {
3105
+ tlsPolicy: "REQUIRE"
3106
+ // Require TLS 1.2+ for all emails
3107
+ } : void 0,
3108
+ suppressionOptions: {
3109
+ // Automatically suppress hard bounces and complaints at the configuration set level
3110
+ // This provides protection even if account-level suppression isn't configured
3111
+ suppressedReasons: ["BOUNCE", "COMPLAINT"]
3112
+ },
2814
3113
  tags: {
2815
3114
  ManagedBy: "wraps-cli",
2816
3115
  Description: "Wraps email tracking configuration set"
@@ -2834,13 +3133,8 @@ async function createSESResources(config2) {
2834
3133
  const defaultEventBus = aws5.cloudwatch.getEventBusOutput({
2835
3134
  name: "default"
2836
3135
  });
2837
- const eventDestName = "wraps-email-eventbridge";
2838
- const eventDestExists = await eventDestinationExists(
2839
- configSetName,
2840
- eventDestName,
2841
- config2.region
2842
- );
2843
- if (!eventDestExists) {
3136
+ if (config2.eventTrackingEnabled) {
3137
+ const eventDestName = "wraps-email-eventbridge";
2844
3138
  new aws5.sesv2.ConfigurationSetEventDestination("wraps-email-all-events", {
2845
3139
  configurationSetName: configSet.configurationSetName,
2846
3140
  eventDestinationName: eventDestName,
@@ -3068,7 +3362,9 @@ async function deployEmailStack(config2) {
3068
3362
  cloudFrontResources = await createCloudFrontTracking2({
3069
3363
  customTrackingDomain: emailConfig.tracking.customRedirectDomain,
3070
3364
  region: config2.region,
3071
- certificateArn
3365
+ certificateArn,
3366
+ hostedZoneId: hostedZone?.id
3367
+ // Pass hosted zone ID for automatic DNS record creation
3072
3368
  });
3073
3369
  }
3074
3370
  let sesResources;
@@ -3078,7 +3374,11 @@ async function deployEmailStack(config2) {
3078
3374
  mailFromDomain: emailConfig.mailFromDomain,
3079
3375
  region: config2.region,
3080
3376
  trackingConfig: emailConfig.tracking,
3081
- eventTypes: emailConfig.eventTracking?.events
3377
+ eventTypes: emailConfig.eventTracking?.events,
3378
+ eventTrackingEnabled: emailConfig.eventTracking?.enabled,
3379
+ // Pass flag to create EventBridge destination
3380
+ tlsRequired: emailConfig.tlsRequired
3381
+ // Require TLS encryption for all emails
3082
3382
  });
3083
3383
  }
3084
3384
  let dynamoTables;
@@ -3143,142 +3443,8 @@ async function deployEmailStack(config2) {
3143
3443
 
3144
3444
  // src/commands/email/config.ts
3145
3445
  init_aws();
3146
-
3147
- // src/utils/shared/fs.ts
3148
- init_esm_shims();
3149
- import { existsSync as existsSync2 } from "fs";
3150
- import { mkdir } from "fs/promises";
3151
- import { homedir } from "os";
3152
- import { join as join2 } from "path";
3153
- function getWrapsDir() {
3154
- return join2(homedir(), ".wraps");
3155
- }
3156
- function getPulumiWorkDir() {
3157
- return join2(getWrapsDir(), "pulumi");
3158
- }
3159
- async function ensureWrapsDir() {
3160
- const wrapsDir = getWrapsDir();
3161
- if (!existsSync2(wrapsDir)) {
3162
- await mkdir(wrapsDir, { recursive: true });
3163
- }
3164
- }
3165
- async function ensurePulumiWorkDir() {
3166
- await ensureWrapsDir();
3167
- const pulumiDir = getPulumiWorkDir();
3168
- if (!existsSync2(pulumiDir)) {
3169
- await mkdir(pulumiDir, { recursive: true });
3170
- }
3171
- process.env.PULUMI_BACKEND_URL = `file://${pulumiDir}`;
3172
- process.env.PULUMI_CONFIG_PASSPHRASE = "";
3173
- }
3174
-
3175
- // src/utils/shared/metadata.ts
3176
- init_esm_shims();
3177
- import { existsSync as existsSync3 } from "fs";
3178
- import { readFile, writeFile } from "fs/promises";
3179
- import { join as join3 } from "path";
3180
- function getConnectionsDir() {
3181
- return join3(getWrapsDir(), "connections");
3182
- }
3183
- function getMetadataPath(accountId, region) {
3184
- return join3(getConnectionsDir(), `${accountId}-${region}.json`);
3185
- }
3186
- async function ensureConnectionsDir() {
3187
- await ensureWrapsDir();
3188
- const connectionsDir = getConnectionsDir();
3189
- if (!existsSync3(connectionsDir)) {
3190
- const { mkdir: mkdir2 } = await import("fs/promises");
3191
- await mkdir2(connectionsDir, { recursive: true });
3192
- }
3193
- }
3194
- function migrateLegacyMetadata(legacy) {
3195
- return {
3196
- version: "1.0.0",
3197
- accountId: legacy.accountId,
3198
- region: legacy.region,
3199
- provider: legacy.provider,
3200
- timestamp: legacy.timestamp,
3201
- vercel: legacy.vercel,
3202
- services: {
3203
- email: {
3204
- preset: legacy.preset,
3205
- config: legacy.emailConfig,
3206
- pulumiStackName: legacy.pulumiStackName,
3207
- deployedAt: legacy.timestamp
3208
- }
3209
- }
3210
- };
3211
- }
3212
- function isLegacyMetadata(data) {
3213
- return "emailConfig" in data && !("services" in data) && typeof data.emailConfig === "object";
3214
- }
3215
- async function loadConnectionMetadata(accountId, region) {
3216
- const metadataPath = getMetadataPath(accountId, region);
3217
- if (!existsSync3(metadataPath)) {
3218
- return null;
3219
- }
3220
- try {
3221
- const content = await readFile(metadataPath, "utf-8");
3222
- const data = JSON.parse(content);
3223
- if (isLegacyMetadata(data)) {
3224
- const migrated = migrateLegacyMetadata(data);
3225
- await saveConnectionMetadata(migrated);
3226
- return migrated;
3227
- }
3228
- if (!data.version) {
3229
- data.version = "1.0.0";
3230
- await saveConnectionMetadata(data);
3231
- }
3232
- return data;
3233
- } catch (error) {
3234
- console.error("Error loading connection metadata:", error.message);
3235
- return null;
3236
- }
3237
- }
3238
- async function saveConnectionMetadata(metadata) {
3239
- await ensureConnectionsDir();
3240
- const metadataPath = getMetadataPath(metadata.accountId, metadata.region);
3241
- try {
3242
- const content = JSON.stringify(metadata, null, 2);
3243
- await writeFile(metadataPath, content, "utf-8");
3244
- } catch (error) {
3245
- console.error("Error saving connection metadata:", error.message);
3246
- throw error;
3247
- }
3248
- }
3249
- async function deleteConnectionMetadata(accountId, region) {
3250
- const metadataPath = getMetadataPath(accountId, region);
3251
- if (existsSync3(metadataPath)) {
3252
- const { unlink } = await import("fs/promises");
3253
- await unlink(metadataPath);
3254
- }
3255
- }
3256
- function createConnectionMetadata(accountId, region, provider, emailConfig, preset) {
3257
- return {
3258
- version: "1.0.0",
3259
- accountId,
3260
- region,
3261
- provider,
3262
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3263
- services: {
3264
- email: {
3265
- preset,
3266
- config: emailConfig,
3267
- deployedAt: (/* @__PURE__ */ new Date()).toISOString()
3268
- }
3269
- }
3270
- };
3271
- }
3272
- function updateEmailConfig(metadata, emailConfig) {
3273
- if (!metadata.services.email) {
3274
- throw new Error("Email service not configured in metadata");
3275
- }
3276
- metadata.services.email.config = {
3277
- ...metadata.services.email.config,
3278
- ...emailConfig
3279
- };
3280
- metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
3281
- }
3446
+ init_fs();
3447
+ init_metadata();
3282
3448
 
3283
3449
  // src/utils/shared/output.ts
3284
3450
  init_esm_shims();
@@ -3853,6 +4019,8 @@ import * as pulumi6 from "@pulumi/pulumi";
3853
4019
  import pc5 from "picocolors";
3854
4020
  init_presets();
3855
4021
  init_aws();
4022
+ init_fs();
4023
+ init_metadata();
3856
4024
  init_prompts();
3857
4025
 
3858
4026
  // src/utils/shared/scanner.ts
@@ -4236,6 +4404,32 @@ async function connect(options) {
4236
4404
  }
4237
4405
  throw new Error(`Pulumi deployment failed: ${error.message}`);
4238
4406
  }
4407
+ if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
4408
+ const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
4409
+ const hostedZone = await findHostedZone2(outputs.domain, region);
4410
+ if (hostedZone) {
4411
+ try {
4412
+ progress.start("Creating DNS records in Route53");
4413
+ const mailFromDomain = emailConfig.mailFromDomain || `mail.${outputs.domain}`;
4414
+ await createDNSRecords2(
4415
+ hostedZone.id,
4416
+ outputs.domain,
4417
+ outputs.dkimTokens,
4418
+ region,
4419
+ outputs.customTrackingDomain,
4420
+ mailFromDomain
4421
+ );
4422
+ progress.succeed("DNS records created in Route53");
4423
+ } catch (error) {
4424
+ progress.fail(
4425
+ `Failed to create DNS records automatically: ${error.message}`
4426
+ );
4427
+ progress.info(
4428
+ "You can manually add the required DNS records shown below"
4429
+ );
4430
+ }
4431
+ }
4432
+ }
4239
4433
  const metadata = createConnectionMetadata(
4240
4434
  identity.accountId,
4241
4435
  region,
@@ -4704,6 +4898,8 @@ import pc7 from "picocolors";
4704
4898
  init_costs();
4705
4899
  init_presets();
4706
4900
  init_aws();
4901
+ init_fs();
4902
+ init_metadata();
4707
4903
  init_prompts();
4708
4904
  async function init(options) {
4709
4905
  clack7.intro(pc7.bold("Wraps Email Infrastructure Setup"));
@@ -4927,6 +5123,8 @@ ${pc7.yellow(pc7.bold("Configuration Warnings:"))}`);
4927
5123
  // src/commands/email/restore.ts
4928
5124
  init_esm_shims();
4929
5125
  init_aws();
5126
+ init_fs();
5127
+ init_metadata();
4930
5128
  import * as clack8 from "@clack/prompts";
4931
5129
  import * as pulumi8 from "@pulumi/pulumi";
4932
5130
  import pc8 from "picocolors";
@@ -5043,6 +5241,8 @@ import pc9 from "picocolors";
5043
5241
  init_costs();
5044
5242
  init_presets();
5045
5243
  init_aws();
5244
+ init_fs();
5245
+ init_metadata();
5046
5246
  init_prompts();
5047
5247
  async function upgrade(options) {
5048
5248
  clack9.intro(pc9.bold("Wraps Upgrade - Enhance Your Email Infrastructure"));
@@ -5210,12 +5410,9 @@ ${pc9.bold("Current Configuration:")}
5210
5410
  clack9.cancel("Upgrade cancelled.");
5211
5411
  process.exit(0);
5212
5412
  }
5413
+ const { applyConfigUpdates: applyConfigUpdates2 } = await Promise.resolve().then(() => (init_metadata(), metadata_exports));
5213
5414
  const presetConfig = getPreset(selectedPreset);
5214
- updatedConfig = {
5215
- ...presetConfig,
5216
- domain: config2.domain
5217
- // Preserve original domain
5218
- };
5415
+ updatedConfig = applyConfigUpdates2(config2, presetConfig);
5219
5416
  newPreset = selectedPreset;
5220
5417
  break;
5221
5418
  }
@@ -5648,11 +5845,9 @@ ${pc9.bold("Current Configuration:")}
5648
5845
  }
5649
5846
  case "custom": {
5650
5847
  const { promptCustomConfig: promptCustomConfig2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
5651
- const customConfig = await promptCustomConfig2();
5652
- updatedConfig = {
5653
- ...customConfig,
5654
- domain: config2.domain
5655
- };
5848
+ const { applyConfigUpdates: applyConfigUpdates2 } = await Promise.resolve().then(() => (init_metadata(), metadata_exports));
5849
+ const customConfig = await promptCustomConfig2(config2);
5850
+ updatedConfig = applyConfigUpdates2(config2, customConfig);
5656
5851
  newPreset = void 0;
5657
5852
  break;
5658
5853
  }
@@ -5773,6 +5968,33 @@ ${pc9.bold("Cost Impact:")}`);
5773
5968
  }
5774
5969
  throw new Error(`Pulumi upgrade failed: ${error.message}`);
5775
5970
  }
5971
+ if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
5972
+ const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
5973
+ const hostedZone = await findHostedZone2(outputs.domain, region);
5974
+ if (hostedZone) {
5975
+ try {
5976
+ progress.start("Creating DNS records in Route53");
5977
+ const mailFromDomain = updatedConfig.mailFromDomain || `mail.${outputs.domain}`;
5978
+ await createDNSRecords2(
5979
+ hostedZone.id,
5980
+ outputs.domain,
5981
+ outputs.dkimTokens,
5982
+ region,
5983
+ outputs.customTrackingDomain,
5984
+ mailFromDomain,
5985
+ outputs.cloudFrontDomain
5986
+ );
5987
+ progress.succeed("DNS records created in Route53");
5988
+ } catch (error) {
5989
+ progress.fail(
5990
+ `Failed to create DNS records automatically: ${error.message}`
5991
+ );
5992
+ progress.info(
5993
+ "You can manually add the required DNS records shown below"
5994
+ );
5995
+ }
5996
+ }
5997
+ }
5776
5998
  updateEmailConfig(metadata, updatedConfig);
5777
5999
  if (metadata.services.email) {
5778
6000
  metadata.services.email.preset = newPreset;
@@ -6592,6 +6814,7 @@ function createMetricsRouter(config2) {
6592
6814
 
6593
6815
  // src/console/routes/settings.ts
6594
6816
  init_esm_shims();
6817
+ init_metadata();
6595
6818
  import dns from "dns/promises";
6596
6819
  import { Router as createRouter4 } from "express";
6597
6820
 
@@ -6934,6 +7157,7 @@ function createSettingsRouter(config2) {
6934
7157
 
6935
7158
  // src/console/routes/user.ts
6936
7159
  init_esm_shims();
7160
+ init_metadata();
6937
7161
  import { Router as createRouter5 } from "express";
6938
7162
  function createUserRouter(config2) {
6939
7163
  const router = createRouter5();
@@ -7079,6 +7303,7 @@ async function startConsoleServer(config2) {
7079
7303
 
7080
7304
  // src/commands/shared/dashboard.ts
7081
7305
  init_aws();
7306
+ init_fs();
7082
7307
  async function dashboard(options) {
7083
7308
  clack10.intro(pc10.bold("Wraps Dashboard"));
7084
7309
  const progress = new DeploymentProgress();
@@ -7135,6 +7360,8 @@ async function dashboard(options) {
7135
7360
  // src/commands/shared/destroy.ts
7136
7361
  init_esm_shims();
7137
7362
  init_aws();
7363
+ init_fs();
7364
+ init_metadata();
7138
7365
  import * as clack11 from "@clack/prompts";
7139
7366
  import * as pulumi11 from "@pulumi/pulumi";
7140
7367
  import pc11 from "picocolors";
@@ -7201,6 +7428,7 @@ Run ${pc11.cyan("wraps email init")} to deploy infrastructure again.
7201
7428
  // src/commands/shared/status.ts
7202
7429
  init_esm_shims();
7203
7430
  init_aws();
7431
+ init_fs();
7204
7432
  import * as clack12 from "@clack/prompts";
7205
7433
  import * as pulumi12 from "@pulumi/pulumi";
7206
7434
  import pc12 from "picocolors";