@stacksjs/ts-cloud 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/aws/s3.d.ts +1 -1
  2. package/dist/bin/cli.js +223 -222
  3. package/dist/index.js +132 -132
  4. package/package.json +18 -16
  5. package/src/aws/acm.ts +768 -0
  6. package/src/aws/application-autoscaling.ts +845 -0
  7. package/src/aws/bedrock.ts +4074 -0
  8. package/src/aws/client.ts +891 -0
  9. package/src/aws/cloudformation.ts +896 -0
  10. package/src/aws/cloudfront.ts +1531 -0
  11. package/src/aws/cloudwatch-logs.ts +154 -0
  12. package/src/aws/comprehend.ts +839 -0
  13. package/src/aws/connect.ts +1056 -0
  14. package/src/aws/deploy-imap.ts +384 -0
  15. package/src/aws/dynamodb.ts +340 -0
  16. package/src/aws/ec2.ts +1385 -0
  17. package/src/aws/ecr.ts +621 -0
  18. package/src/aws/ecs.ts +615 -0
  19. package/src/aws/elasticache.ts +301 -0
  20. package/src/aws/elbv2.ts +942 -0
  21. package/src/aws/email.ts +928 -0
  22. package/src/aws/eventbridge.ts +248 -0
  23. package/src/aws/iam.ts +1689 -0
  24. package/src/aws/imap-server.ts +2100 -0
  25. package/src/aws/index.ts +213 -0
  26. package/src/aws/kendra.ts +1097 -0
  27. package/src/aws/lambda.ts +786 -0
  28. package/src/aws/opensearch.ts +158 -0
  29. package/src/aws/personalize.ts +977 -0
  30. package/src/aws/polly.ts +559 -0
  31. package/src/aws/rds.ts +888 -0
  32. package/src/aws/rekognition.ts +846 -0
  33. package/src/aws/route53-domains.ts +359 -0
  34. package/src/aws/route53.ts +1046 -0
  35. package/src/aws/s3.ts +2334 -0
  36. package/src/aws/scheduler.ts +571 -0
  37. package/src/aws/secrets-manager.ts +769 -0
  38. package/src/aws/ses.ts +1081 -0
  39. package/src/aws/setup-phone.ts +104 -0
  40. package/src/aws/setup-sms.ts +580 -0
  41. package/src/aws/sms.ts +1735 -0
  42. package/src/aws/smtp-server.ts +531 -0
  43. package/src/aws/sns.ts +758 -0
  44. package/src/aws/sqs.ts +382 -0
  45. package/src/aws/ssm.ts +807 -0
  46. package/src/aws/sts.ts +92 -0
  47. package/src/aws/support.ts +391 -0
  48. package/src/aws/test-imap.ts +86 -0
  49. package/src/aws/textract.ts +780 -0
  50. package/src/aws/transcribe.ts +108 -0
  51. package/src/aws/translate.ts +641 -0
  52. package/src/aws/voice.ts +1379 -0
  53. package/src/config.ts +35 -0
  54. package/src/deploy/index.ts +7 -0
  55. package/src/deploy/static-site-external-dns.ts +945 -0
  56. package/src/deploy/static-site.ts +1175 -0
  57. package/src/dns/cloudflare.ts +548 -0
  58. package/src/dns/godaddy.ts +412 -0
  59. package/src/dns/index.ts +205 -0
  60. package/src/dns/porkbun.ts +362 -0
  61. package/src/dns/route53-adapter.ts +414 -0
  62. package/src/dns/types.ts +119 -0
  63. package/src/dns/validator.ts +369 -0
  64. package/src/generators/index.ts +5 -0
  65. package/src/generators/infrastructure.ts +1660 -0
  66. package/src/index.ts +163 -0
  67. package/src/push/apns.ts +452 -0
  68. package/src/push/fcm.ts +506 -0
  69. package/src/push/index.ts +58 -0
  70. package/src/security/pre-deploy-scanner.ts +655 -0
  71. package/src/ssl/acme-client.ts +478 -0
  72. package/src/ssl/index.ts +7 -0
  73. package/src/ssl/letsencrypt.ts +747 -0
  74. package/src/types.ts +2 -0
  75. package/src/utils/cli.ts +398 -0
  76. package/src/validation/index.ts +5 -0
  77. package/src/validation/template.ts +405 -0
@@ -0,0 +1,1660 @@
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
+ }