@stacksjs/ts-cloud 0.1.2 → 0.1.5

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 (187) hide show
  1. package/README.md +98 -13
  2. package/dist/aws/acm.d.ts +129 -0
  3. package/dist/aws/application-autoscaling.d.ts +282 -0
  4. package/dist/aws/bedrock.d.ts +2292 -0
  5. package/dist/aws/client.d.ts +79 -0
  6. package/dist/aws/cloudformation.d.ts +105 -0
  7. package/dist/aws/cloudfront.d.ts +265 -0
  8. package/dist/aws/cloudwatch-logs.d.ts +48 -0
  9. package/dist/aws/comprehend.d.ts +505 -0
  10. package/dist/aws/connect.d.ts +377 -0
  11. package/dist/aws/deploy-imap.d.ts +14 -0
  12. package/dist/aws/dynamodb.d.ts +176 -0
  13. package/dist/aws/ec2.d.ts +272 -0
  14. package/dist/aws/ecr.d.ts +149 -0
  15. package/dist/aws/ecs.d.ts +162 -0
  16. package/dist/aws/elasticache.d.ts +71 -0
  17. package/dist/aws/elbv2.d.ts +248 -0
  18. package/dist/aws/email.d.ts +175 -0
  19. package/dist/aws/eventbridge.d.ts +142 -0
  20. package/dist/aws/iam.d.ts +638 -0
  21. package/dist/aws/imap-server.d.ts +119 -0
  22. package/{src/aws/index.ts → dist/aws/index.d.ts} +62 -83
  23. package/{src/aws/kendra.ts → dist/aws/kendra.d.ts} +71 -386
  24. package/dist/aws/lambda.d.ts +232 -0
  25. package/dist/aws/opensearch.d.ts +87 -0
  26. package/dist/aws/personalize.d.ts +516 -0
  27. package/dist/aws/polly.d.ts +214 -0
  28. package/dist/aws/rds.d.ts +240 -0
  29. package/dist/aws/rekognition.d.ts +543 -0
  30. package/dist/aws/route53-domains.d.ts +113 -0
  31. package/dist/aws/route53.d.ts +215 -0
  32. package/dist/aws/s3.d.ts +212 -0
  33. package/dist/aws/scheduler.d.ts +140 -0
  34. package/dist/aws/secrets-manager.d.ts +170 -0
  35. package/dist/aws/ses.d.ts +288 -0
  36. package/dist/aws/setup-phone.d.ts +0 -0
  37. package/dist/aws/setup-sms.d.ts +115 -0
  38. package/dist/aws/sms.d.ts +304 -0
  39. package/dist/aws/smtp-server.d.ts +61 -0
  40. package/dist/aws/sns.d.ts +117 -0
  41. package/dist/aws/sqs.d.ts +65 -0
  42. package/dist/aws/ssm.d.ts +179 -0
  43. package/dist/aws/sts.d.ts +15 -0
  44. package/dist/aws/support.d.ts +104 -0
  45. package/dist/aws/test-imap.d.ts +0 -0
  46. package/dist/aws/textract.d.ts +403 -0
  47. package/dist/aws/transcribe.d.ts +60 -0
  48. package/dist/aws/translate.d.ts +358 -0
  49. package/dist/aws/voice.d.ts +219 -0
  50. package/dist/bin/cli.js +1724 -0
  51. package/dist/config.d.ts +7 -0
  52. package/dist/deploy/index.d.ts +2 -0
  53. package/dist/deploy/static-site-external-dns.d.ts +51 -0
  54. package/dist/deploy/static-site.d.ts +71 -0
  55. package/dist/dns/cloudflare.d.ts +52 -0
  56. package/dist/dns/godaddy.d.ts +38 -0
  57. package/dist/dns/index.d.ts +45 -0
  58. package/dist/dns/porkbun.d.ts +18 -0
  59. package/dist/dns/route53-adapter.d.ts +38 -0
  60. package/{src/dns/types.ts → dist/dns/types.d.ts} +26 -63
  61. package/dist/dns/validator.d.ts +78 -0
  62. package/dist/generators/index.d.ts +1 -0
  63. package/dist/generators/infrastructure.d.ts +30 -0
  64. package/{src/index.ts → dist/index.d.ts} +70 -93
  65. package/dist/index.js +7881 -0
  66. package/dist/push/apns.d.ts +60 -0
  67. package/dist/push/fcm.d.ts +117 -0
  68. package/dist/push/index.d.ts +14 -0
  69. package/dist/security/pre-deploy-scanner.d.ts +69 -0
  70. package/dist/ssl/acme-client.d.ts +67 -0
  71. package/dist/ssl/index.d.ts +2 -0
  72. package/dist/ssl/letsencrypt.d.ts +48 -0
  73. package/dist/types.d.ts +1 -0
  74. package/dist/utils/cli.d.ts +123 -0
  75. package/dist/validation/index.d.ts +1 -0
  76. package/dist/validation/template.d.ts +23 -0
  77. package/package.json +8 -8
  78. package/bin/cli.ts +0 -133
  79. package/bin/commands/analytics.ts +0 -328
  80. package/bin/commands/api.ts +0 -379
  81. package/bin/commands/assets.ts +0 -221
  82. package/bin/commands/audit.ts +0 -501
  83. package/bin/commands/backup.ts +0 -682
  84. package/bin/commands/cache.ts +0 -294
  85. package/bin/commands/cdn.ts +0 -281
  86. package/bin/commands/config.ts +0 -202
  87. package/bin/commands/container.ts +0 -105
  88. package/bin/commands/cost.ts +0 -208
  89. package/bin/commands/database.ts +0 -401
  90. package/bin/commands/deploy.ts +0 -674
  91. package/bin/commands/domain.ts +0 -397
  92. package/bin/commands/email.ts +0 -423
  93. package/bin/commands/environment.ts +0 -285
  94. package/bin/commands/events.ts +0 -424
  95. package/bin/commands/firewall.ts +0 -145
  96. package/bin/commands/function.ts +0 -116
  97. package/bin/commands/generate.ts +0 -280
  98. package/bin/commands/git.ts +0 -139
  99. package/bin/commands/iam.ts +0 -464
  100. package/bin/commands/index.ts +0 -48
  101. package/bin/commands/init.ts +0 -120
  102. package/bin/commands/logs.ts +0 -148
  103. package/bin/commands/network.ts +0 -579
  104. package/bin/commands/notify.ts +0 -489
  105. package/bin/commands/queue.ts +0 -407
  106. package/bin/commands/scheduler.ts +0 -370
  107. package/bin/commands/secrets.ts +0 -54
  108. package/bin/commands/server.ts +0 -629
  109. package/bin/commands/shared.ts +0 -97
  110. package/bin/commands/ssl.ts +0 -138
  111. package/bin/commands/stack.ts +0 -325
  112. package/bin/commands/status.ts +0 -385
  113. package/bin/commands/storage.ts +0 -450
  114. package/bin/commands/team.ts +0 -96
  115. package/bin/commands/tunnel.ts +0 -489
  116. package/bin/commands/utils.ts +0 -202
  117. package/build.ts +0 -15
  118. package/cloud +0 -2
  119. package/src/aws/acm.ts +0 -768
  120. package/src/aws/application-autoscaling.ts +0 -845
  121. package/src/aws/bedrock.ts +0 -4074
  122. package/src/aws/client.ts +0 -878
  123. package/src/aws/cloudformation.ts +0 -896
  124. package/src/aws/cloudfront.ts +0 -1531
  125. package/src/aws/cloudwatch-logs.ts +0 -154
  126. package/src/aws/comprehend.ts +0 -839
  127. package/src/aws/connect.ts +0 -1056
  128. package/src/aws/deploy-imap.ts +0 -384
  129. package/src/aws/dynamodb.ts +0 -340
  130. package/src/aws/ec2.ts +0 -1385
  131. package/src/aws/ecr.ts +0 -621
  132. package/src/aws/ecs.ts +0 -615
  133. package/src/aws/elasticache.ts +0 -301
  134. package/src/aws/elbv2.ts +0 -942
  135. package/src/aws/email.ts +0 -928
  136. package/src/aws/eventbridge.ts +0 -248
  137. package/src/aws/iam.ts +0 -1689
  138. package/src/aws/imap-server.ts +0 -2100
  139. package/src/aws/lambda.ts +0 -786
  140. package/src/aws/opensearch.ts +0 -158
  141. package/src/aws/personalize.ts +0 -977
  142. package/src/aws/polly.ts +0 -559
  143. package/src/aws/rds.ts +0 -888
  144. package/src/aws/rekognition.ts +0 -846
  145. package/src/aws/route53-domains.ts +0 -359
  146. package/src/aws/route53.ts +0 -1046
  147. package/src/aws/s3.ts +0 -2318
  148. package/src/aws/scheduler.ts +0 -571
  149. package/src/aws/secrets-manager.ts +0 -769
  150. package/src/aws/ses.ts +0 -1081
  151. package/src/aws/setup-phone.ts +0 -104
  152. package/src/aws/setup-sms.ts +0 -580
  153. package/src/aws/sms.ts +0 -1735
  154. package/src/aws/smtp-server.ts +0 -531
  155. package/src/aws/sns.ts +0 -758
  156. package/src/aws/sqs.ts +0 -382
  157. package/src/aws/ssm.ts +0 -807
  158. package/src/aws/sts.ts +0 -92
  159. package/src/aws/support.ts +0 -391
  160. package/src/aws/test-imap.ts +0 -86
  161. package/src/aws/textract.ts +0 -780
  162. package/src/aws/transcribe.ts +0 -108
  163. package/src/aws/translate.ts +0 -641
  164. package/src/aws/voice.ts +0 -1379
  165. package/src/config.ts +0 -35
  166. package/src/deploy/index.ts +0 -7
  167. package/src/deploy/static-site-external-dns.ts +0 -906
  168. package/src/deploy/static-site.ts +0 -1125
  169. package/src/dns/godaddy.ts +0 -412
  170. package/src/dns/index.ts +0 -183
  171. package/src/dns/porkbun.ts +0 -362
  172. package/src/dns/route53-adapter.ts +0 -414
  173. package/src/dns/validator.ts +0 -369
  174. package/src/generators/index.ts +0 -5
  175. package/src/generators/infrastructure.ts +0 -1660
  176. package/src/push/apns.ts +0 -452
  177. package/src/push/fcm.ts +0 -506
  178. package/src/push/index.ts +0 -58
  179. package/src/ssl/acme-client.ts +0 -478
  180. package/src/ssl/index.ts +0 -7
  181. package/src/ssl/letsencrypt.ts +0 -747
  182. package/src/types.ts +0 -2
  183. package/src/utils/cli.ts +0 -398
  184. package/src/validation/index.ts +0 -5
  185. package/src/validation/template.ts +0 -405
  186. package/test/index.test.ts +0 -128
  187. package/tsconfig.json +0 -18
@@ -1,1660 +0,0 @@
1
- /**
2
- * Infrastructure Generator
3
- * Generates CloudFormation templates from cloud.config.ts using all Phase 2 modules
4
- */
5
-
6
- import type { CloudConfig } from '@stacksjs/ts-cloud-types'
7
- import {
8
- Storage,
9
- CDN,
10
- DNS,
11
- Security,
12
- Compute,
13
- Network,
14
- FileSystem,
15
- Email,
16
- Queue,
17
- AI,
18
- Database,
19
- Cache,
20
- Permissions,
21
- ApiGateway,
22
- Messaging,
23
- Workflow,
24
- Monitoring,
25
- Auth,
26
- Deployment,
27
- TemplateBuilder,
28
- } from '@stacksjs/ts-cloud-core'
29
-
30
- export interface GenerationOptions {
31
- config: CloudConfig
32
- environment: 'production' | 'staging' | 'development'
33
- modules?: string[]
34
- }
35
-
36
- export class InfrastructureGenerator {
37
- private builder: TemplateBuilder
38
- private config: CloudConfig
39
- private environment: 'production' | 'staging' | 'development'
40
- private mergedConfig: CloudConfig
41
-
42
- constructor(options: GenerationOptions) {
43
- this.config = options.config
44
- this.environment = options.environment
45
- this.builder = new TemplateBuilder(
46
- `${this.config.project.name} - ${this.environment}`,
47
- )
48
-
49
- // Merge environment-specific infrastructure overrides
50
- this.mergedConfig = this.mergeEnvironmentConfig()
51
- }
52
-
53
- /**
54
- * Merge base config with environment-specific overrides
55
- */
56
- private mergeEnvironmentConfig(): CloudConfig {
57
- const envConfig = this.config.environments[this.environment]
58
- const envInfra = envConfig?.infrastructure
59
-
60
- if (!envInfra) {
61
- return this.config
62
- }
63
-
64
- return {
65
- ...this.config,
66
- infrastructure: {
67
- ...this.config.infrastructure,
68
- ...envInfra,
69
- // Deep merge for nested objects
70
- storage: { ...this.config.infrastructure?.storage, ...envInfra.storage },
71
- functions: { ...this.config.infrastructure?.functions, ...envInfra.functions },
72
- servers: { ...this.config.infrastructure?.servers, ...envInfra.servers },
73
- databases: { ...this.config.infrastructure?.databases, ...envInfra.databases },
74
- cdn: { ...this.config.infrastructure?.cdn, ...envInfra.cdn },
75
- queues: { ...this.config.infrastructure?.queues, ...envInfra.queues },
76
- realtime: { ...this.config.infrastructure?.realtime, ...envInfra.realtime },
77
- },
78
- }
79
- }
80
-
81
- /**
82
- * Check if a resource should be deployed based on conditions
83
- */
84
- private shouldDeploy(resource: any): boolean {
85
- // Check environment conditions
86
- if (resource.environments && !resource.environments.includes(this.environment)) {
87
- return false
88
- }
89
-
90
- // Check feature flag requirements
91
- if (resource.requiresFeatures) {
92
- const features = this.config.features || {}
93
- const hasRequiredFeatures = resource.requiresFeatures.every(
94
- (feature: string) => features[feature] === true
95
- )
96
- if (!hasRequiredFeatures) {
97
- return false
98
- }
99
- }
100
-
101
- // Check region conditions
102
- if (resource.regions) {
103
- const currentRegion = this.config.environments[this.environment]?.region || this.config.project.region
104
- if (!resource.regions.includes(currentRegion)) {
105
- return false
106
- }
107
- }
108
-
109
- // Check custom condition function
110
- if (resource.condition && typeof resource.condition === 'function') {
111
- return resource.condition(this.config, this.environment)
112
- }
113
-
114
- return true
115
- }
116
-
117
- /**
118
- * Generate complete infrastructure
119
- * Auto-detects what to generate based on configuration
120
- */
121
- generate(): this {
122
- const slug = this.mergedConfig.project.slug
123
- const env = this.environment
124
-
125
- // Auto-detect and generate based on what's configured (using merged config)
126
- // If functions or API are defined, generate serverless resources
127
- const hasServerlessConfig = !!(
128
- this.mergedConfig.infrastructure?.functions
129
- || this.mergedConfig.infrastructure?.api
130
- )
131
-
132
- // If servers are defined, generate server resources
133
- const hasServerConfig = !!(
134
- this.mergedConfig.infrastructure?.servers
135
- )
136
-
137
- if (hasServerlessConfig) {
138
- this.generateServerless(slug, env)
139
- }
140
-
141
- if (hasServerConfig) {
142
- this.generateServer(slug, env)
143
- }
144
-
145
- // Always generate shared infrastructure (storage, CDN, databases, etc.)
146
- this.generateSharedInfrastructure(slug, env)
147
-
148
- // Apply global tags if specified
149
- if (this.config.tags) {
150
- this.applyGlobalTags(this.config.tags)
151
- }
152
-
153
- return this
154
- }
155
-
156
- /**
157
- * Apply global tags to all resources
158
- */
159
- private applyGlobalTags(tags: Record<string, string>): void {
160
- // This would iterate through all resources in the builder and add tags
161
- // Implementation depends on TemplateBuilder structure
162
- }
163
-
164
- /**
165
- * Generate serverless infrastructure (Lambda, ECS Fargate)
166
- */
167
- private generateServerless(slug: string, env: typeof this.environment): void {
168
- // Example: Lambda function
169
- if (this.mergedConfig.infrastructure?.functions) {
170
- for (const [name, fnConfig] of Object.entries(this.mergedConfig.infrastructure.functions)) {
171
- // Check if this function should be deployed
172
- if (!this.shouldDeploy(fnConfig)) {
173
- continue
174
- }
175
- // Create Lambda execution role
176
- const { role, logicalId: roleLogicalId } = Permissions.createRole({
177
- slug,
178
- environment: env,
179
- roleName: `${slug}-${env}-${name}-role`,
180
- servicePrincipal: 'lambda.amazonaws.com',
181
- managedPolicyArns: [
182
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
183
- ],
184
- })
185
-
186
- this.builder.addResource(roleLogicalId, role)
187
-
188
- const { lambdaFunction, logicalId } = Compute.createLambdaFunction({
189
- slug,
190
- environment: env,
191
- functionName: `${slug}-${env}-${name}`,
192
- handler: fnConfig.handler || 'index.handler',
193
- runtime: fnConfig.runtime || 'nodejs20.x',
194
- code: {
195
- zipFile: fnConfig.code || 'export const handler = async () => ({ statusCode: 200 });',
196
- },
197
- role: roleLogicalId,
198
- timeout: fnConfig.timeout,
199
- memorySize: fnConfig.memorySize,
200
- })
201
-
202
- this.builder.addResource(logicalId, lambdaFunction)
203
- }
204
- }
205
-
206
- // Example: API Gateway
207
- if (this.config.infrastructure?.api) {
208
- const { restApi, logicalId } = ApiGateway.createRestApi({
209
- slug,
210
- environment: env,
211
- apiName: `${slug}-${env}-api`,
212
- })
213
-
214
- this.builder.addResource(logicalId, restApi)
215
- }
216
- }
217
-
218
- /**
219
- * Generate server infrastructure (EC2)
220
- */
221
- private generateServer(slug: string, env: typeof this.environment): void {
222
- // Example: EC2 instance
223
- if (this.config.infrastructure?.servers) {
224
- for (const [name, serverConfig] of Object.entries(this.config.infrastructure.servers)) {
225
- const { instance, logicalId } = Compute.createServer({
226
- slug,
227
- environment: env,
228
- instanceType: serverConfig.size || 't3.micro',
229
- imageId: serverConfig.image || 'ami-0c55b159cbfafe1f0',
230
- userData: serverConfig.startupScript,
231
- })
232
-
233
- this.builder.addResource(logicalId, instance)
234
- }
235
- }
236
- }
237
-
238
- /**
239
- * Generate shared infrastructure (storage, database, etc.)
240
- */
241
- private generateSharedInfrastructure(slug: string, env: typeof this.environment): void {
242
- // Storage buckets
243
- if (this.config.infrastructure?.storage) {
244
- for (const [name, storageConfig] of Object.entries(this.config.infrastructure.storage)) {
245
- const { bucket, logicalId } = Storage.createBucket({
246
- slug,
247
- environment: env,
248
- bucketName: `${slug}-${env}-${name}`,
249
- versioning: storageConfig.versioning,
250
- encryption: storageConfig.encryption,
251
- })
252
-
253
- this.builder.addResource(logicalId, bucket)
254
-
255
- // Enable website hosting if configured
256
- if (storageConfig.website) {
257
- const websiteConfig = typeof storageConfig.website === 'object' ? storageConfig.website : {}
258
- const enhanced = Storage.enableWebsiteHosting(
259
- bucket,
260
- websiteConfig.indexDocument || 'index.html',
261
- websiteConfig.errorDocument,
262
- )
263
- this.builder.addResource(logicalId, enhanced)
264
- }
265
- }
266
- }
267
-
268
- // Databases
269
- if (this.config.infrastructure?.databases) {
270
- for (const [name, dbConfig] of Object.entries(this.config.infrastructure.databases)) {
271
- if (dbConfig.engine === 'dynamodb') {
272
- const { table, logicalId } = Database.createTable({
273
- slug,
274
- environment: env,
275
- tableName: `${slug}-${env}-${name}`,
276
- partitionKey: (dbConfig.partitionKey || { name: 'id', type: 'S' }) as { name: string; type: 'S' | 'N' | 'B' },
277
- sortKey: dbConfig.sortKey as any,
278
- })
279
-
280
- this.builder.addResource(logicalId, table)
281
- }
282
- else if (dbConfig.engine === 'postgres') {
283
- const { dbInstance, logicalId } = Database.createPostgres({
284
- slug,
285
- environment: env,
286
- dbInstanceIdentifier: `${slug}-${env}-${name}`,
287
- masterUsername: dbConfig.username || 'admin',
288
- masterUserPassword: dbConfig.password || 'changeme123',
289
- allocatedStorage: dbConfig.storage || 20,
290
- dbInstanceClass: dbConfig.instanceClass || 'db.t3.micro',
291
- })
292
-
293
- this.builder.addResource(logicalId, dbInstance)
294
- }
295
- else if (dbConfig.engine === 'mysql') {
296
- const { dbInstance, logicalId } = Database.createMysql({
297
- slug,
298
- environment: env,
299
- dbInstanceIdentifier: `${slug}-${env}-${name}`,
300
- masterUsername: dbConfig.username || 'admin',
301
- masterUserPassword: dbConfig.password || 'changeme123',
302
- allocatedStorage: dbConfig.storage || 20,
303
- dbInstanceClass: dbConfig.instanceClass || 'db.t3.micro',
304
- })
305
-
306
- this.builder.addResource(logicalId, dbInstance)
307
- }
308
- }
309
- }
310
-
311
- // CDN
312
- if (this.config.infrastructure?.cdn) {
313
- for (const [name, cdnConfig] of Object.entries(this.config.infrastructure.cdn)) {
314
- const { distribution, logicalId } = CDN.createDistribution({
315
- slug,
316
- environment: env,
317
- origin: {
318
- domainName: cdnConfig.origin,
319
- originId: `${slug}-origin`,
320
- },
321
- })
322
-
323
- this.builder.addResource(logicalId, distribution)
324
- }
325
- }
326
-
327
- // Queues (SQS)
328
- if (this.mergedConfig.infrastructure?.queues) {
329
- for (const [name, queueConfig] of Object.entries(this.mergedConfig.infrastructure.queues)) {
330
- // Check if this queue should be deployed
331
- if (!this.shouldDeploy(queueConfig)) {
332
- continue
333
- }
334
-
335
- // Create the main queue
336
- const { queue, logicalId } = Queue.createQueue({
337
- slug,
338
- environment: env,
339
- name: `${slug}-${env}-${name}`,
340
- fifo: queueConfig.fifo,
341
- visibilityTimeout: queueConfig.visibilityTimeout,
342
- messageRetentionPeriod: queueConfig.messageRetentionPeriod,
343
- delaySeconds: queueConfig.delaySeconds,
344
- maxMessageSize: queueConfig.maxMessageSize,
345
- receiveMessageWaitTime: queueConfig.receiveMessageWaitTime,
346
- contentBasedDeduplication: queueConfig.contentBasedDeduplication,
347
- encrypted: queueConfig.encrypted,
348
- kmsKeyId: queueConfig.kmsKeyId,
349
- })
350
-
351
- this.builder.addResource(logicalId, queue)
352
-
353
- // Create dead letter queue if enabled
354
- let dlqLogicalId: string | undefined
355
- if (queueConfig.deadLetterQueue) {
356
- const {
357
- deadLetterQueue,
358
- updatedSourceQueue,
359
- deadLetterLogicalId,
360
- } = Queue.createDeadLetterQueue(logicalId, {
361
- slug,
362
- environment: env,
363
- maxReceiveCount: queueConfig.maxReceiveCount,
364
- })
365
-
366
- dlqLogicalId = deadLetterLogicalId
367
- this.builder.addResource(deadLetterLogicalId, deadLetterQueue)
368
-
369
- // Update the main queue with redrive policy
370
- const resources = this.builder.getResources()
371
- const existingQueue = resources[logicalId]
372
- if (existingQueue?.Properties) {
373
- existingQueue.Properties.RedrivePolicy = updatedSourceQueue.Properties?.RedrivePolicy
374
- }
375
- }
376
-
377
- // Lambda trigger (Event Source Mapping)
378
- if (queueConfig.trigger) {
379
- const triggerConfig = queueConfig.trigger
380
- const functionLogicalId = `${slug}${env}${triggerConfig.functionName}`.replace(/[^a-zA-Z0-9]/g, '')
381
-
382
- const eventSourceMapping = {
383
- Type: 'AWS::Lambda::EventSourceMapping',
384
- Properties: {
385
- EventSourceArn: { 'Fn::GetAtt': [logicalId, 'Arn'] },
386
- FunctionName: { Ref: functionLogicalId },
387
- BatchSize: triggerConfig.batchSize || 10,
388
- MaximumBatchingWindowInSeconds: triggerConfig.batchWindow || 0,
389
- Enabled: true,
390
- ...(triggerConfig.reportBatchItemFailures !== false && {
391
- FunctionResponseTypes: ['ReportBatchItemFailures'],
392
- }),
393
- ...(triggerConfig.maxConcurrency && {
394
- ScalingConfig: {
395
- MaximumConcurrency: triggerConfig.maxConcurrency,
396
- },
397
- }),
398
- ...(triggerConfig.filterPattern && {
399
- FilterCriteria: {
400
- Filters: [{ Pattern: JSON.stringify(triggerConfig.filterPattern) }],
401
- },
402
- }),
403
- },
404
- DependsOn: [logicalId, functionLogicalId],
405
- }
406
-
407
- this.builder.addResource(`${logicalId}Trigger`, eventSourceMapping as any)
408
- }
409
-
410
- // CloudWatch Alarms
411
- if (queueConfig.alarms?.enabled) {
412
- const alarmsConfig = queueConfig.alarms
413
-
414
- // Create SNS topic for notifications if emails are provided
415
- let alarmTopicArn = alarmsConfig.notificationTopicArn
416
- if (!alarmTopicArn && alarmsConfig.notificationEmails?.length) {
417
- const topicLogicalId = `${logicalId}AlarmTopic`
418
- this.builder.addResource(topicLogicalId, {
419
- Type: 'AWS::SNS::Topic',
420
- Properties: {
421
- TopicName: `${slug}-${env}-${name}-alarms`,
422
- DisplayName: `${name} Queue Alarms`,
423
- },
424
- } as any)
425
-
426
- // Add email subscriptions
427
- alarmsConfig.notificationEmails.forEach((email, idx) => {
428
- this.builder.addResource(`${topicLogicalId}Sub${idx}`, {
429
- Type: 'AWS::SNS::Subscription',
430
- Properties: {
431
- TopicArn: { Ref: topicLogicalId },
432
- Protocol: 'email',
433
- Endpoint: email,
434
- },
435
- } as any)
436
- })
437
-
438
- alarmTopicArn = { Ref: topicLogicalId } as any
439
- }
440
-
441
- // Queue depth alarm
442
- const depthThreshold = alarmsConfig.queueDepthThreshold || 1000
443
- this.builder.addResource(`${logicalId}DepthAlarm`, {
444
- Type: 'AWS::CloudWatch::Alarm',
445
- Properties: {
446
- AlarmName: `${slug}-${env}-${name}-queue-depth`,
447
- AlarmDescription: `Queue ${name} depth exceeds ${depthThreshold} messages`,
448
- MetricName: 'ApproximateNumberOfMessagesVisible',
449
- Namespace: 'AWS/SQS',
450
- Statistic: 'Average',
451
- Period: 300,
452
- EvaluationPeriods: 2,
453
- Threshold: depthThreshold,
454
- ComparisonOperator: 'GreaterThanThreshold',
455
- Dimensions: [{ Name: 'QueueName', Value: { 'Fn::GetAtt': [logicalId, 'QueueName'] } }],
456
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn], OKActions: [alarmTopicArn] }),
457
- },
458
- } as any)
459
-
460
- // Message age alarm
461
- const ageThreshold = alarmsConfig.messageAgeThreshold || 3600
462
- this.builder.addResource(`${logicalId}AgeAlarm`, {
463
- Type: 'AWS::CloudWatch::Alarm',
464
- Properties: {
465
- AlarmName: `${slug}-${env}-${name}-message-age`,
466
- AlarmDescription: `Queue ${name} oldest message exceeds ${ageThreshold} seconds`,
467
- MetricName: 'ApproximateAgeOfOldestMessage',
468
- Namespace: 'AWS/SQS',
469
- Statistic: 'Maximum',
470
- Period: 300,
471
- EvaluationPeriods: 2,
472
- Threshold: ageThreshold,
473
- ComparisonOperator: 'GreaterThanThreshold',
474
- Dimensions: [{ Name: 'QueueName', Value: { 'Fn::GetAtt': [logicalId, 'QueueName'] } }],
475
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn], OKActions: [alarmTopicArn] }),
476
- },
477
- } as any)
478
-
479
- // DLQ alarm (if DLQ is enabled)
480
- if (dlqLogicalId && alarmsConfig.dlqAlarm !== false) {
481
- this.builder.addResource(`${dlqLogicalId}Alarm`, {
482
- Type: 'AWS::CloudWatch::Alarm',
483
- Properties: {
484
- AlarmName: `${slug}-${env}-${name}-dlq-messages`,
485
- AlarmDescription: `Dead letter queue for ${name} has messages`,
486
- MetricName: 'ApproximateNumberOfMessagesVisible',
487
- Namespace: 'AWS/SQS',
488
- Statistic: 'Sum',
489
- Period: 300,
490
- EvaluationPeriods: 1,
491
- Threshold: 0,
492
- ComparisonOperator: 'GreaterThanThreshold',
493
- Dimensions: [{ Name: 'QueueName', Value: { 'Fn::GetAtt': [dlqLogicalId, 'QueueName'] } }],
494
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn] }),
495
- },
496
- } as any)
497
- }
498
- }
499
-
500
- // SNS Subscription
501
- if (queueConfig.subscribe) {
502
- const subConfig = queueConfig.subscribe
503
-
504
- // Determine topic ARN
505
- let topicArn = subConfig.topicArn
506
- if (!topicArn && subConfig.topicName) {
507
- // Reference existing topic in the stack
508
- topicArn = { Ref: subConfig.topicName } as any
509
- }
510
-
511
- if (topicArn) {
512
- // Queue policy to allow SNS to send messages
513
- this.builder.addResource(`${logicalId}SnsPolicy`, {
514
- Type: 'AWS::SQS::QueuePolicy',
515
- Properties: {
516
- Queues: [{ Ref: logicalId }],
517
- PolicyDocument: {
518
- Version: '2012-10-17',
519
- Statement: [{
520
- Effect: 'Allow',
521
- Principal: { Service: 'sns.amazonaws.com' },
522
- Action: 'sqs:SendMessage',
523
- Resource: { 'Fn::GetAtt': [logicalId, 'Arn'] },
524
- Condition: {
525
- ArnEquals: { 'aws:SourceArn': topicArn },
526
- },
527
- }],
528
- },
529
- },
530
- } as any)
531
-
532
- // SNS Subscription
533
- const subscriptionProps: Record<string, any> = {
534
- TopicArn: topicArn,
535
- Protocol: 'sqs',
536
- Endpoint: { 'Fn::GetAtt': [logicalId, 'Arn'] },
537
- RawMessageDelivery: subConfig.rawMessageDelivery || false,
538
- }
539
-
540
- if (subConfig.filterPolicy) {
541
- subscriptionProps.FilterPolicy = subConfig.filterPolicy
542
- subscriptionProps.FilterPolicyScope = subConfig.filterPolicyScope || 'MessageAttributes'
543
- }
544
-
545
- this.builder.addResource(`${logicalId}SnsSub`, {
546
- Type: 'AWS::SNS::Subscription',
547
- Properties: subscriptionProps,
548
- DependsOn: `${logicalId}SnsPolicy`,
549
- } as any)
550
- }
551
- }
552
- }
553
- }
554
-
555
- // Realtime (WebSocket)
556
- if (this.mergedConfig.infrastructure?.realtime?.enabled) {
557
- const realtimeMode = this.mergedConfig.infrastructure.realtime.mode || 'serverless'
558
- if (realtimeMode === 'server') {
559
- this.generateRealtimeServerResources(slug, env)
560
- }
561
- else {
562
- this.generateRealtimeResources(slug, env)
563
- }
564
- }
565
-
566
- // Monitoring
567
- if (this.config.infrastructure?.monitoring?.alarms) {
568
- for (const [name, alarmConfig] of Object.entries(this.config.infrastructure.monitoring.alarms)) {
569
- const { alarm, logicalId } = Monitoring.createAlarm({
570
- slug,
571
- environment: env,
572
- alarmName: `${slug}-${env}-${name}`,
573
- metricName: alarmConfig.metricName || 'Errors',
574
- namespace: alarmConfig.namespace || 'AWS/Lambda',
575
- threshold: alarmConfig.threshold || 1,
576
- comparisonOperator: (alarmConfig.comparisonOperator || 'GreaterThanThreshold') as 'GreaterThanThreshold',
577
- })
578
-
579
- this.builder.addResource(logicalId, alarm)
580
- }
581
- }
582
- }
583
-
584
- /**
585
- * Generate Realtime (WebSocket) infrastructure
586
- * Creates API Gateway WebSocket API, Lambda handlers, DynamoDB tables
587
- */
588
- private generateRealtimeResources(slug: string, env: typeof this.environment): void {
589
- const config = this.mergedConfig.infrastructure?.realtime
590
- if (!config) return
591
-
592
- const apiName = config.name || `${slug}-${env}-realtime`
593
- const scalingConfig = config.scaling || {}
594
- const storageConfig = config.storage || { type: 'dynamodb' }
595
- const handlerMemory = scalingConfig.handlerMemory || 256
596
- const handlerTimeout = scalingConfig.handlerTimeout || 30
597
-
598
- // ========================================
599
- // DynamoDB Tables for Connection Management
600
- // ========================================
601
- const connectionsTableId = `${slug}${env}RealtimeConnections`.replace(/[^a-zA-Z0-9]/g, '')
602
- const channelsTableId = `${slug}${env}RealtimeChannels`.replace(/[^a-zA-Z0-9]/g, '')
603
-
604
- if (storageConfig.type === 'dynamodb') {
605
- const dynamoConfig = storageConfig.dynamodb || {}
606
- const billingMode = dynamoConfig.billingMode || 'PAY_PER_REQUEST'
607
-
608
- // Connections table - stores active WebSocket connections
609
- this.builder.addResource(connectionsTableId, {
610
- Type: 'AWS::DynamoDB::Table',
611
- Properties: {
612
- TableName: `${slug}-${env}-realtime-connections`,
613
- BillingMode: billingMode,
614
- AttributeDefinitions: [
615
- { AttributeName: 'connectionId', AttributeType: 'S' },
616
- { AttributeName: 'userId', AttributeType: 'S' },
617
- ],
618
- KeySchema: [
619
- { AttributeName: 'connectionId', KeyType: 'HASH' },
620
- ],
621
- GlobalSecondaryIndexes: [
622
- {
623
- IndexName: 'userId-index',
624
- KeySchema: [
625
- { AttributeName: 'userId', KeyType: 'HASH' },
626
- ],
627
- Projection: { ProjectionType: 'ALL' },
628
- ...(billingMode === 'PROVISIONED' && {
629
- ProvisionedThroughput: {
630
- ReadCapacityUnits: dynamoConfig.readCapacity || 5,
631
- WriteCapacityUnits: dynamoConfig.writeCapacity || 5,
632
- },
633
- }),
634
- },
635
- ],
636
- TimeToLiveSpecification: {
637
- AttributeName: 'ttl',
638
- Enabled: true,
639
- },
640
- ...(billingMode === 'PROVISIONED' && {
641
- ProvisionedThroughput: {
642
- ReadCapacityUnits: dynamoConfig.readCapacity || 5,
643
- WriteCapacityUnits: dynamoConfig.writeCapacity || 5,
644
- },
645
- }),
646
- ...(dynamoConfig.pointInTimeRecovery && {
647
- PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
648
- }),
649
- Tags: [
650
- { Key: 'Name', Value: `${slug}-${env}-realtime-connections` },
651
- { Key: 'Environment', Value: env },
652
- ],
653
- },
654
- } as any)
655
-
656
- // Channels table - stores channel subscriptions
657
- this.builder.addResource(channelsTableId, {
658
- Type: 'AWS::DynamoDB::Table',
659
- Properties: {
660
- TableName: `${slug}-${env}-realtime-channels`,
661
- BillingMode: billingMode,
662
- AttributeDefinitions: [
663
- { AttributeName: 'channel', AttributeType: 'S' },
664
- { AttributeName: 'connectionId', AttributeType: 'S' },
665
- ],
666
- KeySchema: [
667
- { AttributeName: 'channel', KeyType: 'HASH' },
668
- { AttributeName: 'connectionId', KeyType: 'RANGE' },
669
- ],
670
- GlobalSecondaryIndexes: [
671
- {
672
- IndexName: 'connectionId-index',
673
- KeySchema: [
674
- { AttributeName: 'connectionId', KeyType: 'HASH' },
675
- ],
676
- Projection: { ProjectionType: 'ALL' },
677
- ...(billingMode === 'PROVISIONED' && {
678
- ProvisionedThroughput: {
679
- ReadCapacityUnits: dynamoConfig.readCapacity || 5,
680
- WriteCapacityUnits: dynamoConfig.writeCapacity || 5,
681
- },
682
- }),
683
- },
684
- ],
685
- TimeToLiveSpecification: {
686
- AttributeName: 'ttl',
687
- Enabled: true,
688
- },
689
- ...(billingMode === 'PROVISIONED' && {
690
- ProvisionedThroughput: {
691
- ReadCapacityUnits: dynamoConfig.readCapacity || 5,
692
- WriteCapacityUnits: dynamoConfig.writeCapacity || 5,
693
- },
694
- }),
695
- Tags: [
696
- { Key: 'Name', Value: `${slug}-${env}-realtime-channels` },
697
- { Key: 'Environment', Value: env },
698
- ],
699
- },
700
- } as any)
701
- }
702
-
703
- // ========================================
704
- // IAM Role for WebSocket Handlers
705
- // ========================================
706
- const roleId = `${slug}${env}RealtimeRole`.replace(/[^a-zA-Z0-9]/g, '')
707
-
708
- this.builder.addResource(roleId, {
709
- Type: 'AWS::IAM::Role',
710
- Properties: {
711
- RoleName: `${slug}-${env}-realtime-handler-role`,
712
- AssumeRolePolicyDocument: {
713
- Version: '2012-10-17',
714
- Statement: [{
715
- Effect: 'Allow',
716
- Principal: { Service: 'lambda.amazonaws.com' },
717
- Action: 'sts:AssumeRole',
718
- }],
719
- },
720
- ManagedPolicyArns: [
721
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
722
- ],
723
- Policies: [{
724
- PolicyName: 'RealtimeHandlerPolicy',
725
- PolicyDocument: {
726
- Version: '2012-10-17',
727
- Statement: [
728
- {
729
- Effect: 'Allow',
730
- Action: [
731
- 'dynamodb:GetItem',
732
- 'dynamodb:PutItem',
733
- 'dynamodb:DeleteItem',
734
- 'dynamodb:Query',
735
- 'dynamodb:Scan',
736
- 'dynamodb:UpdateItem',
737
- ],
738
- Resource: [
739
- { 'Fn::GetAtt': [connectionsTableId, 'Arn'] },
740
- { 'Fn::Sub': `\${${connectionsTableId}.Arn}/index/*` },
741
- { 'Fn::GetAtt': [channelsTableId, 'Arn'] },
742
- { 'Fn::Sub': `\${${channelsTableId}.Arn}/index/*` },
743
- ],
744
- },
745
- {
746
- Effect: 'Allow',
747
- Action: 'execute-api:ManageConnections',
748
- Resource: { 'Fn::Sub': 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:*/*' },
749
- },
750
- ],
751
- },
752
- }],
753
- },
754
- } as any)
755
-
756
- // ========================================
757
- // Lambda Handlers for WebSocket Routes
758
- // ========================================
759
- const connectHandlerId = `${slug}${env}RealtimeConnect`.replace(/[^a-zA-Z0-9]/g, '')
760
- const disconnectHandlerId = `${slug}${env}RealtimeDisconnect`.replace(/[^a-zA-Z0-9]/g, '')
761
- const messageHandlerId = `${slug}${env}RealtimeMessage`.replace(/[^a-zA-Z0-9]/g, '')
762
-
763
- // $connect handler
764
- this.builder.addResource(connectHandlerId, {
765
- Type: 'AWS::Lambda::Function',
766
- Properties: {
767
- FunctionName: `${slug}-${env}-realtime-connect`,
768
- Runtime: 'nodejs20.x',
769
- Handler: 'index.handler',
770
- Role: { 'Fn::GetAtt': [roleId, 'Arn'] },
771
- MemorySize: handlerMemory,
772
- Timeout: handlerTimeout,
773
- Environment: {
774
- Variables: {
775
- CONNECTIONS_TABLE: { Ref: connectionsTableId },
776
- CHANNELS_TABLE: { Ref: channelsTableId },
777
- ENVIRONMENT: env,
778
- },
779
- },
780
- Code: {
781
- ZipFile: this.generateConnectHandlerCode(),
782
- },
783
- },
784
- DependsOn: [roleId, connectionsTableId],
785
- } as any)
786
-
787
- // $disconnect handler
788
- this.builder.addResource(disconnectHandlerId, {
789
- Type: 'AWS::Lambda::Function',
790
- Properties: {
791
- FunctionName: `${slug}-${env}-realtime-disconnect`,
792
- Runtime: 'nodejs20.x',
793
- Handler: 'index.handler',
794
- Role: { 'Fn::GetAtt': [roleId, 'Arn'] },
795
- MemorySize: handlerMemory,
796
- Timeout: handlerTimeout,
797
- Environment: {
798
- Variables: {
799
- CONNECTIONS_TABLE: { Ref: connectionsTableId },
800
- CHANNELS_TABLE: { Ref: channelsTableId },
801
- ENVIRONMENT: env,
802
- },
803
- },
804
- Code: {
805
- ZipFile: this.generateDisconnectHandlerCode(),
806
- },
807
- },
808
- DependsOn: [roleId, connectionsTableId, channelsTableId],
809
- } as any)
810
-
811
- // $default (message) handler
812
- this.builder.addResource(messageHandlerId, {
813
- Type: 'AWS::Lambda::Function',
814
- Properties: {
815
- FunctionName: `${slug}-${env}-realtime-message`,
816
- Runtime: 'nodejs20.x',
817
- Handler: 'index.handler',
818
- Role: { 'Fn::GetAtt': [roleId, 'Arn'] },
819
- MemorySize: handlerMemory,
820
- Timeout: handlerTimeout,
821
- Environment: {
822
- Variables: {
823
- CONNECTIONS_TABLE: { Ref: connectionsTableId },
824
- CHANNELS_TABLE: { Ref: channelsTableId },
825
- ENVIRONMENT: env,
826
- },
827
- },
828
- Code: {
829
- ZipFile: this.generateMessageHandlerCode(),
830
- },
831
- },
832
- DependsOn: [roleId, connectionsTableId, channelsTableId],
833
- } as any)
834
-
835
- // ========================================
836
- // API Gateway WebSocket API
837
- // ========================================
838
- const apiId = `${slug}${env}RealtimeApi`.replace(/[^a-zA-Z0-9]/g, '')
839
-
840
- this.builder.addResource(apiId, {
841
- Type: 'AWS::ApiGatewayV2::Api',
842
- Properties: {
843
- Name: apiName,
844
- ProtocolType: 'WEBSOCKET',
845
- RouteSelectionExpression: '$request.body.action',
846
- Tags: {
847
- Name: apiName,
848
- Environment: env,
849
- },
850
- },
851
- } as any)
852
-
853
- // Lambda permissions for API Gateway
854
- const connectPermId = `${connectHandlerId}Permission`
855
- const disconnectPermId = `${disconnectHandlerId}Permission`
856
- const messagePermId = `${messageHandlerId}Permission`
857
-
858
- this.builder.addResource(connectPermId, {
859
- Type: 'AWS::Lambda::Permission',
860
- Properties: {
861
- FunctionName: { Ref: connectHandlerId },
862
- Action: 'lambda:InvokeFunction',
863
- Principal: 'apigateway.amazonaws.com',
864
- SourceArn: { 'Fn::Sub': `arn:aws:execute-api:\${AWS::Region}:\${AWS::AccountId}:\${${apiId}}/*/$connect` },
865
- },
866
- } as any)
867
-
868
- this.builder.addResource(disconnectPermId, {
869
- Type: 'AWS::Lambda::Permission',
870
- Properties: {
871
- FunctionName: { Ref: disconnectHandlerId },
872
- Action: 'lambda:InvokeFunction',
873
- Principal: 'apigateway.amazonaws.com',
874
- SourceArn: { 'Fn::Sub': `arn:aws:execute-api:\${AWS::Region}:\${AWS::AccountId}:\${${apiId}}/*/$disconnect` },
875
- },
876
- } as any)
877
-
878
- this.builder.addResource(messagePermId, {
879
- Type: 'AWS::Lambda::Permission',
880
- Properties: {
881
- FunctionName: { Ref: messageHandlerId },
882
- Action: 'lambda:InvokeFunction',
883
- Principal: 'apigateway.amazonaws.com',
884
- SourceArn: { 'Fn::Sub': `arn:aws:execute-api:\${AWS::Region}:\${AWS::AccountId}:\${${apiId}}/*/$default` },
885
- },
886
- } as any)
887
-
888
- // Integrations
889
- const connectIntegId = `${apiId}ConnectInteg`
890
- const disconnectIntegId = `${apiId}DisconnectInteg`
891
- const messageIntegId = `${apiId}MessageInteg`
892
-
893
- this.builder.addResource(connectIntegId, {
894
- Type: 'AWS::ApiGatewayV2::Integration',
895
- Properties: {
896
- ApiId: { Ref: apiId },
897
- IntegrationType: 'AWS_PROXY',
898
- IntegrationUri: { 'Fn::Sub': `arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${${connectHandlerId}.Arn}/invocations` },
899
- },
900
- } as any)
901
-
902
- this.builder.addResource(disconnectIntegId, {
903
- Type: 'AWS::ApiGatewayV2::Integration',
904
- Properties: {
905
- ApiId: { Ref: apiId },
906
- IntegrationType: 'AWS_PROXY',
907
- IntegrationUri: { 'Fn::Sub': `arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${${disconnectHandlerId}.Arn}/invocations` },
908
- },
909
- } as any)
910
-
911
- this.builder.addResource(messageIntegId, {
912
- Type: 'AWS::ApiGatewayV2::Integration',
913
- Properties: {
914
- ApiId: { Ref: apiId },
915
- IntegrationType: 'AWS_PROXY',
916
- IntegrationUri: { 'Fn::Sub': `arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${${messageHandlerId}.Arn}/invocations` },
917
- },
918
- } as any)
919
-
920
- // Routes
921
- this.builder.addResource(`${apiId}ConnectRoute`, {
922
- Type: 'AWS::ApiGatewayV2::Route',
923
- Properties: {
924
- ApiId: { Ref: apiId },
925
- RouteKey: '$connect',
926
- AuthorizationType: 'NONE',
927
- Target: { 'Fn::Sub': `integrations/\${${connectIntegId}}` },
928
- },
929
- } as any)
930
-
931
- this.builder.addResource(`${apiId}DisconnectRoute`, {
932
- Type: 'AWS::ApiGatewayV2::Route',
933
- Properties: {
934
- ApiId: { Ref: apiId },
935
- RouteKey: '$disconnect',
936
- Target: { 'Fn::Sub': `integrations/\${${disconnectIntegId}}` },
937
- },
938
- } as any)
939
-
940
- this.builder.addResource(`${apiId}DefaultRoute`, {
941
- Type: 'AWS::ApiGatewayV2::Route',
942
- Properties: {
943
- ApiId: { Ref: apiId },
944
- RouteKey: '$default',
945
- Target: { 'Fn::Sub': `integrations/\${${messageIntegId}}` },
946
- },
947
- } as any)
948
-
949
- // Stage
950
- const stageId = `${apiId}Stage`
951
- this.builder.addResource(stageId, {
952
- Type: 'AWS::ApiGatewayV2::Stage',
953
- Properties: {
954
- ApiId: { Ref: apiId },
955
- StageName: env,
956
- AutoDeploy: true,
957
- DefaultRouteSettings: {
958
- ThrottlingBurstLimit: scalingConfig.messagesPerSecond || 1000,
959
- ThrottlingRateLimit: scalingConfig.messagesPerSecond || 1000,
960
- },
961
- },
962
- } as any)
963
-
964
- // ========================================
965
- // CloudWatch Alarms (if monitoring enabled)
966
- // ========================================
967
- if (config.monitoring?.enabled) {
968
- const monitoringConfig = config.monitoring
969
- let alarmTopicArn = monitoringConfig.notificationTopicArn
970
-
971
- // Create SNS topic if emails provided
972
- if (!alarmTopicArn && monitoringConfig.notificationEmails?.length) {
973
- const topicId = `${apiId}AlarmTopic`
974
- this.builder.addResource(topicId, {
975
- Type: 'AWS::SNS::Topic',
976
- Properties: {
977
- TopicName: `${slug}-${env}-realtime-alarms`,
978
- DisplayName: 'Realtime WebSocket Alarms',
979
- },
980
- } as any)
981
-
982
- monitoringConfig.notificationEmails.forEach((email, idx) => {
983
- this.builder.addResource(`${topicId}Sub${idx}`, {
984
- Type: 'AWS::SNS::Subscription',
985
- Properties: {
986
- TopicArn: { Ref: topicId },
987
- Protocol: 'email',
988
- Endpoint: email,
989
- },
990
- } as any)
991
- })
992
-
993
- alarmTopicArn = { Ref: topicId } as any
994
- }
995
-
996
- // Connection count alarm
997
- if (monitoringConfig.connectionThreshold) {
998
- this.builder.addResource(`${apiId}ConnectionAlarm`, {
999
- Type: 'AWS::CloudWatch::Alarm',
1000
- Properties: {
1001
- AlarmName: `${slug}-${env}-realtime-connections`,
1002
- AlarmDescription: `WebSocket connections exceed ${monitoringConfig.connectionThreshold}`,
1003
- MetricName: 'ConnectCount',
1004
- Namespace: 'AWS/ApiGateway',
1005
- Statistic: 'Sum',
1006
- Period: 300,
1007
- EvaluationPeriods: 2,
1008
- Threshold: monitoringConfig.connectionThreshold,
1009
- ComparisonOperator: 'GreaterThanThreshold',
1010
- Dimensions: [{ Name: 'ApiId', Value: { Ref: apiId } }],
1011
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn] }),
1012
- },
1013
- } as any)
1014
- }
1015
-
1016
- // Error rate alarm
1017
- if (monitoringConfig.errorThreshold) {
1018
- this.builder.addResource(`${apiId}ErrorAlarm`, {
1019
- Type: 'AWS::CloudWatch::Alarm',
1020
- Properties: {
1021
- AlarmName: `${slug}-${env}-realtime-errors`,
1022
- AlarmDescription: `WebSocket errors exceed ${monitoringConfig.errorThreshold}/min`,
1023
- MetricName: 'ExecutionError',
1024
- Namespace: 'AWS/ApiGateway',
1025
- Statistic: 'Sum',
1026
- Period: 60,
1027
- EvaluationPeriods: 3,
1028
- Threshold: monitoringConfig.errorThreshold,
1029
- ComparisonOperator: 'GreaterThanThreshold',
1030
- Dimensions: [{ Name: 'ApiId', Value: { Ref: apiId } }],
1031
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn] }),
1032
- },
1033
- } as any)
1034
- }
1035
- }
1036
-
1037
- // ========================================
1038
- // Outputs
1039
- // ========================================
1040
- this.builder.addOutput(`${apiId}Endpoint`, {
1041
- Description: 'WebSocket API endpoint URL',
1042
- Value: { 'Fn::Sub': `wss://\${${apiId}}.execute-api.\${AWS::Region}.amazonaws.com/${env}` },
1043
- Export: { Name: { 'Fn::Sub': `\${AWS::StackName}-realtime-endpoint` } as any },
1044
- })
1045
-
1046
- this.builder.addOutput(`${apiId}Id`, {
1047
- Description: 'WebSocket API ID',
1048
- Value: { Ref: apiId },
1049
- Export: { Name: { 'Fn::Sub': `\${AWS::StackName}-realtime-api-id` } as any },
1050
- })
1051
- }
1052
-
1053
- /**
1054
- * Generate $connect Lambda handler code
1055
- */
1056
- private generateConnectHandlerCode(): string {
1057
- return `
1058
- const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
1059
- const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb');
1060
-
1061
- const client = new DynamoDBClient({});
1062
- const docClient = DynamoDBDocumentClient.from(client);
1063
-
1064
- exports.handler = async (event) => {
1065
- const connectionId = event.requestContext.connectionId;
1066
- const userId = event.queryStringParameters?.userId || 'anonymous';
1067
- const ttl = Math.floor(Date.now() / 1000) + 86400; // 24 hours
1068
-
1069
- try {
1070
- await docClient.send(new PutCommand({
1071
- TableName: process.env.CONNECTIONS_TABLE,
1072
- Item: {
1073
- connectionId,
1074
- userId,
1075
- connectedAt: new Date().toISOString(),
1076
- ttl,
1077
- },
1078
- }));
1079
-
1080
- return { statusCode: 200, body: 'Connected' };
1081
- } catch (error) {
1082
- console.error('Connect error:', error);
1083
- return { statusCode: 500, body: 'Failed to connect' };
1084
- }
1085
- };
1086
- `.trim()
1087
- }
1088
-
1089
- /**
1090
- * Generate $disconnect Lambda handler code
1091
- */
1092
- private generateDisconnectHandlerCode(): string {
1093
- return `
1094
- const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
1095
- const { DynamoDBDocumentClient, DeleteCommand, QueryCommand } = require('@aws-sdk/lib-dynamodb');
1096
-
1097
- const client = new DynamoDBClient({});
1098
- const docClient = DynamoDBDocumentClient.from(client);
1099
-
1100
- exports.handler = async (event) => {
1101
- const connectionId = event.requestContext.connectionId;
1102
-
1103
- try {
1104
- // Remove connection record
1105
- await docClient.send(new DeleteCommand({
1106
- TableName: process.env.CONNECTIONS_TABLE,
1107
- Key: { connectionId },
1108
- }));
1109
-
1110
- // Remove all channel subscriptions for this connection
1111
- const subscriptions = await docClient.send(new QueryCommand({
1112
- TableName: process.env.CHANNELS_TABLE,
1113
- IndexName: 'connectionId-index',
1114
- KeyConditionExpression: 'connectionId = :cid',
1115
- ExpressionAttributeValues: { ':cid': connectionId },
1116
- }));
1117
-
1118
- if (subscriptions.Items) {
1119
- for (const sub of subscriptions.Items) {
1120
- await docClient.send(new DeleteCommand({
1121
- TableName: process.env.CHANNELS_TABLE,
1122
- Key: { channel: sub.channel, connectionId },
1123
- }));
1124
- }
1125
- }
1126
-
1127
- return { statusCode: 200, body: 'Disconnected' };
1128
- } catch (error) {
1129
- console.error('Disconnect error:', error);
1130
- return { statusCode: 500, body: 'Failed to disconnect' };
1131
- }
1132
- };
1133
- `.trim()
1134
- }
1135
-
1136
- /**
1137
- * Generate $default (message) Lambda handler code
1138
- */
1139
- private generateMessageHandlerCode(): string {
1140
- return `
1141
- const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
1142
- const { DynamoDBDocumentClient, PutCommand, DeleteCommand, QueryCommand, GetCommand } = require('@aws-sdk/lib-dynamodb');
1143
- const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require('@aws-sdk/client-apigatewaymanagementapi');
1144
-
1145
- const dynamoClient = new DynamoDBClient({});
1146
- const docClient = DynamoDBDocumentClient.from(dynamoClient);
1147
-
1148
- exports.handler = async (event) => {
1149
- const connectionId = event.requestContext.connectionId;
1150
- const endpoint = \`https://\${event.requestContext.domainName}/\${event.requestContext.stage}\`;
1151
- const apiClient = new ApiGatewayManagementApiClient({ endpoint });
1152
-
1153
- let body;
1154
- try {
1155
- body = JSON.parse(event.body);
1156
- } catch {
1157
- return { statusCode: 400, body: 'Invalid JSON' };
1158
- }
1159
-
1160
- const { action, channel, data } = body;
1161
-
1162
- try {
1163
- switch (action) {
1164
- case 'subscribe': {
1165
- const ttl = Math.floor(Date.now() / 1000) + 86400;
1166
- await docClient.send(new PutCommand({
1167
- TableName: process.env.CHANNELS_TABLE,
1168
- Item: { channel, connectionId, subscribedAt: new Date().toISOString(), ttl },
1169
- }));
1170
- return { statusCode: 200, body: JSON.stringify({ action: 'subscribed', channel }) };
1171
- }
1172
-
1173
- case 'unsubscribe': {
1174
- await docClient.send(new DeleteCommand({
1175
- TableName: process.env.CHANNELS_TABLE,
1176
- Key: { channel, connectionId },
1177
- }));
1178
- return { statusCode: 200, body: JSON.stringify({ action: 'unsubscribed', channel }) };
1179
- }
1180
-
1181
- case 'broadcast': {
1182
- // Get all subscribers for the channel
1183
- const subscribers = await docClient.send(new QueryCommand({
1184
- TableName: process.env.CHANNELS_TABLE,
1185
- KeyConditionExpression: 'channel = :channel',
1186
- ExpressionAttributeValues: { ':channel': channel },
1187
- }));
1188
-
1189
- const message = JSON.stringify({ channel, event: body.event, data });
1190
-
1191
- // Send to all subscribers
1192
- const sendPromises = (subscribers.Items || []).map(async (sub) => {
1193
- try {
1194
- await apiClient.send(new PostToConnectionCommand({
1195
- ConnectionId: sub.connectionId,
1196
- Data: message,
1197
- }));
1198
- } catch (error) {
1199
- if (error.statusCode === 410) {
1200
- // Connection is stale, remove it
1201
- await docClient.send(new DeleteCommand({
1202
- TableName: process.env.CHANNELS_TABLE,
1203
- Key: { channel, connectionId: sub.connectionId },
1204
- }));
1205
- }
1206
- }
1207
- });
1208
-
1209
- await Promise.all(sendPromises);
1210
- return { statusCode: 200, body: JSON.stringify({ action: 'broadcasted', channel, recipients: subscribers.Items?.length || 0 }) };
1211
- }
1212
-
1213
- case 'ping': {
1214
- return { statusCode: 200, body: JSON.stringify({ action: 'pong', timestamp: Date.now() }) };
1215
- }
1216
-
1217
- default:
1218
- return { statusCode: 400, body: JSON.stringify({ error: 'Unknown action', action }) };
1219
- }
1220
- } catch (error) {
1221
- console.error('Message handler error:', error);
1222
- return { statusCode: 500, body: JSON.stringify({ error: 'Internal error' }) };
1223
- }
1224
- };
1225
- `.trim()
1226
- }
1227
-
1228
- /**
1229
- * Generate Realtime Server Mode infrastructure (ts-broadcasting)
1230
- * Creates ECS/EC2 resources for running the Bun WebSocket server
1231
- */
1232
- private generateRealtimeServerResources(slug: string, env: typeof this.environment): void {
1233
- const config = this.mergedConfig.infrastructure?.realtime
1234
- if (!config) return
1235
-
1236
- const serverConfig = config.server || {}
1237
- const port = serverConfig.port || 6001
1238
- const instances = serverConfig.instances || 1
1239
-
1240
- // ========================================
1241
- // Security Group for WebSocket Server
1242
- // ========================================
1243
- const sgId = `${slug}${env}RealtimeSG`.replace(/[^a-zA-Z0-9]/g, '')
1244
-
1245
- this.builder.addResource(sgId, {
1246
- Type: 'AWS::EC2::SecurityGroup',
1247
- Properties: {
1248
- GroupDescription: `Security group for ${slug} realtime WebSocket server`,
1249
- GroupName: `${slug}-${env}-realtime-sg`,
1250
- VpcId: { Ref: 'VPC' },
1251
- SecurityGroupIngress: [
1252
- {
1253
- IpProtocol: 'tcp',
1254
- FromPort: port,
1255
- ToPort: port,
1256
- CidrIp: '0.0.0.0/0',
1257
- Description: 'WebSocket connections',
1258
- },
1259
- {
1260
- IpProtocol: 'tcp',
1261
- FromPort: 443,
1262
- ToPort: 443,
1263
- CidrIp: '0.0.0.0/0',
1264
- Description: 'HTTPS/WSS connections',
1265
- },
1266
- ],
1267
- SecurityGroupEgress: [
1268
- {
1269
- IpProtocol: '-1',
1270
- CidrIp: '0.0.0.0/0',
1271
- Description: 'Allow all outbound',
1272
- },
1273
- ],
1274
- Tags: [
1275
- { Key: 'Name', Value: `${slug}-${env}-realtime-sg` },
1276
- { Key: 'Environment', Value: env },
1277
- ],
1278
- },
1279
- } as any)
1280
-
1281
- // ========================================
1282
- // ECS Task Definition for ts-broadcasting
1283
- // ========================================
1284
- const taskDefId = `${slug}${env}RealtimeTaskDef`.replace(/[^a-zA-Z0-9]/g, '')
1285
- const taskRoleId = `${slug}${env}RealtimeTaskRole`.replace(/[^a-zA-Z0-9]/g, '')
1286
- const execRoleId = `${slug}${env}RealtimeExecRole`.replace(/[^a-zA-Z0-9]/g, '')
1287
-
1288
- // Task execution role
1289
- this.builder.addResource(execRoleId, {
1290
- Type: 'AWS::IAM::Role',
1291
- Properties: {
1292
- RoleName: `${slug}-${env}-realtime-exec-role`,
1293
- AssumeRolePolicyDocument: {
1294
- Version: '2012-10-17',
1295
- Statement: [{
1296
- Effect: 'Allow',
1297
- Principal: { Service: 'ecs-tasks.amazonaws.com' },
1298
- Action: 'sts:AssumeRole',
1299
- }],
1300
- },
1301
- ManagedPolicyArns: [
1302
- 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
1303
- ],
1304
- },
1305
- } as any)
1306
-
1307
- // Task role (for the application)
1308
- this.builder.addResource(taskRoleId, {
1309
- Type: 'AWS::IAM::Role',
1310
- Properties: {
1311
- RoleName: `${slug}-${env}-realtime-task-role`,
1312
- AssumeRolePolicyDocument: {
1313
- Version: '2012-10-17',
1314
- Statement: [{
1315
- Effect: 'Allow',
1316
- Principal: { Service: 'ecs-tasks.amazonaws.com' },
1317
- Action: 'sts:AssumeRole',
1318
- }],
1319
- },
1320
- Policies: [{
1321
- PolicyName: 'RealtimeTaskPolicy',
1322
- PolicyDocument: {
1323
- Version: '2012-10-17',
1324
- Statement: [
1325
- {
1326
- Effect: 'Allow',
1327
- Action: [
1328
- 'logs:CreateLogStream',
1329
- 'logs:PutLogEvents',
1330
- ],
1331
- Resource: '*',
1332
- },
1333
- // Add ElastiCache access if Redis is enabled
1334
- ...(serverConfig.redis?.enabled ? [{
1335
- Effect: 'Allow',
1336
- Action: [
1337
- 'elasticache:DescribeCacheClusters',
1338
- 'elasticache:DescribeReplicationGroups',
1339
- ],
1340
- Resource: '*',
1341
- }] : []),
1342
- ],
1343
- },
1344
- }],
1345
- },
1346
- } as any)
1347
-
1348
- // Build environment variables for ts-broadcasting
1349
- const envVars: Array<{ Name: string, Value: any }> = [
1350
- { Name: 'BROADCAST_HOST', Value: serverConfig.host || '0.0.0.0' },
1351
- { Name: 'BROADCAST_PORT', Value: String(port) },
1352
- { Name: 'NODE_ENV', Value: env === 'production' ? 'production' : 'development' },
1353
- ]
1354
-
1355
- if (serverConfig.redis?.enabled) {
1356
- if (serverConfig.redis.useElastiCache) {
1357
- envVars.push({ Name: 'REDIS_HOST', Value: { 'Fn::GetAtt': ['CacheCluster', 'RedisEndpoint.Address'] } })
1358
- envVars.push({ Name: 'REDIS_PORT', Value: { 'Fn::GetAtt': ['CacheCluster', 'RedisEndpoint.Port'] } })
1359
- }
1360
- else {
1361
- envVars.push({ Name: 'REDIS_HOST', Value: serverConfig.redis.host || 'localhost' })
1362
- envVars.push({ Name: 'REDIS_PORT', Value: String(serverConfig.redis.port || 6379) })
1363
- }
1364
- if (serverConfig.redis.keyPrefix) {
1365
- envVars.push({ Name: 'REDIS_KEY_PREFIX', Value: serverConfig.redis.keyPrefix })
1366
- }
1367
- }
1368
-
1369
- // Task definition
1370
- this.builder.addResource(taskDefId, {
1371
- Type: 'AWS::ECS::TaskDefinition',
1372
- Properties: {
1373
- Family: `${slug}-${env}-realtime`,
1374
- NetworkMode: 'awsvpc',
1375
- RequiresCompatibilities: ['FARGATE'],
1376
- Cpu: '512',
1377
- Memory: '1024',
1378
- ExecutionRoleArn: { 'Fn::GetAtt': [execRoleId, 'Arn'] },
1379
- TaskRoleArn: { 'Fn::GetAtt': [taskRoleId, 'Arn'] },
1380
- ContainerDefinitions: [{
1381
- Name: 'realtime',
1382
- Image: { 'Fn::Sub': `\${AWS::AccountId}.dkr.ecr.\${AWS::Region}.amazonaws.com/${slug}-realtime:latest` },
1383
- Essential: true,
1384
- PortMappings: [{
1385
- ContainerPort: port,
1386
- Protocol: 'tcp',
1387
- }],
1388
- Environment: envVars,
1389
- LogConfiguration: {
1390
- LogDriver: 'awslogs',
1391
- Options: {
1392
- 'awslogs-group': `/ecs/${slug}-${env}-realtime`,
1393
- 'awslogs-region': { Ref: 'AWS::Region' },
1394
- 'awslogs-stream-prefix': 'realtime',
1395
- },
1396
- },
1397
- HealthCheck: {
1398
- Command: ['CMD-SHELL', `curl -f http://localhost:${port}${serverConfig.healthCheckPath || '/health'} || exit 1`],
1399
- Interval: 30,
1400
- Timeout: 5,
1401
- Retries: 3,
1402
- StartPeriod: 60,
1403
- },
1404
- }],
1405
- Tags: [
1406
- { Key: 'Name', Value: `${slug}-${env}-realtime` },
1407
- { Key: 'Environment', Value: env },
1408
- ],
1409
- },
1410
- DependsOn: [execRoleId, taskRoleId],
1411
- } as any)
1412
-
1413
- // CloudWatch Log Group
1414
- const logGroupId = `${slug}${env}RealtimeLogs`.replace(/[^a-zA-Z0-9]/g, '')
1415
- this.builder.addResource(logGroupId, {
1416
- Type: 'AWS::Logs::LogGroup',
1417
- Properties: {
1418
- LogGroupName: `/ecs/${slug}-${env}-realtime`,
1419
- RetentionInDays: 30,
1420
- },
1421
- } as any)
1422
-
1423
- // ========================================
1424
- // ECS Service
1425
- // ========================================
1426
- const serviceId = `${slug}${env}RealtimeService`.replace(/[^a-zA-Z0-9]/g, '')
1427
-
1428
- this.builder.addResource(serviceId, {
1429
- Type: 'AWS::ECS::Service',
1430
- Properties: {
1431
- ServiceName: `${slug}-${env}-realtime`,
1432
- Cluster: { Ref: 'ECSCluster' },
1433
- TaskDefinition: { Ref: taskDefId },
1434
- DesiredCount: instances,
1435
- LaunchType: 'FARGATE',
1436
- NetworkConfiguration: {
1437
- AwsvpcConfiguration: {
1438
- AssignPublicIp: 'ENABLED',
1439
- SecurityGroups: [{ Ref: sgId }],
1440
- Subnets: [{ Ref: 'PublicSubnet1' }, { Ref: 'PublicSubnet2' }],
1441
- },
1442
- },
1443
- HealthCheckGracePeriodSeconds: 60,
1444
- Tags: [
1445
- { Key: 'Name', Value: `${slug}-${env}-realtime` },
1446
- { Key: 'Environment', Value: env },
1447
- ],
1448
- },
1449
- DependsOn: [taskDefId, sgId, logGroupId],
1450
- } as any)
1451
-
1452
- // ========================================
1453
- // Auto Scaling (if configured)
1454
- // ========================================
1455
- if (serverConfig.autoScaling) {
1456
- const scalingConfig = serverConfig.autoScaling
1457
- const scalableTargetId = `${slug}${env}RealtimeScalableTarget`.replace(/[^a-zA-Z0-9]/g, '')
1458
-
1459
- this.builder.addResource(scalableTargetId, {
1460
- Type: 'AWS::ApplicationAutoScaling::ScalableTarget',
1461
- Properties: {
1462
- MaxCapacity: scalingConfig.max || 10,
1463
- MinCapacity: scalingConfig.min || 1,
1464
- ResourceId: { 'Fn::Sub': `service/\${ECSCluster}/${slug}-${env}-realtime` },
1465
- RoleARN: { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService' },
1466
- ScalableDimension: 'ecs:service:DesiredCount',
1467
- ServiceNamespace: 'ecs',
1468
- },
1469
- DependsOn: serviceId,
1470
- } as any)
1471
-
1472
- // CPU-based scaling policy
1473
- if (scalingConfig.targetCPU) {
1474
- this.builder.addResource(`${slug}${env}RealtimeCPUPolicy`.replace(/[^a-zA-Z0-9]/g, ''), {
1475
- Type: 'AWS::ApplicationAutoScaling::ScalingPolicy',
1476
- Properties: {
1477
- PolicyName: `${slug}-${env}-realtime-cpu-scaling`,
1478
- PolicyType: 'TargetTrackingScaling',
1479
- ScalingTargetId: { Ref: scalableTargetId },
1480
- TargetTrackingScalingPolicyConfiguration: {
1481
- PredefinedMetricSpecification: {
1482
- PredefinedMetricType: 'ECSServiceAverageCPUUtilization',
1483
- },
1484
- TargetValue: scalingConfig.targetCPU,
1485
- ScaleInCooldown: 300,
1486
- ScaleOutCooldown: 60,
1487
- },
1488
- },
1489
- } as any)
1490
- }
1491
- }
1492
-
1493
- // ========================================
1494
- // CloudWatch Alarms (if monitoring enabled)
1495
- // ========================================
1496
- if (config.monitoring?.enabled) {
1497
- const monitoringConfig = config.monitoring
1498
- let alarmTopicArn = monitoringConfig.notificationTopicArn
1499
-
1500
- if (!alarmTopicArn && monitoringConfig.notificationEmails?.length) {
1501
- const topicId = `${slug}${env}RealtimeAlarmTopic`.replace(/[^a-zA-Z0-9]/g, '')
1502
- this.builder.addResource(topicId, {
1503
- Type: 'AWS::SNS::Topic',
1504
- Properties: {
1505
- TopicName: `${slug}-${env}-realtime-alarms`,
1506
- DisplayName: 'Realtime Server Alarms',
1507
- },
1508
- } as any)
1509
-
1510
- monitoringConfig.notificationEmails.forEach((email, idx) => {
1511
- this.builder.addResource(`${topicId}Sub${idx}`, {
1512
- Type: 'AWS::SNS::Subscription',
1513
- Properties: {
1514
- TopicArn: { Ref: topicId },
1515
- Protocol: 'email',
1516
- Endpoint: email,
1517
- },
1518
- } as any)
1519
- })
1520
-
1521
- alarmTopicArn = { Ref: topicId } as any
1522
- }
1523
-
1524
- // CPU alarm
1525
- this.builder.addResource(`${slug}${env}RealtimeCPUAlarm`.replace(/[^a-zA-Z0-9]/g, ''), {
1526
- Type: 'AWS::CloudWatch::Alarm',
1527
- Properties: {
1528
- AlarmName: `${slug}-${env}-realtime-high-cpu`,
1529
- AlarmDescription: 'Realtime server CPU utilization is high',
1530
- MetricName: 'CPUUtilization',
1531
- Namespace: 'AWS/ECS',
1532
- Statistic: 'Average',
1533
- Period: 300,
1534
- EvaluationPeriods: 2,
1535
- Threshold: 80,
1536
- ComparisonOperator: 'GreaterThanThreshold',
1537
- Dimensions: [
1538
- { Name: 'ClusterName', Value: { Ref: 'ECSCluster' } },
1539
- { Name: 'ServiceName', Value: `${slug}-${env}-realtime` },
1540
- ],
1541
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn] }),
1542
- },
1543
- DependsOn: serviceId,
1544
- } as any)
1545
-
1546
- // Memory alarm
1547
- this.builder.addResource(`${slug}${env}RealtimeMemoryAlarm`.replace(/[^a-zA-Z0-9]/g, ''), {
1548
- Type: 'AWS::CloudWatch::Alarm',
1549
- Properties: {
1550
- AlarmName: `${slug}-${env}-realtime-high-memory`,
1551
- AlarmDescription: 'Realtime server memory utilization is high',
1552
- MetricName: 'MemoryUtilization',
1553
- Namespace: 'AWS/ECS',
1554
- Statistic: 'Average',
1555
- Period: 300,
1556
- EvaluationPeriods: 2,
1557
- Threshold: 80,
1558
- ComparisonOperator: 'GreaterThanThreshold',
1559
- Dimensions: [
1560
- { Name: 'ClusterName', Value: { Ref: 'ECSCluster' } },
1561
- { Name: 'ServiceName', Value: `${slug}-${env}-realtime` },
1562
- ],
1563
- ...(alarmTopicArn && { AlarmActions: [alarmTopicArn] }),
1564
- },
1565
- DependsOn: serviceId,
1566
- } as any)
1567
- }
1568
-
1569
- // ========================================
1570
- // Outputs
1571
- // ========================================
1572
- this.builder.addOutput(`${slug}${env}RealtimeEndpoint`.replace(/[^a-zA-Z0-9]/g, ''), {
1573
- Description: 'Realtime WebSocket server endpoint',
1574
- Value: { 'Fn::Sub': `wss://${slug}-${env}-realtime.\${AWS::Region}.elb.amazonaws.com:${port}` },
1575
- Export: { Name: { 'Fn::Sub': `\${AWS::StackName}-realtime-endpoint` } as any },
1576
- })
1577
- }
1578
-
1579
- /**
1580
- * Generate broadcast.config.ts content for ts-broadcasting
1581
- */
1582
- generateBroadcastConfig(): string {
1583
- const config = this.mergedConfig.infrastructure?.realtime
1584
- if (!config || config.mode !== 'server') return ''
1585
-
1586
- const serverConfig = config.server || {}
1587
-
1588
- return `import type { BroadcastConfig } from 'ts-broadcasting'
1589
-
1590
- export default {
1591
- verbose: ${this.environment !== 'production'},
1592
- driver: '${serverConfig.driver || 'bun'}',
1593
- default: 'bun',
1594
-
1595
- connections: {
1596
- bun: {
1597
- driver: 'bun',
1598
- host: process.env.BROADCAST_HOST || '${serverConfig.host || '0.0.0.0'}',
1599
- port: Number(process.env.BROADCAST_PORT) || ${serverConfig.port || 6001},
1600
- scheme: '${serverConfig.scheme || 'wss'}',
1601
- options: {
1602
- idleTimeout: ${serverConfig.idleTimeout || 120},
1603
- maxPayloadLength: ${serverConfig.maxPayloadLength || 16 * 1024 * 1024},
1604
- backpressureLimit: ${serverConfig.backpressureLimit || 1024 * 1024},
1605
- closeOnBackpressureLimit: ${serverConfig.closeOnBackpressureLimit || false},
1606
- sendPings: ${serverConfig.sendPings !== false},
1607
- perMessageDeflate: ${serverConfig.perMessageDeflate !== false},
1608
- },
1609
- },
1610
- },
1611
- ${serverConfig.redis?.enabled ? `
1612
- redis: {
1613
- host: process.env.REDIS_HOST || '${serverConfig.redis.host || 'localhost'}',
1614
- port: Number(process.env.REDIS_PORT) || ${serverConfig.redis.port || 6379},
1615
- ${serverConfig.redis.password ? `password: process.env.REDIS_PASSWORD || '${serverConfig.redis.password}',` : ''}
1616
- database: ${serverConfig.redis.database || 0},
1617
- keyPrefix: '${serverConfig.redis.keyPrefix || 'broadcasting:'}',
1618
- },
1619
- ` : ''}
1620
- ${serverConfig.rateLimit?.enabled ? `
1621
- rateLimit: {
1622
- max: ${serverConfig.rateLimit.max || 100},
1623
- window: ${serverConfig.rateLimit.window || 60000},
1624
- perChannel: ${serverConfig.rateLimit.perChannel !== false},
1625
- perUser: ${serverConfig.rateLimit.perUser !== false},
1626
- },
1627
- ` : ''}
1628
- ${serverConfig.loadManagement?.enabled ? `
1629
- loadManagement: {
1630
- enabled: true,
1631
- maxConnections: ${serverConfig.loadManagement.maxConnections || 10000},
1632
- maxSubscriptionsPerConnection: ${serverConfig.loadManagement.maxSubscriptionsPerConnection || 100},
1633
- shedLoadThreshold: ${serverConfig.loadManagement.shedLoadThreshold || 0.8},
1634
- },
1635
- ` : ''}
1636
- } satisfies BroadcastConfig
1637
- `
1638
- }
1639
-
1640
- /**
1641
- * Generate YAML output
1642
- */
1643
- toYAML(): string {
1644
- return this.builder.toYAML()
1645
- }
1646
-
1647
- /**
1648
- * Generate JSON output
1649
- */
1650
- toJSON(): string {
1651
- return this.builder.toJSON()
1652
- }
1653
-
1654
- /**
1655
- * Get the template builder
1656
- */
1657
- getBuilder(): TemplateBuilder {
1658
- return this.builder
1659
- }
1660
- }