@wraps.dev/cli 0.3.2 → 0.3.4

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
@@ -9,11 +9,11 @@ var __export = (target, all3) => {
9
9
  __defProp(target, name, { get: all3[name], enumerable: true });
10
10
  };
11
11
 
12
- // ../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.8.3/node_modules/tsup/assets/esm_shims.js
12
+ // ../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
13
13
  import path from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  var init_esm_shims = __esm({
16
- "../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.8.3/node_modules/tsup/assets/esm_shims.js"() {
16
+ "../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js"() {
17
17
  "use strict";
18
18
  }
19
19
  });
@@ -23,11 +23,152 @@ var mail_manager_exports = {};
23
23
  __export(mail_manager_exports, {
24
24
  createMailManagerArchive: () => createMailManagerArchive
25
25
  });
26
+ import {
27
+ CreateArchiveCommand,
28
+ GetArchiveCommand,
29
+ ListArchivesCommand,
30
+ MailManagerClient
31
+ } from "@aws-sdk/client-mailmanager";
32
+ import {
33
+ PutConfigurationSetArchivingOptionsCommand,
34
+ SESv2Client
35
+ } from "@aws-sdk/client-sesv2";
36
+ function retentionToAWSPeriod(retention) {
37
+ switch (retention) {
38
+ case "3months":
39
+ return "THREE_MONTHS";
40
+ case "6months":
41
+ return "SIX_MONTHS";
42
+ case "9months":
43
+ return "NINE_MONTHS";
44
+ case "1year":
45
+ return "ONE_YEAR";
46
+ case "18months":
47
+ return "EIGHTEEN_MONTHS";
48
+ case "2years":
49
+ return "TWO_YEARS";
50
+ case "30months":
51
+ return "THIRTY_MONTHS";
52
+ case "3years":
53
+ return "THREE_YEARS";
54
+ case "4years":
55
+ return "FOUR_YEARS";
56
+ case "5years":
57
+ return "FIVE_YEARS";
58
+ case "6years":
59
+ return "SIX_YEARS";
60
+ case "7years":
61
+ return "SEVEN_YEARS";
62
+ case "8years":
63
+ return "EIGHT_YEARS";
64
+ case "9years":
65
+ return "NINE_YEARS";
66
+ case "10years":
67
+ return "TEN_YEARS";
68
+ case "permanent":
69
+ return "PERMANENT";
70
+ default:
71
+ return "THREE_MONTHS";
72
+ }
73
+ }
26
74
  async function createMailManagerArchive(config) {
27
- void config;
28
- throw new Error(
29
- "Mail Manager Archive is not yet supported in Pulumi AWS provider. Email archiving with Mail Manager is coming soon."
30
- );
75
+ const region = config.region || process.env.AWS_REGION || "us-east-1";
76
+ const archiveName = `wraps-${config.name}-archive`;
77
+ const mailManagerClient = new MailManagerClient({ region });
78
+ const sesClient = new SESv2Client({ region });
79
+ const kmsKeyArn = config.kmsKeyArn;
80
+ if (!kmsKeyArn) {
81
+ }
82
+ const awsRetention = retentionToAWSPeriod(config.retention);
83
+ let archiveId;
84
+ let archiveArn;
85
+ try {
86
+ const listCommand = new ListArchivesCommand({});
87
+ const listResult = await mailManagerClient.send(listCommand);
88
+ const existingArchive = listResult.Archives?.find(
89
+ (archive) => archive.ArchiveName === archiveName
90
+ );
91
+ if (existingArchive?.ArchiveId) {
92
+ console.log(`Using existing Mail Manager archive: ${archiveName}`);
93
+ archiveId = existingArchive.ArchiveId;
94
+ const getCommand = new GetArchiveCommand({ ArchiveId: archiveId });
95
+ const getResult = await mailManagerClient.send(getCommand);
96
+ archiveArn = getResult.ArchiveArn;
97
+ }
98
+ } catch (error) {
99
+ console.log("Error checking for existing archive:", error);
100
+ }
101
+ if (!archiveId) {
102
+ try {
103
+ const createArchiveCommand = new CreateArchiveCommand({
104
+ ArchiveName: archiveName,
105
+ Retention: {
106
+ RetentionPeriod: awsRetention
107
+ },
108
+ ...kmsKeyArn && { KmsKeyArn: kmsKeyArn },
109
+ Tags: [
110
+ { Key: "ManagedBy", Value: "wraps-cli" },
111
+ { Key: "Name", Value: archiveName },
112
+ { Key: "Retention", Value: config.retention }
113
+ ]
114
+ });
115
+ const archiveResult = await mailManagerClient.send(createArchiveCommand);
116
+ archiveId = archiveResult.ArchiveId;
117
+ if (!archiveId) {
118
+ throw new Error(
119
+ "Failed to create Mail Manager Archive: No ArchiveId returned"
120
+ );
121
+ }
122
+ console.log(`Created new Mail Manager archive: ${archiveName}`);
123
+ } catch (error) {
124
+ if (error instanceof Error && error.name === "ConflictException" && error.message.includes("Archive already exists")) {
125
+ console.log(
126
+ "Archive was created concurrently, fetching existing archive..."
127
+ );
128
+ const listCommand = new ListArchivesCommand({});
129
+ const listResult = await mailManagerClient.send(listCommand);
130
+ const existingArchive = listResult.Archives?.find(
131
+ (archive) => archive.ArchiveName === archiveName
132
+ );
133
+ if (!existingArchive?.ArchiveId) {
134
+ throw new Error(
135
+ `Archive exists but couldn't find it: ${archiveName}`
136
+ );
137
+ }
138
+ archiveId = existingArchive.ArchiveId;
139
+ const getCommand = new GetArchiveCommand({ ArchiveId: archiveId });
140
+ const getResult = await mailManagerClient.send(getCommand);
141
+ archiveArn = getResult.ArchiveArn;
142
+ } else {
143
+ throw error;
144
+ }
145
+ }
146
+ }
147
+ if (!archiveArn) {
148
+ const identity = await import("@aws-sdk/client-sts").then(
149
+ (m) => new m.STSClient({ region }).send(new m.GetCallerIdentityCommand({}))
150
+ );
151
+ const accountId = identity.Account;
152
+ archiveArn = `arn:aws:ses:${region}:${accountId}:mailmanager-archive/${archiveId}`;
153
+ }
154
+ const configSetName = await new Promise((resolve) => {
155
+ config.configSetName.apply((name) => {
156
+ resolve(name);
157
+ });
158
+ });
159
+ const putArchivingOptionsCommand = new PutConfigurationSetArchivingOptionsCommand({
160
+ ConfigurationSetName: configSetName,
161
+ ArchiveArn: archiveArn
162
+ });
163
+ await sesClient.send(putArchivingOptionsCommand);
164
+ if (!(archiveId && archiveArn)) {
165
+ throw new Error("Failed to get archive ID or ARN");
166
+ }
167
+ return {
168
+ archiveId,
169
+ archiveArn,
170
+ kmsKeyArn
171
+ };
31
172
  }
32
173
  var init_mail_manager = __esm({
33
174
  "src/infrastructure/resources/mail-manager.ts"() {
@@ -239,13 +380,22 @@ var init_aws = __esm({
239
380
  function estimateStorageSize(emailsPerMonth, retention, numEventTypes = 8) {
240
381
  const avgRecordSizeKB = 2;
241
382
  const retentionMonths = {
242
- "7days": 0.25,
243
- "30days": 1,
244
- "90days": 3,
383
+ "3months": 3,
245
384
  "6months": 6,
385
+ "9months": 9,
246
386
  "1year": 12,
247
387
  "18months": 18,
248
- indefinite: 120
388
+ "2years": 24,
389
+ "30months": 30,
390
+ "3years": 36,
391
+ "4years": 48,
392
+ "5years": 60,
393
+ "6years": 72,
394
+ "7years": 84,
395
+ "8years": 96,
396
+ "9years": 108,
397
+ "10years": 120,
398
+ permanent: 120
249
399
  // Assume 10 years for cost estimation
250
400
  }[retention];
251
401
  const totalKB = emailsPerMonth * numEventTypes * (retentionMonths ?? 12) * avgRecordSizeKB;
@@ -254,13 +404,22 @@ function estimateStorageSize(emailsPerMonth, retention, numEventTypes = 8) {
254
404
  function estimateArchiveStorageSize(emailsPerMonth, retention) {
255
405
  const avgEmailSizeKB = 50;
256
406
  const retentionMonths = {
257
- "7days": 0.25,
258
- "30days": 1,
259
- "90days": 3,
407
+ "3months": 3,
260
408
  "6months": 6,
409
+ "9months": 9,
261
410
  "1year": 12,
262
411
  "18months": 18,
263
- indefinite: 120
412
+ "2years": 24,
413
+ "30months": 30,
414
+ "3years": 36,
415
+ "4years": 48,
416
+ "5years": 60,
417
+ "6years": 72,
418
+ "7years": 84,
419
+ "8years": 96,
420
+ "9years": 108,
421
+ "10years": 120,
422
+ permanent: 120
264
423
  // Assume 10 years for cost estimation
265
424
  }[retention];
266
425
  const totalKB = emailsPerMonth * (retentionMonths ?? 12) * avgEmailSizeKB;
@@ -300,7 +459,7 @@ function calculateDynamoDBCost(config, emailsPerMonth) {
300
459
  if (!config.eventTracking?.dynamoDBHistory) {
301
460
  return;
302
461
  }
303
- const retention = config.eventTracking.archiveRetention || "90days";
462
+ const retention = config.eventTracking.archiveRetention || "3months";
304
463
  const numEventTypes = config.eventTracking.events?.length || 8;
305
464
  const totalEvents = emailsPerMonth * numEventTypes;
306
465
  const writeCost = Math.max(0, totalEvents - FREE_TIER.DYNAMODB_WRITES) / 1e6 * AWS_PRICING.DYNAMODB_WRITE_PER_MILLION;
@@ -561,7 +720,7 @@ function getPresetInfo(preset) {
561
720
  "Everything in Starter",
562
721
  "Reputation metrics dashboard",
563
722
  "Real-time event tracking (EventBridge)",
564
- "90-day email history storage",
723
+ "3-month email history storage",
565
724
  "Optional: Email archiving with rendered viewer",
566
725
  "Complete event visibility"
567
726
  ]
@@ -630,9 +789,9 @@ function validateConfig(config) {
630
789
  "\u{1F4A1} Event tracking is enabled but history storage is disabled. Events will only be available in real-time."
631
790
  );
632
791
  }
633
- if (config.eventTracking?.archiveRetention === "indefinite") {
792
+ if (config.eventTracking?.archiveRetention === "permanent") {
634
793
  warnings.push(
635
- "\u26A0\uFE0F Indefinite retention can become expensive. Consider 90-day or 1-year retention."
794
+ "\u26A0\uFE0F Permanent retention can become expensive. Consider 3-month or 1-year retention."
636
795
  );
637
796
  }
638
797
  return warnings;
@@ -661,7 +820,7 @@ var init_presets = __esm({
661
820
  // Email archiving disabled by default
662
821
  emailArchiving: {
663
822
  enabled: false,
664
- retention: "30days"
823
+ retention: "3months"
665
824
  },
666
825
  sendingEnabled: true
667
826
  };
@@ -691,13 +850,13 @@ var init_presets = __esm({
691
850
  "RENDERING_FAILURE"
692
851
  ],
693
852
  dynamoDBHistory: true,
694
- archiveRetention: "90days"
853
+ archiveRetention: "3months"
695
854
  },
696
- // Email archiving with 90-day retention
855
+ // Email archiving with 3-month retention
697
856
  emailArchiving: {
698
857
  enabled: false,
699
858
  // User can opt-in
700
- retention: "90days"
859
+ retention: "3months"
701
860
  },
702
861
  sendingEnabled: true
703
862
  };
@@ -1125,7 +1284,7 @@ async function promptEmailArchiving() {
1125
1284
  process.exit(0);
1126
1285
  }
1127
1286
  if (!enabled) {
1128
- return { enabled: false, retention: "90days" };
1287
+ return { enabled: false, retention: "3months" };
1129
1288
  }
1130
1289
  const retention = await clack3.select({
1131
1290
  message: "Email archive retention period:",
@@ -1133,7 +1292,7 @@ async function promptEmailArchiving() {
1133
1292
  { value: "7days", label: "7 days", hint: "~$1-2/mo for 10k emails" },
1134
1293
  { value: "30days", label: "30 days", hint: "~$2-4/mo for 10k emails" },
1135
1294
  {
1136
- value: "90days",
1295
+ value: "3months",
1137
1296
  label: "90 days (recommended)",
1138
1297
  hint: "~$5-10/mo for 10k emails"
1139
1298
  },
@@ -1149,7 +1308,7 @@ async function promptEmailArchiving() {
1149
1308
  hint: "~$35-60/mo for 10k emails"
1150
1309
  }
1151
1310
  ],
1152
- initialValue: "90days"
1311
+ initialValue: "3months"
1153
1312
  });
1154
1313
  if (clack3.isCancel(retention)) {
1155
1314
  clack3.cancel("Operation cancelled.");
@@ -1188,7 +1347,7 @@ async function promptCustomConfig() {
1188
1347
  process.exit(0);
1189
1348
  }
1190
1349
  let dynamoDBHistory = false;
1191
- let archiveRetention = "90days";
1350
+ let archiveRetention = "3months";
1192
1351
  if (eventTrackingEnabled) {
1193
1352
  dynamoDBHistory = await clack3.confirm({
1194
1353
  message: "Store email history in DynamoDB?",
@@ -1205,7 +1364,7 @@ async function promptCustomConfig() {
1205
1364
  { value: "7days", label: "7 days", hint: "Minimal storage cost" },
1206
1365
  { value: "30days", label: "30 days", hint: "Development/testing" },
1207
1366
  {
1208
- value: "90days",
1367
+ value: "3months",
1209
1368
  label: "90 days (recommended)",
1210
1369
  hint: "Standard retention"
1211
1370
  },
@@ -1255,7 +1414,7 @@ async function promptCustomConfig() {
1255
1414
  clack3.cancel("Operation cancelled.");
1256
1415
  process.exit(0);
1257
1416
  }
1258
- let emailArchiveRetention = "90days";
1417
+ let emailArchiveRetention = "3months";
1259
1418
  if (emailArchivingEnabled) {
1260
1419
  emailArchiveRetention = await clack3.select({
1261
1420
  message: "Email archive retention period:",
@@ -1263,7 +1422,7 @@ async function promptCustomConfig() {
1263
1422
  { value: "7days", label: "7 days", hint: "~$1-2/mo for 10k emails" },
1264
1423
  { value: "30days", label: "30 days", hint: "~$2-4/mo for 10k emails" },
1265
1424
  {
1266
- value: "90days",
1425
+ value: "3months",
1267
1426
  label: "90 days (recommended)",
1268
1427
  hint: "~$5-10/mo for 10k emails"
1269
1428
  },
@@ -1279,7 +1438,7 @@ async function promptCustomConfig() {
1279
1438
  hint: "~$35-60/mo for 10k emails"
1280
1439
  }
1281
1440
  ],
1282
- initialValue: "90days"
1441
+ initialValue: "3months"
1283
1442
  });
1284
1443
  if (clack3.isCancel(emailArchiveRetention)) {
1285
1444
  clack3.cancel("Operation cancelled.");
@@ -1320,12 +1479,12 @@ async function promptCustomConfig() {
1320
1479
  "RENDERING_FAILURE"
1321
1480
  ],
1322
1481
  dynamoDBHistory: Boolean(dynamoDBHistory),
1323
- archiveRetention: typeof archiveRetention === "string" ? archiveRetention : "90days"
1482
+ archiveRetention: typeof archiveRetention === "string" ? archiveRetention : "3months"
1324
1483
  } : { enabled: false },
1325
1484
  emailArchiving: emailArchivingEnabled ? {
1326
1485
  enabled: true,
1327
- retention: typeof emailArchiveRetention === "string" ? emailArchiveRetention : "90days"
1328
- } : { enabled: false, retention: "90days" },
1486
+ retention: typeof emailArchiveRetention === "string" ? emailArchiveRetention : "3months"
1487
+ } : { enabled: false, retention: "3months" },
1329
1488
  dedicatedIp,
1330
1489
  sendingEnabled: true
1331
1490
  };
@@ -1373,13 +1532,114 @@ var init_assume_role = __esm({
1373
1532
  // src/utils/archive.ts
1374
1533
  import {
1375
1534
  GetArchiveMessageCommand,
1376
- MailManagerClient
1535
+ GetArchiveSearchResultsCommand,
1536
+ MailManagerClient as MailManagerClient2,
1537
+ StartArchiveSearchCommand
1377
1538
  } from "@aws-sdk/client-mailmanager";
1539
+ import DOMPurify from "isomorphic-dompurify";
1378
1540
  import { simpleParser } from "mailparser";
1379
- async function getArchivedEmail(_archiveId, messageId, region) {
1380
- const client = new MailManagerClient({ region });
1541
+ function extractArchiveId(archiveArnOrId) {
1542
+ if (archiveArnOrId.startsWith("arn:")) {
1543
+ const parts = archiveArnOrId.split("/");
1544
+ return parts.at(-1);
1545
+ }
1546
+ return archiveArnOrId;
1547
+ }
1548
+ async function getArchivedEmail(archiveArnOrId, searchCriteria, region) {
1549
+ const client = new MailManagerClient2({ region });
1550
+ const archiveId = extractArchiveId(archiveArnOrId);
1551
+ const searchTime = searchCriteria.timestamp || /* @__PURE__ */ new Date();
1552
+ const dayBefore = new Date(searchTime.getTime() - 24 * 60 * 60 * 1e3);
1553
+ const dayAfter = new Date(searchTime.getTime() + 24 * 60 * 60 * 1e3);
1554
+ const filters = [];
1555
+ if (searchCriteria.from) {
1556
+ filters.push({
1557
+ StringExpression: {
1558
+ Evaluate: {
1559
+ Attribute: "FROM"
1560
+ },
1561
+ Operator: "CONTAINS",
1562
+ Values: [searchCriteria.from]
1563
+ }
1564
+ });
1565
+ }
1566
+ if (searchCriteria.to) {
1567
+ filters.push({
1568
+ StringExpression: {
1569
+ Evaluate: {
1570
+ Attribute: "TO"
1571
+ },
1572
+ Operator: "CONTAINS",
1573
+ Values: [searchCriteria.to]
1574
+ }
1575
+ });
1576
+ }
1577
+ if (searchCriteria.subject) {
1578
+ filters.push({
1579
+ StringExpression: {
1580
+ Evaluate: {
1581
+ Attribute: "SUBJECT"
1582
+ },
1583
+ Operator: "CONTAINS",
1584
+ Values: [searchCriteria.subject]
1585
+ }
1586
+ });
1587
+ }
1588
+ if (filters.length === 0) {
1589
+ throw new Error(
1590
+ "At least one search criterion (from, to, or subject) is required"
1591
+ );
1592
+ }
1593
+ const searchCommand = new StartArchiveSearchCommand({
1594
+ ArchiveId: archiveId,
1595
+ FromTimestamp: dayBefore,
1596
+ ToTimestamp: dayAfter,
1597
+ Filters: {
1598
+ Include: filters
1599
+ },
1600
+ MaxResults: 10
1601
+ // Get a few results in case there are multiple matches
1602
+ });
1603
+ const searchResponse = await client.send(searchCommand);
1604
+ const searchId = searchResponse.SearchId;
1605
+ if (!searchId) {
1606
+ throw new Error("Failed to start archive search");
1607
+ }
1608
+ let archivedMessageId;
1609
+ let attempts = 0;
1610
+ const maxAttempts = 20;
1611
+ const pollInterval = 1e3;
1612
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1613
+ while (attempts < maxAttempts) {
1614
+ try {
1615
+ const resultsCommand = new GetArchiveSearchResultsCommand({
1616
+ SearchId: searchId
1617
+ });
1618
+ const resultsResponse = await client.send(resultsCommand);
1619
+ if (resultsResponse.Rows && resultsResponse.Rows.length > 0) {
1620
+ archivedMessageId = resultsResponse.Rows[0].ArchivedMessageId;
1621
+ break;
1622
+ }
1623
+ if (resultsResponse.Rows && resultsResponse.Rows.length === 0) {
1624
+ break;
1625
+ }
1626
+ } catch (error) {
1627
+ if (error instanceof Error && error.name === "ConflictException" && error.message.includes("still in progress")) {
1628
+ console.log(`Search still in progress, attempt ${attempts + 1}...`);
1629
+ } else {
1630
+ throw error;
1631
+ }
1632
+ }
1633
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
1634
+ attempts++;
1635
+ }
1636
+ if (!archivedMessageId) {
1637
+ throw new Error(
1638
+ "Email not found in archive with the provided search criteria. It may have been sent before archiving was enabled."
1639
+ );
1640
+ }
1381
1641
  const command2 = new GetArchiveMessageCommand({
1382
- ArchivedMessageId: messageId
1642
+ ArchivedMessageId: archivedMessageId
1383
1643
  });
1384
1644
  const response = await client.send(command2);
1385
1645
  if (!response.MessageDownloadLink) {
@@ -1418,8 +1678,7 @@ async function getArchivedEmail(_archiveId, messageId, region) {
1418
1678
  return addr.text || "";
1419
1679
  };
1420
1680
  return {
1421
- messageId,
1422
- // Use the input messageId since response may not have MessageMetadata
1681
+ messageId: parsed.messageId || headers["message-id"]?.toString() || "",
1423
1682
  from: getAddressText(parsed.from),
1424
1683
  to: getAddressText(parsed.to),
1425
1684
  subject: parsed.subject || "",
@@ -1446,14 +1705,20 @@ __export(email_archive_exports, {
1446
1705
  fetchArchivedEmail: () => fetchArchivedEmail
1447
1706
  });
1448
1707
  async function fetchArchivedEmail(messageId, options) {
1449
- const { region, archiveArn } = options;
1708
+ const { region, archiveArn, from, to, subject, timestamp } = options;
1450
1709
  try {
1451
1710
  console.log("Fetching archived email:", {
1452
1711
  messageId,
1453
1712
  archiveArn,
1454
1713
  region
1455
1714
  });
1456
- const email = await getArchivedEmail(archiveArn, messageId, region);
1715
+ const searchCriteria = {
1716
+ from,
1717
+ to,
1718
+ subject,
1719
+ timestamp
1720
+ };
1721
+ const email = await getArchivedEmail(archiveArn, searchCriteria, region);
1457
1722
  console.log("Archived email fetched successfully:", {
1458
1723
  messageId: email.messageId,
1459
1724
  hasHtml: !!email.html,
@@ -1880,11 +2145,18 @@ async function createIAMRole(config) {
1880
2145
  statements.push({
1881
2146
  Effect: "Allow",
1882
2147
  Action: [
1883
- "ses:GetArchive",
2148
+ // Archive search operations
2149
+ "ses:StartArchiveSearch",
2150
+ "ses:GetArchiveSearchResults",
2151
+ // Archive message retrieval
1884
2152
  "ses:GetArchiveMessage",
1885
2153
  "ses:GetArchiveMessageContent",
1886
- "ses:SearchArchive",
1887
- "ses:StartArchiveExport"
2154
+ // Archive metadata
2155
+ "ses:GetArchive",
2156
+ "ses:ListArchives",
2157
+ // Archive export (for future use)
2158
+ "ses:StartArchiveExport",
2159
+ "ses:GetArchiveExport"
1888
2160
  ],
1889
2161
  Resource: "arn:aws:ses:*:*:mailmanager-archive/*"
1890
2162
  });
@@ -2271,7 +2543,8 @@ async function deployEmailStack(config) {
2271
2543
  archiveResources = await createMailManagerArchive2({
2272
2544
  name: "email",
2273
2545
  retention: emailConfig.emailArchiving.retention,
2274
- configSetName: sesResources.configSet.configurationSetName
2546
+ configSetName: sesResources.configSet.configurationSetName,
2547
+ region: config.region
2275
2548
  });
2276
2549
  }
2277
2550
  return {
@@ -2288,7 +2561,8 @@ async function deployEmailStack(config) {
2288
2561
  dlqUrl: sqsResources?.dlq.url,
2289
2562
  customTrackingDomain: sesResources?.customTrackingDomain,
2290
2563
  mailFromDomain: sesResources?.mailFromDomain,
2291
- archiveArn: archiveResources?.archive.arn,
2564
+ archiveId: archiveResources?.archiveId,
2565
+ archiveArn: archiveResources?.archiveArn,
2292
2566
  archivingEnabled: emailConfig.emailArchiving?.enabled,
2293
2567
  archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0
2294
2568
  };
@@ -2606,11 +2880,11 @@ ${domainStrings.join("\n")}`);
2606
2880
  const retentionLabel = {
2607
2881
  "7days": "7 days",
2608
2882
  "30days": "30 days",
2609
- "90days": "90 days",
2883
+ "3months": "90 days",
2610
2884
  "6months": "6 months",
2611
2885
  "1year": "1 year",
2612
2886
  "18months": "18 months"
2613
- }[status2.resources.archiveRetention || "90days"] || "90 days";
2887
+ }[status2.resources.archiveRetention || "3months"] || "90 days";
2614
2888
  featureLines.push(
2615
2889
  ` ${pc2.green("\u2713")} Email Archiving ${pc2.dim(`(${retentionLabel} retention)`)}`
2616
2890
  );
@@ -3199,7 +3473,7 @@ import { Router as createRouter } from "express";
3199
3473
  init_esm_shims();
3200
3474
  init_assume_role();
3201
3475
  import { GetSendQuotaCommand, SESClient as SESClient3 } from "@aws-sdk/client-ses";
3202
- import { GetEmailIdentityCommand, SESv2Client } from "@aws-sdk/client-sesv2";
3476
+ import { GetEmailIdentityCommand, SESv2Client as SESv2Client2 } from "@aws-sdk/client-sesv2";
3203
3477
  async function fetchSendQuota(roleArn, region) {
3204
3478
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
3205
3479
  const ses = new SESClient3({ region, credentials });
@@ -3212,7 +3486,7 @@ async function fetchSendQuota(roleArn, region) {
3212
3486
  }
3213
3487
  async function fetchDomainInfo(roleArn, region, domain) {
3214
3488
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
3215
- const sesv22 = new SESv2Client({ region, credentials });
3489
+ const sesv22 = new SESv2Client2({ region, credentials });
3216
3490
  const response = await sesv22.send(
3217
3491
  new GetEmailIdentityCommand({
3218
3492
  EmailIdentity: domain
@@ -3631,11 +3905,33 @@ function createEmailsRouter(config) {
3631
3905
  error: "Archive ARN not configured."
3632
3906
  });
3633
3907
  }
3908
+ if (!config.tableName) {
3909
+ console.log("No table name configured");
3910
+ return res.status(400).json({
3911
+ error: "Email tracking not enabled. Need email metadata to search archive."
3912
+ });
3913
+ }
3914
+ console.log("Fetching email metadata from DynamoDB...");
3915
+ const emailDetails = await fetchEmailById(id, {
3916
+ region: config.region,
3917
+ tableName: config.tableName
3918
+ });
3919
+ if (!emailDetails) {
3920
+ console.log("Email metadata not found in DynamoDB for ID:", id);
3921
+ return res.status(404).json({
3922
+ error: "Email metadata not found. Cannot search archive."
3923
+ });
3924
+ }
3634
3925
  console.log("Fetching archived email from Mail Manager...");
3635
3926
  const { fetchArchivedEmail: fetchArchivedEmail2 } = await Promise.resolve().then(() => (init_email_archive(), email_archive_exports));
3636
3927
  const archivedEmail = await fetchArchivedEmail2(id, {
3637
3928
  region: config.region,
3638
- archiveArn: config.archiveArn
3929
+ archiveArn: config.archiveArn,
3930
+ from: emailDetails.from,
3931
+ to: emailDetails.to[0],
3932
+ // Use first recipient for search
3933
+ subject: emailDetails.subject,
3934
+ timestamp: new Date(emailDetails.sentAt)
3639
3935
  });
3640
3936
  if (!archivedEmail) {
3641
3937
  console.log("Archived email not found for message ID:", id);
@@ -3882,11 +4178,11 @@ init_assume_role();
3882
4178
  import {
3883
4179
  GetConfigurationSetCommand,
3884
4180
  GetEmailIdentityCommand as GetEmailIdentityCommand2,
3885
- SESv2Client as SESv2Client2
4181
+ SESv2Client as SESv2Client3
3886
4182
  } from "@aws-sdk/client-sesv2";
3887
4183
  async function fetchConfigurationSet(roleArn, region, configSetName) {
3888
4184
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
3889
- const sesv22 = new SESv2Client2({ region, credentials });
4185
+ const sesv22 = new SESv2Client3({ region, credentials });
3890
4186
  const response = await sesv22.send(
3891
4187
  new GetConfigurationSetCommand({
3892
4188
  ConfigurationSetName: configSetName
@@ -3916,7 +4212,7 @@ async function fetchConfigurationSet(roleArn, region, configSetName) {
3916
4212
  }
3917
4213
  async function fetchEmailIdentity(roleArn, region, identityName) {
3918
4214
  const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
3919
- const sesv22 = new SESv2Client2({ region, credentials });
4215
+ const sesv22 = new SESv2Client3({ region, credentials });
3920
4216
  const response = await sesv22.send(
3921
4217
  new GetEmailIdentityCommand2({
3922
4218
  EmailIdentity: identityName
@@ -4108,10 +4404,10 @@ function createSettingsRouter(config) {
4108
4404
  console.log(
4109
4405
  `[Settings] Updating sending options for ${configSetName}: ${enabled}`
4110
4406
  );
4111
- const { SESv2Client: SESv2Client4, PutConfigurationSetSendingOptionsCommand } = await import("@aws-sdk/client-sesv2");
4407
+ const { SESv2Client: SESv2Client5, PutConfigurationSetSendingOptionsCommand } = await import("@aws-sdk/client-sesv2");
4112
4408
  const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
4113
4409
  const credentials = config.roleArn ? await assumeRole2(config.roleArn, config.region) : void 0;
4114
- const sesClient = new SESv2Client4({ region: config.region, credentials });
4410
+ const sesClient = new SESv2Client5({ region: config.region, credentials });
4115
4411
  await sesClient.send(
4116
4412
  new PutConfigurationSetSendingOptionsCommand({
4117
4413
  ConfigurationSetName: configSetName,
@@ -4145,10 +4441,10 @@ function createSettingsRouter(config) {
4145
4441
  console.log(
4146
4442
  `[Settings] Updating reputation options for ${configSetName}: ${enabled}`
4147
4443
  );
4148
- const { SESv2Client: SESv2Client4, PutConfigurationSetReputationOptionsCommand } = await import("@aws-sdk/client-sesv2");
4444
+ const { SESv2Client: SESv2Client5, PutConfigurationSetReputationOptionsCommand } = await import("@aws-sdk/client-sesv2");
4149
4445
  const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
4150
4446
  const credentials = config.roleArn ? await assumeRole2(config.roleArn, config.region) : void 0;
4151
- const sesClient = new SESv2Client4({ region: config.region, credentials });
4447
+ const sesClient = new SESv2Client5({ region: config.region, credentials });
4152
4448
  await sesClient.send(
4153
4449
  new PutConfigurationSetReputationOptionsCommand({
4154
4450
  ConfigurationSetName: configSetName,
@@ -4188,10 +4484,10 @@ function createSettingsRouter(config) {
4188
4484
  console.log(
4189
4485
  `[Settings] Updating tracking domain for ${configSetName}: ${domain}`
4190
4486
  );
4191
- const { SESv2Client: SESv2Client4, PutConfigurationSetTrackingOptionsCommand } = await import("@aws-sdk/client-sesv2");
4487
+ const { SESv2Client: SESv2Client5, PutConfigurationSetTrackingOptionsCommand } = await import("@aws-sdk/client-sesv2");
4192
4488
  const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
4193
4489
  const credentials = config.roleArn ? await assumeRole2(config.roleArn, config.region) : void 0;
4194
- const sesClient = new SESv2Client4({
4490
+ const sesClient = new SESv2Client5({
4195
4491
  region: config.region,
4196
4492
  credentials
4197
4493
  });
@@ -4282,6 +4578,26 @@ async function startConsoleServer(config) {
4282
4578
  const app = express();
4283
4579
  const authToken = crypto.randomBytes(32).toString("hex");
4284
4580
  app.use(express.json());
4581
+ const requestCounts = /* @__PURE__ */ new Map();
4582
+ const RATE_LIMIT_WINDOW = 60 * 1e3;
4583
+ const RATE_LIMIT_MAX_REQUESTS = 1e3;
4584
+ app.use((req, res, next) => {
4585
+ const ip = req.ip || req.socket.remoteAddress || "unknown";
4586
+ const now = Date.now();
4587
+ const record = requestCounts.get(ip);
4588
+ if (!record || now > record.resetTime) {
4589
+ requestCounts.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
4590
+ next();
4591
+ } else if (record.count < RATE_LIMIT_MAX_REQUESTS) {
4592
+ record.count++;
4593
+ next();
4594
+ } else {
4595
+ res.status(429).json({
4596
+ error: "Too many requests, please slow down",
4597
+ retryAfter: Math.ceil((record.resetTime - now) / 1e3)
4598
+ });
4599
+ }
4600
+ });
4285
4601
  app.use((_req, res, next) => {
4286
4602
  res.setHeader("X-Frame-Options", "DENY");
4287
4603
  res.setHeader("X-Content-Type-Options", "nosniff");
@@ -4819,8 +5135,8 @@ Run ${pc9.cyan("wraps init")} to deploy infrastructure.
4819
5135
  process.exit(1);
4820
5136
  }
4821
5137
  const domains = await listSESDomains(region);
4822
- const { SESv2Client: SESv2Client4, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
4823
- const sesv2Client = new SESv2Client4({ region });
5138
+ const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
5139
+ const sesv2Client = new SESv2Client5({ region });
4824
5140
  const domainsWithTokens = await Promise.all(
4825
5141
  domains.map(async (d) => {
4826
5142
  try {
@@ -4981,7 +5297,11 @@ ${pc10.bold("Current Configuration:")}
4981
5297
  lambdaFunctions: result.lambdaFunctions,
4982
5298
  domain: result.domain,
4983
5299
  dkimTokens: result.dkimTokens,
4984
- customTrackingDomain: result.customTrackingDomain
5300
+ customTrackingDomain: result.customTrackingDomain,
5301
+ mailFromDomain: result.mailFromDomain,
5302
+ archiveArn: result.archiveArn,
5303
+ archivingEnabled: result.archivingEnabled,
5304
+ archiveRetention: result.archiveRetention
4985
5305
  };
4986
5306
  }
4987
5307
  },
@@ -5008,7 +5328,11 @@ ${pc10.bold("Current Configuration:")}
5008
5328
  lambdaFunctions: pulumiOutputs.lambdaFunctions?.value,
5009
5329
  domain: pulumiOutputs.domain?.value,
5010
5330
  dkimTokens: pulumiOutputs.dkimTokens?.value,
5011
- customTrackingDomain: pulumiOutputs.customTrackingDomain?.value
5331
+ customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
5332
+ mailFromDomain: pulumiOutputs.mailFromDomain?.value,
5333
+ archiveArn: pulumiOutputs.archiveArn?.value,
5334
+ archivingEnabled: pulumiOutputs.archivingEnabled?.value,
5335
+ archiveRetention: pulumiOutputs.archiveRetention?.value
5012
5336
  };
5013
5337
  }
5014
5338
  );
@@ -5119,7 +5443,7 @@ ${pc11.bold("Current Configuration:")}
5119
5443
  console.log(` ${pc11.green("\u2713")} Event Tracking (EventBridge)`);
5120
5444
  if (config.eventTracking.dynamoDBHistory) {
5121
5445
  console.log(
5122
- ` ${pc11.dim("\u2514\u2500")} Email History: ${pc11.cyan(config.eventTracking.archiveRetention || "90days")}`
5446
+ ` ${pc11.dim("\u2514\u2500")} Email History: ${pc11.cyan(config.eventTracking.archiveRetention || "3months")}`
5123
5447
  );
5124
5448
  }
5125
5449
  }
@@ -5128,14 +5452,23 @@ ${pc11.bold("Current Configuration:")}
5128
5452
  }
5129
5453
  if (config.emailArchiving?.enabled) {
5130
5454
  const retentionLabel = {
5131
- "7days": "7 days",
5132
- "30days": "30 days",
5133
- "90days": "90 days",
5455
+ "3months": "3 months",
5134
5456
  "6months": "6 months",
5457
+ "9months": "9 months",
5135
5458
  "1year": "1 year",
5136
5459
  "18months": "18 months",
5137
- indefinite: "indefinite"
5138
- }[config.emailArchiving.retention] || "90 days";
5460
+ "2years": "2 years",
5461
+ "30months": "30 months",
5462
+ "3years": "3 years",
5463
+ "4years": "4 years",
5464
+ "5years": "5 years",
5465
+ "6years": "6 years",
5466
+ "7years": "7 years",
5467
+ "8years": "8 years",
5468
+ "9years": "9 years",
5469
+ "10years": "10 years",
5470
+ permanent: "permanent"
5471
+ }[config.emailArchiving.retention] || "3 months";
5139
5472
  console.log(` ${pc11.green("\u2713")} Email Archiving (${retentionLabel})`);
5140
5473
  }
5141
5474
  const currentCostData = calculateCosts(config, 5e4);
@@ -5265,18 +5598,8 @@ ${pc11.bold("Current Configuration:")}
5265
5598
  message: "Email archive retention period:",
5266
5599
  options: [
5267
5600
  {
5268
- value: "7days",
5269
- label: "7 days",
5270
- hint: "~$1-2/mo for 10k emails"
5271
- },
5272
- {
5273
- value: "30days",
5274
- label: "30 days",
5275
- hint: "~$2-4/mo for 10k emails"
5276
- },
5277
- {
5278
- value: "90days",
5279
- label: "90 days (recommended)",
5601
+ value: "3months",
5602
+ label: "3 months (minimum)",
5280
5603
  hint: "~$5-10/mo for 10k emails"
5281
5604
  },
5282
5605
  {
@@ -5286,13 +5609,38 @@ ${pc11.bold("Current Configuration:")}
5286
5609
  },
5287
5610
  {
5288
5611
  value: "1year",
5289
- label: "1 year",
5612
+ label: "1 year (recommended)",
5290
5613
  hint: "~$25-40/mo for 10k emails"
5291
5614
  },
5292
5615
  {
5293
- value: "18months",
5294
- label: "18 months",
5295
- hint: "~$35-60/mo for 10k emails"
5616
+ value: "2years",
5617
+ label: "2 years",
5618
+ hint: "~$50-80/mo for 10k emails"
5619
+ },
5620
+ {
5621
+ value: "3years",
5622
+ label: "3 years",
5623
+ hint: "~$75-120/mo for 10k emails"
5624
+ },
5625
+ {
5626
+ value: "5years",
5627
+ label: "5 years",
5628
+ hint: "~$125-200/mo for 10k emails"
5629
+ },
5630
+ {
5631
+ value: "7years",
5632
+ label: "7 years",
5633
+ hint: "~$175-280/mo for 10k emails"
5634
+ },
5635
+ {
5636
+ value: "10years",
5637
+ label: "10 years",
5638
+ hint: "~$250-400/mo for 10k emails"
5639
+ },
5640
+ {
5641
+ value: "permanent",
5642
+ label: "Permanent",
5643
+ hint: "Expensive, not recommended"
5296
5644
  }
5297
5645
  ],
5298
5646
  initialValue: config.emailArchiving.retention
@@ -5326,18 +5674,8 @@ ${pc11.bold("Current Configuration:")}
5326
5674
  message: "Email archive retention period:",
5327
5675
  options: [
5328
5676
  {
5329
- value: "7days",
5330
- label: "7 days",
5331
- hint: "~$1-2/mo for 10k emails"
5332
- },
5333
- {
5334
- value: "30days",
5335
- label: "30 days",
5336
- hint: "~$2-4/mo for 10k emails"
5337
- },
5338
- {
5339
- value: "90days",
5340
- label: "90 days (recommended)",
5677
+ value: "3months",
5678
+ label: "3 months (minimum)",
5341
5679
  hint: "~$5-10/mo for 10k emails"
5342
5680
  },
5343
5681
  {
@@ -5347,16 +5685,41 @@ ${pc11.bold("Current Configuration:")}
5347
5685
  },
5348
5686
  {
5349
5687
  value: "1year",
5350
- label: "1 year",
5688
+ label: "1 year (recommended)",
5351
5689
  hint: "~$25-40/mo for 10k emails"
5352
5690
  },
5353
5691
  {
5354
- value: "18months",
5355
- label: "18 months",
5356
- hint: "~$35-60/mo for 10k emails"
5692
+ value: "2years",
5693
+ label: "2 years",
5694
+ hint: "~$50-80/mo for 10k emails"
5695
+ },
5696
+ {
5697
+ value: "3years",
5698
+ label: "3 years",
5699
+ hint: "~$75-120/mo for 10k emails"
5700
+ },
5701
+ {
5702
+ value: "5years",
5703
+ label: "5 years",
5704
+ hint: "~$125-200/mo for 10k emails"
5705
+ },
5706
+ {
5707
+ value: "7years",
5708
+ label: "7 years",
5709
+ hint: "~$175-280/mo for 10k emails"
5710
+ },
5711
+ {
5712
+ value: "10years",
5713
+ label: "10 years",
5714
+ hint: "~$250-400/mo for 10k emails"
5715
+ },
5716
+ {
5717
+ value: "permanent",
5718
+ label: "Permanent",
5719
+ hint: "Expensive, not recommended"
5357
5720
  }
5358
5721
  ],
5359
- initialValue: "90days"
5722
+ initialValue: "3months"
5360
5723
  });
5361
5724
  if (clack11.isCancel(retention)) {
5362
5725
  clack11.cancel("Upgrade cancelled.");
@@ -5443,11 +5806,9 @@ ${pc11.bold("Current Configuration:")}
5443
5806
  const retention = await clack11.select({
5444
5807
  message: "Email history retention period (event data in DynamoDB):",
5445
5808
  options: [
5446
- { value: "7days", label: "7 days", hint: "Minimal storage cost" },
5447
- { value: "30days", label: "30 days", hint: "Development/testing" },
5448
5809
  {
5449
- value: "90days",
5450
- label: "90 days (recommended)",
5810
+ value: "3months",
5811
+ label: "3 months (recommended)",
5451
5812
  hint: "Standard retention"
5452
5813
  },
5453
5814
  {
@@ -5460,9 +5821,19 @@ ${pc11.bold("Current Configuration:")}
5460
5821
  value: "18months",
5461
5822
  label: "18 months",
5462
5823
  hint: "Long-term retention"
5824
+ },
5825
+ {
5826
+ value: "2years",
5827
+ label: "2 years",
5828
+ hint: "Extended compliance"
5829
+ },
5830
+ {
5831
+ value: "permanent",
5832
+ label: "Permanent",
5833
+ hint: "Not recommended (expensive)"
5463
5834
  }
5464
5835
  ],
5465
- initialValue: config.eventTracking?.archiveRetention || "90days"
5836
+ initialValue: config.eventTracking?.archiveRetention || "3months"
5466
5837
  });
5467
5838
  if (clack11.isCancel(retention)) {
5468
5839
  clack11.cancel("Upgrade cancelled.");
@@ -5728,14 +6099,14 @@ ${pc11.green("\u2713")} ${pc11.bold("Upgrade complete!")}
5728
6099
  init_esm_shims();
5729
6100
  init_aws();
5730
6101
  import { Resolver } from "dns/promises";
5731
- import { GetEmailIdentityCommand as GetEmailIdentityCommand3, SESv2Client as SESv2Client3 } from "@aws-sdk/client-sesv2";
6102
+ import { GetEmailIdentityCommand as GetEmailIdentityCommand3, SESv2Client as SESv2Client4 } from "@aws-sdk/client-sesv2";
5732
6103
  import * as clack12 from "@clack/prompts";
5733
6104
  import pc12 from "picocolors";
5734
6105
  async function verify(options) {
5735
6106
  clack12.intro(pc12.bold(`Verifying ${options.domain}`));
5736
6107
  const progress = new DeploymentProgress();
5737
6108
  const region = await getAWSRegion();
5738
- const sesClient = new SESv2Client3({ region });
6109
+ const sesClient = new SESv2Client4({ region });
5739
6110
  let identity;
5740
6111
  let dkimTokens = [];
5741
6112
  let mailFromDomain;