@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.
- package/dist/aws/s3.d.ts +1 -1
- package/dist/bin/cli.js +223 -222
- package/dist/index.js +132 -132
- package/package.json +18 -16
- package/src/aws/acm.ts +768 -0
- package/src/aws/application-autoscaling.ts +845 -0
- package/src/aws/bedrock.ts +4074 -0
- package/src/aws/client.ts +891 -0
- package/src/aws/cloudformation.ts +896 -0
- package/src/aws/cloudfront.ts +1531 -0
- package/src/aws/cloudwatch-logs.ts +154 -0
- package/src/aws/comprehend.ts +839 -0
- package/src/aws/connect.ts +1056 -0
- package/src/aws/deploy-imap.ts +384 -0
- package/src/aws/dynamodb.ts +340 -0
- package/src/aws/ec2.ts +1385 -0
- package/src/aws/ecr.ts +621 -0
- package/src/aws/ecs.ts +615 -0
- package/src/aws/elasticache.ts +301 -0
- package/src/aws/elbv2.ts +942 -0
- package/src/aws/email.ts +928 -0
- package/src/aws/eventbridge.ts +248 -0
- package/src/aws/iam.ts +1689 -0
- package/src/aws/imap-server.ts +2100 -0
- package/src/aws/index.ts +213 -0
- package/src/aws/kendra.ts +1097 -0
- package/src/aws/lambda.ts +786 -0
- package/src/aws/opensearch.ts +158 -0
- package/src/aws/personalize.ts +977 -0
- package/src/aws/polly.ts +559 -0
- package/src/aws/rds.ts +888 -0
- package/src/aws/rekognition.ts +846 -0
- package/src/aws/route53-domains.ts +359 -0
- package/src/aws/route53.ts +1046 -0
- package/src/aws/s3.ts +2334 -0
- package/src/aws/scheduler.ts +571 -0
- package/src/aws/secrets-manager.ts +769 -0
- package/src/aws/ses.ts +1081 -0
- package/src/aws/setup-phone.ts +104 -0
- package/src/aws/setup-sms.ts +580 -0
- package/src/aws/sms.ts +1735 -0
- package/src/aws/smtp-server.ts +531 -0
- package/src/aws/sns.ts +758 -0
- package/src/aws/sqs.ts +382 -0
- package/src/aws/ssm.ts +807 -0
- package/src/aws/sts.ts +92 -0
- package/src/aws/support.ts +391 -0
- package/src/aws/test-imap.ts +86 -0
- package/src/aws/textract.ts +780 -0
- package/src/aws/transcribe.ts +108 -0
- package/src/aws/translate.ts +641 -0
- package/src/aws/voice.ts +1379 -0
- package/src/config.ts +35 -0
- package/src/deploy/index.ts +7 -0
- package/src/deploy/static-site-external-dns.ts +945 -0
- package/src/deploy/static-site.ts +1175 -0
- package/src/dns/cloudflare.ts +548 -0
- package/src/dns/godaddy.ts +412 -0
- package/src/dns/index.ts +205 -0
- package/src/dns/porkbun.ts +362 -0
- package/src/dns/route53-adapter.ts +414 -0
- package/src/dns/types.ts +119 -0
- package/src/dns/validator.ts +369 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/infrastructure.ts +1660 -0
- package/src/index.ts +163 -0
- package/src/push/apns.ts +452 -0
- package/src/push/fcm.ts +506 -0
- package/src/push/index.ts +58 -0
- package/src/security/pre-deploy-scanner.ts +655 -0
- package/src/ssl/acme-client.ts +478 -0
- package/src/ssl/index.ts +7 -0
- package/src/ssl/letsencrypt.ts +747 -0
- package/src/types.ts +2 -0
- package/src/utils/cli.ts +398 -0
- package/src/validation/index.ts +5 -0
- 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
|
+
}
|