@wraps.dev/cli 2.2.8 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -41
- package/dist/cli.js +565 -204
- 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 +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,6 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
4
4
|
var __esm = (fn, res) => function __init() {
|
|
5
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
6
|
};
|
|
7
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
-
};
|
|
10
7
|
var __export = (target, all5) => {
|
|
11
8
|
for (var name in all5)
|
|
12
9
|
__defProp(target, name, { get: all5[name], enumerable: true });
|
|
@@ -142,118 +139,8 @@ var init_config = __esm({
|
|
|
142
139
|
}
|
|
143
140
|
});
|
|
144
141
|
|
|
145
|
-
// package.json
|
|
146
|
-
var require_package = __commonJS({
|
|
147
|
-
"package.json"(exports, module) {
|
|
148
|
-
module.exports = {
|
|
149
|
-
name: "@wraps.dev/cli",
|
|
150
|
-
version: "2.2.8",
|
|
151
|
-
description: "CLI for deploying Wraps email infrastructure to your AWS account",
|
|
152
|
-
type: "module",
|
|
153
|
-
main: "./dist/cli.js",
|
|
154
|
-
bin: {
|
|
155
|
-
wraps: "./dist/cli.js"
|
|
156
|
-
},
|
|
157
|
-
files: [
|
|
158
|
-
"dist",
|
|
159
|
-
"README.md",
|
|
160
|
-
"LICENSE"
|
|
161
|
-
],
|
|
162
|
-
repository: {
|
|
163
|
-
type: "git",
|
|
164
|
-
url: "https://github.com/wraps-team/wraps.git",
|
|
165
|
-
directory: "packages/cli"
|
|
166
|
-
},
|
|
167
|
-
homepage: "https://wraps.dev",
|
|
168
|
-
bugs: {
|
|
169
|
-
url: "https://github.com/wraps-team/wraps/issues"
|
|
170
|
-
},
|
|
171
|
-
publishConfig: {
|
|
172
|
-
access: "public"
|
|
173
|
-
},
|
|
174
|
-
scripts: {
|
|
175
|
-
dev: "tsup --watch",
|
|
176
|
-
build: "pnpm build:console && pnpm build:lambda && tsup",
|
|
177
|
-
"build:lambda": "tsx scripts/build-lambda.ts",
|
|
178
|
-
"build:console": "pnpm --filter @wraps/console build",
|
|
179
|
-
test: "vitest run",
|
|
180
|
-
"test:watch": "vitest --watch",
|
|
181
|
-
"test:ui": "vitest --ui",
|
|
182
|
-
"test:coverage": "vitest run --coverage",
|
|
183
|
-
typecheck: "tsc --noEmit",
|
|
184
|
-
lint: "eslint src",
|
|
185
|
-
prepublishOnly: "pnpm build"
|
|
186
|
-
},
|
|
187
|
-
keywords: [
|
|
188
|
-
"aws",
|
|
189
|
-
"ses",
|
|
190
|
-
"email",
|
|
191
|
-
"infrastructure",
|
|
192
|
-
"cli"
|
|
193
|
-
],
|
|
194
|
-
author: "Wraps",
|
|
195
|
-
license: "AGPL-3.0-or-later",
|
|
196
|
-
dependencies: {
|
|
197
|
-
"@aws-sdk/client-acm": "3.933.0",
|
|
198
|
-
"@aws-sdk/client-cloudformation": "^3.490.0",
|
|
199
|
-
"@aws-sdk/client-cloudfront": "3.933.0",
|
|
200
|
-
"@aws-sdk/client-cloudwatch": "^3.490.0",
|
|
201
|
-
"@aws-sdk/client-dynamodb": "^3.490.0",
|
|
202
|
-
"@aws-sdk/client-iam": "3.932.0",
|
|
203
|
-
"@aws-sdk/client-lambda": "3.925.0",
|
|
204
|
-
"@aws-sdk/client-mailmanager": "3.925.0",
|
|
205
|
-
"@aws-sdk/client-pinpoint-sms-voice-v2": "3.955.0",
|
|
206
|
-
"@aws-sdk/client-route-53": "3.925.0",
|
|
207
|
-
"@aws-sdk/client-s3": "3.933.0",
|
|
208
|
-
"@aws-sdk/client-ses": "^3.490.0",
|
|
209
|
-
"@aws-sdk/client-sesv2": "3.925.0",
|
|
210
|
-
"@aws-sdk/client-sns": "^3.490.0",
|
|
211
|
-
"@aws-sdk/client-sqs": "3.933.0",
|
|
212
|
-
"@aws-sdk/client-sts": "^3.490.0",
|
|
213
|
-
"@aws-sdk/s3-request-presigner": "3.964.0",
|
|
214
|
-
"@aws-sdk/util-dynamodb": "3.927.0",
|
|
215
|
-
"@clack/prompts": "^0.11.0",
|
|
216
|
-
"@pulumi/aws": "^7.11.1",
|
|
217
|
-
"@pulumi/pulumi": "^3.207.0",
|
|
218
|
-
args: "^5.0.3",
|
|
219
|
-
conf: "^13.0.1",
|
|
220
|
-
cosmiconfig: "^9.0.0",
|
|
221
|
-
esbuild: "^0.25.12",
|
|
222
|
-
express: "^4.21.2",
|
|
223
|
-
"get-port": "^7.1.0",
|
|
224
|
-
"http-terminator": "^3.2.0",
|
|
225
|
-
"isomorphic-dompurify": "2.32.0",
|
|
226
|
-
mailparser: "3.9.0",
|
|
227
|
-
open: "^10.1.0",
|
|
228
|
-
picocolors: "^1.1.1",
|
|
229
|
-
tabtab: "^3.0.2",
|
|
230
|
-
uuid: "^11.0.3"
|
|
231
|
-
},
|
|
232
|
-
devDependencies: {
|
|
233
|
-
"@wraps/core": "workspace:*",
|
|
234
|
-
"@types/args": "5.0.4",
|
|
235
|
-
"@types/express": "^5.0.0",
|
|
236
|
-
"@types/mailparser": "3.4.6",
|
|
237
|
-
"@types/node": "^20.11.0",
|
|
238
|
-
"@types/uuid": "^10.0.0",
|
|
239
|
-
"@vitest/coverage-v8": "4.0.7",
|
|
240
|
-
"@wraps/email-check": "workspace:*",
|
|
241
|
-
"aws-sdk-client-mock": "4.1.0",
|
|
242
|
-
"aws-sdk-client-mock-vitest": "7.0.1",
|
|
243
|
-
eslint: "^8.56.0",
|
|
244
|
-
tsup: "^8.0.1",
|
|
245
|
-
tsx: "4.20.6",
|
|
246
|
-
typescript: "catalog:",
|
|
247
|
-
vitest: "^4.0.7"
|
|
248
|
-
},
|
|
249
|
-
engines: {
|
|
250
|
-
node: ">=20.0.0"
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
|
|
256
142
|
// src/telemetry/client.ts
|
|
143
|
+
import { createRequire } from "module";
|
|
257
144
|
import pc from "picocolors";
|
|
258
145
|
function getTelemetryClient() {
|
|
259
146
|
if (!telemetryInstance) {
|
|
@@ -261,13 +148,14 @@ function getTelemetryClient() {
|
|
|
261
148
|
}
|
|
262
149
|
return telemetryInstance;
|
|
263
150
|
}
|
|
264
|
-
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, TelemetryClient, telemetryInstance;
|
|
151
|
+
var require2, DEFAULT_ENDPOINT, DEFAULT_TIMEOUT, TelemetryClient, telemetryInstance;
|
|
265
152
|
var init_client = __esm({
|
|
266
153
|
"src/telemetry/client.ts"() {
|
|
267
154
|
"use strict";
|
|
268
155
|
init_esm_shims();
|
|
269
156
|
init_ci_detection();
|
|
270
157
|
init_config();
|
|
158
|
+
require2 = createRequire(import.meta.url);
|
|
271
159
|
DEFAULT_ENDPOINT = "https://wraps.dev/api/telemetry";
|
|
272
160
|
DEFAULT_TIMEOUT = 2e3;
|
|
273
161
|
TelemetryClient = class {
|
|
@@ -456,7 +344,7 @@ var init_client = __esm({
|
|
|
456
344
|
*/
|
|
457
345
|
getCLIVersion() {
|
|
458
346
|
try {
|
|
459
|
-
const packageJson2 =
|
|
347
|
+
const packageJson2 = require2("../../package.json");
|
|
460
348
|
return packageJson2.version;
|
|
461
349
|
} catch {
|
|
462
350
|
return "unknown";
|
|
@@ -978,6 +866,18 @@ function calculateSMTPCredentialsCost(config2) {
|
|
|
978
866
|
description: "SMTP credentials (no additional cost)"
|
|
979
867
|
};
|
|
980
868
|
}
|
|
869
|
+
function calculateAlertingCost(config2) {
|
|
870
|
+
if (!config2.alerts?.enabled) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const numAlarms = config2.alerts.dlqAlerts !== false ? 5 : 4;
|
|
874
|
+
const alarmCost = numAlarms * 0.1;
|
|
875
|
+
const snsCost = 0;
|
|
876
|
+
return {
|
|
877
|
+
monthly: alarmCost + snsCost,
|
|
878
|
+
description: `Reputation alerts (${numAlarms} CloudWatch alarms)`
|
|
879
|
+
};
|
|
880
|
+
}
|
|
981
881
|
function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
982
882
|
const tracking = calculateTrackingCost(config2);
|
|
983
883
|
const reputationMetrics = calculateReputationMetricsCost(config2);
|
|
@@ -987,8 +887,9 @@ function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
|
987
887
|
const dedicatedIp = calculateDedicatedIpCost(config2);
|
|
988
888
|
const waf = calculateWafCost(config2, emailsPerMonth);
|
|
989
889
|
const smtpCredentials = calculateSMTPCredentialsCost(config2);
|
|
890
|
+
const alerts = calculateAlertingCost(config2);
|
|
990
891
|
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);
|
|
892
|
+
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
893
|
return {
|
|
993
894
|
tracking,
|
|
994
895
|
reputationMetrics,
|
|
@@ -998,6 +899,7 @@ function calculateCosts(config2, emailsPerMonth = 1e4) {
|
|
|
998
899
|
dedicatedIp,
|
|
999
900
|
waf,
|
|
1000
901
|
smtpCredentials,
|
|
902
|
+
alerts,
|
|
1001
903
|
total: {
|
|
1002
904
|
monthly: totalMonthlyCost,
|
|
1003
905
|
perEmail: AWS_PRICING.SES_PER_EMAIL,
|
|
@@ -1063,6 +965,11 @@ function getCostSummary(config2, emailsPerMonth = 1e4) {
|
|
|
1063
965
|
` - ${costs.smtpCredentials.description}: ${formatCost(costs.smtpCredentials.monthly)}`
|
|
1064
966
|
);
|
|
1065
967
|
}
|
|
968
|
+
if (costs.alerts) {
|
|
969
|
+
lines.push(
|
|
970
|
+
` - ${costs.alerts.description}: ${formatCost(costs.alerts.monthly)}`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
1066
973
|
return lines.join("\n");
|
|
1067
974
|
}
|
|
1068
975
|
var AWS_PRICING, FREE_TIER;
|
|
@@ -1205,6 +1112,7 @@ function getPresetInfo(preset) {
|
|
|
1205
1112
|
"Reputation tracking",
|
|
1206
1113
|
"Real-time event tracking (EventBridge)",
|
|
1207
1114
|
"90-day email history storage",
|
|
1115
|
+
"Reputation alerts (bounce/complaint rate monitoring)",
|
|
1208
1116
|
"Optional: Email archiving with rendered viewer",
|
|
1209
1117
|
"Complete event visibility"
|
|
1210
1118
|
]
|
|
@@ -1218,6 +1126,7 @@ function getPresetInfo(preset) {
|
|
|
1218
1126
|
"Everything in Production",
|
|
1219
1127
|
"Dedicated IP address",
|
|
1220
1128
|
"1-year email history",
|
|
1129
|
+
"Stricter alert thresholds (catch issues earlier)",
|
|
1221
1130
|
"Optional: 1-year+ email archiving",
|
|
1222
1131
|
"All event types tracked",
|
|
1223
1132
|
"Priority support eligibility"
|
|
@@ -1306,6 +1215,10 @@ var init_presets = __esm({
|
|
|
1306
1215
|
enabled: false,
|
|
1307
1216
|
retention: "30days"
|
|
1308
1217
|
},
|
|
1218
|
+
// Alerting disabled for starter (no reputation metrics)
|
|
1219
|
+
alerts: {
|
|
1220
|
+
enabled: false
|
|
1221
|
+
},
|
|
1309
1222
|
sendingEnabled: true
|
|
1310
1223
|
};
|
|
1311
1224
|
PRODUCTION_PRESET = {
|
|
@@ -1342,6 +1255,12 @@ var init_presets = __esm({
|
|
|
1342
1255
|
// User can opt-in
|
|
1343
1256
|
retention: "90days"
|
|
1344
1257
|
},
|
|
1258
|
+
// Alerting enabled - warns before AWS/Gmail take action
|
|
1259
|
+
alerts: {
|
|
1260
|
+
enabled: true,
|
|
1261
|
+
dlqAlerts: true
|
|
1262
|
+
// Uses default thresholds: bounce 2%/4%, complaint 0.05%/0.08%
|
|
1263
|
+
},
|
|
1345
1264
|
sendingEnabled: true
|
|
1346
1265
|
};
|
|
1347
1266
|
ENTERPRISE_PRESET = {
|
|
@@ -1380,6 +1299,22 @@ var init_presets = __esm({
|
|
|
1380
1299
|
// User can opt-in
|
|
1381
1300
|
retention: "1year"
|
|
1382
1301
|
},
|
|
1302
|
+
// Alerting with stricter thresholds for high-volume senders
|
|
1303
|
+
alerts: {
|
|
1304
|
+
enabled: true,
|
|
1305
|
+
dlqAlerts: true,
|
|
1306
|
+
thresholds: {
|
|
1307
|
+
// Stricter thresholds for enterprise - catch issues earlier
|
|
1308
|
+
bounceRateWarning: 0.01,
|
|
1309
|
+
// 1% (vs 2% default)
|
|
1310
|
+
bounceRateCritical: 0.02,
|
|
1311
|
+
// 2% (vs 4% default)
|
|
1312
|
+
complaintRateWarning: 3e-4,
|
|
1313
|
+
// 0.03% (vs 0.05% default)
|
|
1314
|
+
complaintRateCritical: 5e-4
|
|
1315
|
+
// 0.05% (vs 0.08% default)
|
|
1316
|
+
}
|
|
1317
|
+
},
|
|
1383
1318
|
dedicatedIp: true,
|
|
1384
1319
|
sendingEnabled: true
|
|
1385
1320
|
};
|
|
@@ -3223,7 +3158,7 @@ import { existsSync as existsSync4, mkdirSync } from "fs";
|
|
|
3223
3158
|
import { tmpdir } from "os";
|
|
3224
3159
|
import { dirname, join as join4 } from "path";
|
|
3225
3160
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3226
|
-
import * as
|
|
3161
|
+
import * as aws8 from "@pulumi/aws";
|
|
3227
3162
|
import * as pulumi11 from "@pulumi/pulumi";
|
|
3228
3163
|
import { build } from "esbuild";
|
|
3229
3164
|
function getPackageRoot() {
|
|
@@ -3312,7 +3247,7 @@ Try running: pnpm build`
|
|
|
3312
3247
|
}
|
|
3313
3248
|
async function deployLambdaFunctions(config2) {
|
|
3314
3249
|
const eventProcessorCode = await getLambdaCode("event-processor");
|
|
3315
|
-
const lambdaRole = new
|
|
3250
|
+
const lambdaRole = new aws8.iam.Role("wraps-email-lambda-role", {
|
|
3316
3251
|
assumeRolePolicy: JSON.stringify({
|
|
3317
3252
|
Version: "2012-10-17",
|
|
3318
3253
|
Statement: [
|
|
@@ -3327,11 +3262,11 @@ async function deployLambdaFunctions(config2) {
|
|
|
3327
3262
|
ManagedBy: "wraps-cli"
|
|
3328
3263
|
}
|
|
3329
3264
|
});
|
|
3330
|
-
new
|
|
3265
|
+
new aws8.iam.RolePolicyAttachment("wraps-email-lambda-basic-execution", {
|
|
3331
3266
|
role: lambdaRole.name,
|
|
3332
3267
|
policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
3333
3268
|
});
|
|
3334
|
-
new
|
|
3269
|
+
new aws8.iam.RolePolicy("wraps-email-lambda-policy", {
|
|
3335
3270
|
role: lambdaRole.name,
|
|
3336
3271
|
policy: pulumi11.all([config2.tableName, config2.queueArn]).apply(
|
|
3337
3272
|
([tableName, queueArn]) => JSON.stringify({
|
|
@@ -3375,7 +3310,7 @@ async function deployLambdaFunctions(config2) {
|
|
|
3375
3310
|
RETENTION_DAYS: config2.retentionDays.toString()
|
|
3376
3311
|
}
|
|
3377
3312
|
};
|
|
3378
|
-
const eventProcessor = exists ? new
|
|
3313
|
+
const eventProcessor = exists ? new aws8.lambda.Function(
|
|
3379
3314
|
functionName,
|
|
3380
3315
|
{
|
|
3381
3316
|
name: functionName,
|
|
@@ -3396,7 +3331,7 @@ async function deployLambdaFunctions(config2) {
|
|
|
3396
3331
|
import: functionName
|
|
3397
3332
|
// Import existing function
|
|
3398
3333
|
}
|
|
3399
|
-
) : new
|
|
3334
|
+
) : new aws8.lambda.Function(functionName, {
|
|
3400
3335
|
name: functionName,
|
|
3401
3336
|
runtime: "nodejs24.x",
|
|
3402
3337
|
handler: "index.handler",
|
|
@@ -3426,14 +3361,14 @@ async function deployLambdaFunctions(config2) {
|
|
|
3426
3361
|
functionResponseTypes: ["ReportBatchItemFailures"]
|
|
3427
3362
|
// Enable partial batch responses
|
|
3428
3363
|
};
|
|
3429
|
-
const eventSourceMapping = existingMappingUuid ? new
|
|
3364
|
+
const eventSourceMapping = existingMappingUuid ? new aws8.lambda.EventSourceMapping(
|
|
3430
3365
|
"wraps-email-event-source-mapping",
|
|
3431
3366
|
mappingConfig,
|
|
3432
3367
|
{
|
|
3433
3368
|
import: existingMappingUuid
|
|
3434
3369
|
// Import with the UUID
|
|
3435
3370
|
}
|
|
3436
|
-
) : new
|
|
3371
|
+
) : new aws8.lambda.EventSourceMapping(
|
|
3437
3372
|
"wraps-email-event-source-mapping",
|
|
3438
3373
|
mappingConfig
|
|
3439
3374
|
);
|
|
@@ -3454,12 +3389,12 @@ var acm_exports = {};
|
|
|
3454
3389
|
__export(acm_exports, {
|
|
3455
3390
|
createACMCertificate: () => createACMCertificate
|
|
3456
3391
|
});
|
|
3457
|
-
import * as
|
|
3392
|
+
import * as aws12 from "@pulumi/aws";
|
|
3458
3393
|
async function createACMCertificate(config2) {
|
|
3459
|
-
const usEast1Provider = new
|
|
3394
|
+
const usEast1Provider = new aws12.Provider("acm-us-east-1", {
|
|
3460
3395
|
region: "us-east-1"
|
|
3461
3396
|
});
|
|
3462
|
-
const certificate = new
|
|
3397
|
+
const certificate = new aws12.acm.Certificate(
|
|
3463
3398
|
"wraps-email-tracking-cert",
|
|
3464
3399
|
{
|
|
3465
3400
|
domainName: config2.domain,
|
|
@@ -3482,7 +3417,7 @@ async function createACMCertificate(config2) {
|
|
|
3482
3417
|
);
|
|
3483
3418
|
let certificateValidation;
|
|
3484
3419
|
if (config2.hostedZoneId) {
|
|
3485
|
-
const validationRecord = new
|
|
3420
|
+
const validationRecord = new aws12.route53.Record(
|
|
3486
3421
|
"wraps-email-tracking-cert-validation",
|
|
3487
3422
|
{
|
|
3488
3423
|
zoneId: config2.hostedZoneId,
|
|
@@ -3492,7 +3427,7 @@ async function createACMCertificate(config2) {
|
|
|
3492
3427
|
ttl: 60
|
|
3493
3428
|
}
|
|
3494
3429
|
);
|
|
3495
|
-
certificateValidation = new
|
|
3430
|
+
certificateValidation = new aws12.acm.CertificateValidation(
|
|
3496
3431
|
"wraps-email-tracking-cert-validation-waiter",
|
|
3497
3432
|
{
|
|
3498
3433
|
certificateArn: certificate.arn,
|
|
@@ -3521,7 +3456,7 @@ var cloudfront_exports = {};
|
|
|
3521
3456
|
__export(cloudfront_exports, {
|
|
3522
3457
|
createCloudFrontTracking: () => createCloudFrontTracking
|
|
3523
3458
|
});
|
|
3524
|
-
import * as
|
|
3459
|
+
import * as aws13 from "@pulumi/aws";
|
|
3525
3460
|
async function findDistributionByAlias(alias) {
|
|
3526
3461
|
try {
|
|
3527
3462
|
const { CloudFrontClient, ListDistributionsCommand } = await import("@aws-sdk/client-cloudfront");
|
|
@@ -3537,10 +3472,10 @@ async function findDistributionByAlias(alias) {
|
|
|
3537
3472
|
}
|
|
3538
3473
|
}
|
|
3539
3474
|
async function createWAFWebACL() {
|
|
3540
|
-
const usEast1Provider = new
|
|
3475
|
+
const usEast1Provider = new aws13.Provider("waf-us-east-1", {
|
|
3541
3476
|
region: "us-east-1"
|
|
3542
3477
|
});
|
|
3543
|
-
const webAcl = new
|
|
3478
|
+
const webAcl = new aws13.wafv2.WebAcl(
|
|
3544
3479
|
"wraps-email-tracking-waf",
|
|
3545
3480
|
{
|
|
3546
3481
|
scope: "CLOUDFRONT",
|
|
@@ -3655,14 +3590,14 @@ async function createCloudFrontTracking(config2) {
|
|
|
3655
3590
|
Description: "Wraps email tracking CloudFront distribution"
|
|
3656
3591
|
}
|
|
3657
3592
|
};
|
|
3658
|
-
const distribution = existingDistributionId ? new
|
|
3593
|
+
const distribution = existingDistributionId ? new aws13.cloudfront.Distribution(
|
|
3659
3594
|
"wraps-email-tracking-cdn",
|
|
3660
3595
|
distributionConfig,
|
|
3661
3596
|
{
|
|
3662
3597
|
import: existingDistributionId
|
|
3663
3598
|
// Import existing distribution
|
|
3664
3599
|
}
|
|
3665
|
-
) : new
|
|
3600
|
+
) : new aws13.cloudfront.Distribution(
|
|
3666
3601
|
"wraps-email-tracking-cdn",
|
|
3667
3602
|
distributionConfig
|
|
3668
3603
|
);
|
|
@@ -11611,12 +11546,200 @@ import pc14 from "picocolors";
|
|
|
11611
11546
|
// src/infrastructure/email-stack.ts
|
|
11612
11547
|
init_esm_shims();
|
|
11613
11548
|
init_dist();
|
|
11614
|
-
import * as
|
|
11549
|
+
import * as aws14 from "@pulumi/aws";
|
|
11615
11550
|
import * as pulumi12 from "@pulumi/pulumi";
|
|
11616
11551
|
|
|
11617
|
-
// src/infrastructure/resources/
|
|
11552
|
+
// src/infrastructure/resources/alerting.ts
|
|
11618
11553
|
init_esm_shims();
|
|
11619
11554
|
import * as aws4 from "@pulumi/aws";
|
|
11555
|
+
|
|
11556
|
+
// src/types/index.ts
|
|
11557
|
+
init_esm_shims();
|
|
11558
|
+
|
|
11559
|
+
// src/types/email.ts
|
|
11560
|
+
init_esm_shims();
|
|
11561
|
+
var DEFAULT_ALERT_THRESHOLDS = {
|
|
11562
|
+
bounceRateWarning: 0.02,
|
|
11563
|
+
// 2% - gives time before AWS 5% warning
|
|
11564
|
+
bounceRateCritical: 0.04,
|
|
11565
|
+
// 4% - urgent, approaching AWS warning
|
|
11566
|
+
complaintRateWarning: 5e-4,
|
|
11567
|
+
// 0.05% - half of AWS warning threshold
|
|
11568
|
+
complaintRateCritical: 8e-4,
|
|
11569
|
+
// 0.08% - urgent, approaching AWS 0.1% warning
|
|
11570
|
+
dlqMessageThreshold: 1
|
|
11571
|
+
// Any failed message processing
|
|
11572
|
+
};
|
|
11573
|
+
|
|
11574
|
+
// src/infrastructure/resources/alerting.ts
|
|
11575
|
+
function getThresholds(custom) {
|
|
11576
|
+
return {
|
|
11577
|
+
...DEFAULT_ALERT_THRESHOLDS,
|
|
11578
|
+
...custom
|
|
11579
|
+
};
|
|
11580
|
+
}
|
|
11581
|
+
async function createAlertingResources(config2) {
|
|
11582
|
+
const thresholds = getThresholds(config2.alertConfig.thresholds);
|
|
11583
|
+
const topic = new aws4.sns.Topic("wraps-email-alerts", {
|
|
11584
|
+
name: "wraps-email-alerts",
|
|
11585
|
+
displayName: "Wraps Email Alerts",
|
|
11586
|
+
tags: {
|
|
11587
|
+
ManagedBy: "wraps-cli",
|
|
11588
|
+
Description: "Alert notifications for email reputation and health"
|
|
11589
|
+
}
|
|
11590
|
+
});
|
|
11591
|
+
let emailSubscription;
|
|
11592
|
+
if (config2.alertConfig.notificationEmail) {
|
|
11593
|
+
emailSubscription = new aws4.sns.TopicSubscription(
|
|
11594
|
+
"wraps-email-alerts-email",
|
|
11595
|
+
{
|
|
11596
|
+
topic: topic.arn,
|
|
11597
|
+
protocol: "email",
|
|
11598
|
+
endpoint: config2.alertConfig.notificationEmail
|
|
11599
|
+
}
|
|
11600
|
+
);
|
|
11601
|
+
}
|
|
11602
|
+
let webhookSubscription;
|
|
11603
|
+
if (config2.alertConfig.webhookUrl) {
|
|
11604
|
+
webhookSubscription = new aws4.sns.TopicSubscription(
|
|
11605
|
+
"wraps-email-alerts-webhook",
|
|
11606
|
+
{
|
|
11607
|
+
topic: topic.arn,
|
|
11608
|
+
protocol: "https",
|
|
11609
|
+
endpoint: config2.alertConfig.webhookUrl
|
|
11610
|
+
}
|
|
11611
|
+
);
|
|
11612
|
+
}
|
|
11613
|
+
const bounceRateWarningAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11614
|
+
"wraps-bounce-rate-warning",
|
|
11615
|
+
{
|
|
11616
|
+
name: "wraps-email-bounce-rate-warning",
|
|
11617
|
+
alarmDescription: `Bounce rate exceeded ${thresholds.bounceRateWarning * 100}% - investigate before AWS takes action (warns at 5%, suspends at 10%)`,
|
|
11618
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11619
|
+
evaluationPeriods: 2,
|
|
11620
|
+
// Require 2 consecutive periods to reduce noise
|
|
11621
|
+
metricName: "Reputation.BounceRate",
|
|
11622
|
+
namespace: "AWS/SES",
|
|
11623
|
+
period: 300,
|
|
11624
|
+
// 5 minutes
|
|
11625
|
+
statistic: "Average",
|
|
11626
|
+
threshold: thresholds.bounceRateWarning,
|
|
11627
|
+
alarmActions: [topic.arn],
|
|
11628
|
+
okActions: [topic.arn],
|
|
11629
|
+
// Notify when resolved
|
|
11630
|
+
treatMissingData: "notBreaching",
|
|
11631
|
+
// Don't alarm if no data (no emails sent)
|
|
11632
|
+
tags: {
|
|
11633
|
+
ManagedBy: "wraps-cli",
|
|
11634
|
+
Severity: "warning"
|
|
11635
|
+
}
|
|
11636
|
+
}
|
|
11637
|
+
);
|
|
11638
|
+
const bounceRateCriticalAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11639
|
+
"wraps-bounce-rate-critical",
|
|
11640
|
+
{
|
|
11641
|
+
name: "wraps-email-bounce-rate-critical",
|
|
11642
|
+
alarmDescription: `CRITICAL: Bounce rate exceeded ${thresholds.bounceRateCritical * 100}% - approaching AWS warning threshold (5%). Immediate action required!`,
|
|
11643
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11644
|
+
evaluationPeriods: 1,
|
|
11645
|
+
// Alert immediately on critical
|
|
11646
|
+
metricName: "Reputation.BounceRate",
|
|
11647
|
+
namespace: "AWS/SES",
|
|
11648
|
+
period: 300,
|
|
11649
|
+
// 5 minutes
|
|
11650
|
+
statistic: "Average",
|
|
11651
|
+
threshold: thresholds.bounceRateCritical,
|
|
11652
|
+
alarmActions: [topic.arn],
|
|
11653
|
+
okActions: [topic.arn],
|
|
11654
|
+
treatMissingData: "notBreaching",
|
|
11655
|
+
tags: {
|
|
11656
|
+
ManagedBy: "wraps-cli",
|
|
11657
|
+
Severity: "critical"
|
|
11658
|
+
}
|
|
11659
|
+
}
|
|
11660
|
+
);
|
|
11661
|
+
const complaintRateWarningAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11662
|
+
"wraps-complaint-rate-warning",
|
|
11663
|
+
{
|
|
11664
|
+
name: "wraps-email-complaint-rate-warning",
|
|
11665
|
+
alarmDescription: `Complaint rate exceeded ${thresholds.complaintRateWarning * 100}% - investigate before AWS (0.1%) or Gmail (0.3%) take action`,
|
|
11666
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11667
|
+
evaluationPeriods: 2,
|
|
11668
|
+
metricName: "Reputation.ComplaintRate",
|
|
11669
|
+
namespace: "AWS/SES",
|
|
11670
|
+
period: 300,
|
|
11671
|
+
statistic: "Average",
|
|
11672
|
+
threshold: thresholds.complaintRateWarning,
|
|
11673
|
+
alarmActions: [topic.arn],
|
|
11674
|
+
okActions: [topic.arn],
|
|
11675
|
+
treatMissingData: "notBreaching",
|
|
11676
|
+
tags: {
|
|
11677
|
+
ManagedBy: "wraps-cli",
|
|
11678
|
+
Severity: "warning"
|
|
11679
|
+
}
|
|
11680
|
+
}
|
|
11681
|
+
);
|
|
11682
|
+
const complaintRateCriticalAlarm = new aws4.cloudwatch.MetricAlarm(
|
|
11683
|
+
"wraps-complaint-rate-critical",
|
|
11684
|
+
{
|
|
11685
|
+
name: "wraps-email-complaint-rate-critical",
|
|
11686
|
+
alarmDescription: `CRITICAL: Complaint rate exceeded ${thresholds.complaintRateCritical * 100}% - approaching AWS warning (0.1%). Immediate action required!`,
|
|
11687
|
+
comparisonOperator: "GreaterThanThreshold",
|
|
11688
|
+
evaluationPeriods: 1,
|
|
11689
|
+
metricName: "Reputation.ComplaintRate",
|
|
11690
|
+
namespace: "AWS/SES",
|
|
11691
|
+
period: 300,
|
|
11692
|
+
statistic: "Average",
|
|
11693
|
+
threshold: thresholds.complaintRateCritical,
|
|
11694
|
+
alarmActions: [topic.arn],
|
|
11695
|
+
okActions: [topic.arn],
|
|
11696
|
+
treatMissingData: "notBreaching",
|
|
11697
|
+
tags: {
|
|
11698
|
+
ManagedBy: "wraps-cli",
|
|
11699
|
+
Severity: "critical"
|
|
11700
|
+
}
|
|
11701
|
+
}
|
|
11702
|
+
);
|
|
11703
|
+
let dlqAlarm;
|
|
11704
|
+
if (config2.alertConfig.dlqAlerts !== false && config2.dlqName) {
|
|
11705
|
+
dlqAlarm = new aws4.cloudwatch.MetricAlarm("wraps-dlq-alarm", {
|
|
11706
|
+
name: "wraps-email-dlq-messages",
|
|
11707
|
+
alarmDescription: "Messages in dead letter queue - event processing is failing. Check Lambda logs for errors.",
|
|
11708
|
+
comparisonOperator: "GreaterThanOrEqualToThreshold",
|
|
11709
|
+
evaluationPeriods: 1,
|
|
11710
|
+
metricName: "ApproximateNumberOfMessagesVisible",
|
|
11711
|
+
namespace: "AWS/SQS",
|
|
11712
|
+
period: 60,
|
|
11713
|
+
// Check every minute
|
|
11714
|
+
statistic: "Sum",
|
|
11715
|
+
threshold: thresholds.dlqMessageThreshold,
|
|
11716
|
+
dimensions: {
|
|
11717
|
+
QueueName: config2.dlqName
|
|
11718
|
+
},
|
|
11719
|
+
alarmActions: [topic.arn],
|
|
11720
|
+
okActions: [topic.arn],
|
|
11721
|
+
treatMissingData: "notBreaching",
|
|
11722
|
+
tags: {
|
|
11723
|
+
ManagedBy: "wraps-cli",
|
|
11724
|
+
Severity: "warning"
|
|
11725
|
+
}
|
|
11726
|
+
});
|
|
11727
|
+
}
|
|
11728
|
+
return {
|
|
11729
|
+
topic,
|
|
11730
|
+
emailSubscription,
|
|
11731
|
+
webhookSubscription,
|
|
11732
|
+
bounceRateWarningAlarm,
|
|
11733
|
+
bounceRateCriticalAlarm,
|
|
11734
|
+
complaintRateWarningAlarm,
|
|
11735
|
+
complaintRateCriticalAlarm,
|
|
11736
|
+
dlqAlarm
|
|
11737
|
+
};
|
|
11738
|
+
}
|
|
11739
|
+
|
|
11740
|
+
// src/infrastructure/resources/dynamodb.ts
|
|
11741
|
+
init_esm_shims();
|
|
11742
|
+
import * as aws5 from "@pulumi/aws";
|
|
11620
11743
|
async function tableExists(tableName) {
|
|
11621
11744
|
try {
|
|
11622
11745
|
const { DynamoDBClient: DynamoDBClient6, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
|
|
@@ -11636,7 +11759,7 @@ async function tableExists(tableName) {
|
|
|
11636
11759
|
async function createDynamoDBTables(_config) {
|
|
11637
11760
|
const tableName = "wraps-email-history";
|
|
11638
11761
|
const exists = await tableExists(tableName);
|
|
11639
|
-
const emailHistory = exists ? new
|
|
11762
|
+
const emailHistory = exists ? new aws5.dynamodb.Table(
|
|
11640
11763
|
tableName,
|
|
11641
11764
|
{
|
|
11642
11765
|
name: tableName,
|
|
@@ -11668,7 +11791,7 @@ async function createDynamoDBTables(_config) {
|
|
|
11668
11791
|
import: tableName
|
|
11669
11792
|
// Import existing table
|
|
11670
11793
|
}
|
|
11671
|
-
) : new
|
|
11794
|
+
) : new aws5.dynamodb.Table(tableName, {
|
|
11672
11795
|
name: tableName,
|
|
11673
11796
|
billingMode: "PAY_PER_REQUEST",
|
|
11674
11797
|
hashKey: "messageId",
|
|
@@ -11701,11 +11824,11 @@ async function createDynamoDBTables(_config) {
|
|
|
11701
11824
|
|
|
11702
11825
|
// src/infrastructure/resources/eventbridge.ts
|
|
11703
11826
|
init_esm_shims();
|
|
11704
|
-
import * as
|
|
11827
|
+
import * as aws6 from "@pulumi/aws";
|
|
11705
11828
|
import * as pulumi9 from "@pulumi/pulumi";
|
|
11706
11829
|
async function createEventBridgeResources(config2) {
|
|
11707
11830
|
const eventBusName = config2.eventBusArn.apply((arn) => arn.split("/").pop());
|
|
11708
|
-
const rule = new
|
|
11831
|
+
const rule = new aws6.cloudwatch.EventRule("wraps-email-events-rule", {
|
|
11709
11832
|
name: "wraps-email-events-to-sqs",
|
|
11710
11833
|
description: "Route all SES email events to SQS for processing",
|
|
11711
11834
|
eventBusName,
|
|
@@ -11718,7 +11841,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11718
11841
|
ManagedBy: "wraps-cli"
|
|
11719
11842
|
}
|
|
11720
11843
|
});
|
|
11721
|
-
new
|
|
11844
|
+
new aws6.sqs.QueuePolicy("wraps-email-events-queue-policy", {
|
|
11722
11845
|
queueUrl: config2.queueUrl,
|
|
11723
11846
|
policy: pulumi9.all([config2.queueArn, rule.arn]).apply(
|
|
11724
11847
|
([queueArn, ruleArn]) => JSON.stringify({
|
|
@@ -11741,7 +11864,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11741
11864
|
})
|
|
11742
11865
|
)
|
|
11743
11866
|
});
|
|
11744
|
-
const target = new
|
|
11867
|
+
const target = new aws6.cloudwatch.EventTarget("wraps-email-events-target", {
|
|
11745
11868
|
rule: rule.name,
|
|
11746
11869
|
eventBusName,
|
|
11747
11870
|
arn: config2.queueArn
|
|
@@ -11752,7 +11875,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11752
11875
|
if (config2.webhook) {
|
|
11753
11876
|
const { awsAccountNumber, webhookSecret, webhookUrl } = config2.webhook;
|
|
11754
11877
|
const baseUrl = webhookUrl || "https://api.wraps.dev";
|
|
11755
|
-
webhookConnection = new
|
|
11878
|
+
webhookConnection = new aws6.cloudwatch.EventConnection(
|
|
11756
11879
|
"wraps-webhook-connection",
|
|
11757
11880
|
{
|
|
11758
11881
|
name: "wraps-webhook-connection",
|
|
@@ -11766,7 +11889,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11766
11889
|
}
|
|
11767
11890
|
}
|
|
11768
11891
|
);
|
|
11769
|
-
webhookApiDestination = new
|
|
11892
|
+
webhookApiDestination = new aws6.cloudwatch.EventApiDestination(
|
|
11770
11893
|
"wraps-webhook-destination",
|
|
11771
11894
|
{
|
|
11772
11895
|
name: "wraps-webhook-destination",
|
|
@@ -11778,7 +11901,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11778
11901
|
// Rate limit
|
|
11779
11902
|
}
|
|
11780
11903
|
);
|
|
11781
|
-
const webhookRole = new
|
|
11904
|
+
const webhookRole = new aws6.iam.Role("wraps-webhook-role", {
|
|
11782
11905
|
name: "wraps-eventbridge-webhook-role",
|
|
11783
11906
|
assumeRolePolicy: JSON.stringify({
|
|
11784
11907
|
Version: "2012-10-17",
|
|
@@ -11796,7 +11919,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11796
11919
|
ManagedBy: "wraps-cli"
|
|
11797
11920
|
}
|
|
11798
11921
|
});
|
|
11799
|
-
new
|
|
11922
|
+
new aws6.iam.RolePolicy("wraps-webhook-policy", {
|
|
11800
11923
|
role: webhookRole.name,
|
|
11801
11924
|
policy: webhookApiDestination.arn.apply(
|
|
11802
11925
|
(destArn) => JSON.stringify({
|
|
@@ -11811,7 +11934,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11811
11934
|
})
|
|
11812
11935
|
)
|
|
11813
11936
|
});
|
|
11814
|
-
webhookTarget = new
|
|
11937
|
+
webhookTarget = new aws6.cloudwatch.EventTarget("wraps-webhook-target", {
|
|
11815
11938
|
rule: rule.name,
|
|
11816
11939
|
eventBusName,
|
|
11817
11940
|
arn: webhookApiDestination.arn,
|
|
@@ -11829,7 +11952,7 @@ async function createEventBridgeResources(config2) {
|
|
|
11829
11952
|
|
|
11830
11953
|
// src/infrastructure/resources/iam.ts
|
|
11831
11954
|
init_esm_shims();
|
|
11832
|
-
import * as
|
|
11955
|
+
import * as aws7 from "@pulumi/aws";
|
|
11833
11956
|
import * as pulumi10 from "@pulumi/pulumi";
|
|
11834
11957
|
async function roleExists2(roleName) {
|
|
11835
11958
|
try {
|
|
@@ -11884,7 +12007,7 @@ async function createIAMRole(config2) {
|
|
|
11884
12007
|
}
|
|
11885
12008
|
const roleName = "wraps-email-role";
|
|
11886
12009
|
const exists = await roleExists2(roleName);
|
|
11887
|
-
const role = exists ? new
|
|
12010
|
+
const role = exists ? new aws7.iam.Role(
|
|
11888
12011
|
roleName,
|
|
11889
12012
|
{
|
|
11890
12013
|
name: roleName,
|
|
@@ -11898,7 +12021,7 @@ async function createIAMRole(config2) {
|
|
|
11898
12021
|
import: roleName
|
|
11899
12022
|
// Import existing role (use role name, not ARN)
|
|
11900
12023
|
}
|
|
11901
|
-
) : new
|
|
12024
|
+
) : new aws7.iam.Role(roleName, {
|
|
11902
12025
|
name: roleName,
|
|
11903
12026
|
assumeRolePolicy,
|
|
11904
12027
|
tags: {
|
|
@@ -11918,6 +12041,9 @@ async function createIAMRole(config2) {
|
|
|
11918
12041
|
// SES v2 API for listing/getting email identities (domains)
|
|
11919
12042
|
"ses:ListEmailIdentities",
|
|
11920
12043
|
"ses:GetEmailIdentity",
|
|
12044
|
+
// SES v2 API for configuration set scanning (needed by dashboard)
|
|
12045
|
+
"ses:GetConfigurationSet",
|
|
12046
|
+
"ses:GetConfigurationSetEventDestinations",
|
|
11921
12047
|
"cloudwatch:GetMetricData",
|
|
11922
12048
|
"cloudwatch:GetMetricStatistics"
|
|
11923
12049
|
],
|
|
@@ -11993,7 +12119,7 @@ async function createIAMRole(config2) {
|
|
|
11993
12119
|
Resource: "arn:aws:ses:*:*:mailmanager-archive/*"
|
|
11994
12120
|
});
|
|
11995
12121
|
}
|
|
11996
|
-
new
|
|
12122
|
+
new aws7.iam.RolePolicy("wraps-email-policy", {
|
|
11997
12123
|
role: role.name,
|
|
11998
12124
|
policy: JSON.stringify({
|
|
11999
12125
|
Version: "2012-10-17",
|
|
@@ -12008,7 +12134,7 @@ init_lambda();
|
|
|
12008
12134
|
|
|
12009
12135
|
// src/infrastructure/resources/ses.ts
|
|
12010
12136
|
init_esm_shims();
|
|
12011
|
-
import * as
|
|
12137
|
+
import * as aws9 from "@pulumi/aws";
|
|
12012
12138
|
async function configurationSetExists(configSetName, region) {
|
|
12013
12139
|
try {
|
|
12014
12140
|
const { SESv2Client: SESv2Client6, GetConfigurationSetCommand: GetConfigurationSetCommand2 } = await import("@aws-sdk/client-sesv2");
|
|
@@ -12086,16 +12212,16 @@ async function createSESResources(config2) {
|
|
|
12086
12212
|
}
|
|
12087
12213
|
const configSetName = "wraps-email-tracking";
|
|
12088
12214
|
const exists = await configurationSetExists(configSetName, config2.region);
|
|
12089
|
-
const configSet = exists ? new
|
|
12215
|
+
const configSet = exists ? new aws9.sesv2.ConfigurationSet(configSetName, configSetOptions, {
|
|
12090
12216
|
import: configSetName
|
|
12091
12217
|
// Import existing configuration set
|
|
12092
|
-
}) : new
|
|
12093
|
-
const defaultEventBus =
|
|
12218
|
+
}) : new aws9.sesv2.ConfigurationSet(configSetName, configSetOptions);
|
|
12219
|
+
const defaultEventBus = aws9.cloudwatch.getEventBusOutput({
|
|
12094
12220
|
name: "default"
|
|
12095
12221
|
});
|
|
12096
12222
|
if (config2.eventTrackingEnabled) {
|
|
12097
12223
|
const eventDestName = "wraps-email-eventbridge";
|
|
12098
|
-
new
|
|
12224
|
+
new aws9.sesv2.ConfigurationSetEventDestination(
|
|
12099
12225
|
"wraps-email-all-events",
|
|
12100
12226
|
{
|
|
12101
12227
|
configurationSetName: configSet.configurationSetName,
|
|
@@ -12135,7 +12261,7 @@ async function createSESResources(config2) {
|
|
|
12135
12261
|
config2.domain,
|
|
12136
12262
|
config2.region
|
|
12137
12263
|
);
|
|
12138
|
-
domainIdentity = identityExists ? new
|
|
12264
|
+
domainIdentity = identityExists ? new aws9.sesv2.EmailIdentity(
|
|
12139
12265
|
"wraps-email-domain",
|
|
12140
12266
|
{
|
|
12141
12267
|
emailIdentity: config2.domain,
|
|
@@ -12152,7 +12278,7 @@ async function createSESResources(config2) {
|
|
|
12152
12278
|
import: config2.domain
|
|
12153
12279
|
// Import existing identity
|
|
12154
12280
|
}
|
|
12155
|
-
) : new
|
|
12281
|
+
) : new aws9.sesv2.EmailIdentity("wraps-email-domain", {
|
|
12156
12282
|
emailIdentity: config2.domain,
|
|
12157
12283
|
configurationSetName: configSet.configurationSetName,
|
|
12158
12284
|
// Link configuration set to domain
|
|
@@ -12168,7 +12294,7 @@ async function createSESResources(config2) {
|
|
|
12168
12294
|
);
|
|
12169
12295
|
if (config2.mailFromDomain) {
|
|
12170
12296
|
mailFromDomain = config2.mailFromDomain;
|
|
12171
|
-
new
|
|
12297
|
+
new aws9.sesv2.EmailIdentityMailFromAttributes(
|
|
12172
12298
|
"wraps-email-mail-from",
|
|
12173
12299
|
{
|
|
12174
12300
|
emailIdentity: config2.domain,
|
|
@@ -12199,7 +12325,7 @@ async function createSESResources(config2) {
|
|
|
12199
12325
|
// src/infrastructure/resources/smtp-credentials.ts
|
|
12200
12326
|
init_esm_shims();
|
|
12201
12327
|
import { createHmac as createHmac2 } from "crypto";
|
|
12202
|
-
import * as
|
|
12328
|
+
import * as aws10 from "@pulumi/aws";
|
|
12203
12329
|
function convertToSMTPPassword2(secretAccessKey, region) {
|
|
12204
12330
|
const DATE = "11111111";
|
|
12205
12331
|
const SERVICE = "ses";
|
|
@@ -12235,7 +12361,7 @@ async function userExists(userName) {
|
|
|
12235
12361
|
async function createSMTPCredentials(config2) {
|
|
12236
12362
|
const userName = "wraps-email-smtp-user";
|
|
12237
12363
|
const userAlreadyExists = await userExists(userName);
|
|
12238
|
-
const iamUser = userAlreadyExists ? new
|
|
12364
|
+
const iamUser = userAlreadyExists ? new aws10.iam.User(
|
|
12239
12365
|
userName,
|
|
12240
12366
|
{
|
|
12241
12367
|
name: userName,
|
|
@@ -12245,14 +12371,14 @@ async function createSMTPCredentials(config2) {
|
|
|
12245
12371
|
}
|
|
12246
12372
|
},
|
|
12247
12373
|
{ import: userName }
|
|
12248
|
-
) : new
|
|
12374
|
+
) : new aws10.iam.User(userName, {
|
|
12249
12375
|
name: userName,
|
|
12250
12376
|
tags: {
|
|
12251
12377
|
ManagedBy: "wraps-cli",
|
|
12252
12378
|
Purpose: "SES SMTP Authentication"
|
|
12253
12379
|
}
|
|
12254
12380
|
});
|
|
12255
|
-
new
|
|
12381
|
+
new aws10.iam.UserPolicy("wraps-email-smtp-policy", {
|
|
12256
12382
|
user: iamUser.name,
|
|
12257
12383
|
policy: JSON.stringify({
|
|
12258
12384
|
Version: "2012-10-17",
|
|
@@ -12270,7 +12396,7 @@ async function createSMTPCredentials(config2) {
|
|
|
12270
12396
|
]
|
|
12271
12397
|
})
|
|
12272
12398
|
});
|
|
12273
|
-
const accessKey = new
|
|
12399
|
+
const accessKey = new aws10.iam.AccessKey("wraps-email-smtp-key", {
|
|
12274
12400
|
user: iamUser.name
|
|
12275
12401
|
});
|
|
12276
12402
|
const smtpPassword = accessKey.secret.apply(
|
|
@@ -12285,9 +12411,9 @@ async function createSMTPCredentials(config2) {
|
|
|
12285
12411
|
|
|
12286
12412
|
// src/infrastructure/resources/sqs.ts
|
|
12287
12413
|
init_esm_shims();
|
|
12288
|
-
import * as
|
|
12414
|
+
import * as aws11 from "@pulumi/aws";
|
|
12289
12415
|
async function createSQSResources() {
|
|
12290
|
-
const dlq = new
|
|
12416
|
+
const dlq = new aws11.sqs.Queue("wraps-email-events-dlq", {
|
|
12291
12417
|
name: "wraps-email-events-dlq",
|
|
12292
12418
|
messageRetentionSeconds: 1209600,
|
|
12293
12419
|
// 14 days
|
|
@@ -12296,7 +12422,7 @@ async function createSQSResources() {
|
|
|
12296
12422
|
Description: "Dead letter queue for failed SES event processing"
|
|
12297
12423
|
}
|
|
12298
12424
|
});
|
|
12299
|
-
const queue = new
|
|
12425
|
+
const queue = new aws11.sqs.Queue("wraps-email-events", {
|
|
12300
12426
|
name: "wraps-email-events",
|
|
12301
12427
|
visibilityTimeoutSeconds: 60,
|
|
12302
12428
|
// Must be >= Lambda timeout
|
|
@@ -12324,7 +12450,7 @@ async function createSQSResources() {
|
|
|
12324
12450
|
|
|
12325
12451
|
// src/infrastructure/email-stack.ts
|
|
12326
12452
|
async function deployEmailStack(config2) {
|
|
12327
|
-
const identity = await
|
|
12453
|
+
const identity = await aws14.getCallerIdentity();
|
|
12328
12454
|
const accountId = identity.accountId;
|
|
12329
12455
|
let oidcProvider;
|
|
12330
12456
|
if (config2.provider === "vercel" && config2.vercel) {
|
|
@@ -12440,6 +12566,15 @@ async function deployEmailStack(config2) {
|
|
|
12440
12566
|
region: config2.region
|
|
12441
12567
|
});
|
|
12442
12568
|
}
|
|
12569
|
+
let alertingResources;
|
|
12570
|
+
if (emailConfig.alerts?.enabled) {
|
|
12571
|
+
alertingResources = await createAlertingResources({
|
|
12572
|
+
alertConfig: emailConfig.alerts,
|
|
12573
|
+
configSetName: sesResources?.configSet.configurationSetName,
|
|
12574
|
+
dlqName: sqsResources ? "wraps-email-events-dlq" : void 0,
|
|
12575
|
+
region: config2.region
|
|
12576
|
+
});
|
|
12577
|
+
}
|
|
12443
12578
|
return {
|
|
12444
12579
|
roleArn: role.arn,
|
|
12445
12580
|
configSetName: sesResources?.configSet.configurationSetName,
|
|
@@ -12464,7 +12599,10 @@ async function deployEmailStack(config2) {
|
|
|
12464
12599
|
smtpUserArn: smtpResources?.iamUser.arn,
|
|
12465
12600
|
smtpUsername: smtpResources?.accessKey.id,
|
|
12466
12601
|
smtpPassword: smtpResources?.smtpPassword,
|
|
12467
|
-
smtpEndpoint: smtpResources ? `email-smtp.${config2.region}.amazonaws.com` : void 0
|
|
12602
|
+
smtpEndpoint: smtpResources ? `email-smtp.${config2.region}.amazonaws.com` : void 0,
|
|
12603
|
+
// Alerting outputs
|
|
12604
|
+
alertsEnabled: emailConfig.alerts?.enabled,
|
|
12605
|
+
alertTopicArn: alertingResources?.topic.arn
|
|
12468
12606
|
};
|
|
12469
12607
|
}
|
|
12470
12608
|
|
|
@@ -12837,14 +12975,14 @@ async function scanSESConfigurationSets(region) {
|
|
|
12837
12975
|
}
|
|
12838
12976
|
}
|
|
12839
12977
|
async function scanSNSTopics(region) {
|
|
12840
|
-
const
|
|
12978
|
+
const sns3 = new SNSClient({ region });
|
|
12841
12979
|
const topics = [];
|
|
12842
12980
|
try {
|
|
12843
|
-
const listResponse = await
|
|
12981
|
+
const listResponse = await sns3.send(new ListTopicsCommand({}));
|
|
12844
12982
|
const topicArns = listResponse.Topics?.map((t) => t.TopicArn).filter(Boolean) || [];
|
|
12845
12983
|
for (const arn of topicArns) {
|
|
12846
12984
|
try {
|
|
12847
|
-
const attrsResponse = await
|
|
12985
|
+
const attrsResponse = await sns3.send(
|
|
12848
12986
|
new GetTopicAttributesCommand({ TopicArn: arn })
|
|
12849
12987
|
);
|
|
12850
12988
|
const name = arn.split(":").pop() || arn;
|
|
@@ -14777,6 +14915,14 @@ ${pc21.bold("Current Configuration:")}
|
|
|
14777
14915
|
}[config2.emailArchiving.retention] || "90 days";
|
|
14778
14916
|
console.log(` ${pc21.green("\u2713")} Email Archiving (${retentionLabel})`);
|
|
14779
14917
|
}
|
|
14918
|
+
if (config2.alerts?.enabled) {
|
|
14919
|
+
console.log(` ${pc21.green("\u2713")} Reputation Alerts`);
|
|
14920
|
+
if (config2.alerts.notificationEmail) {
|
|
14921
|
+
console.log(
|
|
14922
|
+
` ${pc21.dim("\u2514\u2500")} Email: ${pc21.cyan(config2.alerts.notificationEmail)}`
|
|
14923
|
+
);
|
|
14924
|
+
}
|
|
14925
|
+
}
|
|
14780
14926
|
const currentCostData = calculateCosts(config2, 5e4);
|
|
14781
14927
|
console.log(
|
|
14782
14928
|
`
|
|
@@ -14816,6 +14962,11 @@ ${pc21.bold("Current Configuration:")}
|
|
|
14816
14962
|
label: "Enable dedicated IP address",
|
|
14817
14963
|
hint: "Requires 100k+ emails/day ($50-100/mo)"
|
|
14818
14964
|
},
|
|
14965
|
+
{
|
|
14966
|
+
value: "alerts",
|
|
14967
|
+
label: config2.alerts?.enabled ? "Manage reputation alerts" : "Enable reputation alerts",
|
|
14968
|
+
hint: config2.alerts?.enabled ? "Update thresholds or notification settings" : "Get notified before AWS suspends your account"
|
|
14969
|
+
},
|
|
14819
14970
|
{
|
|
14820
14971
|
value: "custom",
|
|
14821
14972
|
label: "Custom configuration",
|
|
@@ -15297,6 +15448,208 @@ ${pc21.bold("Current Configuration:")}
|
|
|
15297
15448
|
newPreset = void 0;
|
|
15298
15449
|
break;
|
|
15299
15450
|
}
|
|
15451
|
+
case "alerts": {
|
|
15452
|
+
if (!config2.reputationMetrics) {
|
|
15453
|
+
clack20.log.warn("Reputation metrics must be enabled to use alerting.");
|
|
15454
|
+
clack20.log.info(
|
|
15455
|
+
"This requires the Production or Enterprise preset, or enabling reputation metrics manually."
|
|
15456
|
+
);
|
|
15457
|
+
const enableReputationMetrics = await clack20.confirm({
|
|
15458
|
+
message: "Enable reputation metrics now?",
|
|
15459
|
+
initialValue: true
|
|
15460
|
+
});
|
|
15461
|
+
if (clack20.isCancel(enableReputationMetrics) || !enableReputationMetrics) {
|
|
15462
|
+
clack20.cancel("Alerting not enabled.");
|
|
15463
|
+
process.exit(0);
|
|
15464
|
+
}
|
|
15465
|
+
updatedConfig = {
|
|
15466
|
+
...config2,
|
|
15467
|
+
reputationMetrics: true
|
|
15468
|
+
};
|
|
15469
|
+
}
|
|
15470
|
+
if (config2.alerts?.enabled) {
|
|
15471
|
+
clack20.log.info(`Alerting is currently ${pc21.green("enabled")}`);
|
|
15472
|
+
if (config2.alerts.notificationEmail) {
|
|
15473
|
+
clack20.log.info(
|
|
15474
|
+
` Notification email: ${pc21.cyan(config2.alerts.notificationEmail)}`
|
|
15475
|
+
);
|
|
15476
|
+
}
|
|
15477
|
+
const alertsAction = await clack20.select({
|
|
15478
|
+
message: "What would you like to do?",
|
|
15479
|
+
options: [
|
|
15480
|
+
{
|
|
15481
|
+
value: "change-email",
|
|
15482
|
+
label: "Change notification email",
|
|
15483
|
+
hint: config2.alerts.notificationEmail || "Not set"
|
|
15484
|
+
},
|
|
15485
|
+
{
|
|
15486
|
+
value: "change-thresholds",
|
|
15487
|
+
label: "Customize alert thresholds",
|
|
15488
|
+
hint: "Adjust bounce/complaint rate thresholds"
|
|
15489
|
+
},
|
|
15490
|
+
{
|
|
15491
|
+
value: "disable",
|
|
15492
|
+
label: "Disable alerting",
|
|
15493
|
+
hint: "Remove CloudWatch alarms and SNS topic"
|
|
15494
|
+
}
|
|
15495
|
+
]
|
|
15496
|
+
});
|
|
15497
|
+
if (clack20.isCancel(alertsAction)) {
|
|
15498
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15499
|
+
process.exit(0);
|
|
15500
|
+
}
|
|
15501
|
+
if (alertsAction === "disable") {
|
|
15502
|
+
const confirmDisable = await clack20.confirm({
|
|
15503
|
+
message: "Are you sure? You won't be notified if your reputation degrades.",
|
|
15504
|
+
initialValue: false
|
|
15505
|
+
});
|
|
15506
|
+
if (clack20.isCancel(confirmDisable) || !confirmDisable) {
|
|
15507
|
+
clack20.log.info("Alerting not disabled.");
|
|
15508
|
+
process.exit(0);
|
|
15509
|
+
}
|
|
15510
|
+
updatedConfig = {
|
|
15511
|
+
...config2,
|
|
15512
|
+
alerts: { enabled: false }
|
|
15513
|
+
};
|
|
15514
|
+
} else if (alertsAction === "change-email") {
|
|
15515
|
+
const notificationEmail = await clack20.text({
|
|
15516
|
+
message: "Notification email address:",
|
|
15517
|
+
placeholder: "alerts@yourcompany.com",
|
|
15518
|
+
initialValue: config2.alerts.notificationEmail || "",
|
|
15519
|
+
validate: (value) => {
|
|
15520
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
15521
|
+
return "Please enter a valid email address";
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
});
|
|
15525
|
+
if (clack20.isCancel(notificationEmail)) {
|
|
15526
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15527
|
+
process.exit(0);
|
|
15528
|
+
}
|
|
15529
|
+
updatedConfig = {
|
|
15530
|
+
...config2,
|
|
15531
|
+
alerts: {
|
|
15532
|
+
...config2.alerts,
|
|
15533
|
+
enabled: true,
|
|
15534
|
+
notificationEmail: notificationEmail || void 0
|
|
15535
|
+
}
|
|
15536
|
+
};
|
|
15537
|
+
} else if (alertsAction === "change-thresholds") {
|
|
15538
|
+
clack20.log.info(`
|
|
15539
|
+
${pc21.bold("Alert Thresholds")}`);
|
|
15540
|
+
clack20.log.info(
|
|
15541
|
+
pc21.dim("These thresholds warn you BEFORE AWS takes action:")
|
|
15542
|
+
);
|
|
15543
|
+
clack20.log.info(pc21.dim(" AWS warns at 5% bounce, 0.1% complaint"));
|
|
15544
|
+
clack20.log.info(pc21.dim(" Gmail blocks at 0.3% complaint rate\n"));
|
|
15545
|
+
const thresholdPreset = await clack20.select({
|
|
15546
|
+
message: "Choose threshold sensitivity:",
|
|
15547
|
+
options: [
|
|
15548
|
+
{
|
|
15549
|
+
value: "standard",
|
|
15550
|
+
label: "Standard (recommended)",
|
|
15551
|
+
hint: "Bounce: 2%/4%, Complaint: 0.05%/0.08%"
|
|
15552
|
+
},
|
|
15553
|
+
{
|
|
15554
|
+
value: "strict",
|
|
15555
|
+
label: "Strict (enterprise)",
|
|
15556
|
+
hint: "Bounce: 1%/2%, Complaint: 0.03%/0.05%"
|
|
15557
|
+
},
|
|
15558
|
+
{
|
|
15559
|
+
value: "relaxed",
|
|
15560
|
+
label: "Relaxed",
|
|
15561
|
+
hint: "Bounce: 3%/5%, Complaint: 0.08%/0.1%"
|
|
15562
|
+
}
|
|
15563
|
+
]
|
|
15564
|
+
});
|
|
15565
|
+
if (clack20.isCancel(thresholdPreset)) {
|
|
15566
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15567
|
+
process.exit(0);
|
|
15568
|
+
}
|
|
15569
|
+
const thresholdConfigs = {
|
|
15570
|
+
standard: {
|
|
15571
|
+
bounceRateWarning: 0.02,
|
|
15572
|
+
bounceRateCritical: 0.04,
|
|
15573
|
+
complaintRateWarning: 5e-4,
|
|
15574
|
+
complaintRateCritical: 8e-4
|
|
15575
|
+
},
|
|
15576
|
+
strict: {
|
|
15577
|
+
bounceRateWarning: 0.01,
|
|
15578
|
+
bounceRateCritical: 0.02,
|
|
15579
|
+
complaintRateWarning: 3e-4,
|
|
15580
|
+
complaintRateCritical: 5e-4
|
|
15581
|
+
},
|
|
15582
|
+
relaxed: {
|
|
15583
|
+
bounceRateWarning: 0.03,
|
|
15584
|
+
bounceRateCritical: 0.05,
|
|
15585
|
+
complaintRateWarning: 8e-4,
|
|
15586
|
+
complaintRateCritical: 1e-3
|
|
15587
|
+
}
|
|
15588
|
+
};
|
|
15589
|
+
updatedConfig = {
|
|
15590
|
+
...config2,
|
|
15591
|
+
alerts: {
|
|
15592
|
+
...config2.alerts,
|
|
15593
|
+
enabled: true,
|
|
15594
|
+
thresholds: thresholdConfigs[thresholdPreset]
|
|
15595
|
+
}
|
|
15596
|
+
};
|
|
15597
|
+
}
|
|
15598
|
+
} else {
|
|
15599
|
+
clack20.log.info(`
|
|
15600
|
+
${pc21.bold("Reputation Alerts")}
|
|
15601
|
+
`);
|
|
15602
|
+
clack20.log.info(
|
|
15603
|
+
pc21.dim("Get notified when your email reputation is at risk:")
|
|
15604
|
+
);
|
|
15605
|
+
clack20.log.info(pc21.dim(" - Bounce rate warnings (before AWS review)"));
|
|
15606
|
+
clack20.log.info(
|
|
15607
|
+
pc21.dim(" - Complaint rate warnings (before Gmail blocks you)")
|
|
15608
|
+
);
|
|
15609
|
+
clack20.log.info(pc21.dim(" - DLQ alerts (event processing failures)"));
|
|
15610
|
+
clack20.log.info(pc21.dim("\nCost: ~$0.50/mo (5 CloudWatch alarms)\n"));
|
|
15611
|
+
const enableAlerts = await clack20.confirm({
|
|
15612
|
+
message: "Enable reputation alerts?",
|
|
15613
|
+
initialValue: true
|
|
15614
|
+
});
|
|
15615
|
+
if (clack20.isCancel(enableAlerts) || !enableAlerts) {
|
|
15616
|
+
clack20.log.info("Alerting not enabled.");
|
|
15617
|
+
process.exit(0);
|
|
15618
|
+
}
|
|
15619
|
+
const notificationEmail = await clack20.text({
|
|
15620
|
+
message: "Notification email address:",
|
|
15621
|
+
placeholder: "alerts@yourcompany.com",
|
|
15622
|
+
validate: (value) => {
|
|
15623
|
+
if (!value) {
|
|
15624
|
+
return "Email address is required for alerts";
|
|
15625
|
+
}
|
|
15626
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
15627
|
+
return "Please enter a valid email address";
|
|
15628
|
+
}
|
|
15629
|
+
}
|
|
15630
|
+
});
|
|
15631
|
+
if (clack20.isCancel(notificationEmail)) {
|
|
15632
|
+
clack20.cancel("Upgrade cancelled.");
|
|
15633
|
+
process.exit(0);
|
|
15634
|
+
}
|
|
15635
|
+
clack20.log.info(
|
|
15636
|
+
pc21.dim("\nYou'll receive an email to confirm your subscription.")
|
|
15637
|
+
);
|
|
15638
|
+
updatedConfig = {
|
|
15639
|
+
...config2,
|
|
15640
|
+
reputationMetrics: true,
|
|
15641
|
+
// Required for alerts
|
|
15642
|
+
alerts: {
|
|
15643
|
+
enabled: true,
|
|
15644
|
+
notificationEmail,
|
|
15645
|
+
dlqAlerts: true
|
|
15646
|
+
// Uses default thresholds
|
|
15647
|
+
}
|
|
15648
|
+
};
|
|
15649
|
+
}
|
|
15650
|
+
newPreset = void 0;
|
|
15651
|
+
break;
|
|
15652
|
+
}
|
|
15300
15653
|
case "custom": {
|
|
15301
15654
|
const { promptCustomConfig: promptCustomConfig2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
|
|
15302
15655
|
const customConfig = await promptCustomConfig2(config2);
|
|
@@ -15905,6 +16258,9 @@ ${pc21.green("\u2713")} ${pc21.bold("Upgrade complete!")}
|
|
|
15905
16258
|
if (updatedConfig.smtpCredentials?.enabled) {
|
|
15906
16259
|
enabledFeatures.push("smtp_credentials");
|
|
15907
16260
|
}
|
|
16261
|
+
if (updatedConfig.alerts?.enabled) {
|
|
16262
|
+
enabledFeatures.push("alerts");
|
|
16263
|
+
}
|
|
15908
16264
|
trackServiceUpgrade("email", {
|
|
15909
16265
|
from_preset: metadata.services.email?.preset,
|
|
15910
16266
|
to_preset: newPreset,
|
|
@@ -16111,12 +16467,17 @@ function buildConsolePolicyDocument(emailConfig, smsConfig) {
|
|
|
16111
16467
|
statements.push({
|
|
16112
16468
|
Effect: "Allow",
|
|
16113
16469
|
Action: [
|
|
16470
|
+
"ses:GetAccount",
|
|
16471
|
+
// Get SES rate limits and quotas
|
|
16114
16472
|
"ses:GetSendStatistics",
|
|
16115
16473
|
"ses:ListIdentities",
|
|
16116
16474
|
"ses:GetIdentityVerificationAttributes",
|
|
16117
16475
|
// SES v2 API for listing/getting email identities (domains)
|
|
16118
16476
|
"ses:ListEmailIdentities",
|
|
16119
16477
|
"ses:GetEmailIdentity",
|
|
16478
|
+
// SES v2 API for configuration set scanning (needed by dashboard)
|
|
16479
|
+
"ses:GetConfigurationSet",
|
|
16480
|
+
"ses:GetConfigurationSetEventDestinations",
|
|
16120
16481
|
"cloudwatch:GetMetricData",
|
|
16121
16482
|
"cloudwatch:GetMetricStatistics"
|
|
16122
16483
|
],
|
|
@@ -17261,7 +17622,7 @@ import {
|
|
|
17261
17622
|
} from "@aws-sdk/client-cloudwatch";
|
|
17262
17623
|
async function fetchSESMetrics(roleArn, region, timeRange, tableName) {
|
|
17263
17624
|
const credentials = roleArn ? await assumeRole(roleArn, region) : void 0;
|
|
17264
|
-
const
|
|
17625
|
+
const cloudwatch4 = new CloudWatchClient({ region, credentials });
|
|
17265
17626
|
const queries = [
|
|
17266
17627
|
{
|
|
17267
17628
|
Id: "sends",
|
|
@@ -17309,7 +17670,7 @@ async function fetchSESMetrics(roleArn, region, timeRange, tableName) {
|
|
|
17309
17670
|
}
|
|
17310
17671
|
}
|
|
17311
17672
|
];
|
|
17312
|
-
const response = await
|
|
17673
|
+
const response = await cloudwatch4.send(
|
|
17313
17674
|
new GetMetricDataCommand({
|
|
17314
17675
|
MetricDataQueries: queries,
|
|
17315
17676
|
StartTime: timeRange.start,
|
|
@@ -18094,7 +18455,7 @@ import {
|
|
|
18094
18455
|
import { unmarshall as unmarshall4 } from "@aws-sdk/util-dynamodb";
|
|
18095
18456
|
async function fetchSMSSpendLimits(region) {
|
|
18096
18457
|
const smsClient = new PinpointSMSVoiceV2Client({ region });
|
|
18097
|
-
const
|
|
18458
|
+
const cloudwatch4 = new CloudWatchClient2({ region });
|
|
18098
18459
|
try {
|
|
18099
18460
|
const spendLimits = await smsClient.send(
|
|
18100
18461
|
new DescribeSpendLimitsCommand({})
|
|
@@ -18108,7 +18469,7 @@ async function fetchSMSSpendLimits(region) {
|
|
|
18108
18469
|
}
|
|
18109
18470
|
const now = /* @__PURE__ */ new Date();
|
|
18110
18471
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
18111
|
-
const metricsResponse = await
|
|
18472
|
+
const metricsResponse = await cloudwatch4.send(
|
|
18112
18473
|
new GetMetricDataCommand2({
|
|
18113
18474
|
MetricDataQueries: [
|
|
18114
18475
|
{
|
|
@@ -19095,7 +19456,7 @@ import pc28 from "picocolors";
|
|
|
19095
19456
|
|
|
19096
19457
|
// src/infrastructure/sms-stack.ts
|
|
19097
19458
|
init_esm_shims();
|
|
19098
|
-
import * as
|
|
19459
|
+
import * as aws15 from "@pulumi/aws";
|
|
19099
19460
|
import * as pulumi22 from "@pulumi/pulumi";
|
|
19100
19461
|
async function roleExists3(roleName) {
|
|
19101
19462
|
try {
|
|
@@ -19173,7 +19534,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19173
19534
|
}
|
|
19174
19535
|
const roleName = "wraps-sms-role";
|
|
19175
19536
|
const exists = await roleExists3(roleName);
|
|
19176
|
-
const role = exists ? new
|
|
19537
|
+
const role = exists ? new aws15.iam.Role(
|
|
19177
19538
|
roleName,
|
|
19178
19539
|
{
|
|
19179
19540
|
name: roleName,
|
|
@@ -19188,7 +19549,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19188
19549
|
import: roleName,
|
|
19189
19550
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19190
19551
|
}
|
|
19191
|
-
) : new
|
|
19552
|
+
) : new aws15.iam.Role(
|
|
19192
19553
|
roleName,
|
|
19193
19554
|
{
|
|
19194
19555
|
name: roleName,
|
|
@@ -19283,7 +19644,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19283
19644
|
Resource: "arn:aws:logs:*:*:log-group:/aws/lambda/wraps-sms-*"
|
|
19284
19645
|
});
|
|
19285
19646
|
}
|
|
19286
|
-
new
|
|
19647
|
+
new aws15.iam.RolePolicy("wraps-sms-policy", {
|
|
19287
19648
|
role: role.name,
|
|
19288
19649
|
policy: JSON.stringify({
|
|
19289
19650
|
Version: "2012-10-17",
|
|
@@ -19293,7 +19654,7 @@ async function createSMSIAMRole(config2) {
|
|
|
19293
19654
|
return role;
|
|
19294
19655
|
}
|
|
19295
19656
|
function createSMSConfigurationSet() {
|
|
19296
|
-
return new
|
|
19657
|
+
return new aws15.pinpoint.Smsvoicev2ConfigurationSet("wraps-sms-config", {
|
|
19297
19658
|
name: "wraps-sms-config",
|
|
19298
19659
|
defaultMessageType: "TRANSACTIONAL",
|
|
19299
19660
|
tags: {
|
|
@@ -19303,7 +19664,7 @@ function createSMSConfigurationSet() {
|
|
|
19303
19664
|
});
|
|
19304
19665
|
}
|
|
19305
19666
|
function createSMSOptOutList() {
|
|
19306
|
-
return new
|
|
19667
|
+
return new aws15.pinpoint.Smsvoicev2OptOutList("wraps-sms-optouts", {
|
|
19307
19668
|
name: "wraps-sms-optouts",
|
|
19308
19669
|
tags: {
|
|
19309
19670
|
ManagedBy: "wraps-cli",
|
|
@@ -19367,7 +19728,7 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
|
|
|
19367
19728
|
}
|
|
19368
19729
|
};
|
|
19369
19730
|
if (existingArn) {
|
|
19370
|
-
return new
|
|
19731
|
+
return new aws15.pinpoint.Smsvoicev2PhoneNumber(
|
|
19371
19732
|
"wraps-sms-number",
|
|
19372
19733
|
phoneConfig,
|
|
19373
19734
|
{
|
|
@@ -19376,7 +19737,7 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
|
|
|
19376
19737
|
}
|
|
19377
19738
|
);
|
|
19378
19739
|
}
|
|
19379
|
-
return new
|
|
19740
|
+
return new aws15.pinpoint.Smsvoicev2PhoneNumber(
|
|
19380
19741
|
"wraps-sms-number",
|
|
19381
19742
|
phoneConfig,
|
|
19382
19743
|
{
|
|
@@ -19419,10 +19780,10 @@ async function createSMSSQSResources() {
|
|
|
19419
19780
|
Description: "Dead letter queue for failed SMS event processing"
|
|
19420
19781
|
}
|
|
19421
19782
|
};
|
|
19422
|
-
const dlq = dlqUrl ? new
|
|
19783
|
+
const dlq = dlqUrl ? new aws15.sqs.Queue(dlqName, dlqConfig, {
|
|
19423
19784
|
import: dlqUrl,
|
|
19424
19785
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19425
|
-
}) : new
|
|
19786
|
+
}) : new aws15.sqs.Queue(dlqName, dlqConfig, {
|
|
19426
19787
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19427
19788
|
});
|
|
19428
19789
|
const queueConfig = {
|
|
@@ -19445,10 +19806,10 @@ async function createSMSSQSResources() {
|
|
|
19445
19806
|
Description: "Queue for SMS events from SNS"
|
|
19446
19807
|
}
|
|
19447
19808
|
};
|
|
19448
|
-
const queue = queueUrl ? new
|
|
19809
|
+
const queue = queueUrl ? new aws15.sqs.Queue(queueName, queueConfig, {
|
|
19449
19810
|
import: queueUrl,
|
|
19450
19811
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19451
|
-
}) : new
|
|
19812
|
+
}) : new aws15.sqs.Queue(queueName, queueConfig, {
|
|
19452
19813
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19453
19814
|
});
|
|
19454
19815
|
return { queue, dlq };
|
|
@@ -19456,12 +19817,12 @@ async function createSMSSQSResources() {
|
|
|
19456
19817
|
async function snsTopicExists(topicName) {
|
|
19457
19818
|
try {
|
|
19458
19819
|
const { SNSClient: SNSClient2, ListTopicsCommand: ListTopicsCommand2 } = await import("@aws-sdk/client-sns");
|
|
19459
|
-
const
|
|
19820
|
+
const sns3 = new SNSClient2({
|
|
19460
19821
|
region: process.env.AWS_REGION || "us-east-1"
|
|
19461
19822
|
});
|
|
19462
19823
|
let nextToken;
|
|
19463
19824
|
do {
|
|
19464
|
-
const response = await
|
|
19825
|
+
const response = await sns3.send(
|
|
19465
19826
|
new ListTopicsCommand2({ NextToken: nextToken })
|
|
19466
19827
|
);
|
|
19467
19828
|
const found = response.Topics?.find(
|
|
@@ -19488,13 +19849,13 @@ async function createSMSSNSResources(config2) {
|
|
|
19488
19849
|
Description: "SNS topic for SMS delivery events"
|
|
19489
19850
|
}
|
|
19490
19851
|
};
|
|
19491
|
-
const topic = topicArn ? new
|
|
19852
|
+
const topic = topicArn ? new aws15.sns.Topic("wraps-sms-events-topic", topicConfig, {
|
|
19492
19853
|
import: topicArn,
|
|
19493
19854
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19494
|
-
}) : new
|
|
19855
|
+
}) : new aws15.sns.Topic("wraps-sms-events-topic", topicConfig, {
|
|
19495
19856
|
customTimeouts: { create: "2m", update: "2m", delete: "2m" }
|
|
19496
19857
|
});
|
|
19497
|
-
new
|
|
19858
|
+
new aws15.sns.TopicPolicy("wraps-sms-events-topic-policy", {
|
|
19498
19859
|
arn: topic.arn,
|
|
19499
19860
|
policy: topic.arn.apply(
|
|
19500
19861
|
(topicArn2) => JSON.stringify({
|
|
@@ -19511,7 +19872,7 @@ async function createSMSSNSResources(config2) {
|
|
|
19511
19872
|
})
|
|
19512
19873
|
)
|
|
19513
19874
|
});
|
|
19514
|
-
new
|
|
19875
|
+
new aws15.sqs.QueuePolicy("wraps-sms-events-queue-policy", {
|
|
19515
19876
|
queueUrl: config2.queueUrl,
|
|
19516
19877
|
policy: pulumi22.all([config2.queueArn, topic.arn]).apply(
|
|
19517
19878
|
([queueArn, topicArn2]) => JSON.stringify({
|
|
@@ -19530,7 +19891,7 @@ async function createSMSSNSResources(config2) {
|
|
|
19530
19891
|
})
|
|
19531
19892
|
)
|
|
19532
19893
|
});
|
|
19533
|
-
const subscription = new
|
|
19894
|
+
const subscription = new aws15.sns.TopicSubscription(
|
|
19534
19895
|
"wraps-sms-events-subscription",
|
|
19535
19896
|
{
|
|
19536
19897
|
topic: topic.arn,
|
|
@@ -19579,17 +19940,17 @@ async function createSMSDynamoDBTable() {
|
|
|
19579
19940
|
Service: "sms"
|
|
19580
19941
|
}
|
|
19581
19942
|
};
|
|
19582
|
-
return exists ? new
|
|
19943
|
+
return exists ? new aws15.dynamodb.Table(tableName, tableConfig, {
|
|
19583
19944
|
import: tableName,
|
|
19584
19945
|
customTimeouts: { create: "5m", update: "5m", delete: "5m" }
|
|
19585
|
-
}) : new
|
|
19946
|
+
}) : new aws15.dynamodb.Table(tableName, tableConfig, {
|
|
19586
19947
|
customTimeouts: { create: "5m", update: "5m", delete: "5m" }
|
|
19587
19948
|
});
|
|
19588
19949
|
}
|
|
19589
19950
|
async function deploySMSLambdaFunction(config2) {
|
|
19590
19951
|
const { getLambdaCode: getLambdaCode2 } = await Promise.resolve().then(() => (init_lambda(), lambda_exports));
|
|
19591
19952
|
const codeDir = await getLambdaCode2("sms-event-processor");
|
|
19592
|
-
const lambdaRole = new
|
|
19953
|
+
const lambdaRole = new aws15.iam.Role("wraps-sms-lambda-role", {
|
|
19593
19954
|
name: "wraps-sms-lambda-role",
|
|
19594
19955
|
assumeRolePolicy: JSON.stringify({
|
|
19595
19956
|
Version: "2012-10-17",
|
|
@@ -19606,11 +19967,11 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19606
19967
|
Service: "sms"
|
|
19607
19968
|
}
|
|
19608
19969
|
});
|
|
19609
|
-
new
|
|
19970
|
+
new aws15.iam.RolePolicyAttachment("wraps-sms-lambda-basic-execution", {
|
|
19610
19971
|
role: lambdaRole.name,
|
|
19611
19972
|
policyArn: "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
19612
19973
|
});
|
|
19613
|
-
new
|
|
19974
|
+
new aws15.iam.RolePolicy("wraps-sms-lambda-policy", {
|
|
19614
19975
|
role: lambdaRole.name,
|
|
19615
19976
|
policy: pulumi22.all([config2.tableName, config2.queueArn]).apply(
|
|
19616
19977
|
([tableName, queueArn]) => JSON.stringify({
|
|
@@ -19642,7 +20003,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19642
20003
|
})
|
|
19643
20004
|
)
|
|
19644
20005
|
});
|
|
19645
|
-
const eventProcessor = new
|
|
20006
|
+
const eventProcessor = new aws15.lambda.Function(
|
|
19646
20007
|
"wraps-sms-event-processor",
|
|
19647
20008
|
{
|
|
19648
20009
|
name: "wraps-sms-event-processor",
|
|
@@ -19670,7 +20031,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19670
20031
|
customTimeouts: { create: "5m", update: "5m", delete: "2m" }
|
|
19671
20032
|
}
|
|
19672
20033
|
);
|
|
19673
|
-
new
|
|
20034
|
+
new aws15.lambda.EventSourceMapping(
|
|
19674
20035
|
"wraps-sms-event-source-mapping",
|
|
19675
20036
|
{
|
|
19676
20037
|
eventSourceArn: config2.queueArn,
|
|
@@ -19686,7 +20047,7 @@ async function deploySMSLambdaFunction(config2) {
|
|
|
19686
20047
|
return eventProcessor;
|
|
19687
20048
|
}
|
|
19688
20049
|
async function deploySMSStack(config2) {
|
|
19689
|
-
const identity = await
|
|
20050
|
+
const identity = await aws15.getCallerIdentity();
|
|
19690
20051
|
const accountId = identity.accountId;
|
|
19691
20052
|
let oidcProvider;
|
|
19692
20053
|
if (config2.provider === "vercel" && config2.vercel) {
|