@wraps.dev/cli 2.2.6 → 2.2.8

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.
@@ -0,0 +1 @@
1
+ Built at: 2026-01-16T21:29:27.587Z
@@ -0,0 +1 @@
1
+ import{randomUUID as w}from"node:crypto";import{DynamoDBClient as k,PutItemCommand as I}from"@aws-sdk/client-dynamodb";var A=new k({});async function E(m,y){let l=y.awsRequestId,d=w().slice(0,8),a=(s,o)=>{console.log(JSON.stringify({requestId:l,batchId:d,msg:s,...o}))},T=(s,o,e)=>{console.error(JSON.stringify({requestId:l,batchId:d,msg:s,error:String(o),...e}))};a("Processing SES batch",{recordCount:m.Records.length});let u=process.env.TABLE_NAME;if(!u)throw new Error("TABLE_NAME environment variable not set");let g=Number.parseInt(process.env.RETENTION_DAYS||"90",10);for(let s of m.Records)try{let e=JSON.parse(s.body).detail,t=e.eventType||e.notificationType,r=e.mail,p=r.messageId,f=new Date(r.timestamp).getTime(),S=r.source,b=r.destination||[],v=r.commonHeaders?.subject||"";a("Processing email event",{messageId:p,eventType:t,recipientCount:b.length});let n={},i=f;if(t==="Send"&&e.send)n={tags:r.tags||{}};else if(t==="Delivery"&&e.delivery)i=new Date(e.delivery.timestamp).getTime(),n={timestamp:e.delivery.timestamp,processingTimeMillis:e.delivery.processingTimeMillis,recipients:e.delivery.recipients,smtpResponse:e.delivery.smtpResponse,remoteMtaIp:e.delivery.remoteMtaIp};else if(t==="Open"&&e.open)i=new Date(e.open.timestamp).getTime(),n={timestamp:e.open.timestamp,userAgent:e.open.userAgent,ipAddress:e.open.ipAddress};else if(t==="Click"&&e.click)i=new Date(e.click.timestamp).getTime(),n={timestamp:e.click.timestamp,link:e.click.link,linkTags:e.click.linkTags||{},userAgent:e.click.userAgent,ipAddress:e.click.ipAddress};else if(t==="Bounce"&&e.bounce){let c=e.bounce.bounceSubType;c==="Suppressed"||c==="OnAccountSuppressionList"?(t="Suppressed",i=new Date(e.bounce.timestamp).getTime(),n={reason:c,suppressedRecipients:e.bounce.bouncedRecipients,timestamp:e.bounce.timestamp,feedbackId:e.bounce.feedbackId}):(i=new Date(e.bounce.timestamp).getTime(),n={bounceType:e.bounce.bounceType,bounceSubType:e.bounce.bounceSubType,bouncedRecipients:e.bounce.bouncedRecipients,timestamp:e.bounce.timestamp,feedbackId:e.bounce.feedbackId})}else t==="Complaint"&&e.complaint?(i=new Date(e.complaint.timestamp).getTime(),n={complainedRecipients:e.complaint.complainedRecipients,timestamp:e.complaint.timestamp,feedbackId:e.complaint.feedbackId,complaintFeedbackType:e.complaint.complaintFeedbackType,userAgent:e.complaint.userAgent}):t==="Reject"&&e.reject?n={reason:e.reject.reason}:t==="Rendering Failure"&&e.failure?n={errorMessage:e.failure.errorMessage,templateName:e.failure.templateName}:t==="DeliveryDelay"&&e.deliveryDelay?(i=new Date(e.deliveryDelay.timestamp).getTime(),n={timestamp:e.deliveryDelay.timestamp,delayType:e.deliveryDelay.delayType,expirationTime:e.deliveryDelay.expirationTime,delayedRecipients:e.deliveryDelay.delayedRecipients}):t==="Subscription"&&e.subscription&&(i=new Date(e.subscription.timestamp).getTime(),n={contactList:e.subscription.contactList,timestamp:e.subscription.timestamp,source:e.subscription.source,newTopicPreferences:e.subscription.newTopicPreferences,oldTopicPreferences:e.subscription.oldTopicPreferences});let D=g>0?Date.now()+g*24*60*60*1e3:Date.now()+365*24*60*60*1e3;await A.send(new I({TableName:u,Item:{messageId:{S:p},sentAt:{N:i.toString()},accountId:{S:process.env.AWS_ACCOUNT_ID||"unknown"},from:{S},to:{L:b.map(c=>({S:c}))},subject:{S:v},eventType:{S:t},eventData:{S:JSON.stringify(e)},additionalData:{S:JSON.stringify(n)},createdAt:{N:Date.now().toString()},expiresAt:{N:D.toString()}}})),a("Stored event",{eventType:t,messageId:p})}catch(o){T("Error processing record",o,{sqsMessageId:s.messageId})}return{statusCode:200,body:JSON.stringify({message:"Events processed successfully"})}}export{E as handler};
@@ -0,0 +1,214 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
3
+ import type { Context, SQSEvent } from "aws-lambda";
4
+
5
+ const dynamodb = new DynamoDBClient({});
6
+
7
+ /**
8
+ * Lambda handler for processing SES events from SQS (via EventBridge)
9
+ * Stores all SES events in DynamoDB:
10
+ * - Send: Email sent from SES
11
+ * - Delivery: Email delivered to recipient
12
+ * - Open: Email opened by recipient
13
+ * - Click: Link clicked in email
14
+ * - Bounce: Email bounced (permanent or transient)
15
+ * - Suppressed: Email blocked due to recipient on suppression list
16
+ * - Complaint: Recipient marked email as spam
17
+ * - Reject: Email rejected before sending
18
+ * - Rendering Failure: Template rendering failed
19
+ * - DeliveryDelay: Temporary delivery delay
20
+ * - Subscription: Recipient unsubscribed/changed preferences
21
+ */
22
+ export async function handler(event: SQSEvent, context: Context) {
23
+ const requestId = context.awsRequestId;
24
+ const batchId = randomUUID().slice(0, 8);
25
+
26
+ const log = (msg: string, data?: Record<string, unknown>) => {
27
+ console.log(JSON.stringify({ requestId, batchId, msg, ...data }));
28
+ };
29
+ const logError = (
30
+ msg: string,
31
+ error: unknown,
32
+ data?: Record<string, unknown>
33
+ ) => {
34
+ console.error(
35
+ JSON.stringify({ requestId, batchId, msg, error: String(error), ...data })
36
+ );
37
+ };
38
+
39
+ log("Processing SES batch", { recordCount: event.Records.length });
40
+
41
+ const tableName = process.env.TABLE_NAME;
42
+ if (!tableName) {
43
+ throw new Error("TABLE_NAME environment variable not set");
44
+ }
45
+
46
+ // Get retention days from environment, default to 90
47
+ const retentionDays = Number.parseInt(process.env.RETENTION_DAYS || "90", 10);
48
+
49
+ for (const record of event.Records) {
50
+ try {
51
+ // Parse the SQS message body (which contains the EventBridge event)
52
+ const eventBridgeEvent = JSON.parse(record.body);
53
+
54
+ // The actual SES event is in the 'detail' field of the EventBridge event
55
+ const message = eventBridgeEvent.detail;
56
+ let eventType = message.eventType || message.notificationType;
57
+
58
+ // Extract email details
59
+ const mail = message.mail;
60
+ const messageId = mail.messageId;
61
+ const mailTimestamp = new Date(mail.timestamp).getTime();
62
+ const from = mail.source;
63
+ const to = mail.destination || [];
64
+ const subject = mail.commonHeaders?.subject || "";
65
+
66
+ log("Processing email event", {
67
+ messageId,
68
+ eventType,
69
+ recipientCount: to.length,
70
+ });
71
+
72
+ // Extract additional data and event-specific timestamp based on event type
73
+ let additionalData: Record<string, unknown> = {};
74
+ let eventTimestamp = mailTimestamp; // Default to mail timestamp
75
+
76
+ if (eventType === "Send" && message.send) {
77
+ // Send event uses mail timestamp
78
+ additionalData = {
79
+ tags: mail.tags || {},
80
+ };
81
+ } else if (eventType === "Delivery" && message.delivery) {
82
+ eventTimestamp = new Date(message.delivery.timestamp).getTime();
83
+ additionalData = {
84
+ timestamp: message.delivery.timestamp,
85
+ processingTimeMillis: message.delivery.processingTimeMillis,
86
+ recipients: message.delivery.recipients,
87
+ smtpResponse: message.delivery.smtpResponse,
88
+ remoteMtaIp: message.delivery.remoteMtaIp,
89
+ };
90
+ } else if (eventType === "Open" && message.open) {
91
+ eventTimestamp = new Date(message.open.timestamp).getTime();
92
+ additionalData = {
93
+ timestamp: message.open.timestamp,
94
+ userAgent: message.open.userAgent,
95
+ ipAddress: message.open.ipAddress,
96
+ };
97
+ } else if (eventType === "Click" && message.click) {
98
+ eventTimestamp = new Date(message.click.timestamp).getTime();
99
+ additionalData = {
100
+ timestamp: message.click.timestamp,
101
+ link: message.click.link,
102
+ linkTags: message.click.linkTags || {},
103
+ userAgent: message.click.userAgent,
104
+ ipAddress: message.click.ipAddress,
105
+ };
106
+ } else if (eventType === "Bounce" && message.bounce) {
107
+ const bounceSubType = message.bounce.bounceSubType;
108
+
109
+ // Treat suppression as a distinct event type, not a bounce
110
+ // SES sends suppressions as bounces with bounceSubType of "Suppressed" or "OnAccountSuppressionList"
111
+ if (
112
+ bounceSubType === "Suppressed" ||
113
+ bounceSubType === "OnAccountSuppressionList"
114
+ ) {
115
+ eventType = "Suppressed";
116
+ eventTimestamp = new Date(message.bounce.timestamp).getTime();
117
+ additionalData = {
118
+ reason: bounceSubType,
119
+ suppressedRecipients: message.bounce.bouncedRecipients,
120
+ timestamp: message.bounce.timestamp,
121
+ feedbackId: message.bounce.feedbackId,
122
+ };
123
+ } else {
124
+ // Regular bounce handling
125
+ eventTimestamp = new Date(message.bounce.timestamp).getTime();
126
+ additionalData = {
127
+ bounceType: message.bounce.bounceType,
128
+ bounceSubType: message.bounce.bounceSubType,
129
+ bouncedRecipients: message.bounce.bouncedRecipients,
130
+ timestamp: message.bounce.timestamp,
131
+ feedbackId: message.bounce.feedbackId,
132
+ };
133
+ }
134
+ } else if (eventType === "Complaint" && message.complaint) {
135
+ eventTimestamp = new Date(message.complaint.timestamp).getTime();
136
+ additionalData = {
137
+ complainedRecipients: message.complaint.complainedRecipients,
138
+ timestamp: message.complaint.timestamp,
139
+ feedbackId: message.complaint.feedbackId,
140
+ complaintFeedbackType: message.complaint.complaintFeedbackType,
141
+ userAgent: message.complaint.userAgent,
142
+ };
143
+ } else if (eventType === "Reject" && message.reject) {
144
+ // Reject doesn't have a specific timestamp, use mail timestamp
145
+ additionalData = {
146
+ reason: message.reject.reason,
147
+ };
148
+ } else if (eventType === "Rendering Failure" && message.failure) {
149
+ // Rendering failure doesn't have a specific timestamp, use mail timestamp
150
+ additionalData = {
151
+ errorMessage: message.failure.errorMessage,
152
+ templateName: message.failure.templateName,
153
+ };
154
+ } else if (eventType === "DeliveryDelay" && message.deliveryDelay) {
155
+ eventTimestamp = new Date(message.deliveryDelay.timestamp).getTime();
156
+ additionalData = {
157
+ timestamp: message.deliveryDelay.timestamp,
158
+ delayType: message.deliveryDelay.delayType,
159
+ expirationTime: message.deliveryDelay.expirationTime,
160
+ delayedRecipients: message.deliveryDelay.delayedRecipients,
161
+ };
162
+ } else if (eventType === "Subscription" && message.subscription) {
163
+ eventTimestamp = new Date(message.subscription.timestamp).getTime();
164
+ additionalData = {
165
+ contactList: message.subscription.contactList,
166
+ timestamp: message.subscription.timestamp,
167
+ source: message.subscription.source,
168
+ newTopicPreferences: message.subscription.newTopicPreferences,
169
+ oldTopicPreferences: message.subscription.oldTopicPreferences,
170
+ };
171
+ }
172
+
173
+ // Calculate TTL based on retention days (0 or negative means no TTL)
174
+ const expiresAt =
175
+ retentionDays > 0
176
+ ? Date.now() + retentionDays * 24 * 60 * 60 * 1000
177
+ : Date.now() + 365 * 24 * 60 * 60 * 1000; // Default 1 year if not specified
178
+
179
+ // Store event in DynamoDB
180
+ // Use eventTimestamp as sort key to ensure each event type creates a unique record
181
+ // Note: DynamoDB String Sets (SS) cannot be empty, so we use a List (L) for recipients
182
+ await dynamodb.send(
183
+ new PutItemCommand({
184
+ TableName: tableName,
185
+ Item: {
186
+ messageId: { S: messageId },
187
+ sentAt: { N: eventTimestamp.toString() },
188
+ accountId: { S: process.env.AWS_ACCOUNT_ID || "unknown" },
189
+ from: { S: from },
190
+ to: { L: to.map((email: string) => ({ S: email })) },
191
+ subject: { S: subject },
192
+ eventType: { S: eventType },
193
+ eventData: { S: JSON.stringify(message) },
194
+ additionalData: { S: JSON.stringify(additionalData) },
195
+ createdAt: { N: Date.now().toString() },
196
+ expiresAt: { N: expiresAt.toString() },
197
+ },
198
+ })
199
+ );
200
+
201
+ log("Stored event", { eventType, messageId });
202
+ } catch (error) {
203
+ logError("Error processing record", error, {
204
+ sqsMessageId: record.messageId,
205
+ });
206
+ // Don't throw - continue processing other records
207
+ }
208
+ }
209
+
210
+ return {
211
+ statusCode: 200,
212
+ body: JSON.stringify({ message: "Events processed successfully" }),
213
+ };
214
+ }
@@ -0,0 +1 @@
1
+ Built at: 2026-01-16T21:29:27.591Z
@@ -0,0 +1 @@
1
+ import{DynamoDBClient as N,PutItemCommand as E}from"@aws-sdk/client-dynamodb";var u=new N({});async function v(r){console.log("Processing SMS event from SQS:",JSON.stringify(r,null,2));let a=process.env.TABLE_NAME;if(!a)throw new Error("TABLE_NAME environment variable not set");let i=Number.parseInt(process.env.RETENTION_DAYS||"90",10);for(let m of r.Records)try{let e=JSON.parse(m.body),t=e.eventType||e.messageStatus||"UNKNOWN",n=e.messageId,d=e.destinationPhoneNumber,c=e.originationPhoneNumber,o=e.messageBody||"",T=e.isoCountryCode||"US",S=e.messageType||"TRANSACTIONAL",p=e.eventTimestamp?new Date(e.eventTimestamp).getTime():Date.now();console.log("Processing SMS event:",{messageId:n,eventType:t,destinationNumber:d,originationNumber:c});let s={isoCountryCode:T,messageType:S};t==="TEXT_DELIVERED"||t==="TEXT_SUCCESSFUL"?s={...s,deliveryTimestamp:e.deliveryTimestamp,carrierName:e.carrierName,providerResponse:e.providerResponse}:t==="TEXT_FAILED"||t==="TEXT_INVALID"||t==="TEXT_CARRIER_UNREACHABLE"||t==="TEXT_BLOCKED"?s={...s,failureReason:e.failureReason||e.statusMessage,failureCode:e.failureCode||e.statusCode,providerResponse:e.providerResponse}:t==="TEXT_QUEUED"||t==="TEXT_SENT"?s={...s,queuedTimestamp:e.queuedTimestamp,sentTimestamp:e.sentTimestamp}:t==="TEXT_TTL_EXPIRED"&&(s={...s,expirationTimestamp:e.expirationTimestamp,ttlSeconds:e.ttlSeconds});let l=o?Math.ceil(o.length/160):1,g=i>0?Date.now()+i*24*60*60*1e3:Date.now()+365*24*60*60*1e3;await u.send(new E({TableName:a,Item:{messageId:{S:n},sentAt:{N:p.toString()},accountId:{S:process.env.AWS_ACCOUNT_ID||"unknown"},destinationNumber:{S:d||""},originationNumber:{S:c||""},messageBody:{S:o},eventType:{S:t},segments:{N:l.toString()},eventData:{S:JSON.stringify(e)},additionalData:{S:JSON.stringify(s)},createdAt:{N:Date.now().toString()},expiresAt:{N:g.toString()}}})),console.log(`Stored ${t} event for message ${n}`,s)}catch(e){console.error("Error processing record:",e),console.error("Record:",JSON.stringify(m,null,2))}return{statusCode:200,body:JSON.stringify({message:"SMS events processed successfully"})}}export{v as handler};
@@ -0,0 +1,145 @@
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 SMS events from SQS (via SNS)
8
+ * Events come from AWS End User Messaging via SNS → SQS (raw message delivery)
9
+ *
10
+ * Stores all SMS events in DynamoDB:
11
+ * - TEXT_QUEUED: Message queued for delivery
12
+ * - TEXT_SENT: Message sent to carrier
13
+ * - TEXT_DELIVERED: Message delivered to device
14
+ * - TEXT_PENDING: Delivery pending
15
+ * - TEXT_SUCCESSFUL: Delivery confirmed
16
+ * - TEXT_INVALID: Invalid destination number
17
+ * - TEXT_CARRIER_UNREACHABLE: Carrier unavailable
18
+ * - TEXT_TTL_EXPIRED: Message TTL expired
19
+ * - TEXT_BLOCKED: Blocked by carrier/opted-out
20
+ * - TEXT_UNKNOWN: Unknown status
21
+ */
22
+ export async function handler(event: SQSEvent) {
23
+ console.log("Processing SMS event from SQS:", JSON.stringify(event, null, 2));
24
+
25
+ const tableName = process.env.TABLE_NAME;
26
+ if (!tableName) {
27
+ throw new Error("TABLE_NAME environment variable not set");
28
+ }
29
+
30
+ // Get retention days from environment, default to 90
31
+ const retentionDays = Number.parseInt(process.env.RETENTION_DAYS || "90", 10);
32
+
33
+ for (const record of event.Records) {
34
+ try {
35
+ // Parse the SQS message body
36
+ // With raw message delivery from SNS, this is the actual SMS event
37
+ const detail = JSON.parse(record.body);
38
+
39
+ // Extract SMS event details
40
+ // AWS End User Messaging event structure
41
+ const eventType = detail.eventType || detail.messageStatus || "UNKNOWN";
42
+ const messageId = detail.messageId;
43
+ const destinationNumber = detail.destinationPhoneNumber;
44
+ const originationNumber = detail.originationPhoneNumber;
45
+ const messageBody = detail.messageBody || "";
46
+ const isoCountryCode = detail.isoCountryCode || "US";
47
+ const messageType = detail.messageType || "TRANSACTIONAL";
48
+
49
+ // Event timestamp
50
+ const eventTimestamp = detail.eventTimestamp
51
+ ? new Date(detail.eventTimestamp).getTime()
52
+ : Date.now();
53
+
54
+ console.log("Processing SMS event:", {
55
+ messageId,
56
+ eventType,
57
+ destinationNumber,
58
+ originationNumber,
59
+ });
60
+
61
+ // Extract additional data based on event type
62
+ let additionalData: Record<string, unknown> = {
63
+ isoCountryCode,
64
+ messageType,
65
+ };
66
+
67
+ if (eventType === "TEXT_DELIVERED" || eventType === "TEXT_SUCCESSFUL") {
68
+ additionalData = {
69
+ ...additionalData,
70
+ deliveryTimestamp: detail.deliveryTimestamp,
71
+ carrierName: detail.carrierName,
72
+ providerResponse: detail.providerResponse,
73
+ };
74
+ } else if (
75
+ eventType === "TEXT_FAILED" ||
76
+ eventType === "TEXT_INVALID" ||
77
+ eventType === "TEXT_CARRIER_UNREACHABLE" ||
78
+ eventType === "TEXT_BLOCKED"
79
+ ) {
80
+ additionalData = {
81
+ ...additionalData,
82
+ failureReason: detail.failureReason || detail.statusMessage,
83
+ failureCode: detail.failureCode || detail.statusCode,
84
+ providerResponse: detail.providerResponse,
85
+ };
86
+ } else if (eventType === "TEXT_QUEUED" || eventType === "TEXT_SENT") {
87
+ additionalData = {
88
+ ...additionalData,
89
+ queuedTimestamp: detail.queuedTimestamp,
90
+ sentTimestamp: detail.sentTimestamp,
91
+ };
92
+ } else if (eventType === "TEXT_TTL_EXPIRED") {
93
+ additionalData = {
94
+ ...additionalData,
95
+ expirationTimestamp: detail.expirationTimestamp,
96
+ ttlSeconds: detail.ttlSeconds,
97
+ };
98
+ }
99
+
100
+ // Calculate message segments (SMS is 160 chars for GSM, 70 for Unicode)
101
+ const segments = messageBody ? Math.ceil(messageBody.length / 160) : 1;
102
+
103
+ // Calculate TTL based on retention days (0 or negative means no TTL)
104
+ const expiresAt =
105
+ retentionDays > 0
106
+ ? Date.now() + retentionDays * 24 * 60 * 60 * 1000
107
+ : Date.now() + 365 * 24 * 60 * 60 * 1000; // Default 1 year if not specified
108
+
109
+ // Store event in DynamoDB
110
+ await dynamodb.send(
111
+ new PutItemCommand({
112
+ TableName: tableName,
113
+ Item: {
114
+ messageId: { S: messageId },
115
+ sentAt: { N: eventTimestamp.toString() },
116
+ accountId: { S: process.env.AWS_ACCOUNT_ID || "unknown" },
117
+ destinationNumber: { S: destinationNumber || "" },
118
+ originationNumber: { S: originationNumber || "" },
119
+ messageBody: { S: messageBody },
120
+ eventType: { S: eventType },
121
+ segments: { N: segments.toString() },
122
+ eventData: { S: JSON.stringify(detail) },
123
+ additionalData: { S: JSON.stringify(additionalData) },
124
+ createdAt: { N: Date.now().toString() },
125
+ expiresAt: { N: expiresAt.toString() },
126
+ },
127
+ })
128
+ );
129
+
130
+ console.log(
131
+ `Stored ${eventType} event for message ${messageId}`,
132
+ additionalData
133
+ );
134
+ } catch (error) {
135
+ console.error("Error processing record:", error);
136
+ console.error("Record:", JSON.stringify(record, null, 2));
137
+ // Don't throw - continue processing other records
138
+ }
139
+ }
140
+
141
+ return {
142
+ statusCode: 200,
143
+ body: JSON.stringify({ message: "SMS events processed successfully" }),
144
+ };
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wraps.dev/cli",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "description": "CLI for deploying Wraps email infrastructure to your AWS account",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",