@stacksjs/ts-cloud-core 0.1.1

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 (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +321 -0
  3. package/package.json +31 -0
  4. package/src/advanced-features.test.ts +465 -0
  5. package/src/aws/cloudformation.ts +421 -0
  6. package/src/aws/cloudfront.ts +158 -0
  7. package/src/aws/credentials.test.ts +132 -0
  8. package/src/aws/credentials.ts +545 -0
  9. package/src/aws/index.ts +87 -0
  10. package/src/aws/s3.test.ts +188 -0
  11. package/src/aws/s3.ts +1088 -0
  12. package/src/aws/signature.test.ts +670 -0
  13. package/src/aws/signature.ts +1155 -0
  14. package/src/backup/disaster-recovery.test.ts +726 -0
  15. package/src/backup/disaster-recovery.ts +500 -0
  16. package/src/backup/index.ts +34 -0
  17. package/src/backup/manager.test.ts +498 -0
  18. package/src/backup/manager.ts +432 -0
  19. package/src/cicd/circleci.ts +430 -0
  20. package/src/cicd/github-actions.ts +424 -0
  21. package/src/cicd/gitlab-ci.ts +255 -0
  22. package/src/cicd/index.ts +8 -0
  23. package/src/cli/history.ts +396 -0
  24. package/src/cli/index.ts +10 -0
  25. package/src/cli/progress.ts +458 -0
  26. package/src/cli/repl.ts +454 -0
  27. package/src/cli/suggestions.ts +327 -0
  28. package/src/cli/table.test.ts +319 -0
  29. package/src/cli/table.ts +332 -0
  30. package/src/cloudformation/builder.test.ts +327 -0
  31. package/src/cloudformation/builder.ts +378 -0
  32. package/src/cloudformation/builders/api-gateway.ts +449 -0
  33. package/src/cloudformation/builders/cache.ts +334 -0
  34. package/src/cloudformation/builders/cdn.ts +278 -0
  35. package/src/cloudformation/builders/compute.ts +485 -0
  36. package/src/cloudformation/builders/database.ts +392 -0
  37. package/src/cloudformation/builders/functions.ts +343 -0
  38. package/src/cloudformation/builders/messaging.ts +140 -0
  39. package/src/cloudformation/builders/monitoring.ts +300 -0
  40. package/src/cloudformation/builders/network.ts +264 -0
  41. package/src/cloudformation/builders/queue.ts +147 -0
  42. package/src/cloudformation/builders/security.ts +399 -0
  43. package/src/cloudformation/builders/storage.ts +285 -0
  44. package/src/cloudformation/index.ts +30 -0
  45. package/src/cloudformation/types.ts +173 -0
  46. package/src/compliance/aws-config.ts +543 -0
  47. package/src/compliance/cloudtrail.ts +376 -0
  48. package/src/compliance/compliance.test.ts +423 -0
  49. package/src/compliance/guardduty.ts +446 -0
  50. package/src/compliance/index.ts +66 -0
  51. package/src/compliance/security-hub.ts +456 -0
  52. package/src/containers/build-optimization.ts +416 -0
  53. package/src/containers/containers.test.ts +508 -0
  54. package/src/containers/image-scanning.ts +360 -0
  55. package/src/containers/index.ts +9 -0
  56. package/src/containers/registry.ts +293 -0
  57. package/src/containers/service-mesh.ts +520 -0
  58. package/src/database/database.test.ts +762 -0
  59. package/src/database/index.ts +9 -0
  60. package/src/database/migrations.ts +444 -0
  61. package/src/database/performance.ts +528 -0
  62. package/src/database/replicas.ts +534 -0
  63. package/src/database/users.ts +494 -0
  64. package/src/dependency-graph.ts +143 -0
  65. package/src/deployment/ab-testing.ts +582 -0
  66. package/src/deployment/blue-green.ts +452 -0
  67. package/src/deployment/canary.ts +500 -0
  68. package/src/deployment/deployment.test.ts +526 -0
  69. package/src/deployment/index.ts +61 -0
  70. package/src/deployment/progressive.ts +62 -0
  71. package/src/dns/dns.test.ts +641 -0
  72. package/src/dns/dnssec.ts +315 -0
  73. package/src/dns/index.ts +8 -0
  74. package/src/dns/resolver.ts +496 -0
  75. package/src/dns/routing.ts +593 -0
  76. package/src/email/advanced/analytics.ts +445 -0
  77. package/src/email/advanced/index.ts +11 -0
  78. package/src/email/advanced/rules.ts +465 -0
  79. package/src/email/advanced/scheduling.ts +352 -0
  80. package/src/email/advanced/search.ts +412 -0
  81. package/src/email/advanced/shared-mailboxes.ts +404 -0
  82. package/src/email/advanced/templates.ts +455 -0
  83. package/src/email/advanced/threading.ts +281 -0
  84. package/src/email/analytics.ts +467 -0
  85. package/src/email/bounce-handling.ts +425 -0
  86. package/src/email/email.test.ts +431 -0
  87. package/src/email/handlers/__tests__/inbound.test.ts +38 -0
  88. package/src/email/handlers/__tests__/outbound.test.ts +37 -0
  89. package/src/email/handlers/converter.ts +227 -0
  90. package/src/email/handlers/feedback.ts +228 -0
  91. package/src/email/handlers/inbound.ts +169 -0
  92. package/src/email/handlers/outbound.ts +178 -0
  93. package/src/email/index.ts +15 -0
  94. package/src/email/reputation.ts +303 -0
  95. package/src/email/templates.ts +352 -0
  96. package/src/errors/index.test.ts +434 -0
  97. package/src/errors/index.ts +416 -0
  98. package/src/health-checks/index.ts +40 -0
  99. package/src/index.ts +360 -0
  100. package/src/intrinsic-functions.ts +118 -0
  101. package/src/lambda/concurrency.ts +330 -0
  102. package/src/lambda/destinations.ts +345 -0
  103. package/src/lambda/dlq.ts +425 -0
  104. package/src/lambda/index.ts +11 -0
  105. package/src/lambda/lambda.test.ts +840 -0
  106. package/src/lambda/layers.ts +263 -0
  107. package/src/lambda/versions.ts +376 -0
  108. package/src/lambda/vpc.ts +399 -0
  109. package/src/local/config.ts +114 -0
  110. package/src/local/index.ts +6 -0
  111. package/src/local/mock-aws.ts +351 -0
  112. package/src/modules/ai.ts +340 -0
  113. package/src/modules/api.ts +478 -0
  114. package/src/modules/auth.ts +805 -0
  115. package/src/modules/cache.ts +417 -0
  116. package/src/modules/cdn.ts +1062 -0
  117. package/src/modules/communication.ts +1094 -0
  118. package/src/modules/compute.ts +3348 -0
  119. package/src/modules/database.ts +554 -0
  120. package/src/modules/deployment.ts +1079 -0
  121. package/src/modules/dns.ts +337 -0
  122. package/src/modules/email.ts +1538 -0
  123. package/src/modules/filesystem.ts +515 -0
  124. package/src/modules/index.ts +32 -0
  125. package/src/modules/messaging.ts +486 -0
  126. package/src/modules/monitoring.ts +2086 -0
  127. package/src/modules/network.ts +664 -0
  128. package/src/modules/parameter-store.ts +325 -0
  129. package/src/modules/permissions.ts +1081 -0
  130. package/src/modules/phone.ts +494 -0
  131. package/src/modules/queue.ts +1260 -0
  132. package/src/modules/redirects.ts +464 -0
  133. package/src/modules/registry.ts +699 -0
  134. package/src/modules/search.ts +401 -0
  135. package/src/modules/secrets.ts +416 -0
  136. package/src/modules/security.ts +731 -0
  137. package/src/modules/sms.ts +389 -0
  138. package/src/modules/storage.ts +1120 -0
  139. package/src/modules/workflow.ts +680 -0
  140. package/src/multi-account/config.ts +521 -0
  141. package/src/multi-account/index.ts +7 -0
  142. package/src/multi-account/manager.ts +427 -0
  143. package/src/multi-region/cross-region.ts +410 -0
  144. package/src/multi-region/index.ts +8 -0
  145. package/src/multi-region/manager.ts +483 -0
  146. package/src/multi-region/regions.ts +435 -0
  147. package/src/network-security/index.ts +48 -0
  148. package/src/observability/index.ts +9 -0
  149. package/src/observability/logs.ts +522 -0
  150. package/src/observability/metrics.ts +460 -0
  151. package/src/observability/observability.test.ts +782 -0
  152. package/src/observability/synthetics.ts +568 -0
  153. package/src/observability/xray.ts +358 -0
  154. package/src/phone/advanced/analytics.ts +349 -0
  155. package/src/phone/advanced/callbacks.ts +428 -0
  156. package/src/phone/advanced/index.ts +8 -0
  157. package/src/phone/advanced/ivr-builder.ts +504 -0
  158. package/src/phone/advanced/recording.ts +310 -0
  159. package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
  160. package/src/phone/handlers/incoming-call.ts +117 -0
  161. package/src/phone/handlers/missed-call.ts +116 -0
  162. package/src/phone/handlers/voicemail.ts +179 -0
  163. package/src/phone/index.ts +9 -0
  164. package/src/presets/api-backend.ts +134 -0
  165. package/src/presets/data-pipeline.ts +204 -0
  166. package/src/presets/extend.test.ts +295 -0
  167. package/src/presets/extend.ts +297 -0
  168. package/src/presets/fullstack-app.ts +144 -0
  169. package/src/presets/index.ts +27 -0
  170. package/src/presets/jamstack.ts +135 -0
  171. package/src/presets/microservices.ts +167 -0
  172. package/src/presets/ml-api.ts +208 -0
  173. package/src/presets/nodejs-server.ts +104 -0
  174. package/src/presets/nodejs-serverless.ts +114 -0
  175. package/src/presets/realtime-app.ts +184 -0
  176. package/src/presets/static-site.ts +64 -0
  177. package/src/presets/traditional-web-app.ts +339 -0
  178. package/src/presets/wordpress.ts +138 -0
  179. package/src/preview/github.test.ts +249 -0
  180. package/src/preview/github.ts +297 -0
  181. package/src/preview/index.ts +37 -0
  182. package/src/preview/manager.test.ts +440 -0
  183. package/src/preview/manager.ts +326 -0
  184. package/src/preview/notifications.test.ts +582 -0
  185. package/src/preview/notifications.ts +341 -0
  186. package/src/queue/batch-processing.ts +402 -0
  187. package/src/queue/dlq-monitoring.ts +402 -0
  188. package/src/queue/fifo.ts +342 -0
  189. package/src/queue/index.ts +9 -0
  190. package/src/queue/management.ts +428 -0
  191. package/src/queue/queue.test.ts +429 -0
  192. package/src/resource-mgmt/index.ts +39 -0
  193. package/src/resource-naming.ts +62 -0
  194. package/src/s3/index.ts +523 -0
  195. package/src/schema/cloud-config.schema.json +554 -0
  196. package/src/schema/index.ts +68 -0
  197. package/src/security/certificate-manager.ts +492 -0
  198. package/src/security/index.ts +9 -0
  199. package/src/security/scanning.ts +545 -0
  200. package/src/security/secrets-manager.ts +476 -0
  201. package/src/security/secrets-rotation.ts +456 -0
  202. package/src/security/security.test.ts +738 -0
  203. package/src/sms/advanced/ab-testing.ts +389 -0
  204. package/src/sms/advanced/analytics.ts +336 -0
  205. package/src/sms/advanced/campaigns.ts +523 -0
  206. package/src/sms/advanced/chatbot.ts +224 -0
  207. package/src/sms/advanced/index.ts +10 -0
  208. package/src/sms/advanced/link-tracking.ts +248 -0
  209. package/src/sms/advanced/mms.ts +308 -0
  210. package/src/sms/handlers/__tests__/send.test.ts +40 -0
  211. package/src/sms/handlers/delivery-status.ts +133 -0
  212. package/src/sms/handlers/receive.ts +162 -0
  213. package/src/sms/handlers/send.ts +174 -0
  214. package/src/sms/index.ts +9 -0
  215. package/src/stack-diff.ts +389 -0
  216. package/src/static-site/index.ts +85 -0
  217. package/src/template-builder.ts +110 -0
  218. package/src/template-validator.ts +574 -0
  219. package/src/utils/cache.ts +291 -0
  220. package/src/utils/diff.ts +269 -0
  221. package/src/utils/hash.ts +227 -0
  222. package/src/utils/index.ts +8 -0
  223. package/src/utils/parallel.ts +294 -0
  224. package/src/validators/credentials.test.ts +274 -0
  225. package/src/validators/credentials.ts +233 -0
  226. package/src/validators/quotas.test.ts +434 -0
  227. package/src/validators/quotas.ts +217 -0
  228. package/test/ai.test.ts +327 -0
  229. package/test/api.test.ts +511 -0
  230. package/test/auth.test.ts +632 -0
  231. package/test/cache.test.ts +406 -0
  232. package/test/cdn.test.ts +247 -0
  233. package/test/compute.test.ts +861 -0
  234. package/test/database.test.ts +523 -0
  235. package/test/deployment.test.ts +499 -0
  236. package/test/dns.test.ts +270 -0
  237. package/test/email.test.ts +439 -0
  238. package/test/filesystem.test.ts +382 -0
  239. package/test/integration.test.ts +350 -0
  240. package/test/messaging.test.ts +514 -0
  241. package/test/monitoring.test.ts +634 -0
  242. package/test/network.test.ts +425 -0
  243. package/test/permissions.test.ts +488 -0
  244. package/test/queue.test.ts +484 -0
  245. package/test/registry.test.ts +306 -0
  246. package/test/security.test.ts +462 -0
  247. package/test/storage.test.ts +463 -0
  248. package/test/template-validator.test.ts +559 -0
  249. package/test/workflow.test.ts +592 -0
  250. package/tsconfig.json +16 -0
  251. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,523 @@
1
+ /**
2
+ * SMS Campaigns and Scheduling
3
+ *
4
+ * Provides campaign management and scheduled SMS sending
5
+ */
6
+
7
+ export interface SmsCampaign {
8
+ id: string
9
+ name: string
10
+ description?: string
11
+ status: 'draft' | 'scheduled' | 'running' | 'paused' | 'completed' | 'cancelled'
12
+ message: {
13
+ body: string
14
+ template?: string
15
+ variables?: string[]
16
+ }
17
+ audience: CampaignAudience
18
+ schedule: CampaignSchedule
19
+ settings: CampaignSettings
20
+ stats: CampaignStats
21
+ createdAt: string
22
+ updatedAt: string
23
+ startedAt?: string
24
+ completedAt?: string
25
+ }
26
+
27
+ export interface CampaignAudience {
28
+ type: 'list' | 'segment' | 'all'
29
+ listId?: string
30
+ segmentId?: string
31
+ filters?: AudienceFilter[]
32
+ estimatedSize?: number
33
+ }
34
+
35
+ export interface AudienceFilter {
36
+ field: string
37
+ operator: 'equals' | 'not-equals' | 'contains' | 'greater-than' | 'less-than'
38
+ value: string
39
+ }
40
+
41
+ export interface CampaignSchedule {
42
+ type: 'immediate' | 'scheduled' | 'recurring'
43
+ scheduledFor?: string
44
+ timezone?: string
45
+ recurrence?: {
46
+ frequency: 'daily' | 'weekly' | 'monthly'
47
+ interval: number
48
+ endDate?: string
49
+ maxOccurrences?: number
50
+ }
51
+ }
52
+
53
+ export interface CampaignSettings {
54
+ messageType: 'TRANSACTIONAL' | 'PROMOTIONAL'
55
+ senderId?: string
56
+ originationNumber?: string
57
+ throttleRate?: number // messages per second
58
+ quietHours?: {
59
+ start: string // HH:MM
60
+ end: string
61
+ timezone: string
62
+ }
63
+ optOutHandling: boolean
64
+ }
65
+
66
+ export interface CampaignStats {
67
+ totalRecipients: number
68
+ sent: number
69
+ delivered: number
70
+ failed: number
71
+ optedOut: number
72
+ deliveryRate: number
73
+ cost: number
74
+ }
75
+
76
+ /**
77
+ * SMS Campaigns Module
78
+ */
79
+ export class SmsCampaigns {
80
+ /**
81
+ * Lambda code for campaign management
82
+ */
83
+ static CampaignManagerCode = `
84
+ const { DynamoDBClient, PutItemCommand, GetItemCommand, UpdateItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb');
85
+
86
+ const dynamodb = new DynamoDBClient({});
87
+ const CAMPAIGNS_TABLE = process.env.CAMPAIGNS_TABLE;
88
+
89
+ exports.handler = async (event) => {
90
+ console.log('Campaign manager event:', JSON.stringify(event, null, 2));
91
+
92
+ const { httpMethod, body, pathParameters } = event;
93
+ const campaignId = pathParameters?.id;
94
+
95
+ try {
96
+ switch (httpMethod) {
97
+ case 'POST':
98
+ return await createCampaign(JSON.parse(body || '{}'));
99
+ case 'GET':
100
+ if (campaignId) {
101
+ return await getCampaign(campaignId);
102
+ }
103
+ return await listCampaigns(event.queryStringParameters);
104
+ case 'PUT':
105
+ return await updateCampaign(campaignId, JSON.parse(body || '{}'));
106
+ case 'DELETE':
107
+ return await cancelCampaign(campaignId);
108
+ default:
109
+ return { statusCode: 405, body: JSON.stringify({ error: 'Method not allowed' }) };
110
+ }
111
+ } catch (error) {
112
+ console.error('Error:', error);
113
+ return { statusCode: 500, body: JSON.stringify({ error: error.message }) };
114
+ }
115
+ };
116
+
117
+ async function createCampaign(data) {
118
+ const id = \`camp-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`;
119
+ const now = new Date().toISOString();
120
+
121
+ const campaign = {
122
+ id: { S: id },
123
+ name: { S: data.name },
124
+ description: { S: data.description || '' },
125
+ status: { S: 'draft' },
126
+ message: { S: JSON.stringify(data.message || {}) },
127
+ audience: { S: JSON.stringify(data.audience || {}) },
128
+ schedule: { S: JSON.stringify(data.schedule || { type: 'immediate' }) },
129
+ settings: { S: JSON.stringify(data.settings || { messageType: 'TRANSACTIONAL', optOutHandling: true }) },
130
+ stats: { S: JSON.stringify({ totalRecipients: 0, sent: 0, delivered: 0, failed: 0, optedOut: 0, deliveryRate: 0, cost: 0 }) },
131
+ createdAt: { S: now },
132
+ updatedAt: { S: now },
133
+ };
134
+
135
+ await dynamodb.send(new PutItemCommand({
136
+ TableName: CAMPAIGNS_TABLE,
137
+ Item: campaign,
138
+ }));
139
+
140
+ return {
141
+ statusCode: 201,
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({ id, status: 'draft', createdAt: now }),
144
+ };
145
+ }
146
+
147
+ async function getCampaign(id) {
148
+ const result = await dynamodb.send(new GetItemCommand({
149
+ TableName: CAMPAIGNS_TABLE,
150
+ Key: { id: { S: id } },
151
+ }));
152
+
153
+ if (!result.Item) {
154
+ return { statusCode: 404, body: JSON.stringify({ error: 'Campaign not found' }) };
155
+ }
156
+
157
+ return {
158
+ statusCode: 200,
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify(unmarshallCampaign(result.Item)),
161
+ };
162
+ }
163
+
164
+ async function listCampaigns(params) {
165
+ const result = await dynamodb.send(new ScanCommand({
166
+ TableName: CAMPAIGNS_TABLE,
167
+ }));
168
+
169
+ const campaigns = (result.Items || []).map(unmarshallCampaign);
170
+ campaigns.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
171
+
172
+ return {
173
+ statusCode: 200,
174
+ headers: { 'Content-Type': 'application/json' },
175
+ body: JSON.stringify(campaigns),
176
+ };
177
+ }
178
+
179
+ async function updateCampaign(id, data) {
180
+ const now = new Date().toISOString();
181
+
182
+ const updateExpressions = ['updatedAt = :now'];
183
+ const expressionValues = { ':now': { S: now } };
184
+
185
+ if (data.name) {
186
+ updateExpressions.push('name = :name');
187
+ expressionValues[':name'] = { S: data.name };
188
+ }
189
+ if (data.message) {
190
+ updateExpressions.push('message = :message');
191
+ expressionValues[':message'] = { S: JSON.stringify(data.message) };
192
+ }
193
+ if (data.audience) {
194
+ updateExpressions.push('audience = :audience');
195
+ expressionValues[':audience'] = { S: JSON.stringify(data.audience) };
196
+ }
197
+ if (data.schedule) {
198
+ updateExpressions.push('schedule = :schedule');
199
+ expressionValues[':schedule'] = { S: JSON.stringify(data.schedule) };
200
+ }
201
+ if (data.settings) {
202
+ updateExpressions.push('settings = :settings');
203
+ expressionValues[':settings'] = { S: JSON.stringify(data.settings) };
204
+ }
205
+ if (data.status) {
206
+ updateExpressions.push('#status = :status');
207
+ expressionValues[':status'] = { S: data.status };
208
+ }
209
+
210
+ await dynamodb.send(new UpdateItemCommand({
211
+ TableName: CAMPAIGNS_TABLE,
212
+ Key: { id: { S: id } },
213
+ UpdateExpression: 'SET ' + updateExpressions.join(', '),
214
+ ExpressionAttributeNames: data.status ? { '#status': 'status' } : undefined,
215
+ ExpressionAttributeValues: expressionValues,
216
+ }));
217
+
218
+ return {
219
+ statusCode: 200,
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: JSON.stringify({ id, updatedAt: now }),
222
+ };
223
+ }
224
+
225
+ async function cancelCampaign(id) {
226
+ await dynamodb.send(new UpdateItemCommand({
227
+ TableName: CAMPAIGNS_TABLE,
228
+ Key: { id: { S: id } },
229
+ UpdateExpression: 'SET #status = :status, updatedAt = :now',
230
+ ExpressionAttributeNames: { '#status': 'status' },
231
+ ExpressionAttributeValues: {
232
+ ':status': { S: 'cancelled' },
233
+ ':now': { S: new Date().toISOString() },
234
+ },
235
+ }));
236
+
237
+ return { statusCode: 200, body: JSON.stringify({ id, status: 'cancelled' }) };
238
+ }
239
+
240
+ function unmarshallCampaign(item) {
241
+ return {
242
+ id: item.id.S,
243
+ name: item.name.S,
244
+ description: item.description?.S,
245
+ status: item.status.S,
246
+ message: JSON.parse(item.message?.S || '{}'),
247
+ audience: JSON.parse(item.audience?.S || '{}'),
248
+ schedule: JSON.parse(item.schedule?.S || '{}'),
249
+ settings: JSON.parse(item.settings?.S || '{}'),
250
+ stats: JSON.parse(item.stats?.S || '{}'),
251
+ createdAt: item.createdAt.S,
252
+ updatedAt: item.updatedAt.S,
253
+ startedAt: item.startedAt?.S,
254
+ completedAt: item.completedAt?.S,
255
+ };
256
+ }
257
+ `
258
+
259
+ /**
260
+ * Lambda code for campaign execution
261
+ */
262
+ static CampaignExecutorCode = `
263
+ const { DynamoDBClient, GetItemCommand, UpdateItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb');
264
+ const { PinpointClient, SendMessagesCommand } = require('@aws-sdk/client-pinpoint');
265
+
266
+ const dynamodb = new DynamoDBClient({});
267
+ const pinpoint = new PinpointClient({});
268
+
269
+ const CAMPAIGNS_TABLE = process.env.CAMPAIGNS_TABLE;
270
+ const CONTACTS_TABLE = process.env.CONTACTS_TABLE;
271
+ const OPT_OUT_TABLE = process.env.OPT_OUT_TABLE;
272
+ const PINPOINT_APP_ID = process.env.PINPOINT_APP_ID;
273
+
274
+ exports.handler = async (event) => {
275
+ console.log('Campaign executor event:', JSON.stringify(event, null, 2));
276
+
277
+ try {
278
+ // Get scheduled campaigns
279
+ const result = await dynamodb.send(new ScanCommand({
280
+ TableName: CAMPAIGNS_TABLE,
281
+ FilterExpression: '#status = :scheduled',
282
+ ExpressionAttributeNames: { '#status': 'status' },
283
+ ExpressionAttributeValues: {
284
+ ':scheduled': { S: 'scheduled' },
285
+ },
286
+ }));
287
+
288
+ const campaigns = result.Items || [];
289
+ const now = new Date();
290
+
291
+ for (const item of campaigns) {
292
+ const campaign = unmarshallCampaign(item);
293
+ const schedule = campaign.schedule;
294
+
295
+ // Check if it's time to run
296
+ if (schedule.type === 'scheduled' && schedule.scheduledFor) {
297
+ const scheduledTime = new Date(schedule.scheduledFor);
298
+ if (scheduledTime > now) continue;
299
+ }
300
+
301
+ // Check quiet hours
302
+ if (campaign.settings.quietHours) {
303
+ const { start, end, timezone } = campaign.settings.quietHours;
304
+ const localTime = new Date().toLocaleTimeString('en-US', { timeZone: timezone, hour12: false });
305
+ if (localTime >= start && localTime <= end) {
306
+ console.log(\`Campaign \${campaign.id} skipped - quiet hours\`);
307
+ continue;
308
+ }
309
+ }
310
+
311
+ // Start campaign
312
+ await executeCampaign(campaign);
313
+ }
314
+
315
+ return { statusCode: 200 };
316
+ } catch (error) {
317
+ console.error('Error executing campaigns:', error);
318
+ return { statusCode: 500, error: error.message };
319
+ }
320
+ };
321
+
322
+ async function executeCampaign(campaign) {
323
+ console.log(\`Executing campaign: \${campaign.id}\`);
324
+
325
+ // Update status to running
326
+ await dynamodb.send(new UpdateItemCommand({
327
+ TableName: CAMPAIGNS_TABLE,
328
+ Key: { id: { S: campaign.id } },
329
+ UpdateExpression: 'SET #status = :status, startedAt = :now',
330
+ ExpressionAttributeNames: { '#status': 'status' },
331
+ ExpressionAttributeValues: {
332
+ ':status': { S: 'running' },
333
+ ':now': { S: new Date().toISOString() },
334
+ },
335
+ }));
336
+
337
+ // Get recipients
338
+ const recipients = await getRecipients(campaign.audience);
339
+ const optedOut = await getOptedOutNumbers();
340
+
341
+ // Filter out opted-out numbers
342
+ const eligibleRecipients = recipients.filter(r => !optedOut.has(r.phoneNumber));
343
+
344
+ let sent = 0;
345
+ let delivered = 0;
346
+ let failed = 0;
347
+ let cost = 0;
348
+
349
+ // Send messages in batches
350
+ const batchSize = campaign.settings.throttleRate || 20;
351
+
352
+ for (let i = 0; i < eligibleRecipients.length; i += batchSize) {
353
+ const batch = eligibleRecipients.slice(i, i + batchSize);
354
+
355
+ for (const recipient of batch) {
356
+ try {
357
+ const messageBody = resolveTemplate(campaign.message.body, recipient);
358
+
359
+ const result = await pinpoint.send(new SendMessagesCommand({
360
+ ApplicationId: PINPOINT_APP_ID,
361
+ MessageRequest: {
362
+ Addresses: {
363
+ [recipient.phoneNumber]: {
364
+ ChannelType: 'SMS',
365
+ },
366
+ },
367
+ MessageConfiguration: {
368
+ SMSMessage: {
369
+ Body: messageBody,
370
+ MessageType: campaign.settings.messageType,
371
+ SenderId: campaign.settings.senderId,
372
+ OriginationNumber: campaign.settings.originationNumber,
373
+ },
374
+ },
375
+ },
376
+ }));
377
+
378
+ const status = result.MessageResponse?.Result?.[recipient.phoneNumber]?.DeliveryStatus;
379
+ if (status === 'SUCCESSFUL') {
380
+ delivered++;
381
+ cost += 0.00645; // Approximate US SMS cost
382
+ } else {
383
+ failed++;
384
+ }
385
+ sent++;
386
+ } catch (error) {
387
+ console.error(\`Failed to send to \${recipient.phoneNumber}:\`, error);
388
+ failed++;
389
+ }
390
+ }
391
+
392
+ // Throttle between batches
393
+ if (i + batchSize < eligibleRecipients.length) {
394
+ await new Promise(resolve => setTimeout(resolve, 1000));
395
+ }
396
+ }
397
+
398
+ // Update campaign stats
399
+ await dynamodb.send(new UpdateItemCommand({
400
+ TableName: CAMPAIGNS_TABLE,
401
+ Key: { id: { S: campaign.id } },
402
+ UpdateExpression: 'SET #status = :status, stats = :stats, completedAt = :now',
403
+ ExpressionAttributeNames: { '#status': 'status' },
404
+ ExpressionAttributeValues: {
405
+ ':status': { S: 'completed' },
406
+ ':stats': { S: JSON.stringify({
407
+ totalRecipients: eligibleRecipients.length,
408
+ sent,
409
+ delivered,
410
+ failed,
411
+ optedOut: recipients.length - eligibleRecipients.length,
412
+ deliveryRate: sent > 0 ? (delivered / sent) * 100 : 0,
413
+ cost: Math.round(cost * 100) / 100,
414
+ })},
415
+ ':now': { S: new Date().toISOString() },
416
+ },
417
+ }));
418
+
419
+ console.log(\`Campaign \${campaign.id} completed: \${delivered}/\${sent} delivered\`);
420
+ }
421
+
422
+ async function getRecipients(audience) {
423
+ // Simplified - in production, query from contacts table based on audience type
424
+ if (audience.type === 'list' && audience.listId) {
425
+ const result = await dynamodb.send(new ScanCommand({
426
+ TableName: CONTACTS_TABLE,
427
+ FilterExpression: 'listId = :listId',
428
+ ExpressionAttributeValues: {
429
+ ':listId': { S: audience.listId },
430
+ },
431
+ }));
432
+ return (result.Items || []).map(item => ({
433
+ phoneNumber: item.phoneNumber.S,
434
+ name: item.name?.S,
435
+ ...JSON.parse(item.attributes?.S || '{}'),
436
+ }));
437
+ }
438
+ return [];
439
+ }
440
+
441
+ async function getOptedOutNumbers() {
442
+ const result = await dynamodb.send(new ScanCommand({
443
+ TableName: OPT_OUT_TABLE,
444
+ }));
445
+ return new Set((result.Items || []).map(item => item.phoneNumber.S));
446
+ }
447
+
448
+ function resolveTemplate(template, data) {
449
+ let result = template;
450
+ for (const [key, value] of Object.entries(data)) {
451
+ result = result.replace(new RegExp(\`{{\\\\s*\${key}\\\\s*}}\`, 'g'), String(value));
452
+ }
453
+ return result;
454
+ }
455
+
456
+ function unmarshallCampaign(item) {
457
+ return {
458
+ id: item.id.S,
459
+ name: item.name.S,
460
+ status: item.status.S,
461
+ message: JSON.parse(item.message?.S || '{}'),
462
+ audience: JSON.parse(item.audience?.S || '{}'),
463
+ schedule: JSON.parse(item.schedule?.S || '{}'),
464
+ settings: JSON.parse(item.settings?.S || '{}'),
465
+ stats: JSON.parse(item.stats?.S || '{}'),
466
+ };
467
+ }
468
+ `
469
+
470
+ /**
471
+ * Create campaigns DynamoDB table
472
+ */
473
+ static createCampaignsTable(config: { slug: string }): Record<string, any> {
474
+ return {
475
+ [`${config.slug}SmsCampaignsTable`]: {
476
+ Type: 'AWS::DynamoDB::Table',
477
+ Properties: {
478
+ TableName: `${config.slug}-sms-campaigns`,
479
+ BillingMode: 'PAY_PER_REQUEST',
480
+ AttributeDefinitions: [
481
+ { AttributeName: 'id', AttributeType: 'S' },
482
+ ],
483
+ KeySchema: [
484
+ { AttributeName: 'id', KeyType: 'HASH' },
485
+ ],
486
+ },
487
+ },
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Create campaign manager Lambda
493
+ */
494
+ static createCampaignManagerLambda(config: {
495
+ slug: string
496
+ roleArn: string
497
+ campaignsTable: string
498
+ }): Record<string, any> {
499
+ return {
500
+ [`${config.slug}SmsCampaignManagerLambda`]: {
501
+ Type: 'AWS::Lambda::Function',
502
+ Properties: {
503
+ FunctionName: `${config.slug}-sms-campaign-manager`,
504
+ Runtime: 'nodejs20.x',
505
+ Handler: 'index.handler',
506
+ Role: config.roleArn,
507
+ Timeout: 30,
508
+ MemorySize: 256,
509
+ Code: {
510
+ ZipFile: SmsCampaigns.CampaignManagerCode,
511
+ },
512
+ Environment: {
513
+ Variables: {
514
+ CAMPAIGNS_TABLE: config.campaignsTable,
515
+ },
516
+ },
517
+ },
518
+ },
519
+ }
520
+ }
521
+ }
522
+
523
+ export default SmsCampaigns