@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
package/src/aws/sns.ts ADDED
@@ -0,0 +1,758 @@
1
+ /**
2
+ * AWS SNS (Simple Notification Service) Operations
3
+ * Direct API calls without AWS SDK dependency
4
+ */
5
+
6
+ import { AWSClient } from './client'
7
+
8
+ export interface SNSTopicAttributes {
9
+ TopicArn?: string
10
+ DisplayName?: string
11
+ Policy?: string
12
+ Owner?: string
13
+ SubscriptionsPending?: string
14
+ SubscriptionsConfirmed?: string
15
+ SubscriptionsDeleted?: string
16
+ DeliveryPolicy?: string
17
+ EffectiveDeliveryPolicy?: string
18
+ KmsMasterKeyId?: string
19
+ }
20
+
21
+ export interface SNSSubscriptionAttributes {
22
+ SubscriptionArn?: string
23
+ TopicArn?: string
24
+ Protocol?: string
25
+ Endpoint?: string
26
+ Owner?: string
27
+ ConfirmationWasAuthenticated?: string
28
+ RawMessageDelivery?: string
29
+ FilterPolicy?: string
30
+ }
31
+
32
+ export type SNSProtocol = 'http' | 'https' | 'email' | 'email-json' | 'sms' | 'sqs' | 'application' | 'lambda'
33
+
34
+ /**
35
+ * SNS service management using direct API calls
36
+ */
37
+ export class SNSClient {
38
+ private client: AWSClient
39
+ private region: string
40
+
41
+ constructor(region: string = 'us-east-1') {
42
+ this.region = region
43
+ this.client = new AWSClient()
44
+ }
45
+
46
+ /**
47
+ * Build form-encoded body for SNS API
48
+ */
49
+ private buildFormBody(params: Record<string, string | undefined>): string {
50
+ const entries = Object.entries(params)
51
+ .filter(([, value]) => value !== undefined)
52
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value!)}`)
53
+ return entries.join('&')
54
+ }
55
+
56
+ /**
57
+ * Create a new SNS topic
58
+ */
59
+ async createTopic(params: {
60
+ Name: string
61
+ DisplayName?: string
62
+ Tags?: Array<{ Key: string, Value: string }>
63
+ Attributes?: Record<string, string>
64
+ }): Promise<{ TopicArn?: string }> {
65
+ const formParams: Record<string, string | undefined> = {
66
+ Action: 'CreateTopic',
67
+ Version: '2010-03-31',
68
+ Name: params.Name,
69
+ }
70
+
71
+ if (params.DisplayName) {
72
+ formParams['Attributes.entry.1.key'] = 'DisplayName'
73
+ formParams['Attributes.entry.1.value'] = params.DisplayName
74
+ }
75
+
76
+ if (params.Tags) {
77
+ params.Tags.forEach((tag, index) => {
78
+ formParams[`Tags.member.${index + 1}.Key`] = tag.Key
79
+ formParams[`Tags.member.${index + 1}.Value`] = tag.Value
80
+ })
81
+ }
82
+
83
+ if (params.Attributes) {
84
+ let attrIndex = params.DisplayName ? 2 : 1
85
+ Object.entries(params.Attributes).forEach(([key, value]) => {
86
+ formParams[`Attributes.entry.${attrIndex}.key`] = key
87
+ formParams[`Attributes.entry.${attrIndex}.value`] = value
88
+ attrIndex++
89
+ })
90
+ }
91
+
92
+ const result = await this.client.request({
93
+ service: 'sns',
94
+ region: this.region,
95
+ method: 'POST',
96
+ path: '/',
97
+ headers: {
98
+ 'Content-Type': 'application/x-www-form-urlencoded',
99
+ },
100
+ body: this.buildFormBody(formParams),
101
+ })
102
+
103
+ return {
104
+ TopicArn: result?.CreateTopicResponse?.CreateTopicResult?.TopicArn
105
+ || result?.TopicArn,
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Delete an SNS topic
111
+ */
112
+ async deleteTopic(topicArn: string): Promise<void> {
113
+ await this.client.request({
114
+ service: 'sns',
115
+ region: this.region,
116
+ method: 'POST',
117
+ path: '/',
118
+ headers: {
119
+ 'Content-Type': 'application/x-www-form-urlencoded',
120
+ },
121
+ body: this.buildFormBody({
122
+ Action: 'DeleteTopic',
123
+ Version: '2010-03-31',
124
+ TopicArn: topicArn,
125
+ }),
126
+ })
127
+ }
128
+
129
+ /**
130
+ * List all SNS topics
131
+ */
132
+ async listTopics(nextToken?: string): Promise<{
133
+ Topics?: Array<{ TopicArn?: string }>
134
+ NextToken?: string
135
+ }> {
136
+ const formParams: Record<string, string | undefined> = {
137
+ Action: 'ListTopics',
138
+ Version: '2010-03-31',
139
+ }
140
+
141
+ if (nextToken) {
142
+ formParams.NextToken = nextToken
143
+ }
144
+
145
+ const result = await this.client.request({
146
+ service: 'sns',
147
+ region: this.region,
148
+ method: 'POST',
149
+ path: '/',
150
+ headers: {
151
+ 'Content-Type': 'application/x-www-form-urlencoded',
152
+ },
153
+ body: this.buildFormBody(formParams),
154
+ })
155
+
156
+ // Handle both response formats (with and without ListTopicsResponse wrapper)
157
+ const listResult = result?.ListTopicsResponse?.ListTopicsResult || result?.ListTopicsResult
158
+ const topics = listResult?.Topics?.member
159
+ return {
160
+ Topics: Array.isArray(topics) ? topics : topics ? [topics] : [],
161
+ NextToken: listResult?.NextToken,
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Get topic attributes
167
+ */
168
+ async getTopicAttributes(topicArn: string): Promise<SNSTopicAttributes> {
169
+ const result = await this.client.request({
170
+ service: 'sns',
171
+ region: this.region,
172
+ method: 'POST',
173
+ path: '/',
174
+ headers: {
175
+ 'Content-Type': 'application/x-www-form-urlencoded',
176
+ },
177
+ body: this.buildFormBody({
178
+ Action: 'GetTopicAttributes',
179
+ Version: '2010-03-31',
180
+ TopicArn: topicArn,
181
+ }),
182
+ })
183
+
184
+ const attributes = result?.GetTopicAttributesResponse?.GetTopicAttributesResult?.Attributes?.entry
185
+ const attrs: SNSTopicAttributes = { TopicArn: topicArn }
186
+
187
+ if (Array.isArray(attributes)) {
188
+ attributes.forEach((entry: { key: string, value: string }) => {
189
+ (attrs as any)[entry.key] = entry.value
190
+ })
191
+ }
192
+
193
+ return attrs
194
+ }
195
+
196
+ /**
197
+ * Set topic attributes
198
+ */
199
+ async setTopicAttributes(params: {
200
+ TopicArn: string
201
+ AttributeName: string
202
+ AttributeValue: string
203
+ }): Promise<void> {
204
+ await this.client.request({
205
+ service: 'sns',
206
+ region: this.region,
207
+ method: 'POST',
208
+ path: '/',
209
+ headers: {
210
+ 'Content-Type': 'application/x-www-form-urlencoded',
211
+ },
212
+ body: this.buildFormBody({
213
+ Action: 'SetTopicAttributes',
214
+ Version: '2010-03-31',
215
+ TopicArn: params.TopicArn,
216
+ AttributeName: params.AttributeName,
217
+ AttributeValue: params.AttributeValue,
218
+ }),
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Subscribe to a topic
224
+ */
225
+ async subscribe(params: {
226
+ TopicArn: string
227
+ Protocol: SNSProtocol
228
+ Endpoint: string
229
+ Attributes?: Record<string, string>
230
+ ReturnSubscriptionArn?: boolean
231
+ }): Promise<{ SubscriptionArn?: string }> {
232
+ const formParams: Record<string, string | undefined> = {
233
+ Action: 'Subscribe',
234
+ Version: '2010-03-31',
235
+ TopicArn: params.TopicArn,
236
+ Protocol: params.Protocol,
237
+ Endpoint: params.Endpoint,
238
+ }
239
+
240
+ if (params.ReturnSubscriptionArn) {
241
+ formParams.ReturnSubscriptionArn = 'true'
242
+ }
243
+
244
+ if (params.Attributes) {
245
+ let attrIndex = 1
246
+ Object.entries(params.Attributes).forEach(([key, value]) => {
247
+ formParams[`Attributes.entry.${attrIndex}.key`] = key
248
+ formParams[`Attributes.entry.${attrIndex}.value`] = value
249
+ attrIndex++
250
+ })
251
+ }
252
+
253
+ const result = await this.client.request({
254
+ service: 'sns',
255
+ region: this.region,
256
+ method: 'POST',
257
+ path: '/',
258
+ headers: {
259
+ 'Content-Type': 'application/x-www-form-urlencoded',
260
+ },
261
+ body: this.buildFormBody(formParams),
262
+ })
263
+
264
+ return {
265
+ SubscriptionArn: result?.SubscribeResponse?.SubscribeResult?.SubscriptionArn
266
+ || result?.SubscriptionArn,
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Unsubscribe from a topic
272
+ */
273
+ async unsubscribe(subscriptionArn: string): Promise<void> {
274
+ await this.client.request({
275
+ service: 'sns',
276
+ region: this.region,
277
+ method: 'POST',
278
+ path: '/',
279
+ headers: {
280
+ 'Content-Type': 'application/x-www-form-urlencoded',
281
+ },
282
+ body: this.buildFormBody({
283
+ Action: 'Unsubscribe',
284
+ Version: '2010-03-31',
285
+ SubscriptionArn: subscriptionArn,
286
+ }),
287
+ })
288
+ }
289
+
290
+ /**
291
+ * List subscriptions for a topic
292
+ */
293
+ async listSubscriptionsByTopic(topicArn: string, nextToken?: string): Promise<{
294
+ Subscriptions?: SNSSubscriptionAttributes[]
295
+ NextToken?: string
296
+ }> {
297
+ const formParams: Record<string, string | undefined> = {
298
+ Action: 'ListSubscriptionsByTopic',
299
+ Version: '2010-03-31',
300
+ TopicArn: topicArn,
301
+ }
302
+
303
+ if (nextToken) {
304
+ formParams.NextToken = nextToken
305
+ }
306
+
307
+ const result = await this.client.request({
308
+ service: 'sns',
309
+ region: this.region,
310
+ method: 'POST',
311
+ path: '/',
312
+ headers: {
313
+ 'Content-Type': 'application/x-www-form-urlencoded',
314
+ },
315
+ body: this.buildFormBody(formParams),
316
+ })
317
+
318
+ const subs = result?.ListSubscriptionsByTopicResponse?.ListSubscriptionsByTopicResult?.Subscriptions?.member
319
+ return {
320
+ Subscriptions: Array.isArray(subs) ? subs : subs ? [subs] : [],
321
+ NextToken: result?.ListSubscriptionsByTopicResponse?.ListSubscriptionsByTopicResult?.NextToken,
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Publish a message to a topic
327
+ */
328
+ async publish(params: {
329
+ TopicArn?: string
330
+ TargetArn?: string
331
+ PhoneNumber?: string
332
+ Message: string
333
+ Subject?: string
334
+ MessageStructure?: 'json'
335
+ MessageAttributes?: Record<string, {
336
+ DataType: 'String' | 'Number' | 'Binary'
337
+ StringValue?: string
338
+ BinaryValue?: string
339
+ }>
340
+ }): Promise<{ MessageId?: string }> {
341
+ const formParams: Record<string, string | undefined> = {
342
+ Action: 'Publish',
343
+ Version: '2010-03-31',
344
+ Message: params.Message,
345
+ }
346
+
347
+ if (params.TopicArn) formParams.TopicArn = params.TopicArn
348
+ if (params.TargetArn) formParams.TargetArn = params.TargetArn
349
+ if (params.PhoneNumber) formParams.PhoneNumber = params.PhoneNumber
350
+ if (params.Subject) formParams.Subject = params.Subject
351
+ if (params.MessageStructure) formParams.MessageStructure = params.MessageStructure
352
+
353
+ if (params.MessageAttributes) {
354
+ let attrIndex = 1
355
+ Object.entries(params.MessageAttributes).forEach(([name, attr]) => {
356
+ formParams[`MessageAttributes.entry.${attrIndex}.Name`] = name
357
+ formParams[`MessageAttributes.entry.${attrIndex}.Value.DataType`] = attr.DataType
358
+ if (attr.StringValue) {
359
+ formParams[`MessageAttributes.entry.${attrIndex}.Value.StringValue`] = attr.StringValue
360
+ }
361
+ if (attr.BinaryValue) {
362
+ formParams[`MessageAttributes.entry.${attrIndex}.Value.BinaryValue`] = attr.BinaryValue
363
+ }
364
+ attrIndex++
365
+ })
366
+ }
367
+
368
+ const result = await this.client.request({
369
+ service: 'sns',
370
+ region: this.region,
371
+ method: 'POST',
372
+ path: '/',
373
+ headers: {
374
+ 'Content-Type': 'application/x-www-form-urlencoded',
375
+ },
376
+ body: this.buildFormBody(formParams),
377
+ })
378
+
379
+ return {
380
+ MessageId: result?.PublishResponse?.PublishResult?.MessageId
381
+ || result?.MessageId,
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Publish SMS message directly (without topic)
387
+ */
388
+ async publishSMS(phoneNumber: string, message: string, senderId?: string): Promise<{ MessageId?: string }> {
389
+ const messageAttributes: Record<string, { DataType: 'String', StringValue: string }> = {}
390
+
391
+ if (senderId) {
392
+ messageAttributes['AWS.SNS.SMS.SenderID'] = {
393
+ DataType: 'String',
394
+ StringValue: senderId,
395
+ }
396
+ }
397
+
398
+ return this.publish({
399
+ PhoneNumber: phoneNumber,
400
+ Message: message,
401
+ MessageAttributes: Object.keys(messageAttributes).length > 0 ? messageAttributes : undefined,
402
+ })
403
+ }
404
+
405
+ /**
406
+ * Subscribe an email address to a topic
407
+ */
408
+ async subscribeEmail(topicArn: string, email: string): Promise<{ SubscriptionArn?: string }> {
409
+ return this.subscribe({
410
+ TopicArn: topicArn,
411
+ Protocol: 'email',
412
+ Endpoint: email,
413
+ })
414
+ }
415
+
416
+ /**
417
+ * Subscribe a Lambda function to a topic
418
+ */
419
+ async subscribeLambda(topicArn: string, lambdaArn: string): Promise<{ SubscriptionArn?: string }> {
420
+ return this.subscribe({
421
+ TopicArn: topicArn,
422
+ Protocol: 'lambda',
423
+ Endpoint: lambdaArn,
424
+ })
425
+ }
426
+
427
+ /**
428
+ * Subscribe an SQS queue to a topic
429
+ */
430
+ async subscribeSqs(topicArn: string, queueArn: string, rawMessageDelivery?: boolean): Promise<{ SubscriptionArn?: string }> {
431
+ const attributes: Record<string, string> = {}
432
+ if (rawMessageDelivery) {
433
+ attributes.RawMessageDelivery = 'true'
434
+ }
435
+
436
+ return this.subscribe({
437
+ TopicArn: topicArn,
438
+ Protocol: 'sqs',
439
+ Endpoint: queueArn,
440
+ Attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
441
+ })
442
+ }
443
+
444
+ /**
445
+ * Subscribe an HTTP/HTTPS endpoint to a topic
446
+ */
447
+ async subscribeHttp(topicArn: string, url: string, rawMessageDelivery?: boolean): Promise<{ SubscriptionArn?: string }> {
448
+ const protocol: SNSProtocol = url.startsWith('https') ? 'https' : 'http'
449
+ const attributes: Record<string, string> = {}
450
+ if (rawMessageDelivery) {
451
+ attributes.RawMessageDelivery = 'true'
452
+ }
453
+
454
+ return this.subscribe({
455
+ TopicArn: topicArn,
456
+ Protocol: protocol,
457
+ Endpoint: url,
458
+ Attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
459
+ })
460
+ }
461
+
462
+ /**
463
+ * Subscribe an SMS number to a topic
464
+ */
465
+ async subscribeSms(topicArn: string, phoneNumber: string): Promise<{ SubscriptionArn?: string }> {
466
+ return this.subscribe({
467
+ TopicArn: topicArn,
468
+ Protocol: 'sms',
469
+ Endpoint: phoneNumber,
470
+ })
471
+ }
472
+
473
+ /**
474
+ * Check if topic exists
475
+ */
476
+ async topicExists(topicArn: string): Promise<boolean> {
477
+ try {
478
+ await this.getTopicAttributes(topicArn)
479
+ return true
480
+ }
481
+ catch (error: any) {
482
+ if (error.code === 'NotFound' || error.statusCode === 404) {
483
+ return false
484
+ }
485
+ throw error
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Get SMS attributes (sandbox status, spending limits, etc.)
491
+ */
492
+ async getSMSAttributes(): Promise<{
493
+ MonthlySpendLimit?: string
494
+ DeliveryStatusIAMRole?: string
495
+ DeliveryStatusSuccessSamplingRate?: string
496
+ DefaultSenderID?: string
497
+ DefaultSMSType?: 'Promotional' | 'Transactional'
498
+ UsageReportS3Bucket?: string
499
+ }> {
500
+ const result = await this.client.request({
501
+ service: 'sns',
502
+ region: this.region,
503
+ method: 'POST',
504
+ path: '/',
505
+ headers: {
506
+ 'Content-Type': 'application/x-www-form-urlencoded',
507
+ },
508
+ body: this.buildFormBody({
509
+ Action: 'GetSMSAttributes',
510
+ Version: '2010-03-31',
511
+ }),
512
+ })
513
+
514
+ const attrs = result?.GetSMSAttributesResponse?.GetSMSAttributesResult?.attributes?.entry
515
+ const attributes: Record<string, string> = {}
516
+
517
+ if (Array.isArray(attrs)) {
518
+ attrs.forEach((entry: { key: string, value: string }) => {
519
+ attributes[entry.key] = entry.value
520
+ })
521
+ } else if (attrs) {
522
+ attributes[attrs.key] = attrs.value
523
+ }
524
+
525
+ return attributes
526
+ }
527
+
528
+ /**
529
+ * Set SMS attributes (sender ID, message type, etc.)
530
+ */
531
+ async setSMSAttributes(attributes: {
532
+ MonthlySpendLimit?: string
533
+ DeliveryStatusIAMRole?: string
534
+ DeliveryStatusSuccessSamplingRate?: string
535
+ DefaultSenderID?: string
536
+ DefaultSMSType?: 'Promotional' | 'Transactional'
537
+ UsageReportS3Bucket?: string
538
+ }): Promise<void> {
539
+ const formParams: Record<string, string | undefined> = {
540
+ Action: 'SetSMSAttributes',
541
+ Version: '2010-03-31',
542
+ }
543
+
544
+ let attrIndex = 1
545
+ Object.entries(attributes).forEach(([key, value]) => {
546
+ if (value !== undefined) {
547
+ formParams[`attributes.entry.${attrIndex}.key`] = key
548
+ formParams[`attributes.entry.${attrIndex}.value`] = value
549
+ attrIndex++
550
+ }
551
+ })
552
+
553
+ await this.client.request({
554
+ service: 'sns',
555
+ region: this.region,
556
+ method: 'POST',
557
+ path: '/',
558
+ headers: {
559
+ 'Content-Type': 'application/x-www-form-urlencoded',
560
+ },
561
+ body: this.buildFormBody(formParams),
562
+ })
563
+ }
564
+
565
+ /**
566
+ * Check if phone number is opted out
567
+ */
568
+ async checkIfPhoneNumberIsOptedOut(phoneNumber: string): Promise<boolean> {
569
+ const result = await this.client.request({
570
+ service: 'sns',
571
+ region: this.region,
572
+ method: 'POST',
573
+ path: '/',
574
+ headers: {
575
+ 'Content-Type': 'application/x-www-form-urlencoded',
576
+ },
577
+ body: this.buildFormBody({
578
+ Action: 'CheckIfPhoneNumberIsOptedOut',
579
+ Version: '2010-03-31',
580
+ phoneNumber: phoneNumber,
581
+ }),
582
+ })
583
+
584
+ return result?.CheckIfPhoneNumberIsOptedOutResponse?.CheckIfPhoneNumberIsOptedOutResult?.isOptedOut === 'true'
585
+ }
586
+
587
+ /**
588
+ * List phone numbers that have opted out of receiving SMS
589
+ */
590
+ async listPhoneNumbersOptedOut(nextToken?: string): Promise<{
591
+ phoneNumbers?: string[]
592
+ nextToken?: string
593
+ }> {
594
+ const formParams: Record<string, string | undefined> = {
595
+ Action: 'ListPhoneNumbersOptedOut',
596
+ Version: '2010-03-31',
597
+ }
598
+ if (nextToken) formParams.nextToken = nextToken
599
+
600
+ const result = await this.client.request({
601
+ service: 'sns',
602
+ region: this.region,
603
+ method: 'POST',
604
+ path: '/',
605
+ headers: {
606
+ 'Content-Type': 'application/x-www-form-urlencoded',
607
+ },
608
+ body: this.buildFormBody(formParams),
609
+ })
610
+
611
+ const phones = result?.ListPhoneNumbersOptedOutResponse?.ListPhoneNumbersOptedOutResult?.phoneNumbers?.member
612
+ return {
613
+ phoneNumbers: Array.isArray(phones) ? phones : phones ? [phones] : [],
614
+ nextToken: result?.ListPhoneNumbersOptedOutResponse?.ListPhoneNumbersOptedOutResult?.nextToken,
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Opt a phone number back in to receive SMS (requires user consent)
620
+ */
621
+ async optInPhoneNumber(phoneNumber: string): Promise<void> {
622
+ await this.client.request({
623
+ service: 'sns',
624
+ region: this.region,
625
+ method: 'POST',
626
+ path: '/',
627
+ headers: {
628
+ 'Content-Type': 'application/x-www-form-urlencoded',
629
+ },
630
+ body: this.buildFormBody({
631
+ Action: 'OptInPhoneNumber',
632
+ Version: '2010-03-31',
633
+ phoneNumber: phoneNumber,
634
+ }),
635
+ })
636
+ }
637
+
638
+ /**
639
+ * List sandbox phone numbers (for SMS sandbox mode)
640
+ */
641
+ async listSMSSandboxPhoneNumbers(nextToken?: string): Promise<{
642
+ PhoneNumbers?: Array<{
643
+ PhoneNumber?: string
644
+ Status?: 'Pending' | 'Verified'
645
+ }>
646
+ NextToken?: string
647
+ }> {
648
+ const formParams: Record<string, string | undefined> = {
649
+ Action: 'ListSMSSandboxPhoneNumbers',
650
+ Version: '2010-03-31',
651
+ }
652
+ if (nextToken) formParams.NextToken = nextToken
653
+
654
+ const result = await this.client.request({
655
+ service: 'sns',
656
+ region: this.region,
657
+ method: 'POST',
658
+ path: '/',
659
+ headers: {
660
+ 'Content-Type': 'application/x-www-form-urlencoded',
661
+ },
662
+ body: this.buildFormBody(formParams),
663
+ })
664
+
665
+ const phones = result?.ListSMSSandboxPhoneNumbersResponse?.ListSMSSandboxPhoneNumbersResult?.PhoneNumbers?.member
666
+ return {
667
+ PhoneNumbers: Array.isArray(phones) ? phones : phones ? [phones] : [],
668
+ NextToken: result?.ListSMSSandboxPhoneNumbersResponse?.ListSMSSandboxPhoneNumbersResult?.NextToken,
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Create a sandbox phone number for testing
674
+ */
675
+ async createSMSSandboxPhoneNumber(phoneNumber: string, languageCode?: string): Promise<void> {
676
+ await this.client.request({
677
+ service: 'sns',
678
+ region: this.region,
679
+ method: 'POST',
680
+ path: '/',
681
+ headers: {
682
+ 'Content-Type': 'application/x-www-form-urlencoded',
683
+ },
684
+ body: this.buildFormBody({
685
+ Action: 'CreateSMSSandboxPhoneNumber',
686
+ Version: '2010-03-31',
687
+ PhoneNumber: phoneNumber,
688
+ LanguageCode: languageCode || 'en-US',
689
+ }),
690
+ })
691
+ }
692
+
693
+ /**
694
+ * Verify a sandbox phone number with OTP
695
+ */
696
+ async verifySMSSandboxPhoneNumber(phoneNumber: string, oneTimePassword: string): Promise<void> {
697
+ await this.client.request({
698
+ service: 'sns',
699
+ region: this.region,
700
+ method: 'POST',
701
+ path: '/',
702
+ headers: {
703
+ 'Content-Type': 'application/x-www-form-urlencoded',
704
+ },
705
+ body: this.buildFormBody({
706
+ Action: 'VerifySMSSandboxPhoneNumber',
707
+ Version: '2010-03-31',
708
+ PhoneNumber: phoneNumber,
709
+ OneTimePassword: oneTimePassword,
710
+ }),
711
+ })
712
+ }
713
+
714
+ /**
715
+ * Delete a sandbox phone number
716
+ */
717
+ async deleteSMSSandboxPhoneNumber(phoneNumber: string): Promise<void> {
718
+ await this.client.request({
719
+ service: 'sns',
720
+ region: this.region,
721
+ method: 'POST',
722
+ path: '/',
723
+ headers: {
724
+ 'Content-Type': 'application/x-www-form-urlencoded',
725
+ },
726
+ body: this.buildFormBody({
727
+ Action: 'DeleteSMSSandboxPhoneNumber',
728
+ Version: '2010-03-31',
729
+ PhoneNumber: phoneNumber,
730
+ }),
731
+ })
732
+ }
733
+
734
+ /**
735
+ * Get SMS sandbox account status
736
+ */
737
+ async getSMSSandboxAccountStatus(): Promise<{
738
+ IsInSandbox: boolean
739
+ }> {
740
+ const result = await this.client.request({
741
+ service: 'sns',
742
+ region: this.region,
743
+ method: 'POST',
744
+ path: '/',
745
+ headers: {
746
+ 'Content-Type': 'application/x-www-form-urlencoded',
747
+ },
748
+ body: this.buildFormBody({
749
+ Action: 'GetSMSSandboxAccountStatus',
750
+ Version: '2010-03-31',
751
+ }),
752
+ })
753
+
754
+ return {
755
+ IsInSandbox: result?.GetSMSSandboxAccountStatusResponse?.GetSMSSandboxAccountStatusResult?.IsInSandbox === 'true',
756
+ }
757
+ }
758
+ }