@stacksjs/ts-cloud-core 0.1.8 → 0.1.9

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.
Files changed (225) hide show
  1. package/package.json +7 -6
  2. package/src/advanced-features.test.ts +465 -0
  3. package/src/aws/cloudformation.ts +421 -0
  4. package/src/aws/cloudfront.ts +158 -0
  5. package/src/aws/credentials.test.ts +132 -0
  6. package/src/aws/credentials.ts +545 -0
  7. package/src/aws/index.ts +87 -0
  8. package/src/aws/s3.test.ts +188 -0
  9. package/src/aws/s3.ts +1088 -0
  10. package/src/aws/signature.test.ts +670 -0
  11. package/src/aws/signature.ts +1155 -0
  12. package/src/backup/disaster-recovery.test.ts +726 -0
  13. package/src/backup/disaster-recovery.ts +500 -0
  14. package/src/backup/index.ts +34 -0
  15. package/src/backup/manager.test.ts +498 -0
  16. package/src/backup/manager.ts +432 -0
  17. package/src/cicd/circleci.ts +430 -0
  18. package/src/cicd/github-actions.ts +424 -0
  19. package/src/cicd/gitlab-ci.ts +255 -0
  20. package/src/cicd/index.ts +8 -0
  21. package/src/cli/history.ts +396 -0
  22. package/src/cli/index.ts +10 -0
  23. package/src/cli/progress.ts +458 -0
  24. package/src/cli/repl.ts +454 -0
  25. package/src/cli/suggestions.ts +327 -0
  26. package/src/cli/table.test.ts +319 -0
  27. package/src/cli/table.ts +332 -0
  28. package/src/cloudformation/builder.test.ts +327 -0
  29. package/src/cloudformation/builder.ts +378 -0
  30. package/src/cloudformation/builders/api-gateway.ts +449 -0
  31. package/src/cloudformation/builders/cache.ts +334 -0
  32. package/src/cloudformation/builders/cdn.ts +278 -0
  33. package/src/cloudformation/builders/compute.ts +485 -0
  34. package/src/cloudformation/builders/database.ts +392 -0
  35. package/src/cloudformation/builders/functions.ts +343 -0
  36. package/src/cloudformation/builders/messaging.ts +140 -0
  37. package/src/cloudformation/builders/monitoring.ts +300 -0
  38. package/src/cloudformation/builders/network.ts +264 -0
  39. package/src/cloudformation/builders/queue.ts +147 -0
  40. package/src/cloudformation/builders/security.ts +399 -0
  41. package/src/cloudformation/builders/storage.ts +285 -0
  42. package/src/cloudformation/index.ts +30 -0
  43. package/src/cloudformation/types.ts +173 -0
  44. package/src/compliance/aws-config.ts +543 -0
  45. package/src/compliance/cloudtrail.ts +376 -0
  46. package/src/compliance/compliance.test.ts +423 -0
  47. package/src/compliance/guardduty.ts +446 -0
  48. package/src/compliance/index.ts +66 -0
  49. package/src/compliance/security-hub.ts +456 -0
  50. package/src/containers/build-optimization.ts +416 -0
  51. package/src/containers/containers.test.ts +508 -0
  52. package/src/containers/image-scanning.ts +360 -0
  53. package/src/containers/index.ts +9 -0
  54. package/src/containers/registry.ts +293 -0
  55. package/src/containers/service-mesh.ts +520 -0
  56. package/src/database/database.test.ts +762 -0
  57. package/src/database/index.ts +9 -0
  58. package/src/database/migrations.ts +444 -0
  59. package/src/database/performance.ts +528 -0
  60. package/src/database/replicas.ts +534 -0
  61. package/src/database/users.ts +494 -0
  62. package/src/dependency-graph.ts +143 -0
  63. package/src/deployment/ab-testing.ts +582 -0
  64. package/src/deployment/blue-green.ts +452 -0
  65. package/src/deployment/canary.ts +500 -0
  66. package/src/deployment/deployment.test.ts +526 -0
  67. package/src/deployment/index.ts +61 -0
  68. package/src/deployment/progressive.ts +62 -0
  69. package/src/dns/dns.test.ts +641 -0
  70. package/src/dns/dnssec.ts +315 -0
  71. package/src/dns/index.ts +8 -0
  72. package/src/dns/resolver.ts +496 -0
  73. package/src/dns/routing.ts +593 -0
  74. package/src/email/advanced/analytics.ts +445 -0
  75. package/src/email/advanced/index.ts +11 -0
  76. package/src/email/advanced/rules.ts +465 -0
  77. package/src/email/advanced/scheduling.ts +352 -0
  78. package/src/email/advanced/search.ts +412 -0
  79. package/src/email/advanced/shared-mailboxes.ts +404 -0
  80. package/src/email/advanced/templates.ts +455 -0
  81. package/src/email/advanced/threading.ts +281 -0
  82. package/src/email/analytics.ts +467 -0
  83. package/src/email/bounce-handling.ts +425 -0
  84. package/src/email/email.test.ts +431 -0
  85. package/src/email/handlers/__tests__/inbound.test.ts +38 -0
  86. package/src/email/handlers/__tests__/outbound.test.ts +37 -0
  87. package/src/email/handlers/converter.ts +227 -0
  88. package/src/email/handlers/feedback.ts +228 -0
  89. package/src/email/handlers/inbound.ts +169 -0
  90. package/src/email/handlers/outbound.ts +178 -0
  91. package/src/email/index.ts +15 -0
  92. package/src/email/reputation.ts +303 -0
  93. package/src/email/templates.ts +352 -0
  94. package/src/errors/index.test.ts +434 -0
  95. package/src/errors/index.ts +416 -0
  96. package/src/health-checks/index.ts +40 -0
  97. package/src/index.ts +360 -0
  98. package/src/intrinsic-functions.ts +118 -0
  99. package/src/lambda/concurrency.ts +330 -0
  100. package/src/lambda/destinations.ts +345 -0
  101. package/src/lambda/dlq.ts +425 -0
  102. package/src/lambda/index.ts +11 -0
  103. package/src/lambda/lambda.test.ts +840 -0
  104. package/src/lambda/layers.ts +263 -0
  105. package/src/lambda/versions.ts +376 -0
  106. package/src/lambda/vpc.ts +399 -0
  107. package/src/local/config.ts +114 -0
  108. package/src/local/index.ts +6 -0
  109. package/src/local/mock-aws.ts +351 -0
  110. package/src/modules/ai.ts +340 -0
  111. package/src/modules/api.ts +478 -0
  112. package/src/modules/auth.ts +805 -0
  113. package/src/modules/cache.ts +417 -0
  114. package/src/modules/cdn.ts +1062 -0
  115. package/src/modules/communication.ts +1094 -0
  116. package/src/modules/compute.ts +3348 -0
  117. package/src/modules/database.ts +554 -0
  118. package/src/modules/deployment.ts +1079 -0
  119. package/src/modules/dns.ts +337 -0
  120. package/src/modules/email.ts +1538 -0
  121. package/src/modules/filesystem.ts +515 -0
  122. package/src/modules/index.ts +32 -0
  123. package/src/modules/messaging.ts +486 -0
  124. package/src/modules/monitoring.ts +2086 -0
  125. package/src/modules/network.ts +664 -0
  126. package/src/modules/parameter-store.ts +325 -0
  127. package/src/modules/permissions.ts +1081 -0
  128. package/src/modules/phone.ts +494 -0
  129. package/src/modules/queue.ts +1260 -0
  130. package/src/modules/redirects.ts +464 -0
  131. package/src/modules/registry.ts +699 -0
  132. package/src/modules/search.ts +401 -0
  133. package/src/modules/secrets.ts +416 -0
  134. package/src/modules/security.ts +731 -0
  135. package/src/modules/sms.ts +389 -0
  136. package/src/modules/storage.ts +1120 -0
  137. package/src/modules/workflow.ts +680 -0
  138. package/src/multi-account/config.ts +521 -0
  139. package/src/multi-account/index.ts +7 -0
  140. package/src/multi-account/manager.ts +427 -0
  141. package/src/multi-region/cross-region.ts +410 -0
  142. package/src/multi-region/index.ts +8 -0
  143. package/src/multi-region/manager.ts +483 -0
  144. package/src/multi-region/regions.ts +435 -0
  145. package/src/network-security/index.ts +48 -0
  146. package/src/observability/index.ts +9 -0
  147. package/src/observability/logs.ts +522 -0
  148. package/src/observability/metrics.ts +460 -0
  149. package/src/observability/observability.test.ts +782 -0
  150. package/src/observability/synthetics.ts +568 -0
  151. package/src/observability/xray.ts +358 -0
  152. package/src/phone/advanced/analytics.ts +349 -0
  153. package/src/phone/advanced/callbacks.ts +428 -0
  154. package/src/phone/advanced/index.ts +8 -0
  155. package/src/phone/advanced/ivr-builder.ts +504 -0
  156. package/src/phone/advanced/recording.ts +310 -0
  157. package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
  158. package/src/phone/handlers/incoming-call.ts +117 -0
  159. package/src/phone/handlers/missed-call.ts +116 -0
  160. package/src/phone/handlers/voicemail.ts +179 -0
  161. package/src/phone/index.ts +9 -0
  162. package/src/presets/api-backend.ts +134 -0
  163. package/src/presets/data-pipeline.ts +204 -0
  164. package/src/presets/extend.test.ts +295 -0
  165. package/src/presets/extend.ts +297 -0
  166. package/src/presets/fullstack-app.ts +144 -0
  167. package/src/presets/index.ts +27 -0
  168. package/src/presets/jamstack.ts +135 -0
  169. package/src/presets/microservices.ts +167 -0
  170. package/src/presets/ml-api.ts +208 -0
  171. package/src/presets/nodejs-server.ts +104 -0
  172. package/src/presets/nodejs-serverless.ts +114 -0
  173. package/src/presets/realtime-app.ts +184 -0
  174. package/src/presets/static-site.ts +64 -0
  175. package/src/presets/traditional-web-app.ts +339 -0
  176. package/src/presets/wordpress.ts +138 -0
  177. package/src/preview/github.test.ts +249 -0
  178. package/src/preview/github.ts +297 -0
  179. package/src/preview/index.ts +37 -0
  180. package/src/preview/manager.test.ts +440 -0
  181. package/src/preview/manager.ts +326 -0
  182. package/src/preview/notifications.test.ts +582 -0
  183. package/src/preview/notifications.ts +341 -0
  184. package/src/queue/batch-processing.ts +402 -0
  185. package/src/queue/dlq-monitoring.ts +402 -0
  186. package/src/queue/fifo.ts +342 -0
  187. package/src/queue/index.ts +9 -0
  188. package/src/queue/management.ts +428 -0
  189. package/src/queue/queue.test.ts +429 -0
  190. package/src/resource-mgmt/index.ts +39 -0
  191. package/src/resource-naming.ts +62 -0
  192. package/src/s3/index.ts +523 -0
  193. package/src/schema/cloud-config.schema.json +554 -0
  194. package/src/schema/index.ts +68 -0
  195. package/src/security/certificate-manager.ts +492 -0
  196. package/src/security/index.ts +9 -0
  197. package/src/security/scanning.ts +545 -0
  198. package/src/security/secrets-manager.ts +476 -0
  199. package/src/security/secrets-rotation.ts +456 -0
  200. package/src/security/security.test.ts +738 -0
  201. package/src/sms/advanced/ab-testing.ts +389 -0
  202. package/src/sms/advanced/analytics.ts +336 -0
  203. package/src/sms/advanced/campaigns.ts +523 -0
  204. package/src/sms/advanced/chatbot.ts +224 -0
  205. package/src/sms/advanced/index.ts +10 -0
  206. package/src/sms/advanced/link-tracking.ts +248 -0
  207. package/src/sms/advanced/mms.ts +308 -0
  208. package/src/sms/handlers/__tests__/send.test.ts +40 -0
  209. package/src/sms/handlers/delivery-status.ts +133 -0
  210. package/src/sms/handlers/receive.ts +162 -0
  211. package/src/sms/handlers/send.ts +174 -0
  212. package/src/sms/index.ts +9 -0
  213. package/src/stack-diff.ts +389 -0
  214. package/src/static-site/index.ts +85 -0
  215. package/src/template-builder.ts +110 -0
  216. package/src/template-validator.ts +574 -0
  217. package/src/utils/cache.ts +291 -0
  218. package/src/utils/diff.ts +269 -0
  219. package/src/utils/hash.ts +227 -0
  220. package/src/utils/index.ts +8 -0
  221. package/src/utils/parallel.ts +294 -0
  222. package/src/validators/credentials.test.ts +274 -0
  223. package/src/validators/credentials.ts +233 -0
  224. package/src/validators/quotas.test.ts +434 -0
  225. package/src/validators/quotas.ts +217 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * MMS Support (Images, Media)
3
+ *
4
+ * Provides multimedia messaging capabilities
5
+ */
6
+
7
+ export interface MmsMessage {
8
+ to: string
9
+ body?: string
10
+ mediaUrls: string[]
11
+ mediaType?: 'image' | 'video' | 'audio' | 'document'
12
+ fallbackSms?: string
13
+ }
14
+
15
+ export interface MmsMedia {
16
+ url: string
17
+ contentType: string
18
+ size: number
19
+ filename?: string
20
+ }
21
+
22
+ /**
23
+ * MMS Module
24
+ */
25
+ export class MmsSupport {
26
+ /**
27
+ * Lambda code for MMS sending
28
+ */
29
+ static MmsSenderCode = `
30
+ const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
31
+ const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
32
+ const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
33
+
34
+ const s3 = new S3Client({});
35
+ const sns = new SNSClient({});
36
+ const dynamodb = new DynamoDBClient({});
37
+
38
+ const MEDIA_BUCKET = process.env.MEDIA_BUCKET;
39
+ const MESSAGE_LOG_TABLE = process.env.MESSAGE_LOG_TABLE;
40
+ const ORIGINATION_NUMBER = process.env.ORIGINATION_NUMBER;
41
+
42
+ // Supported media types
43
+ const SUPPORTED_TYPES = {
44
+ 'image/jpeg': { maxSize: 1024 * 1024, extension: 'jpg' },
45
+ 'image/png': { maxSize: 1024 * 1024, extension: 'png' },
46
+ 'image/gif': { maxSize: 1024 * 1024, extension: 'gif' },
47
+ 'video/mp4': { maxSize: 5 * 1024 * 1024, extension: 'mp4' },
48
+ 'video/3gpp': { maxSize: 5 * 1024 * 1024, extension: '3gp' },
49
+ 'audio/mpeg': { maxSize: 1024 * 1024, extension: 'mp3' },
50
+ 'audio/wav': { maxSize: 1024 * 1024, extension: 'wav' },
51
+ };
52
+
53
+ exports.handler = async (event) => {
54
+ console.log('MMS sender event:', JSON.stringify(event, null, 2));
55
+
56
+ try {
57
+ const body = JSON.parse(event.body || '{}');
58
+ const { to, text, mediaUrls, fallbackSms } = body;
59
+
60
+ if (!to) {
61
+ return {
62
+ statusCode: 400,
63
+ body: JSON.stringify({ error: 'Missing recipient phone number' }),
64
+ };
65
+ }
66
+
67
+ if (!mediaUrls || mediaUrls.length === 0) {
68
+ return {
69
+ statusCode: 400,
70
+ body: JSON.stringify({ error: 'At least one media URL is required' }),
71
+ };
72
+ }
73
+
74
+ const messageId = \`mms-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`;
75
+ const now = new Date().toISOString();
76
+
77
+ // Validate and process media
78
+ const processedMedia = [];
79
+ for (const url of mediaUrls) {
80
+ try {
81
+ const media = await processMedia(url, messageId);
82
+ processedMedia.push(media);
83
+ } catch (error) {
84
+ console.error(\`Failed to process media \${url}:\`, error);
85
+ }
86
+ }
87
+
88
+ if (processedMedia.length === 0) {
89
+ // Fall back to SMS if no media could be processed
90
+ if (fallbackSms) {
91
+ const smsResult = await sendFallbackSms(to, fallbackSms);
92
+ return {
93
+ statusCode: 200,
94
+ body: JSON.stringify({
95
+ messageId,
96
+ type: 'sms_fallback',
97
+ ...smsResult,
98
+ }),
99
+ };
100
+ }
101
+ return {
102
+ statusCode: 400,
103
+ body: JSON.stringify({ error: 'No valid media could be processed' }),
104
+ };
105
+ }
106
+
107
+ // Send MMS via SNS (carrier-dependent)
108
+ // Note: True MMS requires carrier integration or third-party service
109
+ // This implementation uses SNS with media URLs as a simplified approach
110
+ const message = {
111
+ to,
112
+ text: text || '',
113
+ mediaUrls: processedMedia.map(m => m.publicUrl),
114
+ messageId,
115
+ timestamp: now,
116
+ };
117
+
118
+ const snsResult = await sns.send(new PublishCommand({
119
+ PhoneNumber: to,
120
+ Message: text || 'You have received a multimedia message. View it here: ' + processedMedia[0].publicUrl,
121
+ MessageAttributes: {
122
+ 'AWS.SNS.SMS.SMSType': {
123
+ DataType: 'String',
124
+ StringValue: 'Transactional',
125
+ },
126
+ },
127
+ }));
128
+
129
+ // Log message
130
+ await dynamodb.send(new PutItemCommand({
131
+ TableName: MESSAGE_LOG_TABLE,
132
+ Item: {
133
+ messageId: { S: messageId },
134
+ type: { S: 'mms' },
135
+ to: { S: to },
136
+ text: { S: text || '' },
137
+ mediaUrls: { SS: processedMedia.map(m => m.publicUrl) },
138
+ snsMessageId: { S: snsResult.MessageId || '' },
139
+ sentAt: { S: now },
140
+ ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) },
141
+ },
142
+ }));
143
+
144
+ return {
145
+ statusCode: 200,
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({
148
+ messageId,
149
+ type: 'mms',
150
+ mediaCount: processedMedia.length,
151
+ snsMessageId: snsResult.MessageId,
152
+ }),
153
+ };
154
+ } catch (error) {
155
+ console.error('Error sending MMS:', error);
156
+ return {
157
+ statusCode: 500,
158
+ body: JSON.stringify({ error: error.message }),
159
+ };
160
+ }
161
+ };
162
+
163
+ async function processMedia(url, messageId) {
164
+ // Fetch media
165
+ const response = await fetch(url);
166
+ if (!response.ok) {
167
+ throw new Error(\`Failed to fetch media: \${response.status}\`);
168
+ }
169
+
170
+ const contentType = response.headers.get('content-type') || 'application/octet-stream';
171
+ const typeConfig = SUPPORTED_TYPES[contentType];
172
+
173
+ if (!typeConfig) {
174
+ throw new Error(\`Unsupported media type: \${contentType}\`);
175
+ }
176
+
177
+ const buffer = await response.arrayBuffer();
178
+ if (buffer.byteLength > typeConfig.maxSize) {
179
+ throw new Error(\`Media too large: \${buffer.byteLength} bytes (max: \${typeConfig.maxSize})\`);
180
+ }
181
+
182
+ // Upload to S3
183
+ const key = \`mms/\${messageId}/media.\${typeConfig.extension}\`;
184
+ await s3.send(new PutObjectCommand({
185
+ Bucket: MEDIA_BUCKET,
186
+ Key: key,
187
+ Body: Buffer.from(buffer),
188
+ ContentType: contentType,
189
+ }));
190
+
191
+ // Generate public URL (requires bucket to be configured for public access or use presigned URL)
192
+ const publicUrl = \`https://\${MEDIA_BUCKET}.s3.amazonaws.com/\${key}\`;
193
+
194
+ return {
195
+ originalUrl: url,
196
+ publicUrl,
197
+ contentType,
198
+ size: buffer.byteLength,
199
+ };
200
+ }
201
+
202
+ async function sendFallbackSms(to, message) {
203
+ const result = await sns.send(new PublishCommand({
204
+ PhoneNumber: to,
205
+ Message: message,
206
+ MessageAttributes: {
207
+ 'AWS.SNS.SMS.SMSType': {
208
+ DataType: 'String',
209
+ StringValue: 'Transactional',
210
+ },
211
+ },
212
+ }));
213
+
214
+ return {
215
+ snsMessageId: result.MessageId,
216
+ fallback: true,
217
+ };
218
+ }
219
+ `
220
+
221
+ /**
222
+ * Create media storage bucket
223
+ */
224
+ static createMediaBucket(config: { slug: string }): Record<string, any> {
225
+ return {
226
+ [`${config.slug}MmsMediaBucket`]: {
227
+ Type: 'AWS::S3::Bucket',
228
+ Properties: {
229
+ BucketName: `${config.slug}-mms-media`,
230
+ LifecycleConfiguration: {
231
+ Rules: [
232
+ {
233
+ Id: 'DeleteOldMedia',
234
+ Status: 'Enabled',
235
+ ExpirationInDays: 30,
236
+ },
237
+ ],
238
+ },
239
+ CorsConfiguration: {
240
+ CorsRules: [
241
+ {
242
+ AllowedOrigins: ['*'],
243
+ AllowedMethods: ['GET'],
244
+ AllowedHeaders: ['*'],
245
+ MaxAge: 3600,
246
+ },
247
+ ],
248
+ },
249
+ },
250
+ },
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Create MMS sender Lambda
256
+ */
257
+ static createMmsSenderLambda(config: {
258
+ slug: string
259
+ roleArn: string
260
+ mediaBucket: string
261
+ messageLogTable: string
262
+ originationNumber?: string
263
+ }): Record<string, any> {
264
+ return {
265
+ [`${config.slug}MmsSenderLambda`]: {
266
+ Type: 'AWS::Lambda::Function',
267
+ Properties: {
268
+ FunctionName: `${config.slug}-mms-sender`,
269
+ Runtime: 'nodejs20.x',
270
+ Handler: 'index.handler',
271
+ Role: config.roleArn,
272
+ Timeout: 60,
273
+ MemorySize: 512,
274
+ Code: {
275
+ ZipFile: MmsSupport.MmsSenderCode,
276
+ },
277
+ Environment: {
278
+ Variables: {
279
+ MEDIA_BUCKET: config.mediaBucket,
280
+ MESSAGE_LOG_TABLE: config.messageLogTable,
281
+ ORIGINATION_NUMBER: config.originationNumber || '',
282
+ },
283
+ },
284
+ },
285
+ },
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Supported media types
291
+ */
292
+ static readonly SupportedMediaTypes = {
293
+ image: ['image/jpeg', 'image/png', 'image/gif'],
294
+ video: ['video/mp4', 'video/3gpp'],
295
+ audio: ['audio/mpeg', 'audio/wav'],
296
+ } as const
297
+
298
+ /**
299
+ * Media size limits (in bytes)
300
+ */
301
+ static readonly MediaSizeLimits: { image: number, video: number, audio: number } = {
302
+ image: 1024 * 1024, // 1MB
303
+ video: 5 * 1024 * 1024, // 5MB
304
+ audio: 1024 * 1024, // 1MB
305
+ }
306
+ }
307
+
308
+ export default MmsSupport
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { handler } from '../send'
3
+
4
+ describe('SMS Send Handler', () => {
5
+ it('should export handler code as string', () => {
6
+ expect(typeof handler).toBe('string')
7
+ expect(handler).toContain('exports.handler')
8
+ })
9
+
10
+ it('should contain Pinpoint client import', () => {
11
+ expect(handler).toContain('PinpointClient')
12
+ expect(handler).toContain('SendMessagesCommand')
13
+ })
14
+
15
+ it('should contain SNS client as fallback', () => {
16
+ expect(handler).toContain('SNSClient')
17
+ expect(handler).toContain('PublishCommand')
18
+ })
19
+
20
+ it('should support templated messages', () => {
21
+ expect(handler).toContain('template')
22
+ expect(handler).toContain('templateData')
23
+ expect(handler).toContain('resolveTemplate')
24
+ })
25
+
26
+ it('should log messages to DynamoDB', () => {
27
+ expect(handler).toContain('MESSAGE_LOG_TABLE')
28
+ expect(handler).toContain('messageId')
29
+ })
30
+
31
+ it('should support message types', () => {
32
+ expect(handler).toContain('TRANSACTIONAL')
33
+ expect(handler).toContain('PROMOTIONAL')
34
+ })
35
+
36
+ it('should handle delivery status', () => {
37
+ expect(handler).toContain('DeliveryStatus')
38
+ expect(handler).toContain('SENT')
39
+ })
40
+ })
@@ -0,0 +1,133 @@
1
+ /**
2
+ * SMS Delivery Status Lambda Handler
3
+ *
4
+ * Processes delivery status updates:
5
+ * - Process delivery receipts
6
+ * - Update message status
7
+ * - Handle failures
8
+ */
9
+
10
+ export const handler = `
11
+ const { DynamoDBClient, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
12
+ const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
13
+
14
+ const dynamodb = new DynamoDBClient({});
15
+ const sns = new SNSClient({});
16
+
17
+ exports.handler = async (event) => {
18
+ console.log('SMS delivery status event:', JSON.stringify(event, null, 2));
19
+
20
+ const messageLogTable = process.env.MESSAGE_LOG_TABLE;
21
+ const notificationTopicArn = process.env.NOTIFICATION_TOPIC_ARN;
22
+ const webhookUrl = process.env.WEBHOOK_URL;
23
+
24
+ for (const record of event.Records) {
25
+ try {
26
+ // Parse delivery status from SNS/Pinpoint
27
+ const message = JSON.parse(record.Sns?.Message || record.body || '{}');
28
+
29
+ const {
30
+ eventType,
31
+ messageId,
32
+ destinationPhoneNumber,
33
+ messageStatus,
34
+ messageStatusDescription,
35
+ isoCountryCode,
36
+ mcc,
37
+ mnc,
38
+ priceInMillicentsUSD,
39
+ } = message;
40
+
41
+ if (!messageId) {
42
+ console.log('No messageId in delivery status');
43
+ continue;
44
+ }
45
+
46
+ const timestamp = new Date().toISOString();
47
+ const status = messageStatus || eventType || 'UNKNOWN';
48
+
49
+ console.log(\`Delivery status for \${messageId}: \${status}\`);
50
+
51
+ // Update message log
52
+ if (messageLogTable) {
53
+ await dynamodb.send(new UpdateItemCommand({
54
+ TableName: messageLogTable,
55
+ Key: {
56
+ messageId: { S: messageId },
57
+ },
58
+ UpdateExpression: 'SET deliveryStatus = :status, statusDescription = :desc, deliveredAt = :at, priceMillicents = :price, countryCode = :country',
59
+ ExpressionAttributeValues: {
60
+ ':status': { S: status },
61
+ ':desc': { S: messageStatusDescription || '' },
62
+ ':at': { S: timestamp },
63
+ ':price': { N: String(priceInMillicentsUSD || 0) },
64
+ ':country': { S: isoCountryCode || '' },
65
+ },
66
+ }));
67
+ }
68
+
69
+ // Handle failures - notify admin
70
+ const isFailure = ['FAILED', 'UNREACHABLE', 'UNKNOWN', 'CARRIER_UNREACHABLE', 'BLOCKED', 'CARRIER_BLOCKED', 'INVALID', 'INVALID_MESSAGE', 'OPTED_OUT'].includes(status);
71
+
72
+ if (isFailure) {
73
+ console.log(\`SMS delivery failed: \${status} - \${messageStatusDescription}\`);
74
+
75
+ // Send failure notification
76
+ if (notificationTopicArn) {
77
+ await sns.send(new PublishCommand({
78
+ TopicArn: notificationTopicArn,
79
+ Subject: \`SMS Delivery Failed: \${status}\`,
80
+ Message: JSON.stringify({
81
+ type: 'sms_delivery_failed',
82
+ messageId,
83
+ to: destinationPhoneNumber,
84
+ status,
85
+ description: messageStatusDescription,
86
+ countryCode: isoCountryCode,
87
+ timestamp,
88
+ }, null, 2),
89
+ MessageAttributes: {
90
+ eventType: {
91
+ DataType: 'String',
92
+ StringValue: 'sms_delivery_failed',
93
+ },
94
+ },
95
+ }));
96
+ }
97
+ }
98
+
99
+ // Forward to webhook for all status updates
100
+ if (webhookUrl) {
101
+ try {
102
+ await fetch(webhookUrl, {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify({
106
+ event: 'sms_delivery_status',
107
+ data: {
108
+ messageId,
109
+ to: destinationPhoneNumber,
110
+ status,
111
+ description: messageStatusDescription,
112
+ countryCode: isoCountryCode,
113
+ carrier: { mcc, mnc },
114
+ priceMillicents: priceInMillicentsUSD,
115
+ timestamp,
116
+ },
117
+ }),
118
+ });
119
+ } catch (err) {
120
+ console.error('Webhook failed:', err.message);
121
+ }
122
+ }
123
+
124
+ } catch (error) {
125
+ console.error('Error processing delivery status:', error);
126
+ }
127
+ }
128
+
129
+ return { statusCode: 200, body: 'OK' };
130
+ };
131
+ `
132
+
133
+ export default handler
@@ -0,0 +1,162 @@
1
+ /**
2
+ * SMS Receive Lambda Handler
3
+ *
4
+ * Processes inbound SMS messages:
5
+ * - Process inbound SMS (two-way)
6
+ * - Handle opt-out keywords
7
+ * - Forward to webhook
8
+ */
9
+
10
+ export const handler = `
11
+ const { DynamoDBClient, PutItemCommand, GetItemCommand, DeleteItemCommand } = require('@aws-sdk/client-dynamodb');
12
+ const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
13
+
14
+ const dynamodb = new DynamoDBClient({});
15
+ const sns = new SNSClient({});
16
+
17
+ const OPT_OUT_KEYWORDS = ['STOP', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT', 'OPTOUT', 'OPT OUT'];
18
+ const OPT_IN_KEYWORDS = ['START', 'SUBSCRIBE', 'OPTIN', 'OPT IN', 'YES'];
19
+
20
+ exports.handler = async (event) => {
21
+ console.log('SMS receive event:', JSON.stringify(event, null, 2));
22
+
23
+ const optOutTable = process.env.OPT_OUT_TABLE;
24
+ const messageLogTable = process.env.MESSAGE_LOG_TABLE;
25
+ const notificationTopicArn = process.env.NOTIFICATION_TOPIC_ARN;
26
+ const webhookUrl = process.env.WEBHOOK_URL;
27
+
28
+ for (const record of event.Records) {
29
+ try {
30
+ // Parse SNS message from Pinpoint
31
+ const message = JSON.parse(record.Sns?.Message || record.body || '{}');
32
+
33
+ const {
34
+ originationNumber,
35
+ destinationNumber,
36
+ messageBody,
37
+ messageKeyword,
38
+ inboundMessageId,
39
+ } = message;
40
+
41
+ if (!originationNumber || !messageBody) {
42
+ console.log('Missing required fields');
43
+ continue;
44
+ }
45
+
46
+ const timestamp = new Date().toISOString();
47
+ const normalizedBody = messageBody.trim().toUpperCase();
48
+
49
+ // Check for opt-out keywords
50
+ if (OPT_OUT_KEYWORDS.some(kw => normalizedBody === kw || normalizedBody.startsWith(kw + ' '))) {
51
+ console.log(\`Opt-out request from \${originationNumber}\`);
52
+
53
+ if (optOutTable) {
54
+ await dynamodb.send(new PutItemCommand({
55
+ TableName: optOutTable,
56
+ Item: {
57
+ phoneNumber: { S: originationNumber },
58
+ optedOutAt: { S: timestamp },
59
+ keyword: { S: normalizedBody.split(' ')[0] },
60
+ originalMessage: { S: messageBody },
61
+ },
62
+ }));
63
+ }
64
+
65
+ // Send confirmation (optional - check carrier requirements)
66
+ // await sendOptOutConfirmation(originationNumber, destinationNumber);
67
+
68
+ continue;
69
+ }
70
+
71
+ // Check for opt-in keywords
72
+ if (OPT_IN_KEYWORDS.some(kw => normalizedBody === kw || normalizedBody.startsWith(kw + ' '))) {
73
+ console.log(\`Opt-in request from \${originationNumber}\`);
74
+
75
+ if (optOutTable) {
76
+ await dynamodb.send(new DeleteItemCommand({
77
+ TableName: optOutTable,
78
+ Key: {
79
+ phoneNumber: { S: originationNumber },
80
+ },
81
+ }));
82
+ }
83
+
84
+ continue;
85
+ }
86
+
87
+ // Log inbound message
88
+ if (messageLogTable) {
89
+ await dynamodb.send(new PutItemCommand({
90
+ TableName: messageLogTable,
91
+ Item: {
92
+ messageId: { S: inboundMessageId || \`inbound-\${Date.now()}\` },
93
+ direction: { S: 'inbound' },
94
+ from: { S: originationNumber },
95
+ to: { S: destinationNumber },
96
+ body: { S: messageBody },
97
+ keyword: { S: messageKeyword || '' },
98
+ receivedAt: { S: timestamp },
99
+ ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) },
100
+ },
101
+ }));
102
+ }
103
+
104
+ // Send SNS notification
105
+ if (notificationTopicArn) {
106
+ await sns.send(new PublishCommand({
107
+ TopicArn: notificationTopicArn,
108
+ Subject: 'Inbound SMS',
109
+ Message: JSON.stringify({
110
+ type: 'inbound_sms',
111
+ from: originationNumber,
112
+ to: destinationNumber,
113
+ body: messageBody,
114
+ keyword: messageKeyword,
115
+ timestamp,
116
+ }, null, 2),
117
+ MessageAttributes: {
118
+ eventType: {
119
+ DataType: 'String',
120
+ StringValue: 'inbound_sms',
121
+ },
122
+ },
123
+ }));
124
+ }
125
+
126
+ // Forward to webhook
127
+ if (webhookUrl) {
128
+ try {
129
+ const response = await fetch(webhookUrl, {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify({
133
+ event: 'inbound_sms',
134
+ data: {
135
+ from: originationNumber,
136
+ to: destinationNumber,
137
+ body: messageBody,
138
+ keyword: messageKeyword,
139
+ messageId: inboundMessageId,
140
+ timestamp,
141
+ },
142
+ }),
143
+ });
144
+
145
+ console.log(\`Webhook response: \${response.status}\`);
146
+ } catch (err) {
147
+ console.error('Webhook failed:', err.message);
148
+ }
149
+ }
150
+
151
+ console.log(\`Processed inbound SMS from \${originationNumber}\`);
152
+
153
+ } catch (error) {
154
+ console.error('Error processing inbound SMS:', error);
155
+ }
156
+ }
157
+
158
+ return { statusCode: 200, body: 'OK' };
159
+ };
160
+ `
161
+
162
+ export default handler