@wraps.dev/cli 2.2.8 → 2.3.0
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 +45 -41
- package/dist/cli.js +554 -89
- package/dist/cli.js.map +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -147,7 +147,7 @@ var require_package = __commonJS({
|
|
|
147
147
|
"package.json"(exports, module) {
|
|
148
148
|
module.exports = {
|
|
149
149
|
name: "@wraps.dev/cli",
|
|
150
|
-
version: "2.
|
|
150
|
+
version: "2.3.0",
|
|
151
151
|
description: "CLI for deploying Wraps email infrastructure to your AWS account",
|
|
152
152
|
type: "module",
|
|
153
153
|
main: "./dist/cli.js",
|
|
@@ -978,6 +978,18 @@ function calculateSMTPCredentialsCost(config2) {
|
|
|
978
978
|
description: "SMTP credentials (no additional cost)"
|
|
979
979
|
};
|
|
980
980
|
}
|
|
981
|
+
function calculateAlertingCost(config2) {
|
|
982
|
+
if (!config2.alerts?.enabled) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const numAlarms = config2.alerts.dlqAlerts !== false ? 5 : 4;
|
|
986
|
+
const alarmCost = numAlarms * 0.1;
|
|
987
|
+
const snsCost = 0;
|
|
988
|
+
return {
|
|
989
|
+
monthly: alarmCost + snsCost,
|
|
990
|
+
description: `Reputation alerts (${numAlarms} CloudWatch alarms)`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
981
993
|
function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
982
994
|
const tracking = calculateTrackingCost(config2);
|
|
983
995
|
const reputationMetrics = calculateReputationMetricsCost(config2);
|
|
@@ -987,8 +999,9 @@ function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
|
987
999
|
const dedicatedIp = calculateDedicatedIpCost(config2);
|
|
988
1000
|
const waf = calculateWafCost(config2, emailsPerMonth);
|
|
989
1001
|
const smtpCredentials = calculateSMTPCredentialsCost(config2);
|
|
1002
|
+
const alerts = calculateAlertingCost(config2);
|
|
990
1003
|
const sesEmailCost = Math.max(0, emailsPerMonth - FREE_TIER.SES_EMAILS) * AWS_PRICING.SES_PER_EMAIL;
|
|
991
|
-
const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (emailArchiving?.monthly || 0) + (dedicatedIp?.monthly || 0) + (waf?.monthly || 0) + (smtpCredentials?.monthly || 0);
|
|
1004
|
+
const totalMonthlyCost = sesEmailCost + (tracking?.monthly || 0) + (reputationMetrics?.monthly || 0) + (eventTracking?.monthly || 0) + (dynamoDBHistory?.monthly || 0) + (emailArchiving?.monthly || 0) + (dedicatedIp?.monthly || 0) + (waf?.monthly || 0) + (smtpCredentials?.monthly || 0) + (alerts?.monthly || 0);
|
|
992
1005
|
return {
|
|
993
1006
|
tracking,
|
|
994
1007
|
reputationMetrics,
|
|
@@ -998,6 +1011,7 @@ function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
|
998
1011
|
dedicatedIp,
|
|
999
1012
|
waf,
|
|
1000
1013
|
smtpCredentials,
|
|
1014
|
+
alerts,
|
|
1001
1015
|
total: {
|
|
1002
1016
|
monthly: totalMonthlyCost,
|
|
1003
1017
|
perEmail: AWS_PRICING.SES_PER_EMAIL,
|
|
@@ -1063,6 +1077,11 @@ function getCostSummary(config2, emailsPerMonth = 1e4) {
|
|
|
1063
1077
|
` - ${costs.smtpCredentials.description}: ${formatCost(costs.smtpCredentials.monthly)}`
|
|
1064
1078
|
);
|
|
1065
1079
|
}
|
|
1080
|
+
if (costs.alerts) {
|
|
1081
|
+
lines.push(
|
|
1082
|
+
` - ${costs.alerts.description}: ${formatCost(costs.alerts.monthly)}`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1066
1085
|
return lines.join("\n");
|
|
1067
1086
|
}
|
|
1068
1087
|
var AWS_PRICING, FREE_TIER;
|
|
@@ -1205,6 +1224,7 @@ function getPresetInfo(preset) {
|
|
|
1205
1224
|
"Reputation tracking",
|
|
1206
1225
|
"Real-time event tracking (EventBridge)",
|
|
1207
1226
|
"90-day email history storage",
|
|
1227
|
+
"Reputation alerts (bounce/complaint rate monitoring)",
|
|
1208
1228
|
"Optional: Email archiving with rendered viewer",
|
|
1209
1229
|
"Complete event visibility"
|
|
1210
1230
|
]
|
|
@@ -1218,6 +1238,7 @@ function getPresetInfo(preset) {
|
|
|
1218
1238
|
"Everything in Production",
|
|
1219
1239
|
"Dedicated IP address",
|
|
1220
1240
|
"1-year email history",
|
|
1241
|
+
"Stricter alert thresholds (catch issues earlier)",
|
|
1221
1242
|
"Optional: 1-year+ email archiving",
|
|
1222
1243
|
"All event types tracked",
|
|
1223
1244
|
"Priority support eligibility"
|
|
@@ -1306,6 +1327,10 @@ var init_presets = __esm({
|
|
|
1306
1327
|
enabled: false,
|
|
1307
1328
|
retention: "30days"
|
|
1308
1329
|
},
|
|
1330
|
+
// Alerting disabled for starter (no reputation metrics)
|
|
1331
|
+
alerts: {
|
|
1332
|
+
enabled: false
|
|
1333
|
+
},
|
|
1309
1334
|
sendingEnabled: true
|
|
1310
1335
|
};
|
|
1311
1336
|
PRODUCTION_PRESET = {
|
|
@@ -1342,6 +1367,12 @@ var init_presets = __esm({
|
|
|
1342
1367
|
// User can opt-in
|
|
1343
1368
|
retention: "90days"
|
|
1344
1369
|
},
|
|
1370
|
+
// Alerting enabled - warns before AWS/Gmail take action
|
|
1371
|
+
alerts: {
|
|
1372
|
+
enabled: true,
|
|
1373
|
+
dlqAlerts: true
|
|
1374
|
+
// Uses default thresholds: bounce 2%/4%, complaint 0.05%/0.08%
|
|
1375
|
+
},
|
|
1345
1376
|
sendingEnabled: true
|
|
1346
1377
|
};
|
|
1347
1378
|
ENTERPRISE_PRESET = {
|
|
@@ -1380,6 +1411,22 @@ var init_presets = __esm({
|
|
|
1380
1411
|
// User can opt-in
|
|
1381
1412
|
retention: "1year"
|
|
1382
1413
|
},
|
|
1414
|
+
// Alerting with stricter thresholds for high-volume senders
|
|
1415
|
+
alerts: {
|
|
1416
|
+
enabled: true,
|
|
1417
|
+
dlqAlerts: true,
|
|
1418
|
+
thresholds: {
|
|
1419
|
+
// Stricter thresholds for enterprise - catch issues earlier
|
|
1420
|
+
bounceRateWarning: 0.01,
|
|
1421
|
+
// 1% (vs 2% default)
|
|
1422
|
+
bounceRateCritical: 0.02,
|
|
1423
|
+
// 2% (vs 4% default)
|
|
1424
|
+
complaintRateWarning: 3e-4,
|
|
1425
|
+
// 0.03% (vs 0.05% default)
|
|
1426
|
+
complaintRateCritical: 5e-4
|
|
1427
|
+
// 0.05% (vs 0.08% default)
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1383
1430
|
dedicatedIp: true,
|
|
1384
1431
|
sendingEnabled: true
|
|
1385
1432
|
};
|
|
@@ -3223,7 +3270,7 @@ import { existsSync as existsSync4, mkdirSync } from "fs";
|
|
|
3223
3270
|
import { tmpdir } from "os";
|
|
3224
3271
|
import { dirname, join as join4 } from "path";
|
|
3225
3272
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3226
|
-
import * as
|
|
3273
|
+
import * as aws8 from "@pulumi/aws";
|
|
3227
3274
|
import * as pulumi11 from "@pulumi/pulumi";
|
|
3228
3275
|
import { build } from "esbuild";
|
|
3229
3276
|
function getPackageRoot() {
|
|
@@ -3312,7 +3359,7 @@ Try running: pnpm build`
|
|
|
3312
3359
|
}
|
|
3313
3360
|
async function deployLambdaFunctions(config2) {
|
|
3314
3361
|
const eventProcessorCode = await getLambdaCode("event-processor");
|
|
3315
|
-
const lambdaRole = new
|
|
3362
|
+
const lambdaRole = new aws8.iam.Role("wraps-email-lambda-role", {
|
|
3316
3363
|
assumeRolePolicy: JSON.stringify({
|
|
3317
3364
|
Version: "2012-10-17",
|
|
3318
3365
|
Statement: [
|
|
@@ -3327,11 +3374,11 @@ async function deployLambdaFunctions(config2) {
|
|
|
3327
3374
|
ManagedBy: "wraps-cli"
|
|
3328
3375
|
}
|
|
3329
3376
|
});
|
|
3330
|
-
new
|
|
3377
|
+
new aws8.iam.RolePolicyAttachment("wraps-email-lambda-basic-execution", {
|
|
3331
3378
|
role: lambdaRole.name,
|
|
3332
3379
|
policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
3333
3380
|
});
|
|
3334
|
-
new
|
|
3381
|
+
new aws8.iam.RolePolicy("wraps-email-lambda-policy", {
|
|
3335
3382
|
role: lambdaRole.name,
|
|
3336
3383
|
policy: pulumi11.all([config2.tableName, config2.queueArn]).apply(
|
|
3337
3384
|
([tableName, queueArn]) => JSON.stringify({
|
|
@@ -3375,7 +3422,7 @@ async function deployLambdaFunctions(config2) {
|
|
|
3375
3422
|
RETENTION_DAYS: config2.retentionDays.toString()
|
|
3376
3423
|
}
|
|
3377
3424
|
};
|
|
3378
|
-
const eventProcessor = exists ? new
|
|
3425
|
+
const eventProcessor = exists ? new aws8.lambda.Function(
|
|
3379
3426
|
functionName,
|
|
3380
3427
|
{
|
|
3381
3428
|
name: functionName,
|
|
@@ -3396,7 +3443,7 @@ async function deployLambdaFunctions(config2) {
|
|
|
3396
3443
|
import: functionName
|
|
3397
3444
|
// Import existing function
|
|
3398
3445
|
}
|
|
3399
|
-
) : new
|
|
3446
|
+
) : new aws8.lambda.Function(functionName, {
|
|
3400
3447
|
name: functionName,
|
|
3401
3448
|
runtime: "nodejs24.x",
|
|
3402
3449
|
handler: "index.handler",
|
|
@@ -3426,14 +3473,14 @@ async function deployLambdaFunctions(config2) {
|
|
|
3426
3473
|
functionResponseTypes: ["ReportBatchItemFailures"]
|
|
3427
3474
|
// Enable partial batch responses
|
|
3428
3475
|
};
|
|
3429
|
-
const eventSourceMapping = existingMappingUuid ? new
|
|
3476
|
+
const eventSourceMapping = existingMappingUuid ? new aws8.lambda.EventSourceMapping(
|
|
3430
3477
|
"wraps-email-event-source-mapping",
|
|
3431
3478
|
mappingConfig,
|
|
3432
3479
|
{
|
|
3433
3480
|
import: existingMappingUuid
|
|
3434
3481
|
// Import with the UUID
|
|
3435
3482
|
}
|
|
3436
|
-
) : new
|
|
3483
|
+
) : new aws8.lambda.EventSourceMapping(
|
|
3437
3484
|
"wraps-email-event-source-mapping",
|
|
3438
3485
|
mappingConfig
|
|
3439
3486
|
);
|
|
@@ -3454,12 +3501,12 @@ var acm_exports = {};
|
|
|
3454
3501
|
__export(acm_exports, {
|
|
3455
3502
|
createACMCertificate: () => createACMCertificate
|
|
3456
3503
|
});
|
|
3457
|
-
import * as
|
|
3504
|
+
import * as aws12 from "@pulumi/aws";
|
|
3458
3505
|
async function createACMCertificate(config2) {
|
|
3459
|
-
const usEast1Provider = new
|
|
3506
|
+
const usEast1Provider = new aws12.Provider("acm-us-east-1", {
|
|
3460
3507
|
region: "us-east-1"
|
|
3461
3508
|
});
|
|
3462
|
-
const certificate = new
|
|
3509
|
+
const certificate = new aws12.acm.Certificate(
|
|
3463
3510
|
"wraps-email-tracking-cert",
|
|
3464
3511
|
{
|
|
3465
3512
|
domainName: config2.domain,
|
|
@@ -3482,7 +3529,7 @@ async function createACMCertificate(config2) {
|
|
|
3482
3529
|
);
|
|
3483
3530
|
let certificateValidation;
|
|
3484
3531
|
if (config2.hostedZoneId) {
|
|
3485
|
-
const validationRecord = new
|
|
3532
|
+
const validationRecord = new aws12.route53.Record(
|
|
3486
3533
|
"wraps-email-tracking-cert-validation",
|
|
3487
3534
|
{
|
|
3488
3535
|
zoneId: config2.hostedZoneId,
|
|
@@ -3492,7 +3539,7 @@ async function createACMCertificate(config2) {
|
|
|
3492
3539
|
ttl: 60
|
|
3493
3540
|
}
|
|
3494
3541
|
);
|
|
3495
|
-
certificateValidation = new
|
|
3542
|
+
certificateValidation = new aws12.acm.CertificateValidation(
|
|
3496
3543
|
"wraps-email-tracking-cert-validation-waiter",
|
|
3497
3544
|
{
|
|
3498
3545
|
certificateArn: certificate.arn,
|
|
@@ -3521,7 +3568,7 @@ var cloudfront_exports = {};
|
|
|
3521
3568
|
__export(cloudfront_exports, {
|
|
3522
3569
|
createCloudFrontTracking: () => createCloudFrontTracking
|
|
3523
3570
|
});
|
|
3524
|
-
import * as
|
|
3571
|
+
import * as aws13 from "@pulumi/aws";
|
|
3525
3572
|
async function findDistributionByAlias(alias) {
|
|
3526
3573
|
try {
|
|
3527
3574
|
const { CloudFrontClient, ListDistributionsCommand } = await import("@aws-sdk/client-cloudfront");
|
|
@@ -3537,10 +3584,10 @@ async function findDistributionByAlias(alias) {
|
|
|
3537
3584
|
}
|
|
3538
3585
|
}
|
|
3539
3586
|
async function createWAFWebACL() {
|
|
3540
|
-
const usEast1Provider = new
|
|
3587
|
+
const usEast1Provider = new aws13.Provider("waf-us-east-1", {
|
|
3541
3588
|
region: "us-east-1"
|
|
3542
3589
|
});
|
|
3543
|
-
const webAcl = new
|
|
3590
|
+
const webAcl = new aws13.wafv2.WebAcl(
|
|
3544
3591
|
"wraps-email-tracking-waf",
|
|
3545
3592
|
{
|
|
3546
3593
|
scope: "CLOUDFRONT",
|
|
@@ -3655,14 +3702,14 @@ async function createCloudFrontTracking(config2) {
|
|
|
3655
3702
|
Description: "Wraps email tracking CloudFront distribution"
|
|
3656
3703
|
}
|
|
3657
3704
|
};
|
|
3658
|
-
const distribution = existingDistributionId ? new
|
|
3705
|
+
const distribution = existingDistributionId ? new aws13.cloudfront.Distribution(
|
|
3659
3706
|
"wraps-email-tracking-cdn",
|
|
3660
3707
|
distributionConfig,
|
|
3661
3708
|
{
|
|
3662
3709
|
import: existingDistributionId
|
|
3663
3710
|
// Import existing distribution
|
|
3664
3711
|
}
|
|
3665
|
-
) : new
|
|
3712
|
+
) : new aws13.cloudfront.Distribution(
|
|
3666
3713
|
"wraps-email-tracking-cdn",
|
|
3667
3714
|
distributionConfig
|
|
3668
3715
|
);
|
|
@@ -11611,12 +11658,200 @@ import pc14 from "picocolors";
|
|
|
11611
11658
|
// src/infrastructure/email-stack.ts
|
|
11612
11659
|
init_esm_shims();
|
|
11613
11660
|
init_dist();
|
|
11614
|
-
import * as
|
|
11661
|
+
import * as aws14 from "@pulumi/aws";
|
|
11615
11662
|
import * as pulumi12 from "@pulumi/pulumi";
|
|
11616
11663
|
|
|
11617
|
-
// src/infrastructure/resources/
|
|
11664
|
+
// src/infrastructure/resources/alerting.ts
|
|
11618
11665
|
init_esm_shims();
|
|
11619
11666
|
import * as aws4 from "@pulumi/aws";
|
|
11667
|
+
|
|
11668
|
+
// src/types/index.ts
|
|
11669
|
+
init_esm_shims();
|
|
11670
|
+
|
|
11671
|
+
// src/types/email.ts
|
|
11672
|
+
init_esm_shims();
|
|
11673
|
+
var DEFAULT_ALERT_THRESHOLDS = {
|
|
11674
|
+
bounceRateWarning: 0.02,
|
|
11675
|
+
// 2% - gives time before AWS 5% warning
|
|
11676
|
+
bounceRateCritical: 0.04,
|
|
11677
|
+
// 4% - urgent, approaching AWS warning
|
|
11678
|
+
complaintRateWarning: 5e-4,
|
|
11679
|
+
// 0.05% - half of AWS warning threshold
|
|
11680
|
+
complaintRateCritical: 8e-4,
|
|
11681
|
+
// 0.08% - urgent, approaching AWS 0.1% warning
|
|
11682
|
+
dlqMessageThreshold: 1
|
|
11683
|
+
// Any failed message processing
|
|
11684
|
+
};
|
|
11685
|
+
|
|
11686
|
+
// src/infrastructure/resources/alerting.ts
|
|
11687
|
+
function getThresholds(custom) {
|
|
11688
|
+
return {
|
|
11689
|
+
...DEFAULT_ALERT_THRESHOLDS,
|
|
11690
|
+
...custom
|
|
11691
|
+
};
|
|
11692
|
+
}
|
|
11693
|
+
async function createAlertingResources(config2) {
|
|
11694
|
+
const thresholds = getThresholds(config2.alertConfig.thresholds);
|
|
11695
|
+
const topic = new aws4.sns.Topic("wraps-email-alerts", {
|
|
11696
|
+
name: "wraps-email-alerts",
|
|
11697
|
+
displayName: "Wraps Email Alerts",
|
|
11698
|
+
tags: {
|
|
11699
|
+
ManagedBy: "wraps-cli",
|
|
11700
|
+
Description: "Alert notifications for email reputation and health"
|
|
11701
|
+
}
|
|
11702
|
+
});
|
|
11703
|
+
let emailSubscription;
|
|
11704
|
+
if (config2.alertConfig.notificationEmail) {
|
|
11705
|
+
emailSubscription = new aws4.sns.TopicSubscription(
|
|
11706
|
+
"wraps-email-alerts-email",
|
|
11707
|
+
{
|
|
11708
|
+
topic: topic.arn,
|
|
11709
|
+
protocol: "email",
|
|
11710
|
+
endpoint: config2.alertConfig.notificationEmail
|
|
11711
|
+
}
|
|
11712
|
+
);
|
|
11713
|
+
}
|
|
11714
|
+
let webhookSubscription;
|
|
11715
|
+
if (config2.alertConfig.webhookUrl) {
|
|
11716
|
+
webhookSubscription = new aws4.sns.TopicSubscription(
|
|
11717
|
+
"wraps-email-alerts-webhook",
|
|
11718
|
+
{
|
|
11719
|
+
topic: topic.arn,
|
|
11720
|
+
protocol: "https",
|
|
11721
|
+
endpoint: config2.alertConfig.webhookUrl
|
|
11722
|
+
}
|
|
11723
|
+
);
|
|
11724
|
+
}
|
|
11725
|
+
const bounceRateWarningAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11726
|
+
"wraps-bounce-rate-warning",
|
|
11727
|
+
{
|
|
11728
|
+
name: "wraps-email-bounce-rate-warning",
|
|
11729
|
+
alarmDescription: `Bounce rate exceeded ${thresholds.bounceRateWarning * 100}% - investigate before AWS takes action (warns at 5%, suspends at 10%)`,
|
|
11730
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11731
|
+
evaluationPeriods: 2,
|
|
11732
|
+
// Require 2 consecutive periods to reduce noise
|
|
11733
|
+
metricName: "Reputation.BounceRate",
|
|
11734
|
+
namespace: "AWS/SES",
|
|
11735
|
+
period: 300,
|
|
11736
|
+
// 5 minutes
|
|
11737
|
+
statistic: "Average",
|
|
11738
|
+
threshold: thresholds.bounceRateWarning,
|
|
11739
|
+
alarmActions: [topic.arn],
|
|
11740
|
+
okActions: [topic.arn],
|
|
11741
|
+
// Notify when resolved
|
|
11742
|
+
treatMissingData: "notBreaching",
|
|
11743
|
+
// Don't alarm if no data (no emails sent)
|
|
11744
|
+
tags: {
|
|
11745
|
+
ManagedBy: "wraps-cli",
|
|
11746
|
+
Severity: "warning"
|
|
11747
|
+
}
|
|
11748
|
+
}
|
|
11749
|
+
);
|
|
11750
|
+
const bounceRateCriticalAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11751
|
+
"wraps-bounce-rate-critical",
|
|
11752
|
+
{
|
|
11753
|
+
name: "wraps-email-bounce-rate-critical",
|
|
11754
|
+
alarmDescription: `CRITICAL: Bounce rate exceeded ${thresholds.bounceRateCritical * 100}% - approaching AWS warning threshold (5%). Immediate action required!`,
|
|
11755
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11756
|
+
evaluationPeriods: 1,
|
|
11757
|
+
// Alert immediately on critical
|
|
11758
|
+
metricName: "Reputation.BounceRate",
|
|
11759
|
+
namespace: "AWS/SES",
|
|
11760
|
+
period: 300,
|
|
11761
|
+
// 5 minutes
|
|
11762
|
+
statistic: "Average",
|
|
11763
|
+
threshold: thresholds.bounceRateCritical,
|
|
11764
|
+
alarmActions: [topic.arn],
|
|
11765
|
+
okActions: [topic.arn],
|
|
11766
|
+
treatMissingData: "notBreaching",
|
|
11767
|
+
tags: {
|
|
11768
|
+
ManagedBy: "wraps-cli",
|
|
11769
|
+
Severity: "critical"
|
|
11770
|
+
}
|
|
11771
|
+
}
|
|
11772
|
+
);
|
|
11773
|
+
const complaintRateWarningAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11774
|
+
"wraps-complaint-rate-warning",
|
|
11775
|
+
{
|
|
11776
|
+
name: "wraps-email-complaint-rate-warning",
|
|
11777
|
+
alarmDescription: `Complaint rate exceeded ${thresholds.complaintRateWarning * 100}% - investigate before AWS (0.1%) or Gmail (0.3%) take action`,
|
|
11778
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11779
|
+
evaluationPeriods: 2,
|
|
11780
|
+
metricName: "Reputation.ComplaintRate",
|
|
11781
|
+
namespace: "AWS/SES",
|
|
11782
|
+
period: 300,
|
|
11783
|
+
statistic: "Average",
|
|
11784
|
+
threshold: thresholds.complaintRateWarning,
|
|
11785
|
+
alarmActions: [topic.arn],
|
|
11786
|
+
okActions: [topic.arn],
|
|
11787
|
+
treatMissingData: "notBreaching",
|
|
11788
|
+
tags: {
|
|
11789
|
+
ManagedBy: "wraps-cli",
|
|
11790
|
+
Severity: "warning"
|
|
11791
|
+
}
|
|
11792
|
+
}
|
|
11793
|
+
);
|
|
11794
|
+
const complaintRateCriticalAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11795
|
+
"wraps-complaint-rate-critical",
|
|
11796
|
+
{
|
|
11797
|
+
name: "wraps-email-complaint-rate-critical",
|
|
11798
|
+
alarmDescription: `CRITICAL: Complaint rate exceeded ${thresholds.complaintRateCritical * 100}% - approaching AWS warning (0.1%). Immediate action required!`,
|
|
11799
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11800
|
+
evaluationPeriods: 1,
|
|
11801
|
+
metricName: "Reputation.ComplaintRate",
|
|
11802
|
+
namespace: "AWS/SES",
|
|
11803
|
+
period: 300,
|
|
11804
|
+
statistic: "Average",
|
|
11805
|
+
threshold: thresholds.complaintRateCritical,
|
|
11806
|
+
alarmActions: [topic.arn],
|
|
11807
|
+
okActions: [topic.arn],
|
|
11808
|
+
treatMissingData: "notBreaching",
|
|
11809
|
+
tags: {
|
|
11810
|
+
ManagedBy: "wraps-cli",
|
|
11811
|
+
Severity: "critical"
|
|
11812
|
+
}
|
|
11813
|
+
}
|
|
11814
|
+
);
|
|
11815
|
+
let dlqAlarm;
|
|
11816
|
+
if (config2.alertConfig.dlqAlerts !== false && config2.dlqName) {
|
|
11817
|
+
dlqAlarm = new aws4.cloudwatch.MetricAlarm("wraps-dlq-alarm", {
|
|
11818
|
+
name: "wraps-email-dlq-messages",
|
|
11819
|
+
alarmDescription: "Messages in dead letter queue - event processing is failing. Check Lambda logs for errors.",
|
|
11820
|
+
comparisonOperator: "GreaterThanOrEqualToThreshold",
|
|
11821
|
+
evaluationPeriods: 1,
|
|
11822
|
+
metricName: "ApproximateNumberOfMessagesVisible",
|
|
11823
|
+
namespace: "AWS/SQS",
|
|
11824
|
+
period: 60,
|
|
11825
|
+
// Check every minute
|
|
11826
|
+
statistic: "Sum",
|
|
11827
|
+
threshold: thresholds.dlqMessageThreshold,
|
|
11828
|
+
dimensions: {
|
|
11829
|
+
QueueName: config2.dlqName
|
|
11830
|
+
},
|
|
11831
|
+
alarmActions: [topic.arn],
|
|
11832
|
+
okActions: [topic.arn],
|
|
11833
|
+
treatMissingData: "notBreaching",
|
|
11834
|
+
tags: {
|
|
11835
|
+
ManagedBy: "wraps-cli",
|
|
11836
|
+
Severity: "warning"
|
|
11837
|
+
}
|
|
11838
|
+
});
|
|
11839
|
+
}
|
|
11840
|
+
return {
|
|
11841
|
+
topic,
|
|
11842
|
+
emailSubscription,
|
|
11843
|
+
webhookSubscription,
|
|
11844
|
+
bounceRateWarningAlarm,
|
|
11845
|
+
bounceRateCriticalAlarm,
|
|
11846
|
+
complaintRateWarningAlarm,
|
|
11847
|
+
complaintRateCriticalAlarm,
|
|
11848
|
+
dlqAlarm
|
|
11849
|
+
};
|
|
11850
|
+
}
|
|
11851
|
+
|
|
11852
|
+
// src/infrastructure/resources/dynamodb.ts
|
|
11853
|
+
init_esm_shims();
|
|
11854
|
+
import * as aws5 from "@pulumi/aws";
|
|
11620
11855
|
async function tableExists(tableName) {
|
|
11621
11856
|
try {
|
|
11622
11857
|
const { DynamoDBClient: DynamoDBClient6, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
|
|
@@ -11636,7 +11871,7 @@ async function tableExists(tableName) {
|
|
|
11636
11871
|
async function createDynamoDBTables(_config) {
|
|
11637
11872
|
const tableName = "wraps-email-history";
|
|
11638
11873
|
const exists = await tableExists(tableName);
|
|
11639
|
-
const emailHistory = exists ? new
|
|
11874
|
+
const emailHistory = exists ? new aws5.dynamodb.Table(
|
|
11640
11875
|
tableName,
|
|
11641
11876
|
{
|
|
11642
11877
|
name: tableName,
|
|
@@ -11668,7 +11903,7 @@ async function createDynamoDBTables(_config) {
|
|
|
11668
11903
|
import: tableName
|
|
11669
11904
|
// Import existing table
|
|
11670
11905
|
}
|
|
11671
|
-
) : new
|
|
11906
|
+
) : new aws5.dynamodb.Table(tableName, {
|
|
11672
11907
|
name: tableName,
|
|
11673
11908
|
billingMode: "PAY_PER_REQUEST",
|
|
11674
11909
|
hashKey: "messageId",
|
|
@@ -11701,11 +11936,11 @@ async function createDynamoDBTables(_config) {
|
|
|
11701
11936
|
|
|
11702
11937
|
// src/infrastructure/resources/eventbridge.ts
|
|
11703
11938
|
init_esm_shims();
|
|
11704
|
-
import * as
|
|
11939
|
+
import * as aws6 from "@pulumi/aws";
|
|
11705
11940
|
import * as pulumi9 from "@pulumi/pulumi";
|
|
11706
11941
|
async function createEventBridgeResources(config2) {
|
|
11707
11942
|
const eventBusName = config2.eventBusArn.apply((arn) => arn.split("/").pop());
|
|
11708
|
-
const rule = new
|
|
11943
|
+
const rule = new aws6.cloudwatch.EventRule("wraps-email-events-rule", {
|
|
11709
11944
|
name: "wraps-email-events-to-sqs",
|
|
11710
11945
|
description: "Route all SES email events to SQS for processing",
|
|
11711
11946
|
eventBusName,
|
|
@@ -11718,7 +11953,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11718
11953
|
ManagedBy: "wraps-cli"
|
|
11719
11954
|
}
|
|
11720
11955
|
});
|
|
11721
|
-
new
|
|
11956
|
+
new aws6.sqs.QueuePolicy("wraps-email-events-queue-policy", {
|
|
11722
11957
|
queueUrl: config2.queueUrl,
|
|
11723
11958
|
policy: pulumi9.all([config2.queueArn, rule.arn]).apply(
|
|
11724
11959
|
([queueArn, ruleArn]) => JSON.stringify({
|
|
@@ -11741,7 +11976,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11741
11976
|
})
|
|
11742
11977
|
)
|
|
11743
11978
|
});
|
|
11744
|
-
const target = new
|
|
11979
|
+
const target = new aws6.cloudwatch.EventTarget("wraps-email-events-target", {
|
|
11745
11980
|
rule: rule.name,
|
|
11746
11981
|
eventBusName,
|
|
11747
11982
|
arn: config2.queueArn
|
|
@@ -11752,7 +11987,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11752
11987
|
if (config2.webhook) {
|
|
11753
11988
|
const { awsAccountNumber, webhookSecret, webhookUrl } = config2.webhook;
|
|
11754
11989
|
const baseUrl = webhookUrl || "https://api.wraps.dev";
|
|
11755
|
-
webhookConnection = new
|
|
11990
|
+
webhookConnection = new aws6.cloudwatch.EventConnection(
|
|
11756
11991
|
"wraps-webhook-connection",
|
|
11757
11992
|
{
|
|
11758
11993
|
name: "wraps-webhook-connection",
|
|
@@ -11766,7 +12001,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11766
12001
|
}
|
|
11767
12002
|
}
|
|
11768
12003
|
);
|
|
11769
|
-
webhookApiDestination = new
|
|
12004
|
+
webhookApiDestination = new aws6.cloudwatch.EventApiDestination(
|
|
11770
12005
|
"wraps-webhook-destination",
|
|
11771
12006
|
{
|
|
11772
12007
|
name: "wraps-webhook-destination",
|
|
@@ -11778,7 +12013,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11778
12013
|
// Rate limit
|
|
11779
12014
|
}
|
|
11780
12015
|
);
|
|
11781
|
-
const webhookRole = new
|
|
12016
|
+
const webhookRole = new aws6.iam.Role("wraps-webhook-role", {
|
|
11782
12017
|
name: "wraps-eventbridge-webhook-role",
|
|
11783
12018
|
assumeRolePolicy: JSON.stringify({
|
|
11784
12019
|
Version: "2012-10-17",
|
|
@@ -11796,7 +12031,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11796
12031
|
ManagedBy: "wraps-cli"
|
|
11797
12032
|
}
|
|
11798
12033
|
});
|
|
11799
|
-
new
|
|
12034
|
+
new aws6.iam.RolePolicy("wraps-webhook-policy", {
|
|
11800
12035
|
role: webhookRole.name,
|
|
11801
12036
|
policy: webhookApiDestination.arn.apply(
|
|
11802
12037
|
(destArn) => JSON.stringify({
|
|
@@ -11811,7 +12046,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11811
12046
|
})
|
|
11812
12047
|
)
|
|
11813
12048
|
});
|
|
11814
|
-
webhookTarget = new
|
|
12049
|
+
webhookTarget = new aws6.cloudwatch.EventTarget("wraps-webhook-target", {
|
|
11815
12050
|
rule: rule.name,
|
|
11816
12051
|
eventBusName,
|
|
11817
12052
|
arn: webhookApiDestination.arn,
|
|
@@ -11829,7 +12064,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11829
12064
|
|
|
11830
12065
|
// src/infrastructure/resources/iam.ts
|
|
11831
12066
|
init_esm_shims();
|
|
11832
|
-
import * as
|
|
12067
|
+
import * as aws7 from "@pulumi/aws";
|
|
11833
12068
|
import * as pulumi10 from "@pulumi/pulumi";
|
|
11834
12069
|
async function roleExists2(roleName) {
|
|
11835
12070
|
try {
|
|
@@ -11884,7 +12119,7 @@ async function createIAMRole(config2) {
|
|
|
11884
12119
|
}
|
|
11885
12120
|
const roleName = "wraps-email-role";
|
|
11886
12121
|
const exists = await roleExists2(roleName);
|
|
11887
|
-
const role = exists ? new
|
|
12122
|
+
const role = exists ? new aws7.iam.Role(
|
|
11888
12123
|
roleName,
|
|
11889
12124
|
{
|
|
11890
12125
|
name: roleName,
|
|
@@ -11898,7 +12133,7 @@ async function createIAMRole(config2) {
|
|
|
11898
12133
|
import: roleName
|
|
11899
12134
|
// Import existing role (use role name, not ARN)
|
|
11900
12135
|
}
|
|
11901
|
-
) : new
|
|
12136
|
+
) : new aws7.iam.Role(roleName, {
|
|
11902
12137
|
name: roleName,
|
|
11903
12138
|
assumeRolePolicy,
|
|
11904
12139
|
tags: {
|
|
@@ -11993,7 +12228,7 @@ async function createIAMRole(config2) {
|
|
|
11993
12228
|
Resource: "arn:aws:ses:*:*:mailmanager-archive/*"
|
|
11994
12229
|
});
|
|
11995
12230
|
}
|
|
11996
|
-
new
|
|
12231
|
+
new aws7.iam.RolePolicy("wraps-email-policy", {
|
|
11997
12232
|
role: role.name,
|
|
11998
12233
|
policy: JSON.stringify({
|
|
11999
12234
|
Version: "2012-10-17",
|
|
@@ -12008,7 +12243,7 @@ init_lambda();
|
|
|
12008
12243
|
|
|
12009
12244
|
// src/infrastructure/resources/ses.ts
|
|
12010
12245
|
init_esm_shims();
|
|
12011
|
-
import * as
|
|
12246
|
+
import * as aws9 from "@pulumi/aws";
|
|
12012
12247
|
async function configurationSetExists(configSetName, region) {
|
|
12013
12248
|
try {
|
|
12014
12249
|
const { SESv2Client: SESv2Client6, GetConfigurationSetCommand: GetConfigurationSetCommand2 } = await import("@aws-sdk/client-sesv2");
|
|
@@ -12086,16 +12321,16 @@ async function createSESResources(config2) {
|
|
|
12086
12321
|
}
|
|
12087
12322
|
const configSetName = "wraps-email-tracking";
|
|
12088
12323
|
const exists = await configurationSetExists(configSetName, config2.region);
|
|
12089
|
-
const configSet = exists ? new
|
|
12324
|
+
const configSet = exists ? new aws9.sesv2.ConfigurationSet(configSetName, configSetOptions, {
|
|
12090
12325
|
import: configSetName
|
|
12091
12326
|
// Import existing configuration set
|
|
12092
|
-
}) : new
|
|
12093
|
-
const defaultEventBus =
|
|
12327
|
+
}) : new aws9.sesv2.ConfigurationSet(configSetName, configSetOptions);
|
|
12328
|
+
const defaultEventBus = aws9.cloudwatch.getEventBusOutput({
|
|
12094
12329
|
name: "default"
|
|
12095
12330
|
});
|
|
12096
12331
|
if (config2.eventTrackingEnabled) {
|
|
12097
12332
|
const eventDestName = "wraps-email-eventbridge";
|
|
12098
|
-
new
|
|
12333
|
+
new aws9.sesv2.ConfigurationSetEventDestination(
|
|
12099
12334
|
"wraps-email-all-events",
|
|
12100
12335
|
{
|
|
12101
12336
|
configurationSetName: configSet.configurationSetName,
|
|
@@ -12135,7 +12370,7 @@ async function createSESResources(config2) {
|
|
|
12135
12370
|
config2.domain,
|
|
12136
12371
|
config2.region
|
|
12137
12372
|
);
|
|
12138
|
-
domainIdentity = identityExists ? new
|
|
12373
|
+
domainIdentity = identityExists ? new aws9.sesv2.EmailIdentity(
|
|
12139
12374
|
"wraps-email-domain",
|
|
12140
12375
|
{
|
|
12141
12376
|
emailIdentity: config2.domain,
|
|
@@ -12152,7 +12387,7 @@ async function createSESResources(config2) {
|
|
|
12152
12387
|
import: config2.domain
|
|
12153
12388
|
// Import existing identity
|
|
12154
12389
|
}
|
|
12155
|
-
) : new
|
|
12390
|
+
) : new aws9.sesv2.EmailIdentity("wraps-email-domain", {
|
|
12156
12391
|
emailIdentity: config2.domain,
|
|
12157
12392
|
configurationSetName: configSet.configurationSetName,
|
|
12158
12393
|
// Link configuration set to domain
|
|
@@ -12168,7 +12403,7 @@ async function createSESResources(config2) {
|
|
|
12168
12403
|
);
|
|
12169
12404
|
if (config2.mailFromDomain) {
|
|
12170
12405
|
mailFromDomain = config2.mailFromDomain;
|
|
12171
|
-
new
|
|
12406
|
+
new aws9.sesv2.EmailIdentityMailFromAttributes(
|
|
12172
12407
|
"wraps-email-mail-from",
|
|
12173
12408
|
{
|
|
12174
12409
|
emailIdentity: config2.domain,
|
|
@@ -12199,7 +12434,7 @@ async function createSESResources(config2) {
|
|
|
12199
12434
|
// src/infrastructure/resources/smtp-credentials.ts
|
|
12200
12435
|
init_esm_shims();
|
|
12201
12436
|
import { createHmac as createHmac2 } from "crypto";
|
|
12202
|
-
import * as
|
|
12437
|
+
import * as aws10 from "@pulumi/aws";
|
|
12203
12438
|
function convertToSMTPPassword2(secretAccessKey, region) {
|
|
12204
12439
|
const DATE = "11111111";
|
|
12205
12440
|
const SERVICE = "ses";
|
|
@@ -12235,7 +12470,7 @@ async function userExists(userName) {
|
|
|
12235
12470
|
async function createSMTPCredentials(config2) {
|
|
12236
12471
|
const userName = "wraps-email-smtp-user";
|
|
12237
12472
|
const userAlreadyExists = await userExists(userName);
|
|
12238
|
-
const iamUser = userAlreadyExists ? new
|
|
12473
|
+
const iamUser = userAlreadyExists ? new aws10.iam.User(
|
|
12239
12474
|
userName,
|
|
12240
12475
|
{
|
|
12241
12476
|
name: userName,
|
|
@@ -12245,14 +12480,14 @@ async function createSMTPCredentials(config2) {
|
|
|
12245
12480
|
}
|
|
12246
12481
|
},
|
|
12247
12482
|
{ import: userName }
|
|
12248
|
-
) : new
|
|
12483
|
+
) : new aws10.iam.User(userName, {
|
|
12249
12484
|
name: userName,
|
|
12250
12485
|
tags: {
|
|
12251
12486
|
ManagedBy: "wraps-cli",
|
|
12252
12487
|
Purpose: "SES SMTP Authentication"
|
|
12253
12488
|
}
|
|
12254
12489
|
});
|
|
12255
|
-
new
|
|
12490
|
+
new aws10.iam.UserPolicy("wraps-email-smtp-policy", {
|
|
12256
12491
|
user: iamUser.name,
|
|
12257
12492
|
policy: JSON.stringify({
|
|
12258
12493
|
Version: "2012-10-17",
|
|
@@ -12270,7 +12505,7 @@ async function createSMTPCredentials(config2) {
|
|
|
12270
12505
|
]
|
|
12271
12506
|
})
|
|
12272
12507
|
});
|
|
12273
|
-
const accessKey = new
|
|
12508
|
+
const accessKey = new aws10.iam.AccessKey("wraps-email-smtp-key", {
|
|
12274
12509
|
user: iamUser.name
|
|
12275
12510
|
});
|
|
12276
12511
|
const smtpPassword = accessKey.secret.apply(
|
|
@@ -12285,9 +12520,9 @@ async function createSMTPCredentials(config2) {
|
|
|
12285
12520
|
|
|
12286
12521
|
// src/infrastructure/resources/sqs.ts
|
|
12287
12522
|
init_esm_shims();
|
|
12288
|
-
import * as
|
|
12523
|
+
import * as aws11 from "@pulumi/aws";
|
|
12289
12524
|
async function createSQSResources() {
|
|
12290
|
-
const dlq = new
|
|
12525
|
+
const dlq = new aws11.sqs.Queue("wraps-email-events-dlq", {
|
|
12291
12526
|
name: "wraps-email-events-dlq",
|
|
12292
12527
|
messageRetentionSeconds: 1209600,
|
|
12293
12528
|
// 14 days
|
|
@@ -12296,7 +12531,7 @@ async function createSQSResources() {
|
|
|
12296
12531
|
Description: "Dead letter queue for failed SES event processing"
|
|
12297
12532
|
}
|
|
12298
12533
|
});
|
|
12299
|
-
const queue = new
|
|
12534
|
+
const queue = new aws11.sqs.Queue("wraps-email-events", {
|
|
12300
12535
|
name: "wraps-email-events",
|
|
12301
12536
|
visibilityTimeoutSeconds: 60,
|
|
12302
12537
|
// Must be >= Lambda timeout
|
|
@@ -12324,7 +12559,7 @@ async function createSQSResources() {
|
|
|
12324
12559
|
|
|
12325
12560
|
// src/infrastructure/email-stack.ts
|
|
12326
12561
|
async function deployEmailStack(config2) {
|
|
12327
|
-
const identity = await
|
|
12562
|
+
const identity = await aws14.getCallerIdentity();
|
|
12328
12563
|
const accountId = identity.accountId;
|
|
12329
12564
|
let oidcProvider;
|
|
12330
12565
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -12440,6 +12675,15 @@ async function deployEmailStack(config2) {
|
|
|
12440
12675
|
region: config2.region
|
|
12441
12676
|
});
|
|
12442
12677
|
}
|
|
12678
|
+
let alertingResources;
|
|
12679
|
+
if (emailConfig.alerts?.enabled) {
|
|
12680
|
+
alertingResources = await createAlertingResources({
|
|
12681
|
+
alertConfig: emailConfig.alerts,
|
|
12682
|
+
configSetName: sesResources?.configSet.configurationSetName,
|
|
12683
|
+
dlqName: sqsResources ? "wraps-email-events-dlq" : void 0,
|
|
12684
|
+
region: config2.region
|
|
12685
|
+
});
|
|
12686
|
+
}
|
|
12443
12687
|
return {
|
|
12444
12688
|
roleArn: role.arn,
|
|
12445
12689
|
configSetName: sesResources?.configSet.configurationSetName,
|
|
@@ -12464,7 +12708,10 @@ async function deployEmailStack(config2) {
|
|
|
12464
12708
|
smtpUserArn: smtpResources?.iamUser.arn,
|
|
12465
12709
|
smtpUsername: smtpResources?.accessKey.id,
|
|
12466
12710
|
smtpPassword: smtpResources?.smtpPassword,
|
|
12467
|
-
smtpEndpoint: smtpResources ? `email-smtp.${config2.region}.amazonaws.com` : void 0
|
|
12711
|
+
smtpEndpoint: smtpResources ? `email-smtp.${config2.region}.amazonaws.com` : void 0,
|
|
12712
|
+
// Alerting outputs
|
|
12713
|
+
alertsEnabled: emailConfig.alerts?.enabled,
|
|
12714
|
+
alertTopicArn: alertingResources?.topic.arn
|
|
12468
12715
|
};
|
|
12469
12716
|
}
|
|
12470
12717
|
|
|
@@ -12837,14 +13084,14 @@ async function scanSESConfigurationSets(region) {
|
|
|
12837
13084
|
}
|
|
12838
13085
|
}
|
|
12839
13086
|
async function scanSNSTopics(region) {
|
|
12840
|
-
const
|
|
13087
|
+
const sns3 = new SNSClient({ region });
|
|
12841
13088
|
const topics = [];
|
|
12842
13089
|
try {
|
|
12843
|
-
const listResponse = await
|
|
13090
|
+
const listResponse = await sns3.send(new ListTopicsCommand({}));
|
|
12844
13091
|
const topicArns = listResponse.Topics?.map((t) => t.TopicArn).filter(Boolean) || [];
|
|
12845
13092
|
for (const arn of topicArns) {
|
|
12846
13093
|
try {
|
|
12847
|
-
const attrsResponse = await
|
|
13094
|
+
const attrsResponse = await sns3.send(
|
|
12848
13095
|
new GetTopicAttributesCommand({ TopicArn: arn })
|
|
12849
13096
|
);
|
|
12850
13097
|
const name = arn.split(":").pop() || arn;
|
|
@@ -14777,6 +15024,14 @@ ${pc21.bold("Current Configuration:")}
|
|
|
14777
15024
|
}[config2.emailArchiving.retention] || "90 days";
|
|
14778
15025
|
console.log(` ${pc21.green("\u2713")} Email Archiving (${retentionLabel})`);
|
|
14779
15026
|
}
|
|
15027
|
+
if (config2.alerts?.enabled) {
|
|
15028
|
+
console.log(` ${pc21.green("\u2713")} Reputation Alerts`);
|
|
15029
|
+
if (config2.alerts.notificationEmail) {
|
|
15030
|
+
console.log(
|
|
15031
|
+
` ${pc21.dim("\u2514\u2500")} Email: ${pc21.cyan(config2.alerts.notificationEmail)}`
|
|
15032
|
+
);
|
|
15033
|
+
}
|
|
15034
|
+
}
|
|
14780
15035
|
const currentCostData = calculateCosts(config2, 5e4);
|
|
14781
15036
|
console.log(
|
|
14782
15037
|
`
|
|
@@ -14816,6 +15071,11 @@ ${pc21.bold("Current Configuration:")}
|
|
|
14816
15071
|
label: "Enable dedicated IP address",
|
|
14817
15072
|
hint: "Requires 100k+ emails/day ($50-100/mo)"
|
|
14818
15073
|
},
|
|
15074
|
+
{
|
|
15075
|
+
value: "alerts",
|
|
15076
|
+
label: config2.alerts?.enabled ? "Manage reputation alerts" : "Enable reputation alerts",
|
|
15077
|
+
hint: config2.alerts?.enabled ? "Update thresholds or notification settings" : "Get notified before AWS suspends your account"
|
|
15078
|
+
},
|
|
14819
15079
|
{
|
|
14820
15080
|
value: "custom",
|
|
14821
15081
|
label: "Custom configuration",
|
|
@@ -15297,6 +15557,208 @@ ${pc21.bold("Current Configuration:")}
|
|
|
15297
15557
|
newPreset = void 0;
|
|
15298
15558
|
break;
|
|
15299
15559
|
}
|
|
15560
|
+
case "alerts": {
|
|
15561
|
+
if (!config2.reputationMetrics) {
|
|
15562
|
+
clack20.log.warn("Reputation metrics must be enabled to use alerting.");
|
|
15563
|
+
clack20.log.info(
|
|
15564
|
+
"This requires the Production or Enterprise preset, or enabling reputation metrics manually."
|
|
15565
|
+
);
|
|
15566
|
+
const enableReputationMetrics = await clack20.confirm({
|
|
15567
|
+
message: "Enable reputation metrics now?",
|
|
15568
|
+
initialValue: true
|
|
15569
|
+
});
|
|
15570
|
+
if (clack20.isCancel(enableReputationMetrics) || !enableReputationMetrics) {
|
|
15571
|
+
clack20.cancel("Alerting not enabled.");
|
|
15572
|
+
process.exit(0);
|
|
15573
|
+
}
|
|
15574
|
+
updatedConfig = {
|
|
15575
|
+
...config2,
|
|
15576
|
+
reputationMetrics: true
|
|
15577
|
+
};
|
|
15578
|
+
}
|
|
15579
|
+
if (config2.alerts?.enabled) {
|
|
15580
|
+
clack20.log.info(`Alerting is currently ${pc21.green("enabled")}`);
|
|
15581
|
+
if (config2.alerts.notificationEmail) {
|
|
15582
|
+
clack20.log.info(
|
|
15583
|
+
` Notification email: ${pc21.cyan(config2.alerts.notificationEmail)}`
|
|
15584
|
+
);
|
|
15585
|
+
}
|
|
15586
|
+
const alertsAction = await clack20.select({
|
|
15587
|
+
message: "What would you like to do?",
|
|
15588
|
+
options: [
|
|
15589
|
+
{
|
|
15590
|
+
value: "change-email",
|
|
15591
|
+
label: "Change notification email",
|
|
15592
|
+
hint: config2.alerts.notificationEmail || "Not set"
|
|
15593
|
+
},
|
|
15594
|
+
{
|
|
15595
|
+
value: "change-thresholds",
|
|
15596
|
+
label: "Customize alert thresholds",
|
|
15597
|
+
hint: "Adjust bounce/complaint rate thresholds"
|
|
15598
|
+
},
|
|
15599
|
+
{
|
|
15600
|
+
value: "disable",
|
|
15601
|
+
label: "Disable alerting",
|
|
15602
|
+
hint: "Remove CloudWatch alarms and SNS topic"
|
|
15603
|
+
}
|
|
15604
|
+
]
|
|
15605
|
+
});
|
|
15606
|
+
if (clack20.isCancel(alertsAction)) {
|
|
15607
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15608
|
+
process.exit(0);
|
|
15609
|
+
}
|
|
15610
|
+
if (alertsAction === "disable") {
|
|
15611
|
+
const confirmDisable = await clack20.confirm({
|
|
15612
|
+
message: "Are you sure? You won't be notified if your reputation degrades.",
|
|
15613
|
+
initialValue: false
|
|
15614
|
+
});
|
|
15615
|
+
if (clack20.isCancel(confirmDisable) || !confirmDisable) {
|
|
15616
|
+
clack20.log.info("Alerting not disabled.");
|
|
15617
|
+
process.exit(0);
|
|
15618
|
+
}
|
|
15619
|
+
updatedConfig = {
|
|
15620
|
+
...config2,
|
|
15621
|
+
alerts: { enabled: false }
|
|
15622
|
+
};
|
|
15623
|
+
} else if (alertsAction === "change-email") {
|
|
15624
|
+
const notificationEmail = await clack20.text({
|
|
15625
|
+
message: "Notification email address:",
|
|
15626
|
+
placeholder: "alerts@yourcompany.com",
|
|
15627
|
+
initialValue: config2.alerts.notificationEmail || "",
|
|
15628
|
+
validate: (value) => {
|
|
15629
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
15630
|
+
return "Please enter a valid email address";
|
|
15631
|
+
}
|
|
15632
|
+
}
|
|
15633
|
+
});
|
|
15634
|
+
if (clack20.isCancel(notificationEmail)) {
|
|
15635
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15636
|
+
process.exit(0);
|
|
15637
|
+
}
|
|
15638
|
+
updatedConfig = {
|
|
15639
|
+
...config2,
|
|
15640
|
+
alerts: {
|
|
15641
|
+
...config2.alerts,
|
|
15642
|
+
enabled: true,
|
|
15643
|
+
notificationEmail: notificationEmail || void 0
|
|
15644
|
+
}
|
|
15645
|
+
};
|
|
15646
|
+
} else if (alertsAction === "change-thresholds") {
|
|
15647
|
+
clack20.log.info(`
|
|
15648
|
+
${pc21.bold("Alert Thresholds")}`);
|
|
15649
|
+
clack20.log.info(
|
|
15650
|
+
pc21.dim("These thresholds warn you BEFORE AWS takes action:")
|
|
15651
|
+
);
|
|
15652
|
+
clack20.log.info(pc21.dim(" AWS warns at 5% bounce, 0.1% complaint"));
|
|
15653
|
+
clack20.log.info(pc21.dim(" Gmail blocks at 0.3% complaint rate\n"));
|
|
15654
|
+
const thresholdPreset = await clack20.select({
|
|
15655
|
+
message: "Choose threshold sensitivity:",
|
|
15656
|
+
options: [
|
|
15657
|
+
{
|
|
15658
|
+
value: "standard",
|
|
15659
|
+
label: "Standard (recommended)",
|
|
15660
|
+
hint: "Bounce: 2%/4%, Complaint: 0.05%/0.08%"
|
|
15661
|
+
},
|
|
15662
|
+
{
|
|
15663
|
+
value: "strict",
|
|
15664
|
+
label: "Strict (enterprise)",
|
|
15665
|
+
hint: "Bounce: 1%/2%, Complaint: 0.03%/0.05%"
|
|
15666
|
+
},
|
|
15667
|
+
{
|
|
15668
|
+
value: "relaxed",
|
|
15669
|
+
label: "Relaxed",
|
|
15670
|
+
hint: "Bounce: 3%/5%, Complaint: 0.08%/0.1%"
|
|
15671
|
+
}
|
|
15672
|
+
]
|
|
15673
|
+
});
|
|
15674
|
+
if (clack20.isCancel(thresholdPreset)) {
|
|
15675
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15676
|
+
process.exit(0);
|
|
15677
|
+
}
|
|
15678
|
+
const thresholdConfigs = {
|
|
15679
|
+
standard: {
|
|
15680
|
+
bounceRateWarning: 0.02,
|
|
15681
|
+
bounceRateCritical: 0.04,
|
|
15682
|
+
complaintRateWarning: 5e-4,
|
|
15683
|
+
complaintRateCritical: 8e-4
|
|
15684
|
+
},
|
|
15685
|
+
strict: {
|
|
15686
|
+
bounceRateWarning: 0.01,
|
|
15687
|
+
bounceRateCritical: 0.02,
|
|
15688
|
+
complaintRateWarning: 3e-4,
|
|
15689
|
+
complaintRateCritical: 5e-4
|
|
15690
|
+
},
|
|
15691
|
+
relaxed: {
|
|
15692
|
+
bounceRateWarning: 0.03,
|
|
15693
|
+
bounceRateCritical: 0.05,
|
|
15694
|
+
complaintRateWarning: 8e-4,
|
|
15695
|
+
complaintRateCritical: 1e-3
|
|
15696
|
+
}
|
|
15697
|
+
};
|
|
15698
|
+
updatedConfig = {
|
|
15699
|
+
...config2,
|
|
15700
|
+
alerts: {
|
|
15701
|
+
...config2.alerts,
|
|
15702
|
+
enabled: true,
|
|
15703
|
+
thresholds: thresholdConfigs[thresholdPreset]
|
|
15704
|
+
}
|
|
15705
|
+
};
|
|
15706
|
+
}
|
|
15707
|
+
} else {
|
|
15708
|
+
clack20.log.info(`
|
|
15709
|
+
${pc21.bold("Reputation Alerts")}
|
|
15710
|
+
`);
|
|
15711
|
+
clack20.log.info(
|
|
15712
|
+
pc21.dim("Get notified when your email reputation is at risk:")
|
|
15713
|
+
);
|
|
15714
|
+
clack20.log.info(pc21.dim(" - Bounce rate warnings (before AWS review)"));
|
|
15715
|
+
clack20.log.info(
|
|
15716
|
+
pc21.dim(" - Complaint rate warnings (before Gmail blocks you)")
|
|
15717
|
+
);
|
|
15718
|
+
clack20.log.info(pc21.dim(" - DLQ alerts (event processing failures)"));
|
|
15719
|
+
clack20.log.info(pc21.dim("\nCost: ~$0.50/mo (5 CloudWatch alarms)\n"));
|
|
15720
|
+
const enableAlerts = await clack20.confirm({
|
|
15721
|
+
message: "Enable reputation alerts?",
|
|
15722
|
+
initialValue: true
|
|
15723
|
+
});
|
|
15724
|
+
if (clack20.isCancel(enableAlerts) || !enableAlerts) {
|
|
15725
|
+
clack20.log.info("Alerting not enabled.");
|
|
15726
|
+
process.exit(0);
|
|
15727
|
+
}
|
|
15728
|
+
const notificationEmail = await clack20.text({
|
|
15729
|
+
message: "Notification email address:",
|
|
15730
|
+
placeholder: "alerts@yourcompany.com",
|
|
15731
|
+
validate: (value) => {
|
|
15732
|
+
if (!value) {
|
|
15733
|
+
return "Email address is required for alerts";
|
|
15734
|
+
}
|
|
15735
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
15736
|
+
return "Please enter a valid email address";
|
|
15737
|
+
}
|
|
15738
|
+
}
|
|
15739
|
+
});
|
|
15740
|
+
if (clack20.isCancel(notificationEmail)) {
|
|
15741
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15742
|
+
process.exit(0);
|
|
15743
|
+
}
|
|
15744
|
+
clack20.log.info(
|
|
15745
|
+
pc21.dim("\nYou'll receive an email to confirm your subscription.")
|
|
15746
|
+
);
|
|
15747
|
+
updatedConfig = {
|
|
15748
|
+
...config2,
|
|
15749
|
+
reputationMetrics: true,
|
|
15750
|
+
// Required for alerts
|
|
15751
|
+
alerts: {
|
|
15752
|
+
enabled: true,
|
|
15753
|
+
notificationEmail,
|
|
15754
|
+
dlqAlerts: true
|
|
15755
|
+
// Uses default thresholds
|
|
15756
|
+
}
|
|
15757
|
+
};
|
|
15758
|
+
}
|
|
15759
|
+
newPreset = void 0;
|
|
15760
|
+
break;
|
|
15761
|
+
}
|
|
15300
15762
|
case "custom": {
|
|
15301
15763
|
const { promptCustomConfig: promptCustomConfig2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
|
|
15302
15764
|
const customConfig = await promptCustomConfig2(config2);
|
|
@@ -15905,6 +16367,9 @@ ${pc21.green("\u2713")} ${pc21.bold("Upgrade complete!")}
|
|
|
15905
16367
|
if (updatedConfig.smtpCredentials?.enabled) {
|
|
15906
16368
|
enabledFeatures.push("smtp_credentials");
|
|
15907
16369
|
}
|
|
16370
|
+
if (updatedConfig.alerts?.enabled) {
|
|
16371
|
+
enabledFeatures.push("alerts");
|
|
16372
|
+
}
|
|
15908
16373
|
trackServiceUpgrade("email", {
|
|
15909
16374
|
from_preset: metadata.services.email?.preset,
|
|
15910
16375
|
to_preset: newPreset,
|
|
@@ -17261,7 +17726,7 @@ import {
|
|
|
17261
17726
|
} from "@aws-sdk/client-cloudwatch";
|
|
17262
17727
|
async function fetchSESMetrics(roleArn, region, timeRange, tableName) {
|
|
17263
17728
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
17264
|
-
const
|
|
17729
|
+
const cloudwatch4 = new CloudWatchClient({ region, credentials });
|
|
17265
17730
|
const queries = [
|
|
17266
17731
|
{
|
|
17267
17732
|
Id: "sends",
|
|
@@ -17309,7 +17774,7 @@ async function fetchSESMetrics(roleArn, region, timeRange, tableName) {
|
|
|
17309
17774
|
}
|
|
17310
17775
|
}
|
|
17311
17776
|
];
|
|
17312
|
-
const response = await
|
|
17777
|
+
const response = await cloudwatch4.send(
|
|
17313
17778
|
new GetMetricDataCommand({
|
|
17314
17779
|
MetricDataQueries: queries,
|
|
17315
17780
|
StartTime: timeRange.start,
|
|
@@ -18094,7 +18559,7 @@ import {
|
|
|
18094
18559
|
import { unmarshall as unmarshall4 } from "@aws-sdk/util-dynamodb";
|
|
18095
18560
|
async function fetchSMSSpendLimits(region) {
|
|
18096
18561
|
const smsClient = new PinpointSMSVoiceV2Client({ region });
|
|
18097
|
-
const
|
|
18562
|
+
const cloudwatch4 = new CloudWatchClient2({ region });
|
|
18098
18563
|
try {
|
|
18099
18564
|
const spendLimits = await smsClient.send(
|
|
18100
18565
|
new DescribeSpendLimitsCommand({})
|
|
@@ -18108,7 +18573,7 @@ async function fetchSMSSpendLimits(region) {
|
|
|
18108
18573
|
}
|
|
18109
18574
|
const now = /* @__PURE__ */ new Date();
|
|
18110
18575
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
18111
|
-
const metricsResponse = await
|
|
18576
|
+
const metricsResponse = await cloudwatch4.send(
|
|
18112
18577
|
new GetMetricDataCommand2({
|
|
18113
18578
|
MetricDataQueries: [
|
|
18114
18579
|
{
|
|
@@ -19095,7 +19560,7 @@ import pc28 from "picocolors";
|
|
|
19095
19560
|
|
|
19096
19561
|
// src/infrastructure/sms-stack.ts
|
|
19097
19562
|
init_esm_shims();
|
|
19098
|
-
import * as
|
|
19563
|
+
import * as aws15 from "@pulumi/aws";
|
|
19099
19564
|
import * as pulumi22 from "@pulumi/pulumi";
|
|
19100
19565
|
async function roleExists3(roleName) {
|
|
19101
19566
|
try {
|
|
@@ -19173,7 +19638,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19173
19638
|
}
|
|
19174
19639
|
const roleName = "wraps-sms-role";
|
|
19175
19640
|
const exists = await roleExists3(roleName);
|
|
19176
|
-
const role = exists ? new
|
|
19641
|
+
const role = exists ? new aws15.iam.Role(
|
|
19177
19642
|
roleName,
|
|
19178
19643
|
{
|
|
19179
19644
|
name: roleName,
|
|
@@ -19188,7 +19653,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19188
19653
|
import: roleName,
|
|
19189
19654
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19190
19655
|
}
|
|
19191
|
-
) : new
|
|
19656
|
+
) : new aws15.iam.Role(
|
|
19192
19657
|
roleName,
|
|
19193
19658
|
{
|
|
19194
19659
|
name: roleName,
|
|
@@ -19283,7 +19748,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19283
19748
|
Resource: "arn:aws:logs:*:*:log-group:/aws/lambda/wraps-sms-*"
|
|
19284
19749
|
});
|
|
19285
19750
|
}
|
|
19286
|
-
new
|
|
19751
|
+
new aws15.iam.RolePolicy("wraps-sms-policy", {
|
|
19287
19752
|
role: role.name,
|
|
19288
19753
|
policy: JSON.stringify({
|
|
19289
19754
|
Version: "2012-10-17",
|
|
@@ -19293,7 +19758,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19293
19758
|
return role;
|
|
19294
19759
|
}
|
|
19295
19760
|
function createSMSConfigurationSet() {
|
|
19296
|
-
return new
|
|
19761
|
+
return new aws15.pinpoint.Smsvoicev2ConfigurationSet("wraps-sms-config", {
|
|
19297
19762
|
name: "wraps-sms-config",
|
|
19298
19763
|
defaultMessageType: "TRANSACTIONAL",
|
|
19299
19764
|
tags: {
|
|
@@ -19303,7 +19768,7 @@ function createSMSConfigurationSet() {
|
|
|
19303
19768
|
});
|
|
19304
19769
|
}
|
|
19305
19770
|
function createSMSOptOutList() {
|
|
19306
|
-
return new
|
|
19771
|
+
return new aws15.pinpoint.Smsvoicev2OptOutList("wraps-sms-optouts", {
|
|
19307
19772
|
name: "wraps-sms-optouts",
|
|
19308
19773
|
tags: {
|
|
19309
19774
|
ManagedBy: "wraps-cli",
|
|
@@ -19367,7 +19832,7 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
|
|
|
19367
19832
|
}
|
|
19368
19833
|
};
|
|
19369
19834
|
if (existingArn) {
|
|
19370
|
-
return new
|
|
19835
|
+
return new aws15.pinpoint.Smsvoicev2PhoneNumber(
|
|
19371
19836
|
"wraps-sms-number",
|
|
19372
19837
|
phoneConfig,
|
|
19373
19838
|
{
|
|
@@ -19376,7 +19841,7 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
|
|
|
19376
19841
|
}
|
|
19377
19842
|
);
|
|
19378
19843
|
}
|
|
19379
|
-
return new
|
|
19844
|
+
return new aws15.pinpoint.Smsvoicev2PhoneNumber(
|
|
19380
19845
|
"wraps-sms-number",
|
|
19381
19846
|
phoneConfig,
|
|
19382
19847
|
{
|
|
@@ -19419,10 +19884,10 @@ async function createSMSSQSResources() {
|
|
|
19419
19884
|
Description: "Dead letter queue for failed SMS event processing"
|
|
19420
19885
|
}
|
|
19421
19886
|
};
|
|
19422
|
-
const dlq = dlqUrl ? new
|
|
19887
|
+
const dlq = dlqUrl ? new aws15.sqs.Queue(dlqName, dlqConfig, {
|
|
19423
19888
|
import: dlqUrl,
|
|
19424
19889
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19425
|
-
}) : new
|
|
19890
|
+
}) : new aws15.sqs.Queue(dlqName, dlqConfig, {
|
|
19426
19891
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19427
19892
|
});
|
|
19428
19893
|
const queueConfig = {
|
|
@@ -19445,10 +19910,10 @@ async function createSMSSQSResources() {
|
|
|
19445
19910
|
Description: "Queue for SMS events from SNS"
|
|
19446
19911
|
}
|
|
19447
19912
|
};
|
|
19448
|
-
const queue = queueUrl ? new
|
|
19913
|
+
const queue = queueUrl ? new aws15.sqs.Queue(queueName, queueConfig, {
|
|
19449
19914
|
import: queueUrl,
|
|
19450
19915
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19451
|
-
}) : new
|
|
19916
|
+
}) : new aws15.sqs.Queue(queueName, queueConfig, {
|
|
19452
19917
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19453
19918
|
});
|
|
19454
19919
|
return { queue, dlq };
|
|
@@ -19456,12 +19921,12 @@ async function createSMSSQSResources() {
|
|
|
19456
19921
|
async function snsTopicExists(topicName) {
|
|
19457
19922
|
try {
|
|
19458
19923
|
const { SNSClient: SNSClient2, ListTopicsCommand: ListTopicsCommand2 } = await import("@aws-sdk/client-sns");
|
|
19459
|
-
const
|
|
19924
|
+
const sns3 = new SNSClient2({
|
|
19460
19925
|
region: process.env.AWS_REGION || "us-east-1"
|
|
19461
19926
|
});
|
|
19462
19927
|
let nextToken;
|
|
19463
19928
|
do {
|
|
19464
|
-
const response = await
|
|
19929
|
+
const response = await sns3.send(
|
|
19465
19930
|
new ListTopicsCommand2({ NextToken: nextToken })
|
|
19466
19931
|
);
|
|
19467
19932
|
const found = response.Topics?.find(
|
|
@@ -19488,13 +19953,13 @@ async function createSMSSNSResources(config2) {
|
|
|
19488
19953
|
Description: "SNS topic for SMS delivery events"
|
|
19489
19954
|
}
|
|
19490
19955
|
};
|
|
19491
|
-
const topic = topicArn ? new
|
|
19956
|
+
const topic = topicArn ? new aws15.sns.Topic("wraps-sms-events-topic", topicConfig, {
|
|
19492
19957
|
import: topicArn,
|
|
19493
19958
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19494
|
-
}) : new
|
|
19959
|
+
}) : new aws15.sns.Topic("wraps-sms-events-topic", topicConfig, {
|
|
19495
19960
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19496
19961
|
});
|
|
19497
|
-
new
|
|
19962
|
+
new aws15.sns.TopicPolicy("wraps-sms-events-topic-policy", {
|
|
19498
19963
|
arn: topic.arn,
|
|
19499
19964
|
policy: topic.arn.apply(
|
|
19500
19965
|
(topicArn2) => JSON.stringify({
|
|
@@ -19511,7 +19976,7 @@ async function createSMSSNSResources(config2) {
|
|
|
19511
19976
|
})
|
|
19512
19977
|
)
|
|
19513
19978
|
});
|
|
19514
|
-
new
|
|
19979
|
+
new aws15.sqs.QueuePolicy("wraps-sms-events-queue-policy", {
|
|
19515
19980
|
queueUrl: config2.queueUrl,
|
|
19516
19981
|
policy: pulumi22.all([config2.queueArn, topic.arn]).apply(
|
|
19517
19982
|
([queueArn, topicArn2]) => JSON.stringify({
|
|
@@ -19530,7 +19995,7 @@ async function createSMSSNSResources(config2) {
|
|
|
19530
19995
|
})
|
|
19531
19996
|
)
|
|
19532
19997
|
});
|
|
19533
|
-
const subscription = new
|
|
19998
|
+
const subscription = new aws15.sns.TopicSubscription(
|
|
19534
19999
|
"wraps-sms-events-subscription",
|
|
19535
20000
|
{
|
|
19536
20001
|
topic: topic.arn,
|
|
@@ -19579,17 +20044,17 @@ async function createSMSDynamoDBTable() {
|
|
|
19579
20044
|
Service: "sms"
|
|
19580
20045
|
}
|
|
19581
20046
|
};
|
|
19582
|
-
return exists ? new
|
|
20047
|
+
return exists ? new aws15.dynamodb.Table(tableName, tableConfig, {
|
|
19583
20048
|
import: tableName,
|
|
19584
20049
|
customTimeouts: { create: "5m", update: "5m", delete: "5m" }
|
|
19585
|
-
}) : new
|
|
20050
|
+
}) : new aws15.dynamodb.Table(tableName, tableConfig, {
|
|
19586
20051
|
customTimeouts: { create: "5m", update: "5m", delete: "5m" }
|
|
19587
20052
|
});
|
|
19588
20053
|
}
|
|
19589
20054
|
async function deploySMSLambdaFunction(config2) {
|
|
19590
20055
|
const { getLambdaCode: getLambdaCode2 } = await Promise.resolve().then(() => (init_lambda(), lambda_exports));
|
|
19591
20056
|
const codeDir = await getLambdaCode2("sms-event-processor");
|
|
19592
|
-
const lambdaRole = new
|
|
20057
|
+
const lambdaRole = new aws15.iam.Role("wraps-sms-lambda-role", {
|
|
19593
20058
|
name: "wraps-sms-lambda-role",
|
|
19594
20059
|
assumeRolePolicy: JSON.stringify({
|
|
19595
20060
|
Version: "2012-10-17",
|
|
@@ -19606,11 +20071,11 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19606
20071
|
Service: "sms"
|
|
19607
20072
|
}
|
|
19608
20073
|
});
|
|
19609
|
-
new
|
|
20074
|
+
new aws15.iam.RolePolicyAttachment("wraps-sms-lambda-basic-execution", {
|
|
19610
20075
|
role: lambdaRole.name,
|
|
19611
20076
|
policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
19612
20077
|
});
|
|
19613
|
-
new
|
|
20078
|
+
new aws15.iam.RolePolicy("wraps-sms-lambda-policy", {
|
|
19614
20079
|
role: lambdaRole.name,
|
|
19615
20080
|
policy: pulumi22.all([config2.tableName, config2.queueArn]).apply(
|
|
19616
20081
|
([tableName, queueArn]) => JSON.stringify({
|
|
@@ -19642,7 +20107,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19642
20107
|
})
|
|
19643
20108
|
)
|
|
19644
20109
|
});
|
|
19645
|
-
const eventProcessor = new
|
|
20110
|
+
const eventProcessor = new aws15.lambda.Function(
|
|
19646
20111
|
"wraps-sms-event-processor",
|
|
19647
20112
|
{
|
|
19648
20113
|
name: "wraps-sms-event-processor",
|
|
@@ -19670,7 +20135,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19670
20135
|
customTimeouts: { create: "5m", update: "5m", delete: "2m" }
|
|
19671
20136
|
}
|
|
19672
20137
|
);
|
|
19673
|
-
new
|
|
20138
|
+
new aws15.lambda.EventSourceMapping(
|
|
19674
20139
|
"wraps-sms-event-source-mapping",
|
|
19675
20140
|
{
|
|
19676
20141
|
eventSourceArn: config2.queueArn,
|
|
@@ -19686,7 +20151,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19686
20151
|
return eventProcessor;
|
|
19687
20152
|
}
|
|
19688
20153
|
async function deploySMSStack(config2) {
|
|
19689
|
-
const identity = await
|
|
20154
|
+
const identity = await aws15.getCallerIdentity();
|
|
19690
20155
|
const accountId = identity.accountId;
|
|
19691
20156
|
let oidcProvider;
|
|
19692
20157
|
if (config2.provider === "vercel" && config2.vercel) {
|