@wraps.dev/cli 0.1.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 +322 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +5063 -0
- package/dist/cli.js.map +1 -0
- package/dist/console/assets/index-De4fp0hs.css +1 -0
- package/dist/console/assets/index-R43NIXvY.js +371 -0
- package/dist/console/index.html +20 -0
- package/dist/lambda/event-processor/__tests__/index.test.ts +932 -0
- package/dist/lambda/event-processor/index.ts +165 -0
- package/dist/lambda/event-processor/package.json +13 -0
- package/package.json +88 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import type { SQSEvent } from "aws-lambda";
|
|
3
|
+
|
|
4
|
+
const dynamodb = new DynamoDBClient({});
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lambda handler for processing SES events from SQS (via EventBridge)
|
|
8
|
+
* Stores all SES events in DynamoDB:
|
|
9
|
+
* - Send: Email sent from SES
|
|
10
|
+
* - Delivery: Email delivered to recipient
|
|
11
|
+
* - Open: Email opened by recipient
|
|
12
|
+
* - Click: Link clicked in email
|
|
13
|
+
* - Bounce: Email bounced (permanent or transient)
|
|
14
|
+
* - Complaint: Recipient marked email as spam
|
|
15
|
+
* - Reject: Email rejected before sending
|
|
16
|
+
* - Rendering Failure: Template rendering failed
|
|
17
|
+
* - DeliveryDelay: Temporary delivery delay
|
|
18
|
+
* - Subscription: Recipient unsubscribed/changed preferences
|
|
19
|
+
*/
|
|
20
|
+
export async function handler(event: SQSEvent) {
|
|
21
|
+
console.log("Processing SES event from SQS:", JSON.stringify(event, null, 2));
|
|
22
|
+
|
|
23
|
+
const tableName = process.env.TABLE_NAME;
|
|
24
|
+
if (!tableName) {
|
|
25
|
+
throw new Error("TABLE_NAME environment variable not set");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const record of event.Records) {
|
|
29
|
+
try {
|
|
30
|
+
// Parse the SQS message body (which contains the EventBridge event)
|
|
31
|
+
const eventBridgeEvent = JSON.parse(record.body);
|
|
32
|
+
|
|
33
|
+
// The actual SES event is in the 'detail' field of the EventBridge event
|
|
34
|
+
const message = eventBridgeEvent.detail;
|
|
35
|
+
const eventType = message.eventType || message.notificationType;
|
|
36
|
+
|
|
37
|
+
// Extract email details
|
|
38
|
+
const mail = message.mail;
|
|
39
|
+
const messageId = mail.messageId;
|
|
40
|
+
const mailTimestamp = new Date(mail.timestamp).getTime();
|
|
41
|
+
const from = mail.source;
|
|
42
|
+
const to = mail.destination;
|
|
43
|
+
const subject = mail.commonHeaders?.subject || "";
|
|
44
|
+
|
|
45
|
+
// Extract additional data and event-specific timestamp based on event type
|
|
46
|
+
let additionalData: Record<string, unknown> = {};
|
|
47
|
+
let eventTimestamp = mailTimestamp; // Default to mail timestamp
|
|
48
|
+
|
|
49
|
+
if (eventType === "Send" && message.send) {
|
|
50
|
+
// Send event uses mail timestamp
|
|
51
|
+
additionalData = {
|
|
52
|
+
tags: mail.tags || {},
|
|
53
|
+
};
|
|
54
|
+
} else if (eventType === "Delivery" && message.delivery) {
|
|
55
|
+
eventTimestamp = new Date(message.delivery.timestamp).getTime();
|
|
56
|
+
additionalData = {
|
|
57
|
+
timestamp: message.delivery.timestamp,
|
|
58
|
+
processingTimeMillis: message.delivery.processingTimeMillis,
|
|
59
|
+
recipients: message.delivery.recipients,
|
|
60
|
+
smtpResponse: message.delivery.smtpResponse,
|
|
61
|
+
remoteMtaIp: message.delivery.remoteMtaIp,
|
|
62
|
+
};
|
|
63
|
+
} else if (eventType === "Open" && message.open) {
|
|
64
|
+
eventTimestamp = new Date(message.open.timestamp).getTime();
|
|
65
|
+
additionalData = {
|
|
66
|
+
timestamp: message.open.timestamp,
|
|
67
|
+
userAgent: message.open.userAgent,
|
|
68
|
+
ipAddress: message.open.ipAddress,
|
|
69
|
+
};
|
|
70
|
+
} else if (eventType === "Click" && message.click) {
|
|
71
|
+
eventTimestamp = new Date(message.click.timestamp).getTime();
|
|
72
|
+
additionalData = {
|
|
73
|
+
timestamp: message.click.timestamp,
|
|
74
|
+
link: message.click.link,
|
|
75
|
+
linkTags: message.click.linkTags || {},
|
|
76
|
+
userAgent: message.click.userAgent,
|
|
77
|
+
ipAddress: message.click.ipAddress,
|
|
78
|
+
};
|
|
79
|
+
} else if (eventType === "Bounce" && message.bounce) {
|
|
80
|
+
eventTimestamp = new Date(message.bounce.timestamp).getTime();
|
|
81
|
+
additionalData = {
|
|
82
|
+
bounceType: message.bounce.bounceType,
|
|
83
|
+
bounceSubType: message.bounce.bounceSubType,
|
|
84
|
+
bouncedRecipients: message.bounce.bouncedRecipients,
|
|
85
|
+
timestamp: message.bounce.timestamp,
|
|
86
|
+
feedbackId: message.bounce.feedbackId,
|
|
87
|
+
};
|
|
88
|
+
} else if (eventType === "Complaint" && message.complaint) {
|
|
89
|
+
eventTimestamp = new Date(message.complaint.timestamp).getTime();
|
|
90
|
+
additionalData = {
|
|
91
|
+
complainedRecipients: message.complaint.complainedRecipients,
|
|
92
|
+
timestamp: message.complaint.timestamp,
|
|
93
|
+
feedbackId: message.complaint.feedbackId,
|
|
94
|
+
complaintFeedbackType: message.complaint.complaintFeedbackType,
|
|
95
|
+
userAgent: message.complaint.userAgent,
|
|
96
|
+
};
|
|
97
|
+
} else if (eventType === "Reject" && message.reject) {
|
|
98
|
+
// Reject doesn't have a specific timestamp, use mail timestamp
|
|
99
|
+
additionalData = {
|
|
100
|
+
reason: message.reject.reason,
|
|
101
|
+
};
|
|
102
|
+
} else if (eventType === "Rendering Failure" && message.failure) {
|
|
103
|
+
// Rendering failure doesn't have a specific timestamp, use mail timestamp
|
|
104
|
+
additionalData = {
|
|
105
|
+
errorMessage: message.failure.errorMessage,
|
|
106
|
+
templateName: message.failure.templateName,
|
|
107
|
+
};
|
|
108
|
+
} else if (eventType === "DeliveryDelay" && message.deliveryDelay) {
|
|
109
|
+
eventTimestamp = new Date(message.deliveryDelay.timestamp).getTime();
|
|
110
|
+
additionalData = {
|
|
111
|
+
timestamp: message.deliveryDelay.timestamp,
|
|
112
|
+
delayType: message.deliveryDelay.delayType,
|
|
113
|
+
expirationTime: message.deliveryDelay.expirationTime,
|
|
114
|
+
delayedRecipients: message.deliveryDelay.delayedRecipients,
|
|
115
|
+
};
|
|
116
|
+
} else if (eventType === "Subscription" && message.subscription) {
|
|
117
|
+
eventTimestamp = new Date(message.subscription.timestamp).getTime();
|
|
118
|
+
additionalData = {
|
|
119
|
+
contactList: message.subscription.contactList,
|
|
120
|
+
timestamp: message.subscription.timestamp,
|
|
121
|
+
source: message.subscription.source,
|
|
122
|
+
newTopicPreferences: message.subscription.newTopicPreferences,
|
|
123
|
+
oldTopicPreferences: message.subscription.oldTopicPreferences,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Store event in DynamoDB
|
|
128
|
+
// Use eventTimestamp as sort key to ensure each event type creates a unique record
|
|
129
|
+
await dynamodb.send(
|
|
130
|
+
new PutItemCommand({
|
|
131
|
+
TableName: tableName,
|
|
132
|
+
Item: {
|
|
133
|
+
messageId: { S: messageId },
|
|
134
|
+
sentAt: { N: eventTimestamp.toString() },
|
|
135
|
+
accountId: { S: process.env.AWS_ACCOUNT_ID || "unknown" },
|
|
136
|
+
from: { S: from },
|
|
137
|
+
to: { SS: to },
|
|
138
|
+
subject: { S: subject },
|
|
139
|
+
eventType: { S: eventType },
|
|
140
|
+
eventData: { S: JSON.stringify(message) },
|
|
141
|
+
additionalData: { S: JSON.stringify(additionalData) },
|
|
142
|
+
createdAt: { N: Date.now().toString() },
|
|
143
|
+
expiresAt: {
|
|
144
|
+
N: (Date.now() + 90 * 24 * 60 * 60 * 1000).toString(),
|
|
145
|
+
}, // 90 days TTL
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
console.log(
|
|
151
|
+
`Stored ${eventType} event for message ${messageId}`,
|
|
152
|
+
additionalData
|
|
153
|
+
);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("Error processing record:", error);
|
|
156
|
+
console.error("Record:", JSON.stringify(record, null, 2));
|
|
157
|
+
// Don't throw - continue processing other records
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
statusCode: 200,
|
|
163
|
+
body: JSON.stringify({ message: "Events processed successfully" }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wraps-email-event-processor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lambda function to process SES events",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@aws-sdk/client-dynamodb": "^3.490.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/aws-lambda": "^8.10.130"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wraps.dev/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for deploying Wraps email infrastructure to your AWS account",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"wraps": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/wraps-team/wraps.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://wraps.dev",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/wraps-team/wraps/issues"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "tsup --watch",
|
|
29
|
+
"build": "pnpm build:console && tsup",
|
|
30
|
+
"build:console": "pnpm --filter @wraps/console build",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest --watch",
|
|
33
|
+
"test:ui": "vitest --ui",
|
|
34
|
+
"test:coverage": "vitest run --coverage",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"lint": "eslint src",
|
|
37
|
+
"prepublishOnly": "pnpm build"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"aws",
|
|
41
|
+
"ses",
|
|
42
|
+
"email",
|
|
43
|
+
"infrastructure",
|
|
44
|
+
"cli"
|
|
45
|
+
],
|
|
46
|
+
"author": "Wraps",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@aws-sdk/client-cloudformation": "^3.490.0",
|
|
50
|
+
"@aws-sdk/client-cloudwatch": "^3.490.0",
|
|
51
|
+
"@aws-sdk/client-dynamodb": "^3.490.0",
|
|
52
|
+
"@aws-sdk/client-iam": "3.925.0",
|
|
53
|
+
"@aws-sdk/client-lambda": "3.925.0",
|
|
54
|
+
"@aws-sdk/client-route-53": "3.925.0",
|
|
55
|
+
"@aws-sdk/client-ses": "^3.490.0",
|
|
56
|
+
"@aws-sdk/client-sesv2": "3.925.0",
|
|
57
|
+
"@aws-sdk/client-sns": "^3.490.0",
|
|
58
|
+
"@aws-sdk/client-sts": "^3.490.0",
|
|
59
|
+
"@aws-sdk/util-dynamodb": "3.927.0",
|
|
60
|
+
"@clack/prompts": "^0.11.0",
|
|
61
|
+
"@pulumi/aws": "^6.20.0",
|
|
62
|
+
"@pulumi/pulumi": "^3.100.0",
|
|
63
|
+
"args": "^5.0.3",
|
|
64
|
+
"cosmiconfig": "^9.0.0",
|
|
65
|
+
"esbuild": "^0.19.0",
|
|
66
|
+
"express": "^4.21.2",
|
|
67
|
+
"get-port": "^7.1.0",
|
|
68
|
+
"http-terminator": "^3.2.0",
|
|
69
|
+
"open": "^10.1.0",
|
|
70
|
+
"picocolors": "^1.1.1",
|
|
71
|
+
"tabtab": "^3.0.2"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@types/args": "5.0.4",
|
|
75
|
+
"@types/express": "^5.0.0",
|
|
76
|
+
"@types/node": "^20.11.0",
|
|
77
|
+
"@vitest/coverage-v8": "4.0.7",
|
|
78
|
+
"aws-sdk-client-mock": "4.1.0",
|
|
79
|
+
"aws-sdk-client-mock-vitest": "7.0.1",
|
|
80
|
+
"eslint": "^8.56.0",
|
|
81
|
+
"tsup": "^8.0.1",
|
|
82
|
+
"typescript": "^5.3.3",
|
|
83
|
+
"vitest": "^4.0.7"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=20.0.0"
|
|
87
|
+
}
|
|
88
|
+
}
|