@stacksjs/ts-cloud 0.1.9 → 0.1.14

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 (150) hide show
  1. package/README.md +39 -377
  2. package/dist/bin/cli.js +1047 -424
  3. package/dist/index.d.ts +36 -3
  4. package/dist/index.js +76430 -7096
  5. package/package.json +7 -8
  6. package/dist/aws/acm.d.ts +0 -129
  7. package/dist/aws/application-autoscaling.d.ts +0 -282
  8. package/dist/aws/bedrock.d.ts +0 -2292
  9. package/dist/aws/client.d.ts +0 -79
  10. package/dist/aws/cloudformation.d.ts +0 -105
  11. package/dist/aws/cloudfront.d.ts +0 -265
  12. package/dist/aws/cloudwatch-logs.d.ts +0 -48
  13. package/dist/aws/comprehend.d.ts +0 -505
  14. package/dist/aws/connect.d.ts +0 -377
  15. package/dist/aws/deploy-imap.d.ts +0 -14
  16. package/dist/aws/dynamodb.d.ts +0 -176
  17. package/dist/aws/ec2.d.ts +0 -272
  18. package/dist/aws/ecr.d.ts +0 -149
  19. package/dist/aws/ecs.d.ts +0 -162
  20. package/dist/aws/elasticache.d.ts +0 -71
  21. package/dist/aws/elbv2.d.ts +0 -248
  22. package/dist/aws/email.d.ts +0 -175
  23. package/dist/aws/eventbridge.d.ts +0 -142
  24. package/dist/aws/iam.d.ts +0 -638
  25. package/dist/aws/imap-server.d.ts +0 -119
  26. package/dist/aws/index.d.ts +0 -192
  27. package/dist/aws/kendra.d.ts +0 -782
  28. package/dist/aws/lambda.d.ts +0 -232
  29. package/dist/aws/opensearch.d.ts +0 -87
  30. package/dist/aws/personalize.d.ts +0 -516
  31. package/dist/aws/polly.d.ts +0 -214
  32. package/dist/aws/rds.d.ts +0 -240
  33. package/dist/aws/rekognition.d.ts +0 -543
  34. package/dist/aws/route53-domains.d.ts +0 -113
  35. package/dist/aws/route53.d.ts +0 -215
  36. package/dist/aws/s3.d.ts +0 -212
  37. package/dist/aws/scheduler.d.ts +0 -140
  38. package/dist/aws/secrets-manager.d.ts +0 -170
  39. package/dist/aws/ses.d.ts +0 -288
  40. package/dist/aws/setup-phone.d.ts +0 -0
  41. package/dist/aws/setup-sms.d.ts +0 -115
  42. package/dist/aws/sms.d.ts +0 -304
  43. package/dist/aws/smtp-server.d.ts +0 -61
  44. package/dist/aws/sns.d.ts +0 -117
  45. package/dist/aws/sqs.d.ts +0 -65
  46. package/dist/aws/ssm.d.ts +0 -179
  47. package/dist/aws/sts.d.ts +0 -15
  48. package/dist/aws/support.d.ts +0 -104
  49. package/dist/aws/test-imap.d.ts +0 -0
  50. package/dist/aws/textract.d.ts +0 -403
  51. package/dist/aws/transcribe.d.ts +0 -60
  52. package/dist/aws/translate.d.ts +0 -358
  53. package/dist/aws/voice.d.ts +0 -219
  54. package/dist/config.d.ts +0 -7
  55. package/dist/deploy/index.d.ts +0 -2
  56. package/dist/deploy/static-site-external-dns.d.ts +0 -51
  57. package/dist/deploy/static-site.d.ts +0 -71
  58. package/dist/dns/cloudflare.d.ts +0 -52
  59. package/dist/dns/godaddy.d.ts +0 -38
  60. package/dist/dns/index.d.ts +0 -45
  61. package/dist/dns/porkbun.d.ts +0 -18
  62. package/dist/dns/route53-adapter.d.ts +0 -38
  63. package/dist/dns/types.d.ts +0 -77
  64. package/dist/dns/validator.d.ts +0 -78
  65. package/dist/generators/index.d.ts +0 -1
  66. package/dist/generators/infrastructure.d.ts +0 -30
  67. package/dist/push/apns.d.ts +0 -60
  68. package/dist/push/fcm.d.ts +0 -117
  69. package/dist/push/index.d.ts +0 -14
  70. package/dist/security/pre-deploy-scanner.d.ts +0 -69
  71. package/dist/ssl/acme-client.d.ts +0 -67
  72. package/dist/ssl/index.d.ts +0 -2
  73. package/dist/ssl/letsencrypt.d.ts +0 -48
  74. package/dist/types.d.ts +0 -1
  75. package/dist/utils/cli.d.ts +0 -123
  76. package/dist/validation/index.d.ts +0 -1
  77. package/dist/validation/template.d.ts +0 -23
  78. package/src/aws/acm.ts +0 -768
  79. package/src/aws/application-autoscaling.ts +0 -845
  80. package/src/aws/bedrock.ts +0 -4074
  81. package/src/aws/client.ts +0 -891
  82. package/src/aws/cloudformation.ts +0 -896
  83. package/src/aws/cloudfront.ts +0 -1531
  84. package/src/aws/cloudwatch-logs.ts +0 -154
  85. package/src/aws/comprehend.ts +0 -839
  86. package/src/aws/connect.ts +0 -1056
  87. package/src/aws/deploy-imap.ts +0 -384
  88. package/src/aws/dynamodb.ts +0 -340
  89. package/src/aws/ec2.ts +0 -1385
  90. package/src/aws/ecr.ts +0 -621
  91. package/src/aws/ecs.ts +0 -615
  92. package/src/aws/elasticache.ts +0 -301
  93. package/src/aws/elbv2.ts +0 -942
  94. package/src/aws/email.ts +0 -928
  95. package/src/aws/eventbridge.ts +0 -248
  96. package/src/aws/iam.ts +0 -1689
  97. package/src/aws/imap-server.ts +0 -2100
  98. package/src/aws/index.ts +0 -213
  99. package/src/aws/kendra.ts +0 -1097
  100. package/src/aws/lambda.ts +0 -786
  101. package/src/aws/opensearch.ts +0 -158
  102. package/src/aws/personalize.ts +0 -977
  103. package/src/aws/polly.ts +0 -559
  104. package/src/aws/rds.ts +0 -888
  105. package/src/aws/rekognition.ts +0 -846
  106. package/src/aws/route53-domains.ts +0 -359
  107. package/src/aws/route53.ts +0 -1046
  108. package/src/aws/s3.ts +0 -2334
  109. package/src/aws/scheduler.ts +0 -571
  110. package/src/aws/secrets-manager.ts +0 -769
  111. package/src/aws/ses.ts +0 -1081
  112. package/src/aws/setup-phone.ts +0 -104
  113. package/src/aws/setup-sms.ts +0 -580
  114. package/src/aws/sms.ts +0 -1735
  115. package/src/aws/smtp-server.ts +0 -531
  116. package/src/aws/sns.ts +0 -758
  117. package/src/aws/sqs.ts +0 -382
  118. package/src/aws/ssm.ts +0 -807
  119. package/src/aws/sts.ts +0 -92
  120. package/src/aws/support.ts +0 -391
  121. package/src/aws/test-imap.ts +0 -86
  122. package/src/aws/textract.ts +0 -780
  123. package/src/aws/transcribe.ts +0 -108
  124. package/src/aws/translate.ts +0 -641
  125. package/src/aws/voice.ts +0 -1379
  126. package/src/config.ts +0 -35
  127. package/src/deploy/index.ts +0 -7
  128. package/src/deploy/static-site-external-dns.ts +0 -945
  129. package/src/deploy/static-site.ts +0 -1175
  130. package/src/dns/cloudflare.ts +0 -548
  131. package/src/dns/godaddy.ts +0 -412
  132. package/src/dns/index.ts +0 -205
  133. package/src/dns/porkbun.ts +0 -362
  134. package/src/dns/route53-adapter.ts +0 -414
  135. package/src/dns/types.ts +0 -119
  136. package/src/dns/validator.ts +0 -369
  137. package/src/generators/index.ts +0 -5
  138. package/src/generators/infrastructure.ts +0 -1660
  139. package/src/index.ts +0 -163
  140. package/src/push/apns.ts +0 -452
  141. package/src/push/fcm.ts +0 -506
  142. package/src/push/index.ts +0 -58
  143. package/src/security/pre-deploy-scanner.ts +0 -655
  144. package/src/ssl/acme-client.ts +0 -478
  145. package/src/ssl/index.ts +0 -7
  146. package/src/ssl/letsencrypt.ts +0 -747
  147. package/src/types.ts +0 -2
  148. package/src/utils/cli.ts +0 -398
  149. package/src/validation/index.ts +0 -5
  150. package/src/validation/template.ts +0 -405
@@ -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
- }