@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,506 @@
1
+ /**
2
+ * Firebase Cloud Messaging (FCM) Client
3
+ * Uses FCM HTTP v1 API with Google OAuth2 authentication
4
+ *
5
+ * Prerequisites:
6
+ * - Firebase project
7
+ * - Service account JSON key from Firebase Console
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const fcm = new FCMClient({
12
+ * projectId: 'your-project-id',
13
+ * clientEmail: 'firebase-adminsdk@project.iam.gserviceaccount.com',
14
+ * privateKey: '-----BEGIN PRIVATE KEY-----\n...',
15
+ * })
16
+ *
17
+ * await fcm.send({
18
+ * token: '...',
19
+ * title: 'Hello',
20
+ * body: 'World',
21
+ * })
22
+ * ```
23
+ */
24
+
25
+ import { createSign } from 'node:crypto'
26
+
27
+ export interface FCMConfig {
28
+ /** Firebase project ID */
29
+ projectId: string
30
+ /** Service account client email */
31
+ clientEmail: string
32
+ /** Service account private key (PEM format) */
33
+ privateKey: string
34
+ }
35
+
36
+ export interface FCMNotification {
37
+ /** Device FCM token */
38
+ token?: string
39
+ /** Topic to send to (instead of token) */
40
+ topic?: string
41
+ /** Condition expression for targeting multiple topics */
42
+ condition?: string
43
+ /** Notification title */
44
+ title?: string
45
+ /** Notification body */
46
+ body?: string
47
+ /** Notification image URL */
48
+ imageUrl?: string
49
+ /** Custom data payload */
50
+ data?: Record<string, string>
51
+ /** Android-specific options */
52
+ android?: {
53
+ /** Channel ID for Android O+ */
54
+ channelId?: string
55
+ /** Notification priority */
56
+ priority?: 'normal' | 'high'
57
+ /** Time to live in seconds */
58
+ ttl?: number
59
+ /** Collapse key for message deduplication */
60
+ collapseKey?: string
61
+ /** Notification icon */
62
+ icon?: string
63
+ /** Notification icon color (hex) */
64
+ color?: string
65
+ /** Sound to play */
66
+ sound?: string
67
+ /** Click action */
68
+ clickAction?: string
69
+ /** Tag for notification replacement */
70
+ tag?: string
71
+ /** Direct boot aware */
72
+ directBootOk?: boolean
73
+ /** Visibility: private, public, secret */
74
+ visibility?: 'private' | 'public' | 'secret'
75
+ /** Notification count */
76
+ notificationCount?: number
77
+ }
78
+ /** Web push options */
79
+ webpush?: {
80
+ /** Web notification options */
81
+ notification?: {
82
+ title?: string
83
+ body?: string
84
+ icon?: string
85
+ badge?: string
86
+ image?: string
87
+ requireInteraction?: boolean
88
+ silent?: boolean
89
+ tag?: string
90
+ actions?: Array<{ action: string; title: string; icon?: string }>
91
+ }
92
+ /** FCM options for web */
93
+ fcmOptions?: {
94
+ link?: string
95
+ analyticsLabel?: string
96
+ }
97
+ /** Custom headers */
98
+ headers?: Record<string, string>
99
+ /** Custom data */
100
+ data?: Record<string, string>
101
+ }
102
+ /** APNS options (for iOS via FCM) */
103
+ apns?: {
104
+ /** APNs headers */
105
+ headers?: Record<string, string>
106
+ /** APNs payload */
107
+ payload?: {
108
+ aps?: Record<string, any>
109
+ [key: string]: any
110
+ }
111
+ /** FCM options */
112
+ fcmOptions?: {
113
+ analyticsLabel?: string
114
+ image?: string
115
+ }
116
+ }
117
+ /** FCM options */
118
+ fcmOptions?: {
119
+ analyticsLabel?: string
120
+ }
121
+ }
122
+
123
+ export interface FCMSendResult {
124
+ success: boolean
125
+ messageId?: string
126
+ error?: string
127
+ errorCode?: string
128
+ }
129
+
130
+ export interface FCMBatchResult {
131
+ sent: number
132
+ failed: number
133
+ results: Array<FCMSendResult & { token?: string }>
134
+ }
135
+
136
+ const FCM_API_URL = 'https://fcm.googleapis.com/v1/projects'
137
+ const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
138
+ const TOKEN_EXPIRY_MS = 55 * 60 * 1000 // 55 minutes (tokens valid for 1 hour)
139
+
140
+ /**
141
+ * Firebase Cloud Messaging client
142
+ */
143
+ export class FCMClient {
144
+ private config: FCMConfig
145
+ private accessToken: string | null = null
146
+ private tokenExpiresAt: number = 0
147
+
148
+ constructor(config: FCMConfig) {
149
+ this.config = config
150
+ }
151
+
152
+ /**
153
+ * Load config from service account JSON
154
+ */
155
+ static fromServiceAccount(serviceAccount: {
156
+ project_id: string
157
+ client_email: string
158
+ private_key: string
159
+ }): FCMClient {
160
+ return new FCMClient({
161
+ projectId: serviceAccount.project_id,
162
+ clientEmail: serviceAccount.client_email,
163
+ privateKey: serviceAccount.private_key,
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Generate a JWT for Google OAuth2
169
+ */
170
+ private generateJWT(): string {
171
+ const now = Math.floor(Date.now() / 1000)
172
+ const exp = now + 3600 // 1 hour
173
+
174
+ const header = {
175
+ alg: 'RS256',
176
+ typ: 'JWT',
177
+ }
178
+
179
+ const payload = {
180
+ iss: this.config.clientEmail,
181
+ sub: this.config.clientEmail,
182
+ aud: GOOGLE_TOKEN_URL,
183
+ iat: now,
184
+ exp,
185
+ scope: 'https://www.googleapis.com/auth/firebase.messaging',
186
+ }
187
+
188
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url')
189
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
190
+ const signatureInput = `${encodedHeader}.${encodedPayload}`
191
+
192
+ const sign = createSign('SHA256')
193
+ sign.update(signatureInput)
194
+ const signature = sign.sign(this.config.privateKey, 'base64url')
195
+
196
+ return `${signatureInput}.${signature}`
197
+ }
198
+
199
+ /**
200
+ * Get a valid access token, refreshing if needed
201
+ */
202
+ private async getAccessToken(): Promise<string> {
203
+ if (this.accessToken && Date.now() < this.tokenExpiresAt) {
204
+ return this.accessToken
205
+ }
206
+
207
+ const jwt = this.generateJWT()
208
+
209
+ const response = await fetch(GOOGLE_TOKEN_URL, {
210
+ method: 'POST',
211
+ headers: {
212
+ 'Content-Type': 'application/x-www-form-urlencoded',
213
+ },
214
+ body: new URLSearchParams({
215
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
216
+ assertion: jwt,
217
+ }).toString(),
218
+ })
219
+
220
+ if (!response.ok) {
221
+ const errorText = await response.text()
222
+ throw new Error(`Failed to get access token: ${errorText}`)
223
+ }
224
+
225
+ const data = await response.json() as Record<string, any>
226
+ this.accessToken = data.access_token
227
+ this.tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS
228
+
229
+ return this.accessToken!
230
+ }
231
+
232
+ /**
233
+ * Build FCM message payload
234
+ */
235
+ private buildMessage(notification: FCMNotification): object {
236
+ const message: Record<string, any> = {}
237
+
238
+ // Target (one of: token, topic, condition)
239
+ if (notification.token) {
240
+ message.token = notification.token
241
+ } else if (notification.topic) {
242
+ message.topic = notification.topic
243
+ } else if (notification.condition) {
244
+ message.condition = notification.condition
245
+ }
246
+
247
+ // Notification payload
248
+ if (notification.title || notification.body || notification.imageUrl) {
249
+ message.notification = {}
250
+ if (notification.title) message.notification.title = notification.title
251
+ if (notification.body) message.notification.body = notification.body
252
+ if (notification.imageUrl) message.notification.image = notification.imageUrl
253
+ }
254
+
255
+ // Data payload
256
+ if (notification.data && Object.keys(notification.data).length > 0) {
257
+ message.data = notification.data
258
+ }
259
+
260
+ // Android options
261
+ if (notification.android) {
262
+ message.android = {
263
+ priority: notification.android.priority || 'high',
264
+ }
265
+
266
+ if (notification.android.ttl) {
267
+ message.android.ttl = `${notification.android.ttl}s`
268
+ }
269
+
270
+ if (notification.android.collapseKey) {
271
+ message.android.collapse_key = notification.android.collapseKey
272
+ }
273
+
274
+ if (notification.android.directBootOk) {
275
+ message.android.direct_boot_ok = notification.android.directBootOk
276
+ }
277
+
278
+ // Android notification
279
+ const androidNotification: Record<string, any> = {}
280
+ if (notification.android.channelId) androidNotification.channel_id = notification.android.channelId
281
+ if (notification.android.icon) androidNotification.icon = notification.android.icon
282
+ if (notification.android.color) androidNotification.color = notification.android.color
283
+ if (notification.android.sound) androidNotification.sound = notification.android.sound
284
+ if (notification.android.clickAction) androidNotification.click_action = notification.android.clickAction
285
+ if (notification.android.tag) androidNotification.tag = notification.android.tag
286
+ if (notification.android.visibility) androidNotification.visibility = notification.android.visibility
287
+ if (notification.android.notificationCount !== undefined) {
288
+ androidNotification.notification_count = notification.android.notificationCount
289
+ }
290
+
291
+ if (Object.keys(androidNotification).length > 0) {
292
+ message.android.notification = androidNotification
293
+ }
294
+ }
295
+
296
+ // Web push options
297
+ if (notification.webpush) {
298
+ message.webpush = notification.webpush
299
+ }
300
+
301
+ // APNS options (iOS)
302
+ if (notification.apns) {
303
+ message.apns = notification.apns
304
+ }
305
+
306
+ // FCM options
307
+ if (notification.fcmOptions) {
308
+ message.fcm_options = notification.fcmOptions
309
+ }
310
+
311
+ return { message }
312
+ }
313
+
314
+ /**
315
+ * Send a push notification
316
+ */
317
+ async send(notification: FCMNotification): Promise<FCMSendResult> {
318
+ try {
319
+ const accessToken = await this.getAccessToken()
320
+ const payload = this.buildMessage(notification)
321
+
322
+ const response = await fetch(
323
+ `${FCM_API_URL}/${this.config.projectId}/messages:send`,
324
+ {
325
+ method: 'POST',
326
+ headers: {
327
+ 'Authorization': `Bearer ${accessToken}`,
328
+ 'Content-Type': 'application/json',
329
+ },
330
+ body: JSON.stringify(payload),
331
+ }
332
+ )
333
+
334
+ const data = await response.json() as Record<string, any>
335
+
336
+ if (response.ok) {
337
+ return {
338
+ success: true,
339
+ messageId: data.name,
340
+ }
341
+ } else {
342
+ return {
343
+ success: false,
344
+ error: data.error?.message || 'Unknown error',
345
+ errorCode: data.error?.status,
346
+ }
347
+ }
348
+ } catch (error: any) {
349
+ return {
350
+ success: false,
351
+ error: error.message,
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Send to multiple device tokens
358
+ */
359
+ async sendBatch(
360
+ tokens: string[],
361
+ notification: Omit<FCMNotification, 'token' | 'topic' | 'condition'>,
362
+ options?: { concurrency?: number }
363
+ ): Promise<FCMBatchResult> {
364
+ const concurrency = options?.concurrency || 10
365
+ const results: Array<FCMSendResult & { token?: string }> = []
366
+
367
+ // Process in batches
368
+ for (let i = 0; i < tokens.length; i += concurrency) {
369
+ const batch = tokens.slice(i, i + concurrency)
370
+ const batchPromises = batch.map(async (token) => {
371
+ const result = await this.send({ ...notification, token })
372
+ return { ...result, token }
373
+ })
374
+ const batchResults = await Promise.all(batchPromises)
375
+ results.push(...batchResults)
376
+ }
377
+
378
+ return {
379
+ sent: results.filter(r => r.success).length,
380
+ failed: results.filter(r => !r.success).length,
381
+ results,
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Send to a topic
387
+ */
388
+ async sendToTopic(
389
+ topic: string,
390
+ notification: Omit<FCMNotification, 'token' | 'topic' | 'condition'>
391
+ ): Promise<FCMSendResult> {
392
+ return this.send({ ...notification, topic })
393
+ }
394
+
395
+ /**
396
+ * Send to topics with a condition
397
+ * @example sendToCondition("'TopicA' in topics && 'TopicB' in topics", {...})
398
+ */
399
+ async sendToCondition(
400
+ condition: string,
401
+ notification: Omit<FCMNotification, 'token' | 'topic' | 'condition'>
402
+ ): Promise<FCMSendResult> {
403
+ return this.send({ ...notification, condition })
404
+ }
405
+
406
+ /**
407
+ * Send a simple notification (convenience method)
408
+ */
409
+ async sendSimple(
410
+ token: string,
411
+ title: string,
412
+ body: string,
413
+ data?: Record<string, string>
414
+ ): Promise<FCMSendResult> {
415
+ return this.send({
416
+ token,
417
+ title,
418
+ body,
419
+ data,
420
+ })
421
+ }
422
+
423
+ /**
424
+ * Send a data-only (silent) notification
425
+ */
426
+ async sendSilent(
427
+ token: string,
428
+ data: Record<string, string>
429
+ ): Promise<FCMSendResult> {
430
+ return this.send({
431
+ token,
432
+ data,
433
+ android: {
434
+ priority: 'high',
435
+ },
436
+ })
437
+ }
438
+
439
+ /**
440
+ * Subscribe a token to a topic
441
+ */
442
+ async subscribeToTopic(tokens: string[], topic: string): Promise<{ success: boolean; error?: string }> {
443
+ try {
444
+ const accessToken = await this.getAccessToken()
445
+
446
+ const response = await fetch(
447
+ `https://iid.googleapis.com/iid/v1:batchAdd`,
448
+ {
449
+ method: 'POST',
450
+ headers: {
451
+ 'Authorization': `Bearer ${accessToken}`,
452
+ 'Content-Type': 'application/json',
453
+ },
454
+ body: JSON.stringify({
455
+ to: `/topics/${topic}`,
456
+ registration_tokens: tokens,
457
+ }),
458
+ }
459
+ )
460
+
461
+ if (response.ok) {
462
+ return { success: true }
463
+ } else {
464
+ const data = await response.json() as Record<string, any>
465
+ return { success: false, error: data.error?.message || 'Failed to subscribe' }
466
+ }
467
+ } catch (error: any) {
468
+ return { success: false, error: error.message }
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Unsubscribe a token from a topic
474
+ */
475
+ async unsubscribeFromTopic(tokens: string[], topic: string): Promise<{ success: boolean; error?: string }> {
476
+ try {
477
+ const accessToken = await this.getAccessToken()
478
+
479
+ const response = await fetch(
480
+ `https://iid.googleapis.com/iid/v1:batchRemove`,
481
+ {
482
+ method: 'POST',
483
+ headers: {
484
+ 'Authorization': `Bearer ${accessToken}`,
485
+ 'Content-Type': 'application/json',
486
+ },
487
+ body: JSON.stringify({
488
+ to: `/topics/${topic}`,
489
+ registration_tokens: tokens,
490
+ }),
491
+ }
492
+ )
493
+
494
+ if (response.ok) {
495
+ return { success: true }
496
+ } else {
497
+ const data = await response.json() as Record<string, any>
498
+ return { success: false, error: data.error?.message || 'Failed to unsubscribe' }
499
+ }
500
+ } catch (error: any) {
501
+ return { success: false, error: error.message }
502
+ }
503
+ }
504
+ }
505
+
506
+ export default FCMClient
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Push Notifications Module
3
+ *
4
+ * Provides clients for Apple Push Notification Service (APNs) and
5
+ * Firebase Cloud Messaging (FCM).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // Apple Push Notifications
10
+ * import { APNsClient } from 'ts-cloud/push'
11
+ *
12
+ * const apns = new APNsClient({
13
+ * keyId: 'ABC123DEFG',
14
+ * teamId: 'DEF456GHIJ',
15
+ * privateKey: fs.readFileSync('AuthKey.p8', 'utf8'),
16
+ * bundleId: 'com.example.app',
17
+ * })
18
+ *
19
+ * await apns.send({
20
+ * deviceToken: '...',
21
+ * title: 'Hello',
22
+ * body: 'World',
23
+ * })
24
+ *
25
+ * // Firebase Cloud Messaging
26
+ * import { FCMClient } from 'ts-cloud/push'
27
+ *
28
+ * const fcm = new FCMClient({
29
+ * projectId: 'your-project-id',
30
+ * clientEmail: 'firebase-adminsdk@project.iam.gserviceaccount.com',
31
+ * privateKey: '-----BEGIN PRIVATE KEY-----\n...',
32
+ * })
33
+ *
34
+ * await fcm.send({
35
+ * token: '...',
36
+ * title: 'Hello',
37
+ * body: 'World',
38
+ * })
39
+ * ```
40
+ */
41
+
42
+ export * from './apns'
43
+ export * from './fcm'
44
+
45
+ // Re-export types for convenience
46
+ export type {
47
+ APNsConfig,
48
+ APNsNotification,
49
+ APNsSendResult,
50
+ APNsBatchResult,
51
+ } from './apns'
52
+
53
+ export type {
54
+ FCMConfig,
55
+ FCMNotification,
56
+ FCMSendResult,
57
+ FCMBatchResult,
58
+ } from './fcm'