@wraps.dev/cli 2.2.6 → 2.2.7
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/dist/cli.js +1 -14
- package/dist/cli.js.map +1 -1
- package/dist/lambda/event-processor/.bundled +1 -0
- package/dist/lambda/event-processor/index.mjs +1 -0
- package/dist/lambda/event-processor/index.ts +214 -0
- package/dist/lambda/sms-event-processor/.bundled +1 -0
- package/dist/lambda/sms-event-processor/index.mjs +1 -0
- package/dist/lambda/sms-event-processor/index.ts +145 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Built at: 2026-01-16T17:58:12.170Z
|
|
@@ -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-16T17:58:12.173Z
|
|
@@ -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
|
+
}
|