@stacksjs/ts-cloud 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/package.json +18 -16
  3. package/src/aws/acm.ts +768 -0
  4. package/src/aws/application-autoscaling.ts +845 -0
  5. package/src/aws/bedrock.ts +4074 -0
  6. package/src/aws/client.ts +891 -0
  7. package/src/aws/cloudformation.ts +896 -0
  8. package/src/aws/cloudfront.ts +1531 -0
  9. package/src/aws/cloudwatch-logs.ts +154 -0
  10. package/src/aws/comprehend.ts +839 -0
  11. package/src/aws/connect.ts +1056 -0
  12. package/src/aws/deploy-imap.ts +384 -0
  13. package/src/aws/dynamodb.ts +340 -0
  14. package/src/aws/ec2.ts +1385 -0
  15. package/src/aws/ecr.ts +621 -0
  16. package/src/aws/ecs.ts +615 -0
  17. package/src/aws/elasticache.ts +301 -0
  18. package/src/aws/elbv2.ts +942 -0
  19. package/src/aws/email.ts +928 -0
  20. package/src/aws/eventbridge.ts +248 -0
  21. package/src/aws/iam.ts +1689 -0
  22. package/src/aws/imap-server.ts +2100 -0
  23. package/src/aws/index.ts +213 -0
  24. package/src/aws/kendra.ts +1097 -0
  25. package/src/aws/lambda.ts +786 -0
  26. package/src/aws/opensearch.ts +158 -0
  27. package/src/aws/personalize.ts +977 -0
  28. package/src/aws/polly.ts +559 -0
  29. package/src/aws/rds.ts +888 -0
  30. package/src/aws/rekognition.ts +846 -0
  31. package/src/aws/route53-domains.ts +359 -0
  32. package/src/aws/route53.ts +1046 -0
  33. package/src/aws/s3.ts +2334 -0
  34. package/src/aws/scheduler.ts +571 -0
  35. package/src/aws/secrets-manager.ts +769 -0
  36. package/src/aws/ses.ts +1081 -0
  37. package/src/aws/setup-phone.ts +104 -0
  38. package/src/aws/setup-sms.ts +580 -0
  39. package/src/aws/sms.ts +1735 -0
  40. package/src/aws/smtp-server.ts +531 -0
  41. package/src/aws/sns.ts +758 -0
  42. package/src/aws/sqs.ts +382 -0
  43. package/src/aws/ssm.ts +807 -0
  44. package/src/aws/sts.ts +92 -0
  45. package/src/aws/support.ts +391 -0
  46. package/src/aws/test-imap.ts +86 -0
  47. package/src/aws/textract.ts +780 -0
  48. package/src/aws/transcribe.ts +108 -0
  49. package/src/aws/translate.ts +641 -0
  50. package/src/aws/voice.ts +1379 -0
  51. package/src/config.ts +35 -0
  52. package/src/deploy/index.ts +7 -0
  53. package/src/deploy/static-site-external-dns.ts +945 -0
  54. package/src/deploy/static-site.ts +1175 -0
  55. package/src/dns/cloudflare.ts +548 -0
  56. package/src/dns/godaddy.ts +412 -0
  57. package/src/dns/index.ts +205 -0
  58. package/src/dns/porkbun.ts +362 -0
  59. package/src/dns/route53-adapter.ts +414 -0
  60. package/src/dns/types.ts +119 -0
  61. package/src/dns/validator.ts +369 -0
  62. package/src/generators/index.ts +5 -0
  63. package/src/generators/infrastructure.ts +1660 -0
  64. package/src/index.ts +163 -0
  65. package/src/push/apns.ts +452 -0
  66. package/src/push/fcm.ts +506 -0
  67. package/src/push/index.ts +58 -0
  68. package/src/security/pre-deploy-scanner.ts +655 -0
  69. package/src/ssl/acme-client.ts +478 -0
  70. package/src/ssl/index.ts +7 -0
  71. package/src/ssl/letsencrypt.ts +747 -0
  72. package/src/types.ts +2 -0
  73. package/src/utils/cli.ts +398 -0
  74. package/src/validation/index.ts +5 -0
  75. package/src/validation/template.ts +405 -0
@@ -0,0 +1,104 @@
1
+ import { ConnectClient } from './connect'
2
+
3
+ const connect = new ConnectClient('us-east-1')
4
+ const instanceId = '7deb6488-8555-4304-b893-d6c461449eda'
5
+
6
+ async function main() {
7
+ // First, check which phone number we have
8
+ console.log('Checking claimed phone numbers...')
9
+ const phones = await connect.listPhoneNumbers({ InstanceId: instanceId })
10
+ console.log('Claimed phone numbers:')
11
+ for (const p of phones.ListPhoneNumbersSummaryList || []) {
12
+ console.log(' ', p.PhoneNumber, '(ID:', p.PhoneNumberId + ')')
13
+ }
14
+
15
+ // Use the first phone number
16
+ const phoneNumber = phones.ListPhoneNumbersSummaryList?.[0]
17
+ if (!phoneNumber) {
18
+ console.log('No phone numbers found!')
19
+ process.exit(1)
20
+ }
21
+
22
+ // Create a contact flow with the correct format for call forwarding
23
+ console.log('\nCreating contact flow with call forwarding...')
24
+
25
+ // Use the correct format for TransferToPhoneNumber
26
+ // According to AWS documentation, the action type is "TransferToPhoneNumber"
27
+ const forwardingFlow = {
28
+ Version: '2019-10-30',
29
+ StartAction: 'transfer-to-phone',
30
+ Metadata: {
31
+ entryPointPosition: { x: 40, y: 40 },
32
+ ActionMetadata: {
33
+ 'transfer-to-phone': { position: { x: 190, y: 40 } },
34
+ 'end-call': { position: { x: 440, y: 40 } },
35
+ },
36
+ },
37
+ Actions: [
38
+ {
39
+ Identifier: 'transfer-to-phone',
40
+ Type: 'TransferToPhoneNumber',
41
+ Parameters: {
42
+ PhoneNumber: '+18088218241',
43
+ ContactFlowId: '', // Will use instance default
44
+ },
45
+ Transitions: {
46
+ NextAction: 'end-call',
47
+ Errors: [
48
+ { ErrorType: 'NoMatchingError', NextAction: 'end-call' },
49
+ { ErrorType: 'CallFailed', NextAction: 'end-call' },
50
+ { ErrorType: 'ConnectionTimeLimitExceeded', NextAction: 'end-call' },
51
+ ],
52
+ },
53
+ },
54
+ {
55
+ Identifier: 'end-call',
56
+ Type: 'DisconnectParticipant',
57
+ Parameters: {},
58
+ Transitions: {},
59
+ },
60
+ ],
61
+ }
62
+
63
+ try {
64
+ const flowResult = await connect.createContactFlow({
65
+ InstanceId: instanceId,
66
+ Name: 'Stacks Call Forwarding',
67
+ Type: 'CONTACT_FLOW',
68
+ Content: JSON.stringify(forwardingFlow),
69
+ Description: 'Forwards all calls to +18088218241',
70
+ })
71
+ console.log('Contact flow created!')
72
+ console.log('Flow ID:', flowResult.ContactFlowId)
73
+ console.log('Flow ARN:', flowResult.ContactFlowArn)
74
+
75
+ // Associate the phone number with the contact flow
76
+ console.log('\nAssociating phone number with contact flow...')
77
+ await connect.associatePhoneNumberContactFlow({
78
+ PhoneNumberId: phoneNumber.PhoneNumberId!,
79
+ InstanceId: instanceId,
80
+ ContactFlowId: flowResult.ContactFlowId!,
81
+ })
82
+ console.log('Phone number associated with contact flow!')
83
+
84
+ // Format the phone number
85
+ const num = phoneNumber.PhoneNumber!
86
+ const formatted = `+1 (${num.slice(2, 5)}) ${num.slice(5, 8)}-${num.slice(8)}`
87
+
88
+ console.log('\n========================================')
89
+ console.log(' STACKS PHONE NUMBER IS READY!')
90
+ console.log('========================================')
91
+ console.log('')
92
+ console.log(' Call: ' + formatted)
93
+ console.log(' Raw: ' + num)
94
+ console.log(' Forwards to: +1 (808) 821-8241')
95
+ console.log(' Hours: 11:30 AM - 8:00 PM PST')
96
+ console.log('')
97
+ console.log('========================================')
98
+ }
99
+ catch (e: any) {
100
+ console.log('Error:', e.message)
101
+ }
102
+ }
103
+
104
+ main()
@@ -0,0 +1,580 @@
1
+ /**
2
+ * SMS Setup Automation Module
3
+ * Handles complete SMS infrastructure setup during deploy
4
+ *
5
+ * This module automates:
6
+ * - S3 inbox setup for incoming messages
7
+ * - SNS topics for two-way messaging
8
+ * - Spending limit management
9
+ * - Sandbox exit requests via AWS Support
10
+ * - Delivery receipt configuration
11
+ *
12
+ * Note: Phone number provisioning requires AWS End User Messaging console.
13
+ * SNS uses a shared pool for sending unless you configure an origination number.
14
+ */
15
+
16
+ import { SNSClient } from './sns'
17
+ import { S3Client } from './s3'
18
+ import { IAMClient } from './iam'
19
+ import { LambdaClient } from './lambda'
20
+ import { SupportClient, SupportTemplates } from './support'
21
+ import { AWSClient } from './client'
22
+
23
+ export interface SmsSetupConfig {
24
+ region?: string
25
+ // Account identifier (used for naming resources)
26
+ accountName?: string
27
+ // AWS account ID
28
+ accountId?: string
29
+ // S3 inbox configuration
30
+ inbox?: {
31
+ enabled?: boolean
32
+ bucket?: string
33
+ prefix?: string
34
+ // Create lifecycle rules for retention
35
+ retentionDays?: number
36
+ }
37
+ // SNS topic for incoming SMS
38
+ twoWay?: {
39
+ enabled?: boolean
40
+ topicName?: string
41
+ }
42
+ // Spending configuration
43
+ spending?: {
44
+ monthlyLimit?: number
45
+ // Auto-request increase via AWS Support
46
+ autoRequestIncrease?: boolean
47
+ }
48
+ // Sandbox configuration
49
+ sandbox?: {
50
+ // Auto-request sandbox exit via AWS Support
51
+ autoRequestExit?: boolean
52
+ // Company details for support ticket
53
+ companyName?: string
54
+ // Use case description for support ticket
55
+ useCase?: string
56
+ // Expected monthly SMS volume
57
+ expectedMonthlyVolume?: number
58
+ // Website URL
59
+ websiteUrl?: string
60
+ }
61
+ // Delivery receipts
62
+ deliveryReceipts?: {
63
+ enabled?: boolean
64
+ // SNS topic for delivery receipts
65
+ topicName?: string
66
+ // Store receipts in S3
67
+ s3Bucket?: string
68
+ s3Prefix?: string
69
+ }
70
+ // Lambda function for processing incoming SMS
71
+ inboxLambda?: {
72
+ enabled?: boolean
73
+ functionName?: string
74
+ // Code location (for creating new Lambda)
75
+ codeS3Bucket?: string
76
+ codeS3Key?: string
77
+ }
78
+ }
79
+
80
+ export interface SmsSetupResult {
81
+ success: boolean
82
+ inboxBucket?: string
83
+ inboxPrefix?: string
84
+ twoWayTopicArn?: string
85
+ deliveryReceiptTopicArn?: string
86
+ inboxLambdaArn?: string
87
+ spendingLimit?: number
88
+ sandboxStatus?: 'IN_SANDBOX' | 'OUT_OF_SANDBOX' | 'EXIT_REQUESTED'
89
+ supportCaseId?: string
90
+ errors: string[]
91
+ warnings: string[]
92
+ }
93
+
94
+ /**
95
+ * Set up complete SMS infrastructure
96
+ * Called automatically during `buddy deploy` when SMS is enabled
97
+ */
98
+ export async function setupSmsInfrastructure(config: SmsSetupConfig): Promise<SmsSetupResult> {
99
+ const region = config.region || 'us-east-1'
100
+ const result: SmsSetupResult = {
101
+ success: true,
102
+ errors: [],
103
+ warnings: [],
104
+ }
105
+
106
+ const sns = new SNSClient(region)
107
+ const s3 = new S3Client(region)
108
+ const support = new SupportClient(region)
109
+ const awsClient = new AWSClient()
110
+
111
+ console.log('Setting up SMS infrastructure...')
112
+
113
+ // 1. Check current SMS status
114
+ console.log(' Checking SMS account status...')
115
+ try {
116
+ const accountStatus = await checkSmsAccountStatus(sns)
117
+ result.sandboxStatus = accountStatus.inSandbox ? 'IN_SANDBOX' : 'OUT_OF_SANDBOX'
118
+
119
+ if (accountStatus.inSandbox) {
120
+ console.log(' Account is in SMS sandbox')
121
+
122
+ // Auto-request sandbox exit if configured
123
+ if (config.sandbox?.autoRequestExit && config.sandbox.companyName) {
124
+ console.log(' Requesting SMS sandbox exit via AWS Support...')
125
+ try {
126
+ const caseParams = SupportTemplates.smsSandboxExit({
127
+ companyName: config.sandbox.companyName,
128
+ useCase: config.sandbox.useCase || 'Transactional notifications and verification codes',
129
+ expectedMonthlyVolume: config.sandbox.expectedMonthlyVolume || 1000,
130
+ websiteUrl: config.sandbox.websiteUrl,
131
+ })
132
+ const caseResult = await support.createCase(caseParams)
133
+ result.supportCaseId = caseResult.caseId
134
+ result.sandboxStatus = 'EXIT_REQUESTED'
135
+ console.log(` Support case created: ${caseResult.caseId}`)
136
+ } catch (err: any) {
137
+ result.warnings.push(`Failed to create sandbox exit support case: ${err.message}`)
138
+ console.log(` Warning: Could not create support case: ${err.message}`)
139
+ }
140
+ }
141
+ }
142
+
143
+ // Check spending limit
144
+ result.spendingLimit = accountStatus.spendingLimit
145
+ console.log(` Current spending limit: $${accountStatus.spendingLimit}/month`)
146
+
147
+ // Request spending limit increase if needed
148
+ if (
149
+ config.spending?.autoRequestIncrease &&
150
+ config.spending.monthlyLimit &&
151
+ accountStatus.spendingLimit < config.spending.monthlyLimit
152
+ ) {
153
+ console.log(` Requesting spending limit increase to $${config.spending.monthlyLimit}/month...`)
154
+ try {
155
+ // Try to set it directly via SNS attributes
156
+ await setSnsSpendingLimit(awsClient, region, config.spending.monthlyLimit)
157
+ result.spendingLimit = config.spending.monthlyLimit
158
+ console.log(' Spending limit updated successfully')
159
+ } catch (err: any) {
160
+ // If direct update fails, file a support ticket
161
+ if (config.sandbox?.companyName) {
162
+ try {
163
+ const caseParams = SupportTemplates.smsSpendLimitIncrease({
164
+ companyName: config.sandbox.companyName,
165
+ currentLimit: accountStatus.spendingLimit,
166
+ requestedLimit: config.spending.monthlyLimit,
167
+ useCase: config.sandbox.useCase || 'Production SMS messaging',
168
+ })
169
+ const caseResult = await support.createCase(caseParams)
170
+ result.supportCaseId = caseResult.caseId
171
+ result.warnings.push(`Spending limit increase requested via support case: ${caseResult.caseId}`)
172
+ console.log(` Support case created for limit increase: ${caseResult.caseId}`)
173
+ } catch (supportErr: any) {
174
+ result.warnings.push(`Failed to request spending limit increase: ${supportErr.message}`)
175
+ }
176
+ } else {
177
+ result.warnings.push('Spending limit increase requires AWS Support ticket. Provide companyName in config.')
178
+ }
179
+ }
180
+ }
181
+ } catch (err: any) {
182
+ result.errors.push(`Failed to check SMS account status: ${err.message}`)
183
+ console.log(` Error checking status: ${err.message}`)
184
+ }
185
+
186
+ // 2. Set up S3 inbox
187
+ if (config.inbox?.enabled && config.inbox.bucket) {
188
+ console.log(' Setting up S3 inbox...')
189
+ try {
190
+ await setupS3Inbox(s3, {
191
+ bucket: config.inbox.bucket,
192
+ prefix: config.inbox.prefix || 'sms/inbox/',
193
+ retentionDays: config.inbox.retentionDays,
194
+ })
195
+ result.inboxBucket = config.inbox.bucket
196
+ result.inboxPrefix = config.inbox.prefix || 'sms/inbox/'
197
+ console.log(` Inbox configured: s3://${config.inbox.bucket}/${result.inboxPrefix}`)
198
+ } catch (err: any) {
199
+ result.errors.push(`Failed to set up S3 inbox: ${err.message}`)
200
+ console.log(` Error setting up inbox: ${err.message}`)
201
+ }
202
+ }
203
+
204
+ // 3. Set up SNS topic for two-way SMS
205
+ if (config.twoWay?.enabled) {
206
+ console.log(' Setting up two-way SMS...')
207
+ try {
208
+ const topicName = config.twoWay.topicName || `${config.accountName || 'stacks'}-sms-inbox`
209
+ const topicArn = await setupTwoWayTopic(sns, awsClient, region, topicName, config.accountId)
210
+ result.twoWayTopicArn = topicArn
211
+ console.log(` Two-way topic: ${topicArn}`)
212
+ } catch (err: any) {
213
+ result.errors.push(`Failed to set up two-way SMS topic: ${err.message}`)
214
+ console.log(` Error setting up two-way: ${err.message}`)
215
+ }
216
+ }
217
+
218
+ // 4. Set up delivery receipts topic
219
+ if (config.deliveryReceipts?.enabled) {
220
+ console.log(' Setting up delivery receipts...')
221
+ try {
222
+ const topicName = config.deliveryReceipts.topicName || `${config.accountName || 'stacks'}-sms-delivery-receipts`
223
+ const topicArn = await setupDeliveryReceiptsTopic(sns, awsClient, region, topicName, config.accountId)
224
+ result.deliveryReceiptTopicArn = topicArn
225
+ console.log(` Delivery receipts topic: ${topicArn}`)
226
+ } catch (err: any) {
227
+ result.errors.push(`Failed to set up delivery receipts: ${err.message}`)
228
+ console.log(` Error setting up delivery receipts: ${err.message}`)
229
+ }
230
+ }
231
+
232
+ // Final status
233
+ result.success = result.errors.length === 0
234
+ console.log(result.success ? ' SMS setup completed successfully!' : ' SMS setup completed with errors')
235
+
236
+ // Note about phone numbers
237
+ if (result.success) {
238
+ result.warnings.push(
239
+ 'For dedicated phone numbers, use AWS End User Messaging console. SNS uses shared pool by default.',
240
+ )
241
+ }
242
+
243
+ return result
244
+ }
245
+
246
+ /**
247
+ * Check SMS account status (sandbox, spending limits)
248
+ */
249
+ async function checkSmsAccountStatus(sns: SNSClient): Promise<{
250
+ inSandbox: boolean
251
+ spendingLimit: number
252
+ usedThisMonth: number
253
+ }> {
254
+ let inSandbox = true
255
+ let spendingLimit = 1
256
+ let usedThisMonth = 0
257
+
258
+ // Check SNS sandbox status
259
+ try {
260
+ const sandboxStatus = await sns.getSMSSandboxAccountStatus()
261
+ inSandbox = sandboxStatus.IsInSandbox
262
+ } catch {
263
+ // Assume sandbox if we can't check
264
+ inSandbox = true
265
+ }
266
+
267
+ // Get spending quota from SNS attributes
268
+ try {
269
+ const smsAttrs = await sns.getSMSAttributes()
270
+ if (smsAttrs.MonthlySpendLimit) {
271
+ spendingLimit = parseFloat(smsAttrs.MonthlySpendLimit)
272
+ }
273
+ } catch {
274
+ // Ignore
275
+ }
276
+
277
+ return { inSandbox, spendingLimit, usedThisMonth }
278
+ }
279
+
280
+ /**
281
+ * Set SNS SMS spending limit
282
+ */
283
+ async function setSnsSpendingLimit(awsClient: AWSClient, region: string, limit: number): Promise<void> {
284
+ const params = new URLSearchParams({
285
+ Action: 'SetSMSAttributes',
286
+ Version: '2010-03-31',
287
+ 'attributes.entry.1.key': 'MonthlySpendLimit',
288
+ 'attributes.entry.1.value': limit.toString(),
289
+ })
290
+
291
+ await awsClient.request({
292
+ service: 'sns',
293
+ region,
294
+ method: 'POST',
295
+ path: '/',
296
+ headers: {
297
+ 'Content-Type': 'application/x-www-form-urlencoded',
298
+ },
299
+ body: params.toString(),
300
+ })
301
+ }
302
+
303
+ /**
304
+ * Set up S3 bucket for SMS inbox
305
+ */
306
+ async function setupS3Inbox(
307
+ s3: S3Client,
308
+ config: {
309
+ bucket: string
310
+ prefix: string
311
+ retentionDays?: number
312
+ },
313
+ ): Promise<void> {
314
+ // Check if bucket exists
315
+ try {
316
+ const buckets = await s3.listBuckets()
317
+ const bucketExists = buckets.Buckets?.some(b => b.Name === config.bucket)
318
+
319
+ if (!bucketExists) {
320
+ // Create bucket
321
+ await s3.createBucket(config.bucket)
322
+ }
323
+
324
+ // Create placeholder files to ensure prefixes exist
325
+ const prefixes = [
326
+ `${config.prefix}.keep`,
327
+ 'sms/sent/.keep',
328
+ 'sms/conversations/.keep',
329
+ 'sms/templates/.keep',
330
+ 'sms/scheduled/.keep',
331
+ 'sms/receipts/.keep',
332
+ ]
333
+
334
+ for (const key of prefixes) {
335
+ try {
336
+ await s3.putObject({
337
+ bucket: config.bucket,
338
+ key,
339
+ body: `SMS folder created ${new Date().toISOString()}`,
340
+ contentType: 'text/plain',
341
+ })
342
+ } catch {
343
+ // Ignore if already exists
344
+ }
345
+ }
346
+
347
+ // Set up lifecycle rules for retention
348
+ if (config.retentionDays) {
349
+ try {
350
+ await s3.putBucketLifecycleConfiguration(config.bucket, [
351
+ {
352
+ ID: 'SmsInboxRetention',
353
+ Status: 'Enabled',
354
+ Filter: { Prefix: config.prefix },
355
+ Expiration: { Days: config.retentionDays },
356
+ },
357
+ {
358
+ ID: 'SmsReceiptsRetention',
359
+ Status: 'Enabled',
360
+ Filter: { Prefix: 'sms/receipts/' },
361
+ Expiration: { Days: config.retentionDays },
362
+ },
363
+ ])
364
+ } catch (err: any) {
365
+ // Lifecycle configuration might fail if not owner, continue anyway
366
+ console.log(` Note: Could not set lifecycle rules: ${err.message}`)
367
+ }
368
+ }
369
+ } catch (err: any) {
370
+ throw new Error(`Failed to set up S3 inbox: ${err.message}`)
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Set up SNS topic for two-way SMS
376
+ */
377
+ async function setupTwoWayTopic(
378
+ sns: SNSClient,
379
+ awsClient: AWSClient,
380
+ region: string,
381
+ topicName: string,
382
+ accountId?: string,
383
+ ): Promise<string> {
384
+ // Check if topic exists
385
+ const topics = await sns.listTopics()
386
+ const existingTopic = topics.Topics?.find(t => t.TopicArn?.endsWith(`:${topicName}`))
387
+
388
+ if (existingTopic) {
389
+ return existingTopic.TopicArn!
390
+ }
391
+
392
+ // Create new topic
393
+ const params = new URLSearchParams({
394
+ Action: 'CreateTopic',
395
+ Version: '2010-03-31',
396
+ Name: topicName,
397
+ })
398
+
399
+ const result = await awsClient.request({
400
+ service: 'sns',
401
+ region,
402
+ method: 'POST',
403
+ path: '/',
404
+ headers: {
405
+ 'Content-Type': 'application/x-www-form-urlencoded',
406
+ },
407
+ body: params.toString(),
408
+ })
409
+
410
+ const topicArn = result?.CreateTopicResponse?.CreateTopicResult?.TopicArn
411
+ if (!topicArn) {
412
+ throw new Error('Failed to create SNS topic')
413
+ }
414
+
415
+ // Set up topic policy to allow SMS Voice service to publish
416
+ const policyParams = new URLSearchParams({
417
+ Action: 'SetTopicAttributes',
418
+ Version: '2010-03-31',
419
+ TopicArn: topicArn,
420
+ AttributeName: 'Policy',
421
+ AttributeValue: JSON.stringify({
422
+ Version: '2012-10-17',
423
+ Statement: [
424
+ {
425
+ Sid: 'AllowSMSVoicePublish',
426
+ Effect: 'Allow',
427
+ Principal: {
428
+ Service: 'sms-voice.amazonaws.com',
429
+ },
430
+ Action: 'sns:Publish',
431
+ Resource: topicArn,
432
+ },
433
+ ],
434
+ }),
435
+ })
436
+
437
+ await awsClient.request({
438
+ service: 'sns',
439
+ region,
440
+ method: 'POST',
441
+ path: '/',
442
+ headers: {
443
+ 'Content-Type': 'application/x-www-form-urlencoded',
444
+ },
445
+ body: policyParams.toString(),
446
+ })
447
+
448
+ return topicArn
449
+ }
450
+
451
+ /**
452
+ * Set up SNS topic for delivery receipts
453
+ */
454
+ async function setupDeliveryReceiptsTopic(
455
+ sns: SNSClient,
456
+ awsClient: AWSClient,
457
+ region: string,
458
+ topicName: string,
459
+ accountId?: string,
460
+ ): Promise<string> {
461
+ // Same as two-way topic setup
462
+ return setupTwoWayTopic(sns, awsClient, region, topicName, accountId)
463
+ }
464
+
465
+ /**
466
+ * Get complete SMS infrastructure status
467
+ */
468
+ export async function getSmsInfrastructureStatus(config: {
469
+ region?: string
470
+ accountName?: string
471
+ }): Promise<{
472
+ sandboxStatus: 'IN_SANDBOX' | 'OUT_OF_SANDBOX' | 'UNKNOWN'
473
+ spendingLimit: number
474
+ topics: Array<{
475
+ name: string
476
+ arn: string
477
+ }>
478
+ }> {
479
+ const region = config.region || 'us-east-1'
480
+ const sns = new SNSClient(region)
481
+
482
+ // Get sandbox status
483
+ const accountStatus = await checkSmsAccountStatus(sns)
484
+
485
+ // Get topics
486
+ const topicsResult = await sns.listTopics()
487
+ const smsTopics = (topicsResult.Topics || [])
488
+ .filter(t => t.TopicArn?.includes('sms'))
489
+ .map(t => ({
490
+ name: t.TopicArn?.split(':').pop() || '',
491
+ arn: t.TopicArn || '',
492
+ }))
493
+
494
+ return {
495
+ sandboxStatus: accountStatus.inSandbox ? 'IN_SANDBOX' : 'OUT_OF_SANDBOX',
496
+ spendingLimit: accountStatus.spendingLimit,
497
+ topics: smsTopics,
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Create SMS infrastructure for Stacks deploy
503
+ * This is the main entry point called by the deploy command
504
+ */
505
+ export async function createSmsInfrastructure(smsConfig: {
506
+ enabled: boolean
507
+ provider: 'sns'
508
+ originationNumber?: string
509
+ defaultCountryCode: string
510
+ messageType: 'TRANSACTIONAL' | 'PROMOTIONAL'
511
+ maxSpendPerMonth?: number
512
+ inbox?: {
513
+ enabled: boolean
514
+ bucket: string
515
+ prefix?: string
516
+ retentionDays?: number
517
+ }
518
+ twoWay?: {
519
+ enabled: boolean
520
+ snsTopicArn?: string
521
+ }
522
+ optOut: {
523
+ enabled: boolean
524
+ keywords: string[]
525
+ }
526
+ }): Promise<SmsSetupResult> {
527
+ if (!smsConfig.enabled) {
528
+ return {
529
+ success: true,
530
+ errors: [],
531
+ warnings: ['SMS is disabled in config'],
532
+ }
533
+ }
534
+
535
+ // Build setup config from Stacks SMS config
536
+ const setupConfig: SmsSetupConfig = {
537
+ region: 'us-east-1',
538
+ accountName: 'stacks',
539
+ inbox: smsConfig.inbox
540
+ ? {
541
+ enabled: smsConfig.inbox.enabled,
542
+ bucket: smsConfig.inbox.bucket,
543
+ prefix: smsConfig.inbox.prefix,
544
+ retentionDays: smsConfig.inbox.retentionDays,
545
+ }
546
+ : undefined,
547
+ twoWay: smsConfig.twoWay
548
+ ? {
549
+ enabled: smsConfig.twoWay.enabled,
550
+ }
551
+ : undefined,
552
+ spending: {
553
+ monthlyLimit: smsConfig.maxSpendPerMonth,
554
+ autoRequestIncrease: true,
555
+ },
556
+ sandbox: {
557
+ autoRequestExit: true,
558
+ companyName: 'Stacks',
559
+ useCase:
560
+ 'Transactional notifications, verification codes, and account alerts for web applications built with Stacks framework.',
561
+ expectedMonthlyVolume: 5000,
562
+ websiteUrl: 'https://stacksjs.com',
563
+ },
564
+ deliveryReceipts: {
565
+ enabled: true,
566
+ },
567
+ }
568
+
569
+ return setupSmsInfrastructure(setupConfig)
570
+ }
571
+
572
+ export default {
573
+ setupSmsInfrastructure: setupSmsInfrastructure,
574
+ getSmsInfrastructureStatus: getSmsInfrastructureStatus,
575
+ createSmsInfrastructure: createSmsInfrastructure,
576
+ } as {
577
+ setupSmsInfrastructure: typeof setupSmsInfrastructure
578
+ getSmsInfrastructureStatus: typeof getSmsInfrastructureStatus
579
+ createSmsInfrastructure: typeof createSmsInfrastructure
580
+ }