@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/push/fcm.ts
ADDED
|
@@ -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'
|