@wraps.dev/cli 1.0.0 → 1.1.1
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 +937 -197
- 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";
|
|
@@ -1469,125 +1848,10 @@ async function promptCustomConfig() {
|
|
|
1469
1848
|
} : { enabled: false, retention: "90days" },
|
|
1470
1849
|
dedicatedIp,
|
|
1471
1850
|
sendingEnabled: true
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
var init_prompts = __esm({
|
|
1475
|
-
"src/utils/shared/prompts.ts"() {
|
|
1476
|
-
"use strict";
|
|
1477
|
-
init_esm_shims();
|
|
1478
|
-
}
|
|
1479
|
-
});
|
|
1480
|
-
|
|
1481
|
-
// src/utils/email/route53.ts
|
|
1482
|
-
var route53_exports = {};
|
|
1483
|
-
__export(route53_exports, {
|
|
1484
|
-
createDNSRecords: () => createDNSRecords,
|
|
1485
|
-
findHostedZone: () => findHostedZone
|
|
1486
|
-
});
|
|
1487
|
-
import {
|
|
1488
|
-
ChangeResourceRecordSetsCommand,
|
|
1489
|
-
ListHostedZonesByNameCommand,
|
|
1490
|
-
Route53Client
|
|
1491
|
-
} from "@aws-sdk/client-route-53";
|
|
1492
|
-
async function findHostedZone(domain, region) {
|
|
1493
|
-
const client = new Route53Client({ region });
|
|
1494
|
-
try {
|
|
1495
|
-
const response = await client.send(
|
|
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
|
-
}
|
|
1586
|
-
})
|
|
1587
|
-
);
|
|
1851
|
+
};
|
|
1588
1852
|
}
|
|
1589
|
-
var
|
|
1590
|
-
"src/utils/
|
|
1853
|
+
var init_prompts = __esm({
|
|
1854
|
+
"src/utils/shared/prompts.ts"() {
|
|
1591
1855
|
"use strict";
|
|
1592
1856
|
init_esm_shims();
|
|
1593
1857
|
}
|
|
@@ -1895,15 +2159,63 @@ import pc3 from "picocolors";
|
|
|
1895
2159
|
|
|
1896
2160
|
// src/infrastructure/email-stack.ts
|
|
1897
2161
|
init_esm_shims();
|
|
1898
|
-
import * as
|
|
2162
|
+
import * as aws10 from "@pulumi/aws";
|
|
1899
2163
|
import * as pulumi4 from "@pulumi/pulumi";
|
|
1900
2164
|
|
|
1901
2165
|
// src/infrastructure/resources/dynamodb.ts
|
|
1902
2166
|
init_esm_shims();
|
|
1903
2167
|
import * as aws from "@pulumi/aws";
|
|
2168
|
+
async function tableExists(tableName) {
|
|
2169
|
+
try {
|
|
2170
|
+
const { DynamoDBClient: DynamoDBClient4, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
|
|
2171
|
+
const dynamodb2 = new DynamoDBClient4({});
|
|
2172
|
+
await dynamodb2.send(new DescribeTableCommand2({ TableName: tableName }));
|
|
2173
|
+
return true;
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
if (error.name === "ResourceNotFoundException") {
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
console.error("Error checking for existing DynamoDB table:", error);
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
1904
2182
|
async function createDynamoDBTables(_config) {
|
|
1905
|
-
const
|
|
1906
|
-
|
|
2183
|
+
const tableName = "wraps-email-history";
|
|
2184
|
+
const exists = await tableExists(tableName);
|
|
2185
|
+
const emailHistory = exists ? new aws.dynamodb.Table(
|
|
2186
|
+
tableName,
|
|
2187
|
+
{
|
|
2188
|
+
name: tableName,
|
|
2189
|
+
billingMode: "PAY_PER_REQUEST",
|
|
2190
|
+
hashKey: "messageId",
|
|
2191
|
+
rangeKey: "sentAt",
|
|
2192
|
+
attributes: [
|
|
2193
|
+
{ name: "messageId", type: "S" },
|
|
2194
|
+
{ name: "sentAt", type: "N" },
|
|
2195
|
+
{ name: "accountId", type: "S" }
|
|
2196
|
+
],
|
|
2197
|
+
globalSecondaryIndexes: [
|
|
2198
|
+
{
|
|
2199
|
+
name: "accountId-sentAt-index",
|
|
2200
|
+
hashKey: "accountId",
|
|
2201
|
+
rangeKey: "sentAt",
|
|
2202
|
+
projectionType: "ALL"
|
|
2203
|
+
}
|
|
2204
|
+
],
|
|
2205
|
+
ttl: {
|
|
2206
|
+
enabled: true,
|
|
2207
|
+
attributeName: "expiresAt"
|
|
2208
|
+
},
|
|
2209
|
+
tags: {
|
|
2210
|
+
ManagedBy: "wraps-cli"
|
|
2211
|
+
}
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
import: tableName
|
|
2215
|
+
// Import existing table
|
|
2216
|
+
}
|
|
2217
|
+
) : new aws.dynamodb.Table(tableName, {
|
|
2218
|
+
name: tableName,
|
|
1907
2219
|
billingMode: "PAY_PER_REQUEST",
|
|
1908
2220
|
hashKey: "messageId",
|
|
1909
2221
|
rangeKey: "sentAt",
|
|
@@ -1990,6 +2302,20 @@ async function createEventBridgeResources(config2) {
|
|
|
1990
2302
|
init_esm_shims();
|
|
1991
2303
|
import * as aws3 from "@pulumi/aws";
|
|
1992
2304
|
import * as pulumi2 from "@pulumi/pulumi";
|
|
2305
|
+
async function roleExists(roleName) {
|
|
2306
|
+
try {
|
|
2307
|
+
const { IAMClient: IAMClient2, GetRoleCommand } = await import("@aws-sdk/client-iam");
|
|
2308
|
+
const iam4 = new IAMClient2({});
|
|
2309
|
+
await iam4.send(new GetRoleCommand({ RoleName: roleName }));
|
|
2310
|
+
return true;
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
if (error.name === "NoSuchEntityException") {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
console.error("Error checking for existing IAM role:", error);
|
|
2316
|
+
return false;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
1993
2319
|
async function createIAMRole(config2) {
|
|
1994
2320
|
let assumeRolePolicy;
|
|
1995
2321
|
if (config2.provider === "vercel" && config2.oidcProvider) {
|
|
@@ -2025,8 +2351,24 @@ async function createIAMRole(config2) {
|
|
|
2025
2351
|
} else {
|
|
2026
2352
|
throw new Error("Other providers not yet implemented");
|
|
2027
2353
|
}
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2354
|
+
const roleName = "wraps-email-role";
|
|
2355
|
+
const exists = await roleExists(roleName);
|
|
2356
|
+
const role = exists ? new aws3.iam.Role(
|
|
2357
|
+
roleName,
|
|
2358
|
+
{
|
|
2359
|
+
name: roleName,
|
|
2360
|
+
assumeRolePolicy,
|
|
2361
|
+
tags: {
|
|
2362
|
+
ManagedBy: "wraps-cli",
|
|
2363
|
+
Provider: config2.provider
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
import: roleName
|
|
2368
|
+
// Import existing role (use role name, not ARN)
|
|
2369
|
+
}
|
|
2370
|
+
) : new aws3.iam.Role(roleName, {
|
|
2371
|
+
name: roleName,
|
|
2030
2372
|
assumeRolePolicy,
|
|
2031
2373
|
tags: {
|
|
2032
2374
|
ManagedBy: "wraps-cli",
|
|
@@ -2143,6 +2485,36 @@ function getPackageRoot() {
|
|
|
2143
2485
|
}
|
|
2144
2486
|
throw new Error("Could not find package.json");
|
|
2145
2487
|
}
|
|
2488
|
+
async function lambdaFunctionExists(functionName) {
|
|
2489
|
+
try {
|
|
2490
|
+
const { LambdaClient: LambdaClient2, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
|
|
2491
|
+
const lambda2 = new LambdaClient2({});
|
|
2492
|
+
await lambda2.send(new GetFunctionCommand({ FunctionName: functionName }));
|
|
2493
|
+
return true;
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
if (error.name === "ResourceNotFoundException") {
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2498
|
+
console.error("Error checking for existing Lambda function:", error);
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
async function findEventSourceMapping(functionName, queueArn) {
|
|
2503
|
+
try {
|
|
2504
|
+
const { LambdaClient: LambdaClient2, ListEventSourceMappingsCommand } = await import("@aws-sdk/client-lambda");
|
|
2505
|
+
const lambda2 = new LambdaClient2({});
|
|
2506
|
+
const response = await lambda2.send(
|
|
2507
|
+
new ListEventSourceMappingsCommand({
|
|
2508
|
+
FunctionName: functionName,
|
|
2509
|
+
EventSourceArn: queueArn
|
|
2510
|
+
})
|
|
2511
|
+
);
|
|
2512
|
+
return response.EventSourceMappings?.[0]?.UUID || null;
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
console.error("Error finding event source mapping:", error);
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2146
2518
|
async function getLambdaCode(functionName) {
|
|
2147
2519
|
const packageRoot = getPackageRoot();
|
|
2148
2520
|
const distLambdaPath = join(packageRoot, "dist", "lambda", functionName);
|
|
@@ -2238,10 +2610,12 @@ async function deployLambdaFunctions(config2) {
|
|
|
2238
2610
|
})
|
|
2239
2611
|
)
|
|
2240
2612
|
});
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2613
|
+
const functionName = "wraps-email-event-processor";
|
|
2614
|
+
const exists = await lambdaFunctionExists(functionName);
|
|
2615
|
+
const eventProcessor = exists ? new aws4.lambda.Function(
|
|
2616
|
+
functionName,
|
|
2243
2617
|
{
|
|
2244
|
-
name:
|
|
2618
|
+
name: functionName,
|
|
2245
2619
|
runtime: aws4.lambda.Runtime.NodeJS20dX,
|
|
2246
2620
|
handler: "index.handler",
|
|
2247
2621
|
role: lambdaRole.arn,
|
|
@@ -2259,20 +2633,56 @@ async function deployLambdaFunctions(config2) {
|
|
|
2259
2633
|
ManagedBy: "wraps-cli",
|
|
2260
2634
|
Description: "Process SES email events from SQS and store in DynamoDB"
|
|
2261
2635
|
}
|
|
2636
|
+
},
|
|
2637
|
+
{
|
|
2638
|
+
import: functionName
|
|
2639
|
+
// Import existing function
|
|
2640
|
+
}
|
|
2641
|
+
) : new aws4.lambda.Function(functionName, {
|
|
2642
|
+
name: functionName,
|
|
2643
|
+
runtime: aws4.lambda.Runtime.NodeJS20dX,
|
|
2644
|
+
handler: "index.handler",
|
|
2645
|
+
role: lambdaRole.arn,
|
|
2646
|
+
code: new pulumi3.asset.FileArchive(eventProcessorCode),
|
|
2647
|
+
timeout: 300,
|
|
2648
|
+
// 5 minutes (matches SQS visibility timeout)
|
|
2649
|
+
memorySize: 512,
|
|
2650
|
+
environment: {
|
|
2651
|
+
variables: {
|
|
2652
|
+
TABLE_NAME: config2.tableName,
|
|
2653
|
+
AWS_ACCOUNT_ID: config2.accountId
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
tags: {
|
|
2657
|
+
ManagedBy: "wraps-cli",
|
|
2658
|
+
Description: "Process SES email events from SQS and store in DynamoDB"
|
|
2262
2659
|
}
|
|
2660
|
+
});
|
|
2661
|
+
const queueArnValue = `arn:aws:sqs:${config2.region}:${config2.accountId}:wraps-email-events`;
|
|
2662
|
+
const existingMappingUuid = await findEventSourceMapping(
|
|
2663
|
+
functionName,
|
|
2664
|
+
queueArnValue
|
|
2263
2665
|
);
|
|
2264
|
-
const
|
|
2666
|
+
const mappingConfig = {
|
|
2667
|
+
eventSourceArn: config2.queueArn,
|
|
2668
|
+
functionName: eventProcessor.name,
|
|
2669
|
+
batchSize: 10,
|
|
2670
|
+
// Process up to 10 messages per invocation
|
|
2671
|
+
maximumBatchingWindowInSeconds: 5,
|
|
2672
|
+
// Wait up to 5 seconds to batch messages
|
|
2673
|
+
functionResponseTypes: ["ReportBatchItemFailures"]
|
|
2674
|
+
// Enable partial batch responses
|
|
2675
|
+
};
|
|
2676
|
+
const eventSourceMapping = existingMappingUuid ? new aws4.lambda.EventSourceMapping(
|
|
2265
2677
|
"wraps-email-event-source-mapping",
|
|
2678
|
+
mappingConfig,
|
|
2266
2679
|
{
|
|
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
|
|
2680
|
+
import: existingMappingUuid
|
|
2681
|
+
// Import with the UUID
|
|
2275
2682
|
}
|
|
2683
|
+
) : new aws4.lambda.EventSourceMapping(
|
|
2684
|
+
"wraps-email-event-source-mapping",
|
|
2685
|
+
mappingConfig
|
|
2276
2686
|
);
|
|
2277
2687
|
return {
|
|
2278
2688
|
eventProcessor,
|
|
@@ -2283,6 +2693,58 @@ async function deployLambdaFunctions(config2) {
|
|
|
2283
2693
|
// src/infrastructure/resources/ses.ts
|
|
2284
2694
|
init_esm_shims();
|
|
2285
2695
|
import * as aws5 from "@pulumi/aws";
|
|
2696
|
+
async function configurationSetExists(configSetName, region) {
|
|
2697
|
+
try {
|
|
2698
|
+
const { SESv2Client: SESv2Client5, GetConfigurationSetCommand: GetConfigurationSetCommand2 } = await import("@aws-sdk/client-sesv2");
|
|
2699
|
+
const ses = new SESv2Client5({ region });
|
|
2700
|
+
await ses.send(
|
|
2701
|
+
new GetConfigurationSetCommand2({ ConfigurationSetName: configSetName })
|
|
2702
|
+
);
|
|
2703
|
+
return true;
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
if (error.name === "NotFoundException") {
|
|
2706
|
+
return false;
|
|
2707
|
+
}
|
|
2708
|
+
console.error("Error checking for existing configuration set:", error);
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
async function emailIdentityExists(emailIdentity, region) {
|
|
2713
|
+
try {
|
|
2714
|
+
const { SESv2Client: SESv2Client5, GetEmailIdentityCommand: GetEmailIdentityCommand4 } = await import("@aws-sdk/client-sesv2");
|
|
2715
|
+
const ses = new SESv2Client5({ region });
|
|
2716
|
+
await ses.send(
|
|
2717
|
+
new GetEmailIdentityCommand4({ EmailIdentity: emailIdentity })
|
|
2718
|
+
);
|
|
2719
|
+
return true;
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
if (error.name === "NotFoundException") {
|
|
2722
|
+
return false;
|
|
2723
|
+
}
|
|
2724
|
+
console.error("Error checking for existing email identity:", error);
|
|
2725
|
+
return false;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async function eventDestinationExists(configSetName, eventDestName, region) {
|
|
2729
|
+
try {
|
|
2730
|
+
const { SESv2Client: SESv2Client5, GetConfigurationSetEventDestinationsCommand } = await import("@aws-sdk/client-sesv2");
|
|
2731
|
+
const ses = new SESv2Client5({ region });
|
|
2732
|
+
const response = await ses.send(
|
|
2733
|
+
new GetConfigurationSetEventDestinationsCommand({
|
|
2734
|
+
ConfigurationSetName: configSetName
|
|
2735
|
+
})
|
|
2736
|
+
);
|
|
2737
|
+
return response.EventDestinations?.some(
|
|
2738
|
+
(dest) => dest.Name === eventDestName
|
|
2739
|
+
);
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
if (error.name === "NotFoundException") {
|
|
2742
|
+
return false;
|
|
2743
|
+
}
|
|
2744
|
+
console.error("Error checking for existing event destination:", error);
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2286
2748
|
async function createSESResources(config2) {
|
|
2287
2749
|
const configSetOptions = {
|
|
2288
2750
|
configurationSetName: "wraps-email-tracking",
|
|
@@ -2294,46 +2756,78 @@ async function createSESResources(config2) {
|
|
|
2294
2756
|
if (config2.trackingConfig?.customRedirectDomain) {
|
|
2295
2757
|
configSetOptions.trackingOptions = {
|
|
2296
2758
|
customRedirectDomain: config2.trackingConfig.customRedirectDomain,
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2299
|
-
|
|
2759
|
+
// HTTPS policy depends on whether HTTPS tracking is enabled
|
|
2760
|
+
// - REQUIRE: When using CloudFront with SSL certificate
|
|
2761
|
+
// - OPTIONAL: When using direct SES tracking endpoint (no SSL)
|
|
2762
|
+
httpsPolicy: config2.trackingConfig.httpsEnabled ? "REQUIRE" : "OPTIONAL"
|
|
2300
2763
|
};
|
|
2301
2764
|
}
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2765
|
+
const configSetName = "wraps-email-tracking";
|
|
2766
|
+
const exists = await configurationSetExists(configSetName, config2.region);
|
|
2767
|
+
const configSet = exists ? new aws5.sesv2.ConfigurationSet(configSetName, configSetOptions, {
|
|
2768
|
+
import: configSetName
|
|
2769
|
+
// Import existing configuration set
|
|
2770
|
+
}) : new aws5.sesv2.ConfigurationSet(configSetName, configSetOptions);
|
|
2306
2771
|
const defaultEventBus = aws5.cloudwatch.getEventBusOutput({
|
|
2307
2772
|
name: "default"
|
|
2308
2773
|
});
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2774
|
+
const eventDestName = "wraps-email-eventbridge";
|
|
2775
|
+
const eventDestExists = await eventDestinationExists(
|
|
2776
|
+
configSetName,
|
|
2777
|
+
eventDestName,
|
|
2778
|
+
config2.region
|
|
2779
|
+
);
|
|
2780
|
+
if (!eventDestExists) {
|
|
2781
|
+
new aws5.sesv2.ConfigurationSetEventDestination("wraps-email-all-events", {
|
|
2782
|
+
configurationSetName: configSet.configurationSetName,
|
|
2783
|
+
eventDestinationName: eventDestName,
|
|
2784
|
+
eventDestination: {
|
|
2785
|
+
enabled: true,
|
|
2786
|
+
matchingEventTypes: [
|
|
2787
|
+
"SEND",
|
|
2788
|
+
"DELIVERY",
|
|
2789
|
+
"OPEN",
|
|
2790
|
+
"CLICK",
|
|
2791
|
+
"BOUNCE",
|
|
2792
|
+
"COMPLAINT",
|
|
2793
|
+
"REJECT",
|
|
2794
|
+
"RENDERING_FAILURE",
|
|
2795
|
+
"DELIVERY_DELAY",
|
|
2796
|
+
"SUBSCRIPTION"
|
|
2797
|
+
],
|
|
2798
|
+
eventBridgeDestination: {
|
|
2799
|
+
// SES requires default bus - cannot use custom bus
|
|
2800
|
+
eventBusArn: defaultEventBus.arn
|
|
2801
|
+
}
|
|
2329
2802
|
}
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2332
2805
|
let domainIdentity;
|
|
2333
2806
|
let dkimTokens;
|
|
2334
2807
|
let mailFromDomain;
|
|
2335
2808
|
if (config2.domain) {
|
|
2336
|
-
|
|
2809
|
+
const identityExists = await emailIdentityExists(
|
|
2810
|
+
config2.domain,
|
|
2811
|
+
config2.region
|
|
2812
|
+
);
|
|
2813
|
+
domainIdentity = identityExists ? new aws5.sesv2.EmailIdentity(
|
|
2814
|
+
"wraps-email-domain",
|
|
2815
|
+
{
|
|
2816
|
+
emailIdentity: config2.domain,
|
|
2817
|
+
configurationSetName: configSet.configurationSetName,
|
|
2818
|
+
// Link configuration set to domain
|
|
2819
|
+
dkimSigningAttributes: {
|
|
2820
|
+
nextSigningKeyLength: "RSA_2048_BIT"
|
|
2821
|
+
},
|
|
2822
|
+
tags: {
|
|
2823
|
+
ManagedBy: "wraps-cli"
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2826
|
+
{
|
|
2827
|
+
import: config2.domain
|
|
2828
|
+
// Import existing identity
|
|
2829
|
+
}
|
|
2830
|
+
) : new aws5.sesv2.EmailIdentity("wraps-email-domain", {
|
|
2337
2831
|
emailIdentity: config2.domain,
|
|
2338
2832
|
configurationSetName: configSet.configurationSetName,
|
|
2339
2833
|
// Link configuration set to domain
|
|
@@ -2417,9 +2911,48 @@ async function createSQSResources() {
|
|
|
2417
2911
|
// src/infrastructure/vercel-oidc.ts
|
|
2418
2912
|
init_esm_shims();
|
|
2419
2913
|
import * as aws7 from "@pulumi/aws";
|
|
2914
|
+
async function getExistingOIDCProviderArn(url, accountId) {
|
|
2915
|
+
try {
|
|
2916
|
+
const { IAMClient: IAMClient2, ListOpenIDConnectProvidersCommand } = await import("@aws-sdk/client-iam");
|
|
2917
|
+
const iam4 = new IAMClient2({});
|
|
2918
|
+
const response = await iam4.send(new ListOpenIDConnectProvidersCommand({}));
|
|
2919
|
+
const expectedArnSuffix = url.replace("https://", "");
|
|
2920
|
+
const provider = response.OpenIDConnectProviderList?.find(
|
|
2921
|
+
(p) => p.Arn?.endsWith(expectedArnSuffix)
|
|
2922
|
+
);
|
|
2923
|
+
return provider?.Arn || null;
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
console.error("Error checking for existing OIDC provider:", error);
|
|
2926
|
+
return null;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2420
2929
|
async function createVercelOIDC(config2) {
|
|
2930
|
+
const url = `https://oidc.vercel.com/${config2.teamSlug}`;
|
|
2931
|
+
const existingArn = await getExistingOIDCProviderArn(url, config2.accountId);
|
|
2932
|
+
if (existingArn) {
|
|
2933
|
+
return new aws7.iam.OpenIdConnectProvider(
|
|
2934
|
+
"wraps-vercel-oidc",
|
|
2935
|
+
{
|
|
2936
|
+
url,
|
|
2937
|
+
clientIdLists: [`https://vercel.com/${config2.teamSlug}`],
|
|
2938
|
+
thumbprintLists: [
|
|
2939
|
+
// Vercel OIDC thumbprints
|
|
2940
|
+
"20032e77eca0785eece16b56b42c9b330b906320",
|
|
2941
|
+
"696db3af0dffc17e65c6a20d925c5a7bd24dec7e"
|
|
2942
|
+
],
|
|
2943
|
+
tags: {
|
|
2944
|
+
ManagedBy: "wraps-cli",
|
|
2945
|
+
Provider: "vercel"
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
{
|
|
2949
|
+
import: existingArn
|
|
2950
|
+
// Import existing resource
|
|
2951
|
+
}
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2421
2954
|
return new aws7.iam.OpenIdConnectProvider("wraps-vercel-oidc", {
|
|
2422
|
-
url
|
|
2955
|
+
url,
|
|
2423
2956
|
clientIdLists: [`https://vercel.com/${config2.teamSlug}`],
|
|
2424
2957
|
thumbprintLists: [
|
|
2425
2958
|
// Vercel OIDC thumbprints
|
|
@@ -2435,7 +2968,7 @@ async function createVercelOIDC(config2) {
|
|
|
2435
2968
|
|
|
2436
2969
|
// src/infrastructure/email-stack.ts
|
|
2437
2970
|
async function deployEmailStack(config2) {
|
|
2438
|
-
const identity = await
|
|
2971
|
+
const identity = await aws10.getCallerIdentity();
|
|
2439
2972
|
const accountId = identity.accountId;
|
|
2440
2973
|
let oidcProvider;
|
|
2441
2974
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -2452,6 +2985,27 @@ async function deployEmailStack(config2) {
|
|
|
2452
2985
|
vercelProjectName: config2.vercel?.projectName,
|
|
2453
2986
|
emailConfig
|
|
2454
2987
|
});
|
|
2988
|
+
let cloudFrontResources;
|
|
2989
|
+
let acmResources;
|
|
2990
|
+
if (emailConfig.tracking?.enabled && emailConfig.tracking.customRedirectDomain && emailConfig.tracking.httpsEnabled) {
|
|
2991
|
+
const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
2992
|
+
const hostedZone = await findHostedZone2(
|
|
2993
|
+
emailConfig.tracking.customRedirectDomain,
|
|
2994
|
+
config2.region
|
|
2995
|
+
);
|
|
2996
|
+
const { createACMCertificate: createACMCertificate2 } = await Promise.resolve().then(() => (init_acm(), acm_exports));
|
|
2997
|
+
acmResources = await createACMCertificate2({
|
|
2998
|
+
domain: emailConfig.tracking.customRedirectDomain,
|
|
2999
|
+
hostedZoneId: hostedZone?.id
|
|
3000
|
+
});
|
|
3001
|
+
const { createCloudFrontTracking: createCloudFrontTracking2 } = await Promise.resolve().then(() => (init_cloudfront(), cloudfront_exports));
|
|
3002
|
+
const certificateArn = acmResources.certificateValidation ? acmResources.certificateValidation.certificateArn : acmResources.certificate.arn;
|
|
3003
|
+
cloudFrontResources = await createCloudFrontTracking2({
|
|
3004
|
+
customTrackingDomain: emailConfig.tracking.customRedirectDomain,
|
|
3005
|
+
region: config2.region,
|
|
3006
|
+
certificateArn
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
2455
3009
|
let sesResources;
|
|
2456
3010
|
if (emailConfig.tracking?.enabled || emailConfig.eventTracking?.enabled) {
|
|
2457
3011
|
sesResources = await createSESResources({
|
|
@@ -2485,7 +3039,8 @@ async function deployEmailStack(config2) {
|
|
|
2485
3039
|
roleArn: role.arn,
|
|
2486
3040
|
tableName: dynamoTables.emailHistory.name,
|
|
2487
3041
|
queueArn: sqsResources.queue.arn,
|
|
2488
|
-
accountId
|
|
3042
|
+
accountId,
|
|
3043
|
+
region: config2.region
|
|
2489
3044
|
});
|
|
2490
3045
|
}
|
|
2491
3046
|
let archiveResources;
|
|
@@ -2511,8 +3066,10 @@ async function deployEmailStack(config2) {
|
|
|
2511
3066
|
queueUrl: sqsResources?.queue.url,
|
|
2512
3067
|
dlqUrl: sqsResources?.dlq.url,
|
|
2513
3068
|
customTrackingDomain: sesResources?.customTrackingDomain,
|
|
3069
|
+
httpsTrackingEnabled: emailConfig.tracking?.httpsEnabled,
|
|
3070
|
+
cloudFrontDomain: cloudFrontResources?.domainName,
|
|
3071
|
+
acmCertificateValidationRecords: acmResources?.validationRecords,
|
|
2514
3072
|
mailFromDomain: sesResources?.mailFromDomain,
|
|
2515
|
-
archiveId: archiveResources?.archiveId,
|
|
2516
3073
|
archiveArn: archiveResources?.archiveArn,
|
|
2517
3074
|
archivingEnabled: emailConfig.emailArchiving?.enabled,
|
|
2518
3075
|
archiveRetention: emailConfig.emailArchiving?.enabled ? emailConfig.emailArchiving.retention : void 0
|
|
@@ -2742,7 +3299,7 @@ function displaySuccess(outputs) {
|
|
|
2742
3299
|
"",
|
|
2743
3300
|
pc2.bold("Next steps:"),
|
|
2744
3301
|
` 1. Install SDK: ${pc2.yellow("npm install @wraps/sdk")}`,
|
|
2745
|
-
` 2. View dashboard: ${pc2.blue("https://
|
|
3302
|
+
` 2. View dashboard: ${pc2.blue("https://app.wraps.dev")}`,
|
|
2746
3303
|
""
|
|
2747
3304
|
);
|
|
2748
3305
|
clack2.outro(pc2.green("Email infrastructure deployed successfully!"));
|
|
@@ -2785,9 +3342,29 @@ Verification should complete within a few minutes.`,
|
|
|
2785
3342
|
}
|
|
2786
3343
|
clack2.note(dnsLines.join("\n"), "DNS Records to add:");
|
|
2787
3344
|
}
|
|
3345
|
+
if (outputs.acmValidationRecords && outputs.acmValidationRecords.length > 0) {
|
|
3346
|
+
const acmDnsLines = [
|
|
3347
|
+
pc2.bold("SSL Certificate Validation (ACM):"),
|
|
3348
|
+
...outputs.acmValidationRecords.map(
|
|
3349
|
+
(record) => ` ${pc2.cyan(record.name)} ${pc2.dim(record.type)} "${record.value}"`
|
|
3350
|
+
),
|
|
3351
|
+
"",
|
|
3352
|
+
pc2.dim(
|
|
3353
|
+
"Note: These records are required to validate your SSL certificate."
|
|
3354
|
+
),
|
|
3355
|
+
pc2.dim(
|
|
3356
|
+
"CloudFront will be enabled automatically after certificate validation."
|
|
3357
|
+
)
|
|
3358
|
+
];
|
|
3359
|
+
clack2.note(
|
|
3360
|
+
acmDnsLines.join("\n"),
|
|
3361
|
+
"SSL Certificate Validation DNS Records:"
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
2788
3364
|
if (outputs.trackingDomainDnsRecords && outputs.trackingDomainDnsRecords.length > 0) {
|
|
3365
|
+
const trackingProtocol = outputs.httpsTrackingEnabled ? "HTTPS" : "HTTP";
|
|
2789
3366
|
const trackingDnsLines = [
|
|
2790
|
-
pc2.bold(
|
|
3367
|
+
pc2.bold(`Custom Tracking Domain - ${trackingProtocol} Redirect CNAME:`),
|
|
2791
3368
|
...outputs.trackingDomainDnsRecords.map(
|
|
2792
3369
|
(record) => ` ${pc2.cyan(record.name)} ${pc2.dim(record.type)} "${record.value}"`
|
|
2793
3370
|
),
|
|
@@ -2797,6 +3374,12 @@ Verification should complete within a few minutes.`,
|
|
|
2797
3374
|
),
|
|
2798
3375
|
pc2.dim("your custom domain for open and click tracking.")
|
|
2799
3376
|
];
|
|
3377
|
+
if (outputs.httpsTrackingEnabled) {
|
|
3378
|
+
trackingDnsLines.push(
|
|
3379
|
+
"",
|
|
3380
|
+
pc2.dim("HTTPS tracking is enabled via CloudFront with SSL certificate.")
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
2800
3383
|
clack2.note(
|
|
2801
3384
|
trackingDnsLines.join("\n"),
|
|
2802
3385
|
"Custom Tracking Domain DNS Records:"
|
|
@@ -2811,7 +3394,8 @@ ${pc2.dim("Run:")} ${pc2.yellow(`wraps verify --domain ${outputs.customTrackingD
|
|
|
2811
3394
|
);
|
|
2812
3395
|
}
|
|
2813
3396
|
}
|
|
2814
|
-
if (outputs.customTrackingDomain && !outputs.
|
|
3397
|
+
if (outputs.customTrackingDomain && !outputs.httpsTrackingEnabled && // Only show for HTTP tracking
|
|
3398
|
+
!outputs.dnsAutoCreated && (!outputs.dnsRecords || outputs.dnsRecords.length === 0) && (!outputs.trackingDomainDnsRecords || outputs.trackingDomainDnsRecords.length === 0)) {
|
|
2815
3399
|
const trackingLines = [
|
|
2816
3400
|
pc2.bold("Tracking Domain (CNAME):"),
|
|
2817
3401
|
` ${pc2.cyan(outputs.customTrackingDomain)} ${pc2.dim("CNAME")} "r.${outputs.region}.awstrack.me"`,
|
|
@@ -2884,6 +3468,19 @@ ${domainStrings.join("\n")}`);
|
|
|
2884
3468
|
` ${pc2.dim("\u25CB")} Email Archiving ${pc2.dim("(run 'wraps upgrade' to enable)")}`
|
|
2885
3469
|
);
|
|
2886
3470
|
}
|
|
3471
|
+
if (status2.tracking?.customTrackingDomain) {
|
|
3472
|
+
const protocol = status2.tracking.httpsEnabled ? "HTTPS" : "HTTP";
|
|
3473
|
+
const cloudFrontStatus = status2.tracking.httpsEnabled ? status2.tracking.cloudFrontDomain ? pc2.green("\u2713 Active") : pc2.yellow("\u23F1 Pending") : "";
|
|
3474
|
+
const trackingLabel = status2.tracking.httpsEnabled ? `${protocol} tracking ${cloudFrontStatus}` : `${protocol} tracking`;
|
|
3475
|
+
featureLines.push(
|
|
3476
|
+
` ${pc2.green("\u2713")} Custom Tracking Domain ${pc2.dim(`(${trackingLabel})`)}`
|
|
3477
|
+
);
|
|
3478
|
+
featureLines.push(` ${pc2.cyan(status2.tracking.customTrackingDomain)}`);
|
|
3479
|
+
} else {
|
|
3480
|
+
featureLines.push(
|
|
3481
|
+
` ${pc2.dim("\u25CB")} Custom Tracking Domain ${pc2.dim("(run 'wraps upgrade' to enable)")}`
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
2887
3484
|
featureLines.push(
|
|
2888
3485
|
` ${pc2.green("\u2713")} Console Dashboard ${pc2.dim("(run 'wraps console')")}`
|
|
2889
3486
|
);
|
|
@@ -2965,11 +3562,9 @@ ${pc2.dim("Run:")} ${pc2.yellow(`wraps verify --domain ${exampleDomain}`)} ${pc2
|
|
|
2965
3562
|
`
|
|
2966
3563
|
);
|
|
2967
3564
|
}
|
|
2968
|
-
console.log(
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
);
|
|
2972
|
-
console.log(`${pc2.bold("Docs:")} ${pc2.blue("https://docs.wraps.dev")}
|
|
3565
|
+
console.log(`
|
|
3566
|
+
${pc2.bold("Dashboard:")} ${pc2.blue("https://app.wraps.dev")}`);
|
|
3567
|
+
console.log(`${pc2.bold("Docs:")} ${pc2.blue("https://wraps.dev/docs")}
|
|
2973
3568
|
`);
|
|
2974
3569
|
}
|
|
2975
3570
|
|
|
@@ -4324,9 +4919,12 @@ ${pc8.bold("The following Wraps resources will be removed:")}
|
|
|
4324
4919
|
if (metadata.services.email?.pulumiStackName) {
|
|
4325
4920
|
await progress.execute("Removing Wraps infrastructure", async () => {
|
|
4326
4921
|
try {
|
|
4922
|
+
if (!metadata.services.email?.pulumiStackName) {
|
|
4923
|
+
throw new Error("No Pulumi stack name found in metadata");
|
|
4924
|
+
}
|
|
4327
4925
|
const stack = await pulumi8.automation.LocalWorkspace.selectStack(
|
|
4328
4926
|
{
|
|
4329
|
-
stackName: metadata.services.email
|
|
4927
|
+
stackName: metadata.services.email.pulumiStackName,
|
|
4330
4928
|
projectName: "wraps-email",
|
|
4331
4929
|
program: async () => {
|
|
4332
4930
|
}
|
|
@@ -4343,7 +4941,7 @@ ${pc8.bold("The following Wraps resources will be removed:")}
|
|
|
4343
4941
|
await stack.destroy({ onOutput: () => {
|
|
4344
4942
|
} });
|
|
4345
4943
|
await stack.workspace.removeStack(
|
|
4346
|
-
metadata.services.email
|
|
4944
|
+
metadata.services.email.pulumiStackName
|
|
4347
4945
|
);
|
|
4348
4946
|
} catch (error) {
|
|
4349
4947
|
throw new Error(`Failed to destroy Pulumi stack: ${error.message}`);
|
|
@@ -4740,14 +5338,94 @@ ${pc9.bold("Current Configuration:")}
|
|
|
4740
5338
|
clack9.cancel("Upgrade cancelled.");
|
|
4741
5339
|
process.exit(0);
|
|
4742
5340
|
}
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
5341
|
+
const enableHttps = await clack9.confirm({
|
|
5342
|
+
message: "Enable HTTPS tracking with CloudFront + SSL certificate?",
|
|
5343
|
+
initialValue: true
|
|
5344
|
+
});
|
|
5345
|
+
if (clack9.isCancel(enableHttps)) {
|
|
5346
|
+
clack9.cancel("Upgrade cancelled.");
|
|
5347
|
+
process.exit(0);
|
|
5348
|
+
}
|
|
5349
|
+
if (enableHttps) {
|
|
5350
|
+
clack9.log.info(
|
|
5351
|
+
pc9.dim(
|
|
5352
|
+
"HTTPS tracking creates a CloudFront distribution with an SSL certificate."
|
|
5353
|
+
)
|
|
5354
|
+
);
|
|
5355
|
+
clack9.log.info(
|
|
5356
|
+
pc9.dim(
|
|
5357
|
+
"This ensures all tracking links use secure HTTPS connections."
|
|
5358
|
+
)
|
|
5359
|
+
);
|
|
5360
|
+
const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
|
|
5361
|
+
const hostedZone = await progress.execute(
|
|
5362
|
+
"Checking for Route53 hosted zone",
|
|
5363
|
+
async () => await findHostedZone2(trackingDomain || config2.domain, region)
|
|
5364
|
+
);
|
|
5365
|
+
if (hostedZone) {
|
|
5366
|
+
progress.info(
|
|
5367
|
+
`Found Route53 hosted zone: ${pc9.cyan(hostedZone.name)} ${pc9.green("\u2713")}`
|
|
5368
|
+
);
|
|
5369
|
+
clack9.log.info(
|
|
5370
|
+
pc9.dim(
|
|
5371
|
+
"DNS records (SSL certificate validation + CloudFront) will be created automatically."
|
|
5372
|
+
)
|
|
5373
|
+
);
|
|
5374
|
+
} else {
|
|
5375
|
+
clack9.log.warn(
|
|
5376
|
+
`No Route53 hosted zone found for ${pc9.cyan(trackingDomain || config2.domain)}`
|
|
5377
|
+
);
|
|
5378
|
+
clack9.log.info(
|
|
5379
|
+
pc9.dim(
|
|
5380
|
+
"You'll need to manually create DNS records for SSL certificate validation and CloudFront."
|
|
5381
|
+
)
|
|
5382
|
+
);
|
|
5383
|
+
clack9.log.info(
|
|
5384
|
+
pc9.dim("DNS record details will be shown after deployment.")
|
|
5385
|
+
);
|
|
4749
5386
|
}
|
|
4750
|
-
|
|
5387
|
+
const confirmHttps = await clack9.confirm({
|
|
5388
|
+
message: hostedZone ? "Proceed with automatic HTTPS setup?" : "Proceed with manual HTTPS setup (requires DNS configuration)?",
|
|
5389
|
+
initialValue: true
|
|
5390
|
+
});
|
|
5391
|
+
if (clack9.isCancel(confirmHttps) || !confirmHttps) {
|
|
5392
|
+
clack9.log.info("HTTPS tracking not enabled. Using HTTP tracking.");
|
|
5393
|
+
updatedConfig = {
|
|
5394
|
+
...config2,
|
|
5395
|
+
tracking: {
|
|
5396
|
+
...config2.tracking,
|
|
5397
|
+
enabled: true,
|
|
5398
|
+
customRedirectDomain: trackingDomain || void 0,
|
|
5399
|
+
httpsEnabled: false
|
|
5400
|
+
}
|
|
5401
|
+
};
|
|
5402
|
+
} else {
|
|
5403
|
+
updatedConfig = {
|
|
5404
|
+
...config2,
|
|
5405
|
+
tracking: {
|
|
5406
|
+
...config2.tracking,
|
|
5407
|
+
enabled: true,
|
|
5408
|
+
customRedirectDomain: trackingDomain || void 0,
|
|
5409
|
+
httpsEnabled: true
|
|
5410
|
+
}
|
|
5411
|
+
};
|
|
5412
|
+
}
|
|
5413
|
+
} else {
|
|
5414
|
+
clack9.log.info(
|
|
5415
|
+
pc9.dim(
|
|
5416
|
+
"Using HTTP tracking (standard). Links will use http:// protocol."
|
|
5417
|
+
)
|
|
5418
|
+
);
|
|
5419
|
+
updatedConfig = {
|
|
5420
|
+
...config2,
|
|
5421
|
+
tracking: {
|
|
5422
|
+
...config2.tracking,
|
|
5423
|
+
enabled: true,
|
|
5424
|
+
customRedirectDomain: trackingDomain || void 0,
|
|
5425
|
+
httpsEnabled: false
|
|
5426
|
+
}
|
|
5427
|
+
};
|
|
5428
|
+
}
|
|
4751
5429
|
newPreset = void 0;
|
|
4752
5430
|
break;
|
|
4753
5431
|
}
|
|
@@ -4954,6 +5632,9 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
4954
5632
|
domain: result.domain,
|
|
4955
5633
|
dkimTokens: result.dkimTokens,
|
|
4956
5634
|
customTrackingDomain: result.customTrackingDomain,
|
|
5635
|
+
httpsTrackingEnabled: result.httpsTrackingEnabled,
|
|
5636
|
+
cloudFrontDomain: result.cloudFrontDomain,
|
|
5637
|
+
acmCertificateValidationRecords: result.acmCertificateValidationRecords,
|
|
4957
5638
|
archiveArn: result.archiveArn,
|
|
4958
5639
|
archivingEnabled: result.archivingEnabled,
|
|
4959
5640
|
archiveRetention: result.archiveRetention
|
|
@@ -4972,6 +5653,8 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
4972
5653
|
metadata.services.email?.pulumiStackName || `wraps-${identity.accountId}-${region}`
|
|
4973
5654
|
);
|
|
4974
5655
|
await stack.setConfig("aws:region", { value: region });
|
|
5656
|
+
await stack.refresh({ onOutput: () => {
|
|
5657
|
+
} });
|
|
4975
5658
|
const upResult = await stack.up({ onOutput: () => {
|
|
4976
5659
|
} });
|
|
4977
5660
|
const pulumiOutputs = upResult.outputs;
|
|
@@ -4984,6 +5667,9 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
4984
5667
|
domain: pulumiOutputs.domain?.value,
|
|
4985
5668
|
dkimTokens: pulumiOutputs.dkimTokens?.value,
|
|
4986
5669
|
customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
|
|
5670
|
+
httpsTrackingEnabled: pulumiOutputs.httpsTrackingEnabled?.value,
|
|
5671
|
+
cloudFrontDomain: pulumiOutputs.cloudFrontDomain?.value,
|
|
5672
|
+
acmCertificateValidationRecords: pulumiOutputs.acmCertificateValidationRecords?.value,
|
|
4987
5673
|
archiveArn: pulumiOutputs.archiveArn?.value,
|
|
4988
5674
|
archivingEnabled: pulumiOutputs.archivingEnabled?.value,
|
|
4989
5675
|
archiveRetention: pulumiOutputs.archiveRetention?.value
|
|
@@ -5007,12 +5693,37 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
5007
5693
|
await saveConnectionMetadata(metadata);
|
|
5008
5694
|
progress.info("Connection metadata updated");
|
|
5009
5695
|
const trackingDomainDnsRecords = [];
|
|
5696
|
+
const acmValidationRecords = [];
|
|
5010
5697
|
if (outputs.customTrackingDomain) {
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5698
|
+
if (outputs.httpsTrackingEnabled) {
|
|
5699
|
+
if (outputs.cloudFrontDomain) {
|
|
5700
|
+
trackingDomainDnsRecords.push({
|
|
5701
|
+
name: outputs.customTrackingDomain,
|
|
5702
|
+
type: "CNAME",
|
|
5703
|
+
value: outputs.cloudFrontDomain
|
|
5704
|
+
});
|
|
5705
|
+
}
|
|
5706
|
+
} else {
|
|
5707
|
+
trackingDomainDnsRecords.push({
|
|
5708
|
+
name: outputs.customTrackingDomain,
|
|
5709
|
+
type: "CNAME",
|
|
5710
|
+
value: `r.${outputs.region}.awstrack.me`
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
if (outputs.httpsTrackingEnabled && outputs.acmCertificateValidationRecords) {
|
|
5715
|
+
acmValidationRecords.push(...outputs.acmCertificateValidationRecords);
|
|
5716
|
+
}
|
|
5717
|
+
let needsCertificateValidation = false;
|
|
5718
|
+
let certificateStatus;
|
|
5719
|
+
if (outputs.httpsTrackingEnabled && acmValidationRecords.length > 0 && !outputs.cloudFrontDomain) {
|
|
5720
|
+
try {
|
|
5721
|
+
const { ACMClient: ACMClient2, DescribeCertificateCommand: DescribeCertificateCommand2 } = await import("@aws-sdk/client-acm");
|
|
5722
|
+
const acmClient = new ACMClient2({ region: "us-east-1" });
|
|
5723
|
+
needsCertificateValidation = true;
|
|
5724
|
+
} catch (error) {
|
|
5725
|
+
needsCertificateValidation = true;
|
|
5726
|
+
}
|
|
5016
5727
|
}
|
|
5017
5728
|
displaySuccess({
|
|
5018
5729
|
roleArn: outputs.roleArn,
|
|
@@ -5020,7 +5731,9 @@ ${pc9.bold("Cost Impact:")}`);
|
|
|
5020
5731
|
region: outputs.region,
|
|
5021
5732
|
tableName: outputs.tableName,
|
|
5022
5733
|
trackingDomainDnsRecords: trackingDomainDnsRecords.length > 0 ? trackingDomainDnsRecords : void 0,
|
|
5023
|
-
|
|
5734
|
+
acmValidationRecords: acmValidationRecords.length > 0 ? acmValidationRecords : void 0,
|
|
5735
|
+
customTrackingDomain: outputs.customTrackingDomain,
|
|
5736
|
+
httpsTrackingEnabled: outputs.httpsTrackingEnabled
|
|
5024
5737
|
});
|
|
5025
5738
|
console.log(`
|
|
5026
5739
|
${pc9.green("\u2713")} ${pc9.bold("Upgrade complete!")}
|
|
@@ -5036,6 +5749,28 @@ ${pc9.green("\u2713")} ${pc9.bold("Upgrade complete!")}
|
|
|
5036
5749
|
`
|
|
5037
5750
|
);
|
|
5038
5751
|
}
|
|
5752
|
+
if (needsCertificateValidation) {
|
|
5753
|
+
console.log(pc9.bold("\u26A0\uFE0F HTTPS Tracking - Next Steps:\n"));
|
|
5754
|
+
console.log(
|
|
5755
|
+
" 1. Add the SSL certificate validation DNS record shown above to your DNS provider"
|
|
5756
|
+
);
|
|
5757
|
+
console.log(
|
|
5758
|
+
" 2. Wait for DNS propagation and certificate validation (5-30 minutes)"
|
|
5759
|
+
);
|
|
5760
|
+
console.log(
|
|
5761
|
+
` 3. Run ${pc9.cyan("wraps email upgrade")} again to complete CloudFront setup
|
|
5762
|
+
`
|
|
5763
|
+
);
|
|
5764
|
+
console.log(
|
|
5765
|
+
pc9.dim(
|
|
5766
|
+
" Note: CloudFront distribution will be created once the certificate is validated.\n"
|
|
5767
|
+
)
|
|
5768
|
+
);
|
|
5769
|
+
} else if (outputs.httpsTrackingEnabled && outputs.cloudFrontDomain) {
|
|
5770
|
+
console.log(
|
|
5771
|
+
pc9.green("\u2713") + " " + pc9.bold("HTTPS tracking is fully configured and ready to use!\n")
|
|
5772
|
+
);
|
|
5773
|
+
}
|
|
5039
5774
|
}
|
|
5040
5775
|
|
|
5041
5776
|
// src/commands/shared/dashboard.ts
|
|
@@ -6480,7 +7215,12 @@ Run ${pc12.cyan("wraps init")} to deploy infrastructure.
|
|
|
6480
7215
|
archiveArn: stackOutputs.archiveArn?.value,
|
|
6481
7216
|
archivingEnabled: stackOutputs.archivingEnabled?.value,
|
|
6482
7217
|
archiveRetention: stackOutputs.archiveRetention?.value
|
|
6483
|
-
}
|
|
7218
|
+
},
|
|
7219
|
+
tracking: stackOutputs.customTrackingDomain?.value ? {
|
|
7220
|
+
customTrackingDomain: stackOutputs.customTrackingDomain?.value,
|
|
7221
|
+
httpsEnabled: stackOutputs.httpsTrackingEnabled?.value,
|
|
7222
|
+
cloudFrontDomain: stackOutputs.cloudFrontDomain?.value
|
|
7223
|
+
} : void 0
|
|
6484
7224
|
});
|
|
6485
7225
|
}
|
|
6486
7226
|
|