@wraps.dev/cli 1.0.0 → 1.1.2
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/README.md +2 -2
- package/dist/cli.js +995 -220
- package/dist/cli.js.map +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,356 @@ var init_esm_shims = __esm({
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
// src/utils/email/route53.ts
|
|
22
|
+
var route53_exports = {};
|
|
23
|
+
__export(route53_exports, {
|
|
24
|
+
createDNSRecords: () => createDNSRecords,
|
|
25
|
+
findHostedZone: () => findHostedZone
|
|
26
|
+
});
|
|
27
|
+
import {
|
|
28
|
+
ChangeResourceRecordSetsCommand,
|
|
29
|
+
ListHostedZonesByNameCommand,
|
|
30
|
+
Route53Client
|
|
31
|
+
} from "@aws-sdk/client-route-53";
|
|
32
|
+
async function findHostedZone(domain, region) {
|
|
33
|
+
const client = new Route53Client({ region });
|
|
34
|
+
try {
|
|
35
|
+
const response = await client.send(
|
|
36
|
+
new ListHostedZonesByNameCommand({
|
|
37
|
+
DNSName: domain,
|
|
38
|
+
MaxItems: 1
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
const zone = response.HostedZones?.[0];
|
|
42
|
+
if (zone && zone.Name === `${domain}.` && zone.Id) {
|
|
43
|
+
return {
|
|
44
|
+
id: zone.Id.replace("/hostedzone/", ""),
|
|
45
|
+
name: zone.Name
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch (_error) {
|
|
49
|
+
}
|
|
50
|
+
const parts = domain.split(".");
|
|
51
|
+
if (parts.length > 2) {
|
|
52
|
+
const parentDomain = parts.slice(1).join(".");
|
|
53
|
+
return findHostedZone(parentDomain, region);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain, cloudFrontDomain) {
|
|
58
|
+
const client = new Route53Client({ region });
|
|
59
|
+
const changes = [];
|
|
60
|
+
for (const token of dkimTokens) {
|
|
61
|
+
changes.push({
|
|
62
|
+
Action: "UPSERT",
|
|
63
|
+
ResourceRecordSet: {
|
|
64
|
+
Name: `${token}._domainkey.${domain}`,
|
|
65
|
+
Type: "CNAME",
|
|
66
|
+
TTL: 1800,
|
|
67
|
+
ResourceRecords: [{ Value: `${token}.dkim.amazonses.com` }]
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
changes.push({
|
|
72
|
+
Action: "UPSERT",
|
|
73
|
+
ResourceRecordSet: {
|
|
74
|
+
Name: domain,
|
|
75
|
+
Type: "TXT",
|
|
76
|
+
TTL: 1800,
|
|
77
|
+
ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
changes.push({
|
|
81
|
+
Action: "UPSERT",
|
|
82
|
+
ResourceRecordSet: {
|
|
83
|
+
Name: `_dmarc.${domain}`,
|
|
84
|
+
Type: "TXT",
|
|
85
|
+
TTL: 1800,
|
|
86
|
+
ResourceRecords: [
|
|
87
|
+
{ Value: `"v=DMARC1; p=quarantine; rua=mailto:postmaster@${domain}"` }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (customTrackingDomain) {
|
|
92
|
+
const targetDomain = cloudFrontDomain || `r.${region}.awstrack.me`;
|
|
93
|
+
changes.push({
|
|
94
|
+
Action: "UPSERT",
|
|
95
|
+
ResourceRecordSet: {
|
|
96
|
+
Name: customTrackingDomain,
|
|
97
|
+
Type: "CNAME",
|
|
98
|
+
TTL: 1800,
|
|
99
|
+
ResourceRecords: [{ Value: targetDomain }]
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (mailFromDomain) {
|
|
104
|
+
changes.push({
|
|
105
|
+
Action: "UPSERT",
|
|
106
|
+
ResourceRecordSet: {
|
|
107
|
+
Name: mailFromDomain,
|
|
108
|
+
Type: "MX",
|
|
109
|
+
TTL: 1800,
|
|
110
|
+
ResourceRecords: [
|
|
111
|
+
{ Value: `10 feedback-smtp.${region}.amazonses.com` }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
changes.push({
|
|
116
|
+
Action: "UPSERT",
|
|
117
|
+
ResourceRecordSet: {
|
|
118
|
+
Name: mailFromDomain,
|
|
119
|
+
Type: "TXT",
|
|
120
|
+
TTL: 1800,
|
|
121
|
+
ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
await client.send(
|
|
126
|
+
new ChangeResourceRecordSetsCommand({
|
|
127
|
+
HostedZoneId: hostedZoneId,
|
|
128
|
+
ChangeBatch: {
|
|
129
|
+
Changes: changes
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
var init_route53 = __esm({
|
|
135
|
+
"src/utils/email/route53.ts"() {
|
|
136
|
+
"use strict";
|
|
137
|
+
init_esm_shims();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// src/infrastructure/resources/acm.ts
|
|
142
|
+
var acm_exports = {};
|
|
143
|
+
__export(acm_exports, {
|
|
144
|
+
createACMCertificate: () => createACMCertificate
|
|
145
|
+
});
|
|
146
|
+
import * as aws8 from "@pulumi/aws";
|
|
147
|
+
async function createACMCertificate(config2) {
|
|
148
|
+
const usEast1Provider = new aws8.Provider("acm-us-east-1", {
|
|
149
|
+
region: "us-east-1"
|
|
150
|
+
});
|
|
151
|
+
const certificate = new aws8.acm.Certificate(
|
|
152
|
+
"wraps-email-tracking-cert",
|
|
153
|
+
{
|
|
154
|
+
domainName: config2.domain,
|
|
155
|
+
validationMethod: "DNS",
|
|
156
|
+
tags: {
|
|
157
|
+
ManagedBy: "wraps-cli",
|
|
158
|
+
Description: "SSL certificate for Wraps email tracking domain"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
provider: usEast1Provider
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
const validationRecords = certificate.domainValidationOptions.apply(
|
|
166
|
+
(options) => options.map((option) => ({
|
|
167
|
+
name: option.resourceRecordName,
|
|
168
|
+
type: option.resourceRecordType,
|
|
169
|
+
value: option.resourceRecordValue
|
|
170
|
+
}))
|
|
171
|
+
);
|
|
172
|
+
let certificateValidation;
|
|
173
|
+
if (config2.hostedZoneId) {
|
|
174
|
+
const validationRecord = new aws8.route53.Record(
|
|
175
|
+
"wraps-email-tracking-cert-validation",
|
|
176
|
+
{
|
|
177
|
+
zoneId: config2.hostedZoneId,
|
|
178
|
+
name: certificate.domainValidationOptions[0].resourceRecordName,
|
|
179
|
+
type: certificate.domainValidationOptions[0].resourceRecordType,
|
|
180
|
+
records: [certificate.domainValidationOptions[0].resourceRecordValue],
|
|
181
|
+
ttl: 60
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
certificateValidation = new aws8.acm.CertificateValidation(
|
|
185
|
+
"wraps-email-tracking-cert-validation-waiter",
|
|
186
|
+
{
|
|
187
|
+
certificateArn: certificate.arn,
|
|
188
|
+
validationRecordFqdns: [validationRecord.fqdn]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
provider: usEast1Provider
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
certificate,
|
|
197
|
+
certificateValidation,
|
|
198
|
+
validationRecords
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
var init_acm = __esm({
|
|
202
|
+
"src/infrastructure/resources/acm.ts"() {
|
|
203
|
+
"use strict";
|
|
204
|
+
init_esm_shims();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// src/infrastructure/resources/cloudfront.ts
|
|
209
|
+
var cloudfront_exports = {};
|
|
210
|
+
__export(cloudfront_exports, {
|
|
211
|
+
createCloudFrontTracking: () => createCloudFrontTracking
|
|
212
|
+
});
|
|
213
|
+
import * as aws9 from "@pulumi/aws";
|
|
214
|
+
async function findDistributionByAlias(alias) {
|
|
215
|
+
try {
|
|
216
|
+
const { CloudFrontClient, ListDistributionsCommand } = await import("@aws-sdk/client-cloudfront");
|
|
217
|
+
const cloudfront2 = new CloudFrontClient({ region: "us-east-1" });
|
|
218
|
+
const response = await cloudfront2.send(new ListDistributionsCommand({}));
|
|
219
|
+
const distribution = response.DistributionList?.Items?.find(
|
|
220
|
+
(dist) => dist.Aliases?.Items?.includes(alias)
|
|
221
|
+
);
|
|
222
|
+
return distribution?.Id || null;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error("Error finding CloudFront distribution:", error);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function createWAFWebACL() {
|
|
229
|
+
const usEast1Provider = new aws9.Provider("waf-us-east-1", {
|
|
230
|
+
region: "us-east-1"
|
|
231
|
+
});
|
|
232
|
+
const webAcl = new aws9.wafv2.WebAcl(
|
|
233
|
+
"wraps-email-tracking-waf",
|
|
234
|
+
{
|
|
235
|
+
scope: "CLOUDFRONT",
|
|
236
|
+
// WAF for CloudFront must use CLOUDFRONT scope
|
|
237
|
+
description: "Rate limiting protection for Wraps email tracking",
|
|
238
|
+
defaultAction: {
|
|
239
|
+
allow: {}
|
|
240
|
+
// Allow by default
|
|
241
|
+
},
|
|
242
|
+
rules: [
|
|
243
|
+
{
|
|
244
|
+
name: "RateLimitRule",
|
|
245
|
+
priority: 1,
|
|
246
|
+
action: {
|
|
247
|
+
block: {}
|
|
248
|
+
// Block requests exceeding rate limit
|
|
249
|
+
},
|
|
250
|
+
statement: {
|
|
251
|
+
rateBasedStatement: {
|
|
252
|
+
limit: 2e3,
|
|
253
|
+
// 2000 requests per 5 minutes per IP
|
|
254
|
+
aggregateKeyType: "IP"
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
visibilityConfig: {
|
|
258
|
+
sampledRequestsEnabled: true,
|
|
259
|
+
cloudwatchMetricsEnabled: true,
|
|
260
|
+
metricName: "RateLimitRule"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
visibilityConfig: {
|
|
265
|
+
sampledRequestsEnabled: true,
|
|
266
|
+
cloudwatchMetricsEnabled: true,
|
|
267
|
+
metricName: "wraps-email-tracking-waf"
|
|
268
|
+
},
|
|
269
|
+
tags: {
|
|
270
|
+
ManagedBy: "wraps-cli",
|
|
271
|
+
Description: "WAF for Wraps email tracking with rate limiting"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
provider: usEast1Provider
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
return webAcl;
|
|
279
|
+
}
|
|
280
|
+
async function createCloudFrontTracking(config2) {
|
|
281
|
+
const sesTrackingOrigin = `r.${config2.region}.awstrack.me`;
|
|
282
|
+
const webAcl = await createWAFWebACL();
|
|
283
|
+
const existingDistributionId = await findDistributionByAlias(
|
|
284
|
+
config2.customTrackingDomain
|
|
285
|
+
);
|
|
286
|
+
const distributionConfig = {
|
|
287
|
+
enabled: true,
|
|
288
|
+
comment: "Wraps email tracking with HTTPS support",
|
|
289
|
+
aliases: [config2.customTrackingDomain],
|
|
290
|
+
// Attach WAF Web ACL for rate limiting protection
|
|
291
|
+
webAclId: webAcl.arn,
|
|
292
|
+
// Origin: SES tracking endpoint
|
|
293
|
+
origins: [
|
|
294
|
+
{
|
|
295
|
+
domainName: sesTrackingOrigin,
|
|
296
|
+
originId: "ses-tracking",
|
|
297
|
+
customOriginConfig: {
|
|
298
|
+
httpPort: 80,
|
|
299
|
+
httpsPort: 443,
|
|
300
|
+
originProtocolPolicy: "http-only",
|
|
301
|
+
// SES tracking endpoint is HTTP
|
|
302
|
+
originSslProtocols: ["TLSv1.2"]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
// Default cache behavior
|
|
307
|
+
defaultCacheBehavior: {
|
|
308
|
+
targetOriginId: "ses-tracking",
|
|
309
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
310
|
+
// Force HTTPS
|
|
311
|
+
allowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
312
|
+
cachedMethods: ["GET", "HEAD"],
|
|
313
|
+
// Forward all query strings and headers (tracking links use query params)
|
|
314
|
+
forwardedValues: {
|
|
315
|
+
queryString: true,
|
|
316
|
+
cookies: {
|
|
317
|
+
forward: "all"
|
|
318
|
+
},
|
|
319
|
+
headers: ["*"]
|
|
320
|
+
// Forward all headers to preserve tracking functionality
|
|
321
|
+
},
|
|
322
|
+
// Minimal caching for tracking redirects
|
|
323
|
+
minTtl: 0,
|
|
324
|
+
defaultTtl: 0,
|
|
325
|
+
maxTtl: 31536e3,
|
|
326
|
+
compress: true
|
|
327
|
+
},
|
|
328
|
+
// Price class (use only North America & Europe for cost optimization)
|
|
329
|
+
priceClass: "PriceClass_100",
|
|
330
|
+
// Restrictions (none)
|
|
331
|
+
restrictions: {
|
|
332
|
+
geoRestriction: {
|
|
333
|
+
restrictionType: "none"
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
// SSL certificate from ACM
|
|
337
|
+
viewerCertificate: {
|
|
338
|
+
acmCertificateArn: config2.certificateArn,
|
|
339
|
+
sslSupportMethod: "sni-only",
|
|
340
|
+
minimumProtocolVersion: "TLSv1.2_2021"
|
|
341
|
+
},
|
|
342
|
+
tags: {
|
|
343
|
+
ManagedBy: "wraps-cli",
|
|
344
|
+
Description: "Wraps email tracking CloudFront distribution"
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const distribution = existingDistributionId ? new aws9.cloudfront.Distribution(
|
|
348
|
+
"wraps-email-tracking-cdn",
|
|
349
|
+
distributionConfig,
|
|
350
|
+
{
|
|
351
|
+
import: existingDistributionId
|
|
352
|
+
// Import existing distribution
|
|
353
|
+
}
|
|
354
|
+
) : new aws9.cloudfront.Distribution(
|
|
355
|
+
"wraps-email-tracking-cdn",
|
|
356
|
+
distributionConfig
|
|
357
|
+
);
|
|
358
|
+
return {
|
|
359
|
+
distribution,
|
|
360
|
+
domainName: distribution.domainName,
|
|
361
|
+
webAcl
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
var init_cloudfront = __esm({
|
|
365
|
+
"src/infrastructure/resources/cloudfront.ts"() {
|
|
366
|
+
"use strict";
|
|
367
|
+
init_esm_shims();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
21
371
|
// src/infrastructure/resources/mail-manager.ts
|
|
22
372
|
var mail_manager_exports = {};
|
|
23
373
|
__export(mail_manager_exports, {
|
|
@@ -224,14 +574,14 @@ var init_errors = __esm({
|
|
|
224
574
|
"AWS credentials not found",
|
|
225
575
|
"NO_AWS_CREDENTIALS",
|
|
226
576
|
"Run: aws configure\nOr set AWS_PROFILE environment variable",
|
|
227
|
-
"https://
|
|
577
|
+
"https://wraps.dev/docs/setup/aws-credentials"
|
|
228
578
|
),
|
|
229
579
|
stackExists: (stackName) => new WrapsError(
|
|
230
580
|
`Stack "${stackName}" already exists`,
|
|
231
581
|
"STACK_EXISTS",
|
|
232
582
|
`To update: wraps upgrade
|
|
233
583
|
To remove: wraps destroy --stack ${stackName}`,
|
|
234
|
-
"https://
|
|
584
|
+
"https://wraps.dev/docs/cli/upgrade"
|
|
235
585
|
),
|
|
236
586
|
invalidRegion: (region) => new WrapsError(
|
|
237
587
|
`Invalid AWS region: ${region}`,
|
|
@@ -243,13 +593,13 @@ To remove: wraps destroy --stack ${stackName}`,
|
|
|
243
593
|
`Infrastructure deployment failed: ${message}`,
|
|
244
594
|
"PULUMI_ERROR",
|
|
245
595
|
"Check your AWS permissions and try again",
|
|
246
|
-
"https://
|
|
596
|
+
"https://wraps.dev/docs/troubleshooting"
|
|
247
597
|
),
|
|
248
598
|
noStack: () => new WrapsError(
|
|
249
599
|
"No Wraps infrastructure found in this AWS account",
|
|
250
600
|
"NO_STACK",
|
|
251
601
|
"Run: wraps init\nTo deploy new infrastructure",
|
|
252
|
-
"https://
|
|
602
|
+
"https://wraps.dev/docs/cli/init"
|
|
253
603
|
),
|
|
254
604
|
pulumiNotInstalled: () => new WrapsError(
|
|
255
605
|
"Pulumi CLI is not installed",
|
|
@@ -265,11 +615,13 @@ To remove: wraps destroy --stack ${stackName}`,
|
|
|
265
615
|
var aws_exports = {};
|
|
266
616
|
__export(aws_exports, {
|
|
267
617
|
checkRegion: () => checkRegion,
|
|
618
|
+
getACMCertificateStatus: () => getACMCertificateStatus,
|
|
268
619
|
getAWSRegion: () => getAWSRegion,
|
|
269
620
|
isSESSandbox: () => isSESSandbox,
|
|
270
621
|
listSESDomains: () => listSESDomains,
|
|
271
622
|
validateAWSCredentials: () => validateAWSCredentials
|
|
272
623
|
});
|
|
624
|
+
import { ACMClient, DescribeCertificateCommand } from "@aws-sdk/client-acm";
|
|
273
625
|
import {
|
|
274
626
|
GetIdentityVerificationAttributesCommand,
|
|
275
627
|
ListIdentitiesCommand,
|
|
@@ -368,6 +720,33 @@ async function isSESSandbox(region) {
|
|
|
368
720
|
throw error;
|
|
369
721
|
}
|
|
370
722
|
}
|
|
723
|
+
async function getACMCertificateStatus(certificateArn) {
|
|
724
|
+
const acm2 = new ACMClient({ region: "us-east-1" });
|
|
725
|
+
try {
|
|
726
|
+
const response = await acm2.send(
|
|
727
|
+
new DescribeCertificateCommand({
|
|
728
|
+
CertificateArn: certificateArn
|
|
729
|
+
})
|
|
730
|
+
);
|
|
731
|
+
const certificate = response.Certificate;
|
|
732
|
+
if (!certificate) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const validationRecords = certificate.DomainValidationOptions?.map((option) => ({
|
|
736
|
+
name: option.ResourceRecord?.Name || "",
|
|
737
|
+
type: option.ResourceRecord?.Type || "",
|
|
738
|
+
value: option.ResourceRecord?.Value || ""
|
|
739
|
+
})) || [];
|
|
740
|
+
return {
|
|
741
|
+
status: certificate.Status || "UNKNOWN",
|
|
742
|
+
domainName: certificate.DomainName || "",
|
|
743
|
+
validationRecords
|
|
744
|
+
};
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error("Error getting ACM certificate status:", error);
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
371
750
|
var init_aws = __esm({
|
|
372
751
|
"src/utils/shared/aws.ts"() {
|
|
373
752
|
"use strict";
|
|
@@ -383,11 +762,23 @@ function estimateStorageSize(emailsPerMonth, retention, numEventTypes = 8) {
|
|
|
383
762
|
"7days": 0.25,
|
|
384
763
|
"30days": 1,
|
|
385
764
|
"90days": 3,
|
|
765
|
+
"3months": 3,
|
|
386
766
|
"6months": 6,
|
|
767
|
+
"9months": 9,
|
|
387
768
|
"1year": 12,
|
|
388
769
|
"18months": 18,
|
|
389
|
-
|
|
390
|
-
|
|
770
|
+
"2years": 24,
|
|
771
|
+
"30months": 30,
|
|
772
|
+
"3years": 36,
|
|
773
|
+
"4years": 48,
|
|
774
|
+
"5years": 60,
|
|
775
|
+
"6years": 72,
|
|
776
|
+
"7years": 84,
|
|
777
|
+
"8years": 96,
|
|
778
|
+
"9years": 108,
|
|
779
|
+
"10years": 120,
|
|
780
|
+
indefinite: 120,
|
|
781
|
+
permanent: 120
|
|
391
782
|
}[retention];
|
|
392
783
|
const totalKB = emailsPerMonth * numEventTypes * (retentionMonths ?? 12) * avgRecordSizeKB;
|
|
393
784
|
return totalKB / 1024 / 1024;
|
|
@@ -398,11 +789,23 @@ function estimateArchiveStorageSize(emailsPerMonth, retention) {
|
|
|
398
789
|
"7days": 0.25,
|
|
399
790
|
"30days": 1,
|
|
400
791
|
"90days": 3,
|
|
792
|
+
"3months": 3,
|
|
401
793
|
"6months": 6,
|
|
794
|
+
"9months": 9,
|
|
402
795
|
"1year": 12,
|
|
403
796
|
"18months": 18,
|
|
404
|
-
|
|
405
|
-
|
|
797
|
+
"2years": 24,
|
|
798
|
+
"30months": 30,
|
|
799
|
+
"3years": 36,
|
|
800
|
+
"4years": 48,
|
|
801
|
+
"5years": 60,
|
|
802
|
+
"6years": 72,
|
|
803
|
+
"7years": 84,
|
|
804
|
+
"8years": 96,
|
|
805
|
+
"9years": 108,
|
|
806
|
+
"10years": 120,
|
|
807
|
+
indefinite: 120,
|
|
808
|
+
permanent: 120
|
|
406
809
|
}[retention];
|
|
407
810
|
const totalKB = emailsPerMonth * (retentionMonths ?? 12) * avgEmailSizeKB;
|
|
408
811
|
return totalKB / 1024 / 1024;
|
|
@@ -1478,116 +1881,34 @@ var init_prompts = __esm({
|
|
|
1478
1881
|
}
|
|
1479
1882
|
});
|
|
1480
1883
|
|
|
1481
|
-
// src/utils/
|
|
1482
|
-
var
|
|
1483
|
-
__export(
|
|
1484
|
-
|
|
1485
|
-
findHostedZone: () => findHostedZone
|
|
1884
|
+
// src/utils/shared/assume-role.ts
|
|
1885
|
+
var assume_role_exports = {};
|
|
1886
|
+
__export(assume_role_exports, {
|
|
1887
|
+
assumeRole: () => assumeRole
|
|
1486
1888
|
});
|
|
1487
|
-
import {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
new ListHostedZonesByNameCommand({
|
|
1497
|
-
DNSName: domain,
|
|
1498
|
-
MaxItems: 1
|
|
1499
|
-
})
|
|
1500
|
-
);
|
|
1501
|
-
const zone = response.HostedZones?.[0];
|
|
1502
|
-
if (zone && zone.Name === `${domain}.` && zone.Id) {
|
|
1503
|
-
return {
|
|
1504
|
-
id: zone.Id.replace("/hostedzone/", ""),
|
|
1505
|
-
name: zone.Name
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
return null;
|
|
1509
|
-
} catch (_error) {
|
|
1510
|
-
return null;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain) {
|
|
1514
|
-
const client = new Route53Client({ region });
|
|
1515
|
-
const changes = [];
|
|
1516
|
-
for (const token of dkimTokens) {
|
|
1517
|
-
changes.push({
|
|
1518
|
-
Action: "UPSERT",
|
|
1519
|
-
ResourceRecordSet: {
|
|
1520
|
-
Name: `${token}._domainkey.${domain}`,
|
|
1521
|
-
Type: "CNAME",
|
|
1522
|
-
TTL: 1800,
|
|
1523
|
-
ResourceRecords: [{ Value: `${token}.dkim.amazonses.com` }]
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
changes.push({
|
|
1528
|
-
Action: "UPSERT",
|
|
1529
|
-
ResourceRecordSet: {
|
|
1530
|
-
Name: domain,
|
|
1531
|
-
Type: "TXT",
|
|
1532
|
-
TTL: 1800,
|
|
1533
|
-
ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
|
|
1534
|
-
}
|
|
1535
|
-
});
|
|
1536
|
-
changes.push({
|
|
1537
|
-
Action: "UPSERT",
|
|
1538
|
-
ResourceRecordSet: {
|
|
1539
|
-
Name: `_dmarc.${domain}`,
|
|
1540
|
-
Type: "TXT",
|
|
1541
|
-
TTL: 1800,
|
|
1542
|
-
ResourceRecords: [
|
|
1543
|
-
{ Value: `"v=DMARC1; p=quarantine; rua=mailto:postmaster@${domain}"` }
|
|
1544
|
-
]
|
|
1545
|
-
}
|
|
1546
|
-
});
|
|
1547
|
-
if (customTrackingDomain) {
|
|
1548
|
-
changes.push({
|
|
1549
|
-
Action: "UPSERT",
|
|
1550
|
-
ResourceRecordSet: {
|
|
1551
|
-
Name: customTrackingDomain,
|
|
1552
|
-
Type: "CNAME",
|
|
1553
|
-
TTL: 1800,
|
|
1554
|
-
ResourceRecords: [{ Value: `r.${region}.awstrack.me` }]
|
|
1555
|
-
}
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
if (mailFromDomain) {
|
|
1559
|
-
changes.push({
|
|
1560
|
-
Action: "UPSERT",
|
|
1561
|
-
ResourceRecordSet: {
|
|
1562
|
-
Name: mailFromDomain,
|
|
1563
|
-
Type: "MX",
|
|
1564
|
-
TTL: 1800,
|
|
1565
|
-
ResourceRecords: [
|
|
1566
|
-
{ Value: `10 feedback-smtp.${region}.amazonses.com` }
|
|
1567
|
-
]
|
|
1568
|
-
}
|
|
1569
|
-
});
|
|
1570
|
-
changes.push({
|
|
1571
|
-
Action: "UPSERT",
|
|
1572
|
-
ResourceRecordSet: {
|
|
1573
|
-
Name: mailFromDomain,
|
|
1574
|
-
Type: "TXT",
|
|
1575
|
-
TTL: 1800,
|
|
1576
|
-
ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
|
|
1577
|
-
}
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
await client.send(
|
|
1581
|
-
new ChangeResourceRecordSetsCommand({
|
|
1582
|
-
HostedZoneId: hostedZoneId,
|
|
1583
|
-
ChangeBatch: {
|
|
1584
|
-
Changes: changes
|
|
1585
|
-
}
|
|
1889
|
+
import { AssumeRoleCommand, STSClient as STSClient2 } from "@aws-sdk/client-sts";
|
|
1890
|
+
async function assumeRole(roleArn, region, sessionName = "wraps-console") {
|
|
1891
|
+
const sts = new STSClient2({ region });
|
|
1892
|
+
const response = await sts.send(
|
|
1893
|
+
new AssumeRoleCommand({
|
|
1894
|
+
RoleArn: roleArn,
|
|
1895
|
+
RoleSessionName: sessionName,
|
|
1896
|
+
DurationSeconds: 3600
|
|
1897
|
+
// 1 hour
|
|
1586
1898
|
})
|
|
1587
1899
|
);
|
|
1900
|
+
if (!response.Credentials) {
|
|
1901
|
+
throw new Error("Failed to assume role: No credentials returned");
|
|
1902
|
+
}
|
|
1903
|
+
return {
|
|
1904
|
+
accessKeyId: response.Credentials.AccessKeyId,
|
|
1905
|
+
secretAccessKey: response.Credentials.SecretAccessKey,
|
|
1906
|
+
sessionToken: response.Credentials.SessionToken,
|
|
1907
|
+
expiration: response.Credentials.Expiration
|
|
1908
|
+
};
|
|
1588
1909
|
}
|
|
1589
|
-
var
|
|
1590
|
-
"src/utils/
|
|
1910
|
+
var init_assume_role = __esm({
|
|
1911
|
+
"src/utils/shared/assume-role.ts"() {
|
|
1591
1912
|
"use strict";
|
|
1592
1913
|
init_esm_shims();
|
|
1593
1914
|
}
|
|
@@ -1895,15 +2216,63 @@ import pc3 from "picocolors";
|
|
|
1895
2216
|
|
|
1896
2217
|
// src/infrastructure/email-stack.ts
|
|
1897
2218
|
init_esm_shims();
|
|
1898
|
-
import * as
|
|
2219
|
+
import * as aws10 from "@pulumi/aws";
|
|
1899
2220
|
import * as pulumi4 from "@pulumi/pulumi";
|
|
1900
2221
|
|
|
1901
2222
|
// src/infrastructure/resources/dynamodb.ts
|
|
1902
2223
|
init_esm_shims();
|
|
1903
2224
|
import * as aws from "@pulumi/aws";
|
|
2225
|
+
async function tableExists(tableName) {
|
|
2226
|
+
try {
|
|
2227
|
+
const { DynamoDBClient: DynamoDBClient4, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
|
|
2228
|
+
const dynamodb2 = new DynamoDBClient4({});
|
|
2229
|
+
await dynamodb2.send(new DescribeTableCommand2({ TableName: tableName }));
|
|
2230
|
+
return true;
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
if (error.name === "ResourceNotFoundException") {
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
console.error("Error checking for existing DynamoDB table:", error);
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
1904
2239
|
async function createDynamoDBTables(_config) {
|
|
1905
|
-
const
|
|
1906
|
-
|
|
2240
|
+
const tableName = "wraps-email-history";
|
|
2241
|
+
const exists = await tableExists(tableName);
|
|
2242
|
+
const emailHistory = exists ? new aws.dynamodb.Table(
|
|
2243
|
+
tableName,
|
|
2244
|
+
{
|
|
2245
|
+
name: tableName,
|
|
2246
|
+
billingMode: "PAY_PER_REQUEST",
|
|
2247
|
+
hashKey: "messageId",
|
|
2248
|
+
rangeKey: "sentAt",
|
|
2249
|
+
attributes: [
|
|
2250
|
+
{ name: "messageId", type: "S" },
|
|
2251
|
+
{ name: "sentAt", type: "N" },
|
|
2252
|
+
{ name: "accountId", type: "S" }
|
|
2253
|
+
],
|
|
2254
|
+
globalSecondaryIndexes: [
|
|
2255
|
+
{
|
|
2256
|
+
name: "accountId-sentAt-index",
|
|
2257
|
+
hashKey: "accountId",
|
|
2258
|
+
rangeKey: "sentAt",
|
|
2259
|
+
projectionType: "ALL"
|
|
2260
|
+
}
|
|
2261
|
+
],
|
|
2262
|
+
ttl: {
|
|
2263
|
+
enabled: true,
|
|
2264
|
+
attributeName: "expiresAt"
|
|
2265
|
+
},
|
|
2266
|
+
tags: {
|
|
2267
|
+
ManagedBy: "wraps-cli"
|
|
2268
|
+
}
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
import: tableName
|
|
2272
|
+
// Import existing table
|
|
2273
|
+
}
|
|
2274
|
+
) : new aws.dynamodb.Table(tableName, {
|
|
2275
|
+
name: tableName,
|
|
1907
2276
|
billingMode: "PAY_PER_REQUEST",
|
|
1908
2277
|
hashKey: "messageId",
|
|
1909
2278
|
rangeKey: "sentAt",
|
|
@@ -1990,6 +2359,20 @@ async function createEventBridgeResources(config2) {
|
|
|
1990
2359
|
init_esm_shims();
|
|
1991
2360
|
import * as aws3 from "@pulumi/aws";
|
|
1992
2361
|
import * as pulumi2 from "@pulumi/pulumi";
|
|
2362
|
+
async function roleExists(roleName) {
|
|
2363
|
+
try {
|
|
2364
|
+
const { IAMClient: IAMClient2, GetRoleCommand } = await import("@aws-sdk/client-iam");
|
|
2365
|
+
const iam4 = new IAMClient2({});
|
|
2366
|
+
await iam4.send(new GetRoleCommand({ RoleName: roleName }));
|
|
2367
|
+
return true;
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
if (error.name === "NoSuchEntityException") {
|
|
2370
|
+
return false;
|
|
2371
|
+
}
|
|
2372
|
+
console.error("Error checking for existing IAM role:", error);
|
|
2373
|
+
return false;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
1993
2376
|
async function createIAMRole(config2) {
|
|
1994
2377
|
let assumeRolePolicy;
|
|
1995
2378
|
if (config2.provider === "vercel" && config2.oidcProvider) {
|
|
@@ -2025,8 +2408,24 @@ async function createIAMRole(config2) {
|
|
|
2025
2408
|
} else {
|
|
2026
2409
|
throw new Error("Other providers not yet implemented");
|
|
2027
2410
|
}
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2411
|
+
const roleName = "wraps-email-role";
|
|
2412
|
+
const exists = await roleExists(roleName);
|
|
2413
|
+
const role = exists ? new aws3.iam.Role(
|
|
2414
|
+
roleName,
|
|
2415
|
+
{
|
|
2416
|
+
name: roleName,
|
|
2417
|
+
assumeRolePolicy,
|
|
2418
|
+
tags: {
|
|
2419
|
+
ManagedBy: "wraps-cli",
|
|
2420
|
+
Provider: config2.provider
|
|
2421
|
+
}
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
import: roleName
|
|
2425
|
+
// Import existing role (use role name, not ARN)
|
|
2426
|
+
}
|
|
2427
|
+
) : new aws3.iam.Role(roleName, {
|
|
2428
|
+
name: roleName,
|
|
2030
2429
|
assumeRolePolicy,
|
|
2031
2430
|
tags: {
|
|
2032
2431
|
ManagedBy: "wraps-cli",
|
|
@@ -2143,6 +2542,36 @@ function getPackageRoot() {
|
|
|
2143
2542
|
}
|
|
2144
2543
|
throw new Error("Could not find package.json");
|
|
2145
2544
|
}
|
|
2545
|
+
async function lambdaFunctionExists(functionName) {
|
|
2546
|
+
try {
|
|
2547
|
+
const { LambdaClient: LambdaClient2, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
|
|
2548
|
+
const lambda2 = new LambdaClient2({});
|
|
2549
|
+
await lambda2.send(new GetFunctionCommand({ FunctionName: functionName }));
|
|
2550
|
+
return true;
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
if (error.name === "ResourceNotFoundException") {
|
|
2553
|
+
return false;
|
|
2554
|
+
}
|
|
2555
|
+
console.error("Error checking for existing Lambda function:", error);
|
|
2556
|
+
return false;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
async function findEventSourceMapping(functionName, queueArn) {
|
|
2560
|
+
try {
|
|
2561
|
+
const { LambdaClient: LambdaClient2, ListEventSourceMappingsCommand } = await import("@aws-sdk/client-lambda");
|
|
2562
|
+
const lambda2 = new LambdaClient2({});
|
|
2563
|
+
const response = await lambda2.send(
|
|
2564
|
+
new ListEventSourceMappingsCommand({
|
|
2565
|
+
FunctionName: functionName,
|
|
2566
|
+
EventSourceArn: queueArn
|
|
2567
|
+
})
|
|
2568
|
+
);
|
|
2569
|
+
return response.EventSourceMappings?.[0]?.UUID || null;
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
console.error("Error finding event source mapping:", error);
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2146
2575
|
async function getLambdaCode(functionName) {
|
|
2147
2576
|
const packageRoot = getPackageRoot();
|
|
2148
2577
|
const distLambdaPath = join(packageRoot, "dist", "lambda", functionName);
|
|
@@ -2238,10 +2667,12 @@ async function deployLambdaFunctions(config2) {
|
|
|
2238
2667
|
})
|
|
2239
2668
|
)
|
|
2240
2669
|
});
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2670
|
+
const functionName = "wraps-email-event-processor";
|
|
2671
|
+
const exists = await lambdaFunctionExists(functionName);
|
|
2672
|
+
const eventProcessor = exists ? new aws4.lambda.Function(
|
|
2673
|
+
functionName,
|
|
2243
2674
|
{
|
|
2244
|
-
name:
|
|
2675
|
+
name: functionName,
|
|
2245
2676
|
runtime: aws4.lambda.Runtime.NodeJS20dX,
|
|
2246
2677
|
handler: "index.handler",
|
|
2247
2678
|
role: lambdaRole.arn,
|
|
@@ -2259,20 +2690,56 @@ async function deployLambdaFunctions(config2) {
|
|
|
2259
2690
|
ManagedBy: "wraps-cli",
|
|
2260
2691
|
Description: "Process SES email events from SQS and store in DynamoDB"
|
|
2261
2692
|
}
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
import: functionName
|
|
2696
|
+
// Import existing function
|
|
2697
|
+
}
|
|
2698
|
+
) : new aws4.lambda.Function(functionName, {
|
|
2699
|
+
name: functionName,
|
|
2700
|
+
runtime: aws4.lambda.Runtime.NodeJS20dX,
|
|
2701
|
+
handler: "index.handler",
|
|
2702
|
+
role: lambdaRole.arn,
|
|
2703
|
+
code: new pulumi3.asset.FileArchive(eventProcessorCode),
|
|
2704
|
+
timeout: 300,
|
|
2705
|
+
// 5 minutes (matches SQS visibility timeout)
|
|
2706
|
+
memorySize: 512,
|
|
2707
|
+
environment: {
|
|
2708
|
+
variables: {
|
|
2709
|
+
TABLE_NAME: config2.tableName,
|
|
2710
|
+
AWS_ACCOUNT_ID: config2.accountId
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2713
|
+
tags: {
|
|
2714
|
+
ManagedBy: "wraps-cli",
|
|
2715
|
+
Description: "Process SES email events from SQS and store in DynamoDB"
|
|
2262
2716
|
}
|
|
2717
|
+
});
|
|
2718
|
+
const queueArnValue = `arn:aws:sqs:${config2.region}:${config2.accountId}:wraps-email-events`;
|
|
2719
|
+
const existingMappingUuid = await findEventSourceMapping(
|
|
2720
|
+
functionName,
|
|
2721
|
+
queueArnValue
|
|
2263
2722
|
);
|
|
2264
|
-
const
|
|
2723
|
+
const mappingConfig = {
|
|
2724
|
+
eventSourceArn: config2.queueArn,
|
|
2725
|
+
functionName: eventProcessor.name,
|
|
2726
|
+
batchSize: 10,
|
|
2727
|
+
// Process up to 10 messages per invocation
|
|
2728
|
+
maximumBatchingWindowInSeconds: 5,
|
|
2729
|
+
// Wait up to 5 seconds to batch messages
|
|
2730
|
+
functionResponseTypes: ["ReportBatchItemFailures"]
|
|
2731
|
+
// Enable partial batch responses
|
|
2732
|
+
};
|
|
2733
|
+
const eventSourceMapping = existingMappingUuid ? new aws4.lambda.EventSourceMapping(
|
|
2265
2734
|
"wraps-email-event-source-mapping",
|
|
2735
|
+
mappingConfig,
|
|
2266
2736
|
{
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
batchSize: 10,
|
|
2270
|
-
// Process up to 10 messages per invocation
|
|
2271
|
-
maximumBatchingWindowInSeconds: 5,
|
|
2272
|
-
// Wait up to 5 seconds to batch messages
|
|
2273
|
-
functionResponseTypes: ["ReportBatchItemFailures"]
|
|
2274
|
-
// Enable partial batch responses
|
|
2737
|
+
import: existingMappingUuid
|
|
2738
|
+
// Import with the UUID
|
|
2275
2739
|
}
|
|
2740
|
+
) : new aws4.lambda.EventSourceMapping(
|
|
2741
|
+
"wraps-email-event-source-mapping",
|
|
2742
|
+
mappingConfig
|
|
2276
2743
|
);
|
|
2277
2744
|
return {
|
|
2278
2745
|
eventProcessor,
|
|
@@ -2283,6 +2750,56 @@ async function deployLambdaFunctions(config2) {
|
|
|
2283
2750
|
// src/infrastructure/resources/ses.ts
|
|
2284
2751
|
init_esm_shims();
|
|
2285
2752
|
import * as aws5 from "@pulumi/aws";
|
|
2753
|
+
async function configurationSetExists(configSetName, region) {
|
|
2754
|
+
try {
|
|
2755
|
+
const { SESv2Client: SESv2Client5, GetConfigurationSetCommand: GetConfigurationSetCommand2 } = await import("@aws-sdk/client-sesv2");
|
|
2756
|
+
const ses = new SESv2Client5({ region });
|
|
2757
|
+
await ses.send(
|
|
2758
|
+
new GetConfigurationSetCommand2({ ConfigurationSetName: configSetName })
|
|
2759
|
+
);
|
|
2760
|
+
return true;
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
if (error.name === "NotFoundException") {
|
|
2763
|
+
return false;
|
|
2764
|
+
}
|
|
2765
|
+
console.error("Error checking for existing configuration set:", error);
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
async function emailIdentityExists(emailIdentity, region) {
|
|
2770
|
+
try {
|
|
2771
|
+
const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
|
|
2772
|
+
const ses = new SESv2Client5({ region });
|
|
2773
|
+
await ses.send(
|
|
2774
|
+
new GetEmailIdentityCommand4({ EmailIdentity: emailIdentity })
|
|
2775
|
+
);
|
|
2776
|
+
return true;
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
if (error.name === "NotFoundException") {
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
console.error("Error checking for existing email identity:", error);
|
|
2782
|
+
return false;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
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
|
+
}
|
|
2286
2803
|
async function createSESResources(config2) {
|
|
2287
2804
|
const configSetOptions = {
|
|
2288
2805
|
configurationSetName: "wraps-email-tracking",
|
|
@@ -2294,46 +2811,78 @@ async function createSESResources(config2) {
|
|
|
2294
2811
|
if (config2.trackingConfig?.customRedirectDomain) {
|
|
2295
2812
|
configSetOptions.trackingOptions = {
|
|
2296
2813
|
customRedirectDomain: config2.trackingConfig.customRedirectDomain,
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2299
|
-
|
|
2814
|
+
// HTTPS policy depends on whether HTTPS tracking is enabled
|
|
2815
|
+
// - REQUIRE: When using CloudFront with SSL certificate
|
|
2816
|
+
// - OPTIONAL: When using direct SES tracking endpoint (no SSL)
|
|
2817
|
+
httpsPolicy: config2.trackingConfig.httpsEnabled ? "REQUIRE" : "OPTIONAL"
|
|
2300
2818
|
};
|
|
2301
2819
|
}
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2820
|
+
const configSetName = "wraps-email-tracking";
|
|
2821
|
+
const exists = await configurationSetExists(configSetName, config2.region);
|
|
2822
|
+
const configSet = exists ? new aws5.sesv2.ConfigurationSet(configSetName, configSetOptions, {
|
|
2823
|
+
import: configSetName
|
|
2824
|
+
// Import existing configuration set
|
|
2825
|
+
}) : new aws5.sesv2.ConfigurationSet(configSetName, configSetOptions);
|
|
2306
2826
|
const defaultEventBus = aws5.cloudwatch.getEventBusOutput({
|
|
2307
2827
|
name: "default"
|
|
2308
2828
|
});
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2829
|
+
const eventDestName = "wraps-email-eventbridge";
|
|
2830
|
+
const eventDestExists = await eventDestinationExists(
|
|
2831
|
+
configSetName,
|
|
2832
|
+
eventDestName,
|
|
2833
|
+
config2.region
|
|
2834
|
+
);
|
|
2835
|
+
if (!eventDestExists) {
|
|
2836
|
+
new aws5.sesv2.ConfigurationSetEventDestination("wraps-email-all-events", {
|
|
2837
|
+
configurationSetName: configSet.configurationSetName,
|
|
2838
|
+
eventDestinationName: eventDestName,
|
|
2839
|
+
eventDestination: {
|
|
2840
|
+
enabled: true,
|
|
2841
|
+
matchingEventTypes: [
|
|
2842
|
+
"SEND",
|
|
2843
|
+
"DELIVERY",
|
|
2844
|
+
"OPEN",
|
|
2845
|
+
"CLICK",
|
|
2846
|
+
"BOUNCE",
|
|
2847
|
+
"COMPLAINT",
|
|
2848
|
+
"REJECT",
|
|
2849
|
+
"RENDERING_FAILURE",
|
|
2850
|
+
"DELIVERY_DELAY",
|
|
2851
|
+
"SUBSCRIPTION"
|
|
2852
|
+
],
|
|
2853
|
+
eventBridgeDestination: {
|
|
2854
|
+
// SES requires default bus - cannot use custom bus
|
|
2855
|
+
eventBusArn: defaultEventBus.arn
|
|
2856
|
+
}
|
|
2329
2857
|
}
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2332
2860
|
let domainIdentity;
|
|
2333
2861
|
let dkimTokens;
|
|
2334
2862
|
let mailFromDomain;
|
|
2335
2863
|
if (config2.domain) {
|
|
2336
|
-
|
|
2864
|
+
const identityExists = await emailIdentityExists(
|
|
2865
|
+
config2.domain,
|
|
2866
|
+
config2.region
|
|
2867
|
+
);
|
|
2868
|
+
domainIdentity = identityExists ? new aws5.sesv2.EmailIdentity(
|
|
2869
|
+
"wraps-email-domain",
|
|
2870
|
+
{
|
|
2871
|
+
emailIdentity: config2.domain,
|
|
2872
|
+
configurationSetName: configSet.configurationSetName,
|
|
2873
|
+
// Link configuration set to domain
|
|
2874
|
+
dkimSigningAttributes: {
|
|
2875
|
+
nextSigningKeyLength: "RSA_2048_BIT"
|
|
2876
|
+
},
|
|
2877
|
+
tags: {
|
|
2878
|
+
ManagedBy: "wraps-cli"
|
|
2879
|
+
}
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
import: config2.domain
|
|
2883
|
+
// Import existing identity
|
|
2884
|
+
}
|
|
2885
|
+
) : new aws5.sesv2.EmailIdentity("wraps-email-domain", {
|
|
2337
2886
|
emailIdentity: config2.domain,
|
|
2338
2887
|
configurationSetName: configSet.configurationSetName,
|
|
2339
2888
|
// Link configuration set to domain
|
|
@@ -2417,9 +2966,48 @@ async function createSQSResources() {
|
|
|
2417
2966
|
// src/infrastructure/vercel-oidc.ts
|
|
2418
2967
|
init_esm_shims();
|
|
2419
2968
|
import * as aws7 from "@pulumi/aws";
|
|
2969
|
+
async function getExistingOIDCProviderArn(url) {
|
|
2970
|
+
try {
|
|
2971
|
+
const { IAMClient: IAMClient2, ListOpenIDConnectProvidersCommand } = await import("@aws-sdk/client-iam");
|
|
2972
|
+
const iam4 = new IAMClient2({});
|
|
2973
|
+
const response = await iam4.send(new ListOpenIDConnectProvidersCommand({}));
|
|
2974
|
+
const expectedArnSuffix = url.replace("https://", "");
|
|
2975
|
+
const provider = response.OpenIDConnectProviderList?.find(
|
|
2976
|
+
(p) => p.Arn?.endsWith(expectedArnSuffix)
|
|
2977
|
+
);
|
|
2978
|
+
return provider?.Arn || null;
|
|
2979
|
+
} catch (error) {
|
|
2980
|
+
console.error("Error checking for existing OIDC provider:", error);
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2420
2984
|
async function createVercelOIDC(config2) {
|
|
2985
|
+
const url = `https://oidc.vercel.com/${config2.teamSlug}`;
|
|
2986
|
+
const existingArn = await getExistingOIDCProviderArn(url);
|
|
2987
|
+
if (existingArn) {
|
|
2988
|
+
return new aws7.iam.OpenIdConnectProvider(
|
|
2989
|
+
"wraps-vercel-oidc",
|
|
2990
|
+
{
|
|
2991
|
+
url,
|
|
2992
|
+
clientIdLists: [`https://vercel.com/${config2.teamSlug}`],
|
|
2993
|
+
thumbprintLists: [
|
|
2994
|
+
// Vercel OIDC thumbprints
|
|
2995
|
+
"20032e77eca0785eece16b56b42c9b330b906320",
|
|
2996
|
+
"696db3af0dffc17e65c6a20d925c5a7bd24dec7e"
|
|
2997
|
+
],
|
|
2998
|
+
tags: {
|
|
2999
|
+
ManagedBy: "wraps-cli",
|
|
3000
|
+
Provider: "vercel"
|
|
3001
|
+
}
|
|
3002
|
+
},
|
|
3003
|
+
{
|
|
3004
|
+
import: existingArn
|
|
3005
|
+
// Import existing resource
|
|
3006
|
+
}
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
2421
3009
|
return new aws7.iam.OpenIdConnectProvider("wraps-vercel-oidc", {
|
|
2422
|
-
url
|
|
3010
|
+
url,
|
|
2423
3011
|
clientIdLists: [`https://vercel.com/${config2.teamSlug}`],
|
|
2424
3012
|
thumbprintLists: [
|
|
2425
3013
|
// Vercel OIDC thumbprints
|
|
@@ -2435,7 +3023,7 @@ async function createVercelOIDC(config2) {
|
|
|
2435
3023
|
|
|
2436
3024
|
// src/infrastructure/email-stack.ts
|
|
2437
3025
|
async function deployEmailStack(config2) {
|
|
2438
|
-
const identity = await
|
|
3026
|
+
const identity = await aws10.getCallerIdentity();
|
|
2439
3027
|
const accountId = identity.accountId;
|
|
2440
3028
|
let oidcProvider;
|
|
2441
3029
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -2452,6 +3040,27 @@ async function deployEmailStack(config2) {
|
|
|
2452
3040
|
vercelProjectName: config2.vercel?.projectName,
|
|
2453
3041
|
emailConfig
|
|
2454
3042
|
});
|
|
3043
|
+
let cloudFrontResources;
|
|
3044
|
+
let acmResources;
|
|
3045
|
+
if (emailConfig.tracking?.enabled && emailConfig.tracking.customRedirectDomain && emailConfig.tracking.httpsEnabled) {
|
|
3046
|
+
const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
3047
|
+
const hostedZone = await findHostedZone2(
|
|
3048
|
+
emailConfig.tracking.customRedirectDomain,
|
|
3049
|
+
config2.region
|
|
3050
|
+
);
|
|
3051
|
+
const { createACMCertificate: createACMCertificate2 } = await Promise.resolve().then(() => (init_acm(), acm_exports));
|
|
3052
|
+
acmResources = await createACMCertificate2({
|
|
3053
|
+
domain: emailConfig.tracking.customRedirectDomain,
|
|
3054
|
+
hostedZoneId: hostedZone?.id
|
|
3055
|
+
});
|
|
3056
|
+
const { createCloudFrontTracking: createCloudFrontTracking2 } = await Promise.resolve().then(() => (init_cloudfront(), cloudfront_exports));
|
|
3057
|
+
const certificateArn = acmResources.certificateValidation ? acmResources.certificateValidation.certificateArn : acmResources.certificate.arn;
|
|
3058
|
+
cloudFrontResources = await createCloudFrontTracking2({
|
|
3059
|
+
customTrackingDomain: emailConfig.tracking.customRedirectDomain,
|
|
3060
|
+
region: config2.region,
|
|
3061
|
+
certificateArn
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
2455
3064
|
let sesResources;
|
|
2456
3065
|
if (emailConfig.tracking?.enabled || emailConfig.eventTracking?.enabled) {
|
|
2457
3066
|
sesResources = await createSESResources({
|
|
@@ -2485,7 +3094,8 @@ async function deployEmailStack(config2) {
|
|
|
2485
3094
|
roleArn: role.arn,
|
|
2486
3095
|
tableName: dynamoTables.emailHistory.name,
|
|
2487
3096
|
queueArn: sqsResources.queue.arn,
|
|
2488
|
-
accountId
|
|
3097
|
+
accountId,
|
|
3098
|
+
region: config2.region
|
|
2489
3099
|
});
|
|
2490
3100
|
}
|
|
2491
3101
|
let archiveResources;
|
|
@@ -2511,8 +3121,10 @@ async function deployEmailStack(config2) {
|
|
|
2511
3121
|
queueUrl: sqsResources?.queue.url,
|
|
2512
3122
|
dlqUrl: sqsResources?.dlq.url,
|
|
2513
3123
|
customTrackingDomain: sesResources?.customTrackingDomain,
|
|
3124
|
+
httpsTrackingEnabled: emailConfig.tracking?.httpsEnabled,
|
|
3125
|
+
cloudFrontDomain: cloudFrontResources?.domainName,
|
|
3126
|
+
acmCertificateValidationRecords: acmResources?.validationRecords,
|
|
2514
3127
|
mailFromDomain: sesResources?.mailFromDomain,
|
|
2515
|
-
archiveId: archiveResources?.archiveId,
|
|
2516
3128
|
archiveArn: archiveResources?.archiveArn,
|
|
2517
3129
|
archivingEnabled: emailConfig.emailArchiving?.enabled,
|
|
2518
3130
|
archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0
|
|
@@ -2742,7 +3354,7 @@ function displaySuccess(outputs) {
|
|
|
2742
3354
|
"",
|
|
2743
3355
|
pc2.bold("Next steps:"),
|
|
2744
3356
|
` 1. Install SDK: ${pc2.yellow("npm install @wraps/sdk")}`,
|
|
2745
|
-
` 2. View dashboard: ${pc2.blue("https://
|
|
3357
|
+
` 2. View dashboard: ${pc2.blue("https://app.wraps.dev")}`,
|
|
2746
3358
|
""
|
|
2747
3359
|
);
|
|
2748
3360
|
clack2.outro(pc2.green("Email infrastructure deployed successfully!"));
|
|
@@ -2785,9 +3397,29 @@ Verification should complete within a few minutes.`,
|
|
|
2785
3397
|
}
|
|
2786
3398
|
clack2.note(dnsLines.join("\n"), "DNS Records to add:");
|
|
2787
3399
|
}
|
|
3400
|
+
if (outputs.acmValidationRecords && outputs.acmValidationRecords.length > 0) {
|
|
3401
|
+
const acmDnsLines = [
|
|
3402
|
+
pc2.bold("SSL Certificate Validation (ACM):"),
|
|
3403
|
+
...outputs.acmValidationRecords.map(
|
|
3404
|
+
(record) => ` ${pc2.cyan(record.name)} ${pc2.dim(record.type)} "${record.value}"`
|
|
3405
|
+
),
|
|
3406
|
+
"",
|
|
3407
|
+
pc2.dim(
|
|
3408
|
+
"Note: These records are required to validate your SSL certificate."
|
|
3409
|
+
),
|
|
3410
|
+
pc2.dim(
|
|
3411
|
+
"CloudFront will be enabled automatically after certificate validation."
|
|
3412
|
+
)
|
|
3413
|
+
];
|
|
3414
|
+
clack2.note(
|
|
3415
|
+
acmDnsLines.join("\n"),
|
|
3416
|
+
"SSL Certificate Validation DNS Records:"
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
2788
3419
|
if (outputs.trackingDomainDnsRecords && outputs.trackingDomainDnsRecords.length > 0) {
|
|
3420
|
+
const trackingProtocol = outputs.httpsTrackingEnabled ? "HTTPS" : "HTTP";
|
|
2789
3421
|
const trackingDnsLines = [
|
|
2790
|
-
pc2.bold(
|
|
3422
|
+
pc2.bold(`Custom Tracking Domain - ${trackingProtocol} Redirect CNAME:`),
|
|
2791
3423
|
...outputs.trackingDomainDnsRecords.map(
|
|
2792
3424
|
(record) => ` ${pc2.cyan(record.name)} ${pc2.dim(record.type)} "${record.value}"`
|
|
2793
3425
|
),
|
|
@@ -2797,6 +3429,12 @@ Verification should complete within a few minutes.`,
|
|
|
2797
3429
|
),
|
|
2798
3430
|
pc2.dim("your custom domain for open and click tracking.")
|
|
2799
3431
|
];
|
|
3432
|
+
if (outputs.httpsTrackingEnabled) {
|
|
3433
|
+
trackingDnsLines.push(
|
|
3434
|
+
"",
|
|
3435
|
+
pc2.dim("HTTPS tracking is enabled via CloudFront with SSL certificate.")
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
2800
3438
|
clack2.note(
|
|
2801
3439
|
trackingDnsLines.join("\n"),
|
|
2802
3440
|
"Custom Tracking Domain DNS Records:"
|
|
@@ -2811,7 +3449,8 @@ ${pc2.dim("Run:")} ${pc2.yellow(`wraps verify --domain ${outputs.customTrackingD
|
|
|
2811
3449
|
);
|
|
2812
3450
|
}
|
|
2813
3451
|
}
|
|
2814
|
-
if (outputs.customTrackingDomain && !outputs.
|
|
3452
|
+
if (outputs.customTrackingDomain && !outputs.httpsTrackingEnabled && // Only show for HTTP tracking
|
|
3453
|
+
!outputs.dnsAutoCreated && (!outputs.dnsRecords || outputs.dnsRecords.length === 0) && (!outputs.trackingDomainDnsRecords || outputs.trackingDomainDnsRecords.length === 0)) {
|
|
2815
3454
|
const trackingLines = [
|
|
2816
3455
|
pc2.bold("Tracking Domain (CNAME):"),
|
|
2817
3456
|
` ${pc2.cyan(outputs.customTrackingDomain)} ${pc2.dim("CNAME")} "r.${outputs.region}.awstrack.me"`,
|
|
@@ -2884,6 +3523,19 @@ ${domainStrings.join("\n")}`);
|
|
|
2884
3523
|
` ${pc2.dim("\u25CB")} Email Archiving ${pc2.dim("(run 'wraps upgrade' to enable)")}`
|
|
2885
3524
|
);
|
|
2886
3525
|
}
|
|
3526
|
+
if (status2.tracking?.customTrackingDomain) {
|
|
3527
|
+
const protocol = status2.tracking.httpsEnabled ? "HTTPS" : "HTTP";
|
|
3528
|
+
const cloudFrontStatus = status2.tracking.httpsEnabled ? status2.tracking.cloudFrontDomain ? pc2.green("\u2713 Active") : pc2.yellow("\u23F1 Pending") : "";
|
|
3529
|
+
const trackingLabel = status2.tracking.httpsEnabled ? `${protocol} tracking ${cloudFrontStatus}` : `${protocol} tracking`;
|
|
3530
|
+
featureLines.push(
|
|
3531
|
+
` ${pc2.green("\u2713")} Custom Tracking Domain ${pc2.dim(`(${trackingLabel})`)}`
|
|
3532
|
+
);
|
|
3533
|
+
featureLines.push(` ${pc2.cyan(status2.tracking.customTrackingDomain)}`);
|
|
3534
|
+
} else {
|
|
3535
|
+
featureLines.push(
|
|
3536
|
+
` ${pc2.dim("\u25CB")} Custom Tracking Domain ${pc2.dim("(run 'wraps upgrade' to enable)")}`
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
2887
3539
|
featureLines.push(
|
|
2888
3540
|
` ${pc2.green("\u2713")} Console Dashboard ${pc2.dim("(run 'wraps console')")}`
|
|
2889
3541
|
);
|
|
@@ -2965,11 +3617,9 @@ ${pc2.dim("Run:")} ${pc2.yellow(`wraps verify --domain ${exampleDomain}`)} ${pc2
|
|
|
2965
3617
|
`
|
|
2966
3618
|
);
|
|
2967
3619
|
}
|
|
2968
|
-
console.log(
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
);
|
|
2972
|
-
console.log(`${pc2.bold("Docs:")} ${pc2.blue("https://docs.wraps.dev")}
|
|
3620
|
+
console.log(`
|
|
3621
|
+
${pc2.bold("Dashboard:")} ${pc2.blue("https://app.wraps.dev")}`);
|
|
3622
|
+
console.log(`${pc2.bold("Docs:")} ${pc2.blue("https://wraps.dev/docs")}
|
|
2973
3623
|
`);
|
|
2974
3624
|
}
|
|
2975
3625
|
|
|
@@ -4324,9 +4974,12 @@ ${pc8.bold("The following Wraps resources will be removed:")}
|
|
|
4324
4974
|
if (metadata.services.email?.pulumiStackName) {
|
|
4325
4975
|
await progress.execute("Removing Wraps infrastructure", async () => {
|
|
4326
4976
|
try {
|
|
4977
|
+
if (!metadata.services.email?.pulumiStackName) {
|
|
4978
|
+
throw new Error("No Pulumi stack name found in metadata");
|
|
4979
|
+
}
|
|
4327
4980
|
const stack = await pulumi8.automation.LocalWorkspace.selectStack(
|
|
4328
4981
|
{
|
|
4329
|
-
stackName: metadata.services.email
|
|
4982
|
+
stackName: metadata.services.email.pulumiStackName,
|
|
4330
4983
|
projectName: "wraps-email",
|
|
4331
4984
|
program: async () => {
|
|
4332
4985
|
}
|
|
@@ -4343,7 +4996,7 @@ ${pc8.bold("The following Wraps resources will be removed:")}
|
|
|
4343
4996
|
await stack.destroy({ onOutput: () => {
|
|
4344
4997
|
} });
|
|
4345
4998
|
await stack.workspace.removeStack(
|
|
4346
|
-
metadata.services.email
|
|
4999
|
+
metadata.services.email.pulumiStackName
|
|
4347
5000
|
);
|
|
4348
5001
|
} catch (error) {
|
|
4349
5002
|
throw new Error(`Failed to destroy Pulumi stack: ${error.message}`);
|
|
@@ -4443,10 +5096,23 @@ ${pc9.bold("Current Configuration:")}
|
|
|
4443
5096
|
"7days": "7 days",
|
|
4444
5097
|
"30days": "30 days",
|
|
4445
5098
|
"90days": "90 days",
|
|
5099
|
+
"3months": "3 months",
|
|
4446
5100
|
"6months": "6 months",
|
|
5101
|
+
"9months": "9 months",
|
|
4447
5102
|
"1year": "1 year",
|
|
4448
5103
|
"18months": "18 months",
|
|
4449
|
-
|
|
5104
|
+
"2years": "2 years",
|
|
5105
|
+
"30months": "30 months",
|
|
5106
|
+
"3years": "3 years",
|
|
5107
|
+
"4years": "4 years",
|
|
5108
|
+
"5years": "5 years",
|
|
5109
|
+
"6years": "6 years",
|
|
5110
|
+
"7years": "7 years",
|
|
5111
|
+
"8years": "8 years",
|
|
5112
|
+
"9years": "9 years",
|
|
5113
|
+
"10years": "10 years",
|
|
5114
|
+
indefinite: "indefinite",
|
|
5115
|
+
permanent: "permanent"
|
|
4450
5116
|
}[config2.emailArchiving.retention] || "90 days";
|
|
4451
5117
|
console.log(` ${pc9.green("\u2713")} Email Archiving (${retentionLabel})`);
|
|
4452
5118
|
}
|
|
@@ -4740,14 +5406,94 @@ ${pc9.bold("Current Configuration:")}
|
|
|
4740
5406
|
clack9.cancel("Upgrade cancelled.");
|
|
4741
5407
|
process.exit(0);
|
|
4742
5408
|
}
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
5409
|
+
const enableHttps = await clack9.confirm({
|
|
5410
|
+
message: "Enable HTTPS tracking with CloudFront + SSL certificate?",
|
|
5411
|
+
initialValue: true
|
|
5412
|
+
});
|
|
5413
|
+
if (clack9.isCancel(enableHttps)) {
|
|
5414
|
+
clack9.cancel("Upgrade cancelled.");
|
|
5415
|
+
process.exit(0);
|
|
5416
|
+
}
|
|
5417
|
+
if (enableHttps) {
|
|
5418
|
+
clack9.log.info(
|
|
5419
|
+
pc9.dim(
|
|
5420
|
+
"HTTPS tracking creates a CloudFront distribution with an SSL certificate."
|
|
5421
|
+
)
|
|
5422
|
+
);
|
|
5423
|
+
clack9.log.info(
|
|
5424
|
+
pc9.dim(
|
|
5425
|
+
"This ensures all tracking links use secure HTTPS connections."
|
|
5426
|
+
)
|
|
5427
|
+
);
|
|
5428
|
+
const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
5429
|
+
const hostedZone = await progress.execute(
|
|
5430
|
+
"Checking for Route53 hosted zone",
|
|
5431
|
+
async () => await findHostedZone2(trackingDomain || config2.domain, region)
|
|
5432
|
+
);
|
|
5433
|
+
if (hostedZone) {
|
|
5434
|
+
progress.info(
|
|
5435
|
+
`Found Route53 hosted zone: ${pc9.cyan(hostedZone.name)} ${pc9.green("\u2713")}`
|
|
5436
|
+
);
|
|
5437
|
+
clack9.log.info(
|
|
5438
|
+
pc9.dim(
|
|
5439
|
+
"DNS records (SSL certificate validation + CloudFront) will be created automatically."
|
|
5440
|
+
)
|
|
5441
|
+
);
|
|
5442
|
+
} else {
|
|
5443
|
+
clack9.log.warn(
|
|
5444
|
+
`No Route53 hosted zone found for ${pc9.cyan(trackingDomain || config2.domain)}`
|
|
5445
|
+
);
|
|
5446
|
+
clack9.log.info(
|
|
5447
|
+
pc9.dim(
|
|
5448
|
+
"You'll need to manually create DNS records for SSL certificate validation and CloudFront."
|
|
5449
|
+
)
|
|
5450
|
+
);
|
|
5451
|
+
clack9.log.info(
|
|
5452
|
+
pc9.dim("DNS record details will be shown after deployment.")
|
|
5453
|
+
);
|
|
4749
5454
|
}
|
|
4750
|
-
|
|
5455
|
+
const confirmHttps = await clack9.confirm({
|
|
5456
|
+
message: hostedZone ? "Proceed with automatic HTTPS setup?" : "Proceed with manual HTTPS setup (requires DNS configuration)?",
|
|
5457
|
+
initialValue: true
|
|
5458
|
+
});
|
|
5459
|
+
if (clack9.isCancel(confirmHttps) || !confirmHttps) {
|
|
5460
|
+
clack9.log.info("HTTPS tracking not enabled. Using HTTP tracking.");
|
|
5461
|
+
updatedConfig = {
|
|
5462
|
+
...config2,
|
|
5463
|
+
tracking: {
|
|
5464
|
+
...config2.tracking,
|
|
5465
|
+
enabled: true,
|
|
5466
|
+
customRedirectDomain: trackingDomain || void 0,
|
|
5467
|
+
httpsEnabled: false
|
|
5468
|
+
}
|
|
5469
|
+
};
|
|
5470
|
+
} else {
|
|
5471
|
+
updatedConfig = {
|
|
5472
|
+
...config2,
|
|
5473
|
+
tracking: {
|
|
5474
|
+
...config2.tracking,
|
|
5475
|
+
enabled: true,
|
|
5476
|
+
customRedirectDomain: trackingDomain || void 0,
|
|
5477
|
+
httpsEnabled: true
|
|
5478
|
+
}
|
|
5479
|
+
};
|
|
5480
|
+
}
|
|
5481
|
+
} else {
|
|
5482
|
+
clack9.log.info(
|
|
5483
|
+
pc9.dim(
|
|
5484
|
+
"Using HTTP tracking (standard). Links will use http:// protocol."
|
|
5485
|
+
)
|
|
5486
|
+
);
|
|
5487
|
+
updatedConfig = {
|
|
5488
|
+
...config2,
|
|
5489
|
+
tracking: {
|
|
5490
|
+
...config2.tracking,
|
|
5491
|
+
enabled: true,
|
|
5492
|
+
customRedirectDomain: trackingDomain || void 0,
|
|
5493
|
+
httpsEnabled: false
|
|
5494
|
+
}
|
|
5495
|
+
};
|
|
5496
|
+
}
|
|
4751
5497
|
newPreset = void 0;
|
|
4752
5498
|
break;
|
|
4753
5499
|
}
|
|
@@ -4954,6 +5700,9 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
4954
5700
|
domain: result.domain,
|
|
4955
5701
|
dkimTokens: result.dkimTokens,
|
|
4956
5702
|
customTrackingDomain: result.customTrackingDomain,
|
|
5703
|
+
httpsTrackingEnabled: result.httpsTrackingEnabled,
|
|
5704
|
+
cloudFrontDomain: result.cloudFrontDomain,
|
|
5705
|
+
acmCertificateValidationRecords: result.acmCertificateValidationRecords,
|
|
4957
5706
|
archiveArn: result.archiveArn,
|
|
4958
5707
|
archivingEnabled: result.archivingEnabled,
|
|
4959
5708
|
archiveRetention: result.archiveRetention
|
|
@@ -4972,6 +5721,8 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
4972
5721
|
metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`
|
|
4973
5722
|
);
|
|
4974
5723
|
await stack.setConfig("aws:region", { value: region });
|
|
5724
|
+
await stack.refresh({ onOutput: () => {
|
|
5725
|
+
} });
|
|
4975
5726
|
const upResult = await stack.up({ onOutput: () => {
|
|
4976
5727
|
} });
|
|
4977
5728
|
const pulumiOutputs = upResult.outputs;
|
|
@@ -4984,6 +5735,9 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
4984
5735
|
domain: pulumiOutputs.domain?.value,
|
|
4985
5736
|
dkimTokens: pulumiOutputs.dkimTokens?.value,
|
|
4986
5737
|
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
|
|
5738
|
+
httpsTrackingEnabled: pulumiOutputs.httpsTrackingEnabled?.value,
|
|
5739
|
+
cloudFrontDomain: pulumiOutputs.cloudFrontDomain?.value,
|
|
5740
|
+
acmCertificateValidationRecords: pulumiOutputs.acmCertificateValidationRecords?.value,
|
|
4987
5741
|
archiveArn: pulumiOutputs.archiveArn?.value,
|
|
4988
5742
|
archivingEnabled: pulumiOutputs.archivingEnabled?.value,
|
|
4989
5743
|
archiveRetention: pulumiOutputs.archiveRetention?.value
|
|
@@ -5007,20 +5761,37 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
5007
5761
|
await saveConnectionMetadata(metadata);
|
|
5008
5762
|
progress.info("Connection metadata updated");
|
|
5009
5763
|
const trackingDomainDnsRecords = [];
|
|
5764
|
+
const acmValidationRecords = [];
|
|
5010
5765
|
if (outputs.customTrackingDomain) {
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5766
|
+
if (outputs.httpsTrackingEnabled) {
|
|
5767
|
+
if (outputs.cloudFrontDomain) {
|
|
5768
|
+
trackingDomainDnsRecords.push({
|
|
5769
|
+
name: outputs.customTrackingDomain,
|
|
5770
|
+
type: "CNAME",
|
|
5771
|
+
value: outputs.cloudFrontDomain
|
|
5772
|
+
});
|
|
5773
|
+
}
|
|
5774
|
+
} else {
|
|
5775
|
+
trackingDomainDnsRecords.push({
|
|
5776
|
+
name: outputs.customTrackingDomain,
|
|
5777
|
+
type: "CNAME",
|
|
5778
|
+
value: `r.${outputs.region}.awstrack.me`
|
|
5779
|
+
});
|
|
5780
|
+
}
|
|
5016
5781
|
}
|
|
5782
|
+
if (outputs.httpsTrackingEnabled && outputs.acmCertificateValidationRecords) {
|
|
5783
|
+
acmValidationRecords.push(...outputs.acmCertificateValidationRecords);
|
|
5784
|
+
}
|
|
5785
|
+
const needsCertificateValidation = outputs.httpsTrackingEnabled && acmValidationRecords.length > 0 && !outputs.cloudFrontDomain;
|
|
5017
5786
|
displaySuccess({
|
|
5018
5787
|
roleArn: outputs.roleArn,
|
|
5019
5788
|
configSetName: outputs.configSetName,
|
|
5020
5789
|
region: outputs.region,
|
|
5021
5790
|
tableName: outputs.tableName,
|
|
5022
5791
|
trackingDomainDnsRecords: trackingDomainDnsRecords.length > 0 ? trackingDomainDnsRecords : void 0,
|
|
5023
|
-
|
|
5792
|
+
acmValidationRecords: acmValidationRecords.length > 0 ? acmValidationRecords : void 0,
|
|
5793
|
+
customTrackingDomain: outputs.customTrackingDomain,
|
|
5794
|
+
httpsTrackingEnabled: outputs.httpsTrackingEnabled
|
|
5024
5795
|
});
|
|
5025
5796
|
console.log(`
|
|
5026
5797
|
${pc9.green("\u2713")} ${pc9.bold("Upgrade complete!")}
|
|
@@ -5036,6 +5807,28 @@ ${pc9.green("\u2713")} ${pc9.bold("Upgrade complete!")}
|
|
|
5036
5807
|
`
|
|
5037
5808
|
);
|
|
5038
5809
|
}
|
|
5810
|
+
if (needsCertificateValidation) {
|
|
5811
|
+
console.log(pc9.bold("\u26A0\uFE0F HTTPS Tracking - Next Steps:\n"));
|
|
5812
|
+
console.log(
|
|
5813
|
+
" 1. Add the SSL certificate validation DNS record shown above to your DNS provider"
|
|
5814
|
+
);
|
|
5815
|
+
console.log(
|
|
5816
|
+
" 2. Wait for DNS propagation and certificate validation (5-30 minutes)"
|
|
5817
|
+
);
|
|
5818
|
+
console.log(
|
|
5819
|
+
` 3. Run ${pc9.cyan("wraps email upgrade")} again to complete CloudFront setup
|
|
5820
|
+
`
|
|
5821
|
+
);
|
|
5822
|
+
console.log(
|
|
5823
|
+
pc9.dim(
|
|
5824
|
+
" Note: CloudFront distribution will be created once the certificate is validated.\n"
|
|
5825
|
+
)
|
|
5826
|
+
);
|
|
5827
|
+
} else if (outputs.httpsTrackingEnabled && outputs.cloudFrontDomain) {
|
|
5828
|
+
console.log(
|
|
5829
|
+
pc9.green("\u2713") + " " + pc9.bold("HTTPS tracking is fully configured and ready to use!\n")
|
|
5830
|
+
);
|
|
5831
|
+
}
|
|
5039
5832
|
}
|
|
5040
5833
|
|
|
5041
5834
|
// src/commands/shared/dashboard.ts
|
|
@@ -5082,34 +5875,9 @@ import { Router as createRouter } from "express";
|
|
|
5082
5875
|
|
|
5083
5876
|
// src/console/services/ses-service.ts
|
|
5084
5877
|
init_esm_shims();
|
|
5878
|
+
init_assume_role();
|
|
5085
5879
|
import { GetSendQuotaCommand, SESClient as SESClient3 } from "@aws-sdk/client-ses";
|
|
5086
5880
|
import { GetEmailIdentityCommand as GetEmailIdentityCommand2, SESv2Client as SESv2Client3 } from "@aws-sdk/client-sesv2";
|
|
5087
|
-
|
|
5088
|
-
// src/utils/shared/assume-role.ts
|
|
5089
|
-
init_esm_shims();
|
|
5090
|
-
import { AssumeRoleCommand, STSClient as STSClient2 } from "@aws-sdk/client-sts";
|
|
5091
|
-
async function assumeRole(roleArn, region, sessionName = "wraps-console") {
|
|
5092
|
-
const sts = new STSClient2({ region });
|
|
5093
|
-
const response = await sts.send(
|
|
5094
|
-
new AssumeRoleCommand({
|
|
5095
|
-
RoleArn: roleArn,
|
|
5096
|
-
RoleSessionName: sessionName,
|
|
5097
|
-
DurationSeconds: 3600
|
|
5098
|
-
// 1 hour
|
|
5099
|
-
})
|
|
5100
|
-
);
|
|
5101
|
-
if (!response.Credentials) {
|
|
5102
|
-
throw new Error("Failed to assume role: No credentials returned");
|
|
5103
|
-
}
|
|
5104
|
-
return {
|
|
5105
|
-
accessKeyId: response.Credentials.AccessKeyId,
|
|
5106
|
-
secretAccessKey: response.Credentials.SecretAccessKey,
|
|
5107
|
-
sessionToken: response.Credentials.SessionToken,
|
|
5108
|
-
expiration: response.Credentials.Expiration
|
|
5109
|
-
};
|
|
5110
|
-
}
|
|
5111
|
-
|
|
5112
|
-
// src/console/services/ses-service.ts
|
|
5113
5881
|
async function fetchSendQuota(roleArn, region) {
|
|
5114
5882
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
5115
5883
|
const ses = new SESClient3({ region, credentials });
|
|
@@ -5596,6 +6364,7 @@ import { Router as createRouter3 } from "express";
|
|
|
5596
6364
|
|
|
5597
6365
|
// src/console/services/aws-metrics.ts
|
|
5598
6366
|
init_esm_shims();
|
|
6367
|
+
init_assume_role();
|
|
5599
6368
|
import {
|
|
5600
6369
|
CloudWatchClient,
|
|
5601
6370
|
GetMetricDataCommand
|
|
@@ -5809,6 +6578,7 @@ import { Router as createRouter4 } from "express";
|
|
|
5809
6578
|
|
|
5810
6579
|
// src/console/services/settings-service.ts
|
|
5811
6580
|
init_esm_shims();
|
|
6581
|
+
init_assume_role();
|
|
5812
6582
|
import {
|
|
5813
6583
|
GetConfigurationSetCommand,
|
|
5814
6584
|
GetEmailIdentityCommand as GetEmailIdentityCommand3,
|
|
@@ -6039,7 +6809,7 @@ function createSettingsRouter(config2) {
|
|
|
6039
6809
|
`[Settings] Updating sending options for ${configSetName}: ${enabled}`
|
|
6040
6810
|
);
|
|
6041
6811
|
const { SESv2Client: SESv2Client5, PutConfigurationSetSendingOptionsCommand } = await import("@aws-sdk/client-sesv2");
|
|
6042
|
-
const { assumeRole: assumeRole2 } = await
|
|
6812
|
+
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
6043
6813
|
const credentials = config2.roleArn ? await assumeRole2(config2.roleArn, config2.region) : void 0;
|
|
6044
6814
|
const sesClient = new SESv2Client5({ region: config2.region, credentials });
|
|
6045
6815
|
await sesClient.send(
|
|
@@ -6076,7 +6846,7 @@ function createSettingsRouter(config2) {
|
|
|
6076
6846
|
`[Settings] Updating reputation options for ${configSetName}: ${enabled}`
|
|
6077
6847
|
);
|
|
6078
6848
|
const { SESv2Client: SESv2Client5, PutConfigurationSetReputationOptionsCommand } = await import("@aws-sdk/client-sesv2");
|
|
6079
|
-
const { assumeRole: assumeRole2 } = await
|
|
6849
|
+
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
6080
6850
|
const credentials = config2.roleArn ? await assumeRole2(config2.roleArn, config2.region) : void 0;
|
|
6081
6851
|
const sesClient = new SESv2Client5({ region: config2.region, credentials });
|
|
6082
6852
|
await sesClient.send(
|
|
@@ -6119,7 +6889,7 @@ function createSettingsRouter(config2) {
|
|
|
6119
6889
|
`[Settings] Updating tracking domain for ${configSetName}: ${domain}`
|
|
6120
6890
|
);
|
|
6121
6891
|
const { SESv2Client: SESv2Client5, PutConfigurationSetTrackingOptionsCommand } = await import("@aws-sdk/client-sesv2");
|
|
6122
|
-
const { assumeRole: assumeRole2 } = await
|
|
6892
|
+
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
6123
6893
|
const credentials = config2.roleArn ? await assumeRole2(config2.roleArn, config2.region) : void 0;
|
|
6124
6894
|
const sesClient = new SESv2Client5({
|
|
6125
6895
|
region: config2.region,
|
|
@@ -6167,7 +6937,7 @@ function createUserRouter(config2) {
|
|
|
6167
6937
|
try {
|
|
6168
6938
|
if (config2.roleArn) {
|
|
6169
6939
|
console.log("[User API] Attempting to fetch account alias via IAM");
|
|
6170
|
-
const { assumeRole: assumeRole2 } = await
|
|
6940
|
+
const { assumeRole: assumeRole2 } = await Promise.resolve().then(() => (init_assume_role(), assume_role_exports));
|
|
6171
6941
|
const { IAMClient: IAMClient2, ListAccountAliasesCommand } = await import("@aws-sdk/client-iam");
|
|
6172
6942
|
const credentials = await assumeRole2(config2.roleArn, region);
|
|
6173
6943
|
const iamClient = new IAMClient2({ region, credentials });
|
|
@@ -6480,7 +7250,12 @@ Run ${pc12.cyan("wraps init")} to deploy infrastructure.
|
|
|
6480
7250
|
archiveArn: stackOutputs.archiveArn?.value,
|
|
6481
7251
|
archivingEnabled: stackOutputs.archivingEnabled?.value,
|
|
6482
7252
|
archiveRetention: stackOutputs.archiveRetention?.value
|
|
6483
|
-
}
|
|
7253
|
+
},
|
|
7254
|
+
tracking: stackOutputs.customTrackingDomain?.value ? {
|
|
7255
|
+
customTrackingDomain: stackOutputs.customTrackingDomain?.value,
|
|
7256
|
+
httpsEnabled: stackOutputs.httpsTrackingEnabled?.value,
|
|
7257
|
+
cloudFrontDomain: stackOutputs.cloudFrontDomain?.value
|
|
7258
|
+
} : void 0
|
|
6484
7259
|
});
|
|
6485
7260
|
}
|
|
6486
7261
|
|