@stacksjs/ts-cloud 0.7.4 → 0.7.6
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/index.js +259 -0
- package/dist/bin/cli.js +316 -316
- package/dist/chunk-0wxyppza.js +146 -0
- package/dist/chunk-3knnr7wh.js +601 -0
- package/dist/chunk-3rsfns7x.js +10427 -0
- package/dist/chunk-93hjhs78.js +1752 -0
- package/dist/chunk-arsh1g5h.js +1749 -0
- package/dist/chunk-b82pbxyp.js +1572 -0
- package/dist/chunk-c6rgvg1j.js +46124 -0
- package/dist/chunk-d2p5n2aq.js +23 -0
- package/dist/chunk-d7p84vz5.js +1699 -0
- package/dist/chunk-dpkk640m.js +4392 -0
- package/dist/chunk-eq08r166.js +8 -0
- package/dist/chunk-hsk6fe6x.js +371 -0
- package/dist/chunk-mj1tmhte.js +13 -0
- package/dist/chunk-p6309384.js +12828 -0
- package/dist/chunk-pegd7rmf.js +10 -0
- package/dist/chunk-pqfzdg68.js +12 -0
- package/dist/chunk-qpj3edwz.js +601 -0
- package/dist/chunk-tjjgajbh.js +1032 -0
- package/dist/chunk-tnztxpcb.js +630 -0
- package/dist/chunk-tt4kxske.js +1788 -0
- package/dist/chunk-v0bahtg2.js +4 -0
- package/dist/chunk-vd87cpvn.js +1754 -0
- package/dist/chunk-zn0nxxa8.js +1779 -0
- package/dist/deploy/index.js +87 -0
- package/dist/dns/index.js +31 -0
- package/dist/drivers/hetzner/provision.d.ts +53 -0
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/index.js +98 -0
- package/dist/drivers/shared/remote-exec.d.ts +47 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4383 -91358
- package/dist/push/index.js +509 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/backups.html +3 -3
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +3 -3
- package/dist/ui/server/firewall.html +3 -3
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/services.html +3 -3
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/terminal.html +3 -3
- package/dist/ui/server/workers.html +3 -3
- package/dist/ui/serverless/alarms.html +3 -3
- package/dist/ui/serverless/assets.html +3 -3
- package/dist/ui/serverless/data.html +3 -3
- package/dist/ui/serverless/deployments.html +3 -3
- package/dist/ui/serverless/functions.html +3 -3
- package/dist/ui/serverless/logs.html +3 -3
- package/dist/ui/serverless/queues.html +3 -3
- package/dist/ui/serverless/scheduler.html +3 -3
- package/dist/ui/serverless/secrets.html +3 -3
- package/dist/ui/serverless/traces.html +3 -3
- package/dist/ui/serverless.html +3 -3
- package/package.json +3 -3
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import"../chunk-v0bahtg2.js";
|
|
2
|
+
|
|
3
|
+
// src/push/apns.ts
|
|
4
|
+
import { createSign } from "node:crypto";
|
|
5
|
+
import * as http2 from "node:http2";
|
|
6
|
+
var APNS_PRODUCTION_HOST = "api.push.apple.com";
|
|
7
|
+
var APNS_SANDBOX_HOST = "api.sandbox.push.apple.com";
|
|
8
|
+
var TOKEN_EXPIRY_MS = 45 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
class APNsClient {
|
|
11
|
+
config;
|
|
12
|
+
token = null;
|
|
13
|
+
tokenGeneratedAt = 0;
|
|
14
|
+
client = null;
|
|
15
|
+
host;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.host = config.production ? APNS_PRODUCTION_HOST : APNS_SANDBOX_HOST;
|
|
19
|
+
}
|
|
20
|
+
generateToken() {
|
|
21
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22
|
+
if (this.token && Date.now() - this.tokenGeneratedAt < TOKEN_EXPIRY_MS) {
|
|
23
|
+
return this.token;
|
|
24
|
+
}
|
|
25
|
+
const header = {
|
|
26
|
+
alg: "ES256",
|
|
27
|
+
kid: this.config.keyId
|
|
28
|
+
};
|
|
29
|
+
const payload = {
|
|
30
|
+
iss: this.config.teamId,
|
|
31
|
+
iat: now
|
|
32
|
+
};
|
|
33
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
34
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
35
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
36
|
+
const sign = createSign("SHA256");
|
|
37
|
+
sign.update(signatureInput);
|
|
38
|
+
const signature = sign.sign(this.config.privateKey);
|
|
39
|
+
const rawSignature = this.derToRaw(signature);
|
|
40
|
+
const encodedSignature = rawSignature.toString("base64url");
|
|
41
|
+
this.token = `${signatureInput}.${encodedSignature}`;
|
|
42
|
+
this.tokenGeneratedAt = Date.now();
|
|
43
|
+
return this.token;
|
|
44
|
+
}
|
|
45
|
+
derToRaw(derSignature) {
|
|
46
|
+
let offset = 2;
|
|
47
|
+
if (derSignature[offset] !== 2) {
|
|
48
|
+
throw new Error("Invalid DER signature: expected 0x02 for R");
|
|
49
|
+
}
|
|
50
|
+
offset++;
|
|
51
|
+
const rLength = derSignature[offset];
|
|
52
|
+
offset++;
|
|
53
|
+
let r = derSignature.subarray(offset, offset + rLength);
|
|
54
|
+
offset += rLength;
|
|
55
|
+
if (derSignature[offset] !== 2) {
|
|
56
|
+
throw new Error("Invalid DER signature: expected 0x02 for S");
|
|
57
|
+
}
|
|
58
|
+
offset++;
|
|
59
|
+
const sLength = derSignature[offset];
|
|
60
|
+
offset++;
|
|
61
|
+
let s = derSignature.subarray(offset, offset + sLength);
|
|
62
|
+
if (r[0] === 0 && r.length === 33) {
|
|
63
|
+
r = r.subarray(1);
|
|
64
|
+
}
|
|
65
|
+
if (s[0] === 0 && s.length === 33) {
|
|
66
|
+
s = s.subarray(1);
|
|
67
|
+
}
|
|
68
|
+
const result = Buffer.alloc(64);
|
|
69
|
+
r.copy(result, 32 - r.length);
|
|
70
|
+
s.copy(result, 64 - s.length);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
async getClient() {
|
|
74
|
+
if (this.client && !this.client.destroyed) {
|
|
75
|
+
return this.client;
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
this.client = http2.connect(`https://${this.host}`);
|
|
79
|
+
this.client.on("error", (err) => {
|
|
80
|
+
reject(err);
|
|
81
|
+
});
|
|
82
|
+
this.client.on("connect", () => {
|
|
83
|
+
resolve(this.client);
|
|
84
|
+
});
|
|
85
|
+
this.client.on("close", () => {
|
|
86
|
+
this.client = null;
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
buildPayload(notification) {
|
|
91
|
+
const aps = {};
|
|
92
|
+
if (notification.title || notification.body || notification.subtitle) {
|
|
93
|
+
aps.alert = {};
|
|
94
|
+
if (notification.title)
|
|
95
|
+
aps.alert.title = notification.title;
|
|
96
|
+
if (notification.subtitle)
|
|
97
|
+
aps.alert.subtitle = notification.subtitle;
|
|
98
|
+
if (notification.body)
|
|
99
|
+
aps.alert.body = notification.body;
|
|
100
|
+
}
|
|
101
|
+
if (notification.badge !== undefined) {
|
|
102
|
+
aps.badge = notification.badge;
|
|
103
|
+
}
|
|
104
|
+
if (notification.sound !== undefined) {
|
|
105
|
+
aps.sound = notification.sound;
|
|
106
|
+
}
|
|
107
|
+
if (notification.category) {
|
|
108
|
+
aps.category = notification.category;
|
|
109
|
+
}
|
|
110
|
+
if (notification.threadId) {
|
|
111
|
+
aps["thread-id"] = notification.threadId;
|
|
112
|
+
}
|
|
113
|
+
if (notification.contentAvailable) {
|
|
114
|
+
aps["content-available"] = 1;
|
|
115
|
+
}
|
|
116
|
+
if (notification.mutableContent) {
|
|
117
|
+
aps["mutable-content"] = 1;
|
|
118
|
+
}
|
|
119
|
+
if (notification.targetContentId) {
|
|
120
|
+
aps["target-content-id"] = notification.targetContentId;
|
|
121
|
+
}
|
|
122
|
+
const payload = { aps };
|
|
123
|
+
if (notification.data) {
|
|
124
|
+
Object.assign(payload, notification.data);
|
|
125
|
+
}
|
|
126
|
+
return payload;
|
|
127
|
+
}
|
|
128
|
+
async send(notification) {
|
|
129
|
+
try {
|
|
130
|
+
const client = await this.getClient();
|
|
131
|
+
const token = this.generateToken();
|
|
132
|
+
const payload = JSON.stringify(this.buildPayload(notification));
|
|
133
|
+
const headers = {
|
|
134
|
+
":method": "POST",
|
|
135
|
+
":path": `/3/device/${notification.deviceToken}`,
|
|
136
|
+
authorization: `bearer ${token}`,
|
|
137
|
+
"apns-topic": this.config.bundleId,
|
|
138
|
+
"apns-push-type": notification.pushType || "alert",
|
|
139
|
+
"apns-priority": String(notification.priority || 10)
|
|
140
|
+
};
|
|
141
|
+
if (notification.expiration !== undefined) {
|
|
142
|
+
headers["apns-expiration"] = String(notification.expiration);
|
|
143
|
+
}
|
|
144
|
+
if (notification.collapseId) {
|
|
145
|
+
headers["apns-collapse-id"] = notification.collapseId;
|
|
146
|
+
}
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const req = client.request(headers);
|
|
149
|
+
let responseData = "";
|
|
150
|
+
let statusCode = 0;
|
|
151
|
+
let apnsId;
|
|
152
|
+
req.on("response", (headers2) => {
|
|
153
|
+
statusCode = headers2[":status"];
|
|
154
|
+
apnsId = headers2["apns-id"];
|
|
155
|
+
});
|
|
156
|
+
req.on("data", (chunk) => {
|
|
157
|
+
responseData += chunk.toString();
|
|
158
|
+
});
|
|
159
|
+
req.on("end", () => {
|
|
160
|
+
if (statusCode === 200) {
|
|
161
|
+
resolve({
|
|
162
|
+
success: true,
|
|
163
|
+
deviceToken: notification.deviceToken,
|
|
164
|
+
apnsId,
|
|
165
|
+
statusCode
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
let error = "Unknown error";
|
|
169
|
+
let reason;
|
|
170
|
+
let timestamp;
|
|
171
|
+
if (responseData) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(responseData);
|
|
174
|
+
reason = parsed.reason;
|
|
175
|
+
timestamp = parsed.timestamp;
|
|
176
|
+
error = reason || error;
|
|
177
|
+
} catch {
|
|
178
|
+
error = responseData;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
resolve({
|
|
182
|
+
success: false,
|
|
183
|
+
deviceToken: notification.deviceToken,
|
|
184
|
+
apnsId,
|
|
185
|
+
statusCode,
|
|
186
|
+
error,
|
|
187
|
+
reason,
|
|
188
|
+
timestamp
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
req.on("error", (err) => {
|
|
193
|
+
resolve({
|
|
194
|
+
success: false,
|
|
195
|
+
deviceToken: notification.deviceToken,
|
|
196
|
+
error: err.message
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
req.write(payload);
|
|
200
|
+
req.end();
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
deviceToken: notification.deviceToken,
|
|
206
|
+
error: error.message
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async sendBatch(notifications, options) {
|
|
211
|
+
const concurrency = options?.concurrency || 10;
|
|
212
|
+
const results = [];
|
|
213
|
+
for (let i = 0;i < notifications.length; i += concurrency) {
|
|
214
|
+
const batch = notifications.slice(i, i + concurrency);
|
|
215
|
+
const batchResults = await Promise.all(batch.map((n) => this.send(n)));
|
|
216
|
+
results.push(...batchResults);
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
sent: results.filter((r) => r.success).length,
|
|
220
|
+
failed: results.filter((r) => !r.success).length,
|
|
221
|
+
results
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async sendSimple(deviceToken, title, body, data) {
|
|
225
|
+
return this.send({
|
|
226
|
+
deviceToken,
|
|
227
|
+
title,
|
|
228
|
+
body,
|
|
229
|
+
data
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async sendSilent(deviceToken, data) {
|
|
233
|
+
return this.send({
|
|
234
|
+
deviceToken,
|
|
235
|
+
contentAvailable: true,
|
|
236
|
+
pushType: "background",
|
|
237
|
+
priority: 5,
|
|
238
|
+
data
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
close() {
|
|
242
|
+
if (this.client) {
|
|
243
|
+
this.client.close();
|
|
244
|
+
this.client = null;
|
|
245
|
+
}
|
|
246
|
+
this.token = null;
|
|
247
|
+
this.tokenGeneratedAt = 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// src/push/fcm.ts
|
|
251
|
+
import { createSign as createSign2 } from "node:crypto";
|
|
252
|
+
var FCM_API_URL = "https://fcm.googleapis.com/v1/projects";
|
|
253
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
254
|
+
var TOKEN_EXPIRY_MS2 = 55 * 60 * 1000;
|
|
255
|
+
|
|
256
|
+
class FCMClient {
|
|
257
|
+
config;
|
|
258
|
+
accessToken = null;
|
|
259
|
+
tokenExpiresAt = 0;
|
|
260
|
+
constructor(config) {
|
|
261
|
+
this.config = config;
|
|
262
|
+
}
|
|
263
|
+
static fromServiceAccount(serviceAccount) {
|
|
264
|
+
return new FCMClient({
|
|
265
|
+
projectId: serviceAccount.project_id,
|
|
266
|
+
clientEmail: serviceAccount.client_email,
|
|
267
|
+
privateKey: serviceAccount.private_key
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
generateJWT() {
|
|
271
|
+
const now = Math.floor(Date.now() / 1000);
|
|
272
|
+
const exp = now + 3600;
|
|
273
|
+
const header = {
|
|
274
|
+
alg: "RS256",
|
|
275
|
+
typ: "JWT"
|
|
276
|
+
};
|
|
277
|
+
const payload = {
|
|
278
|
+
iss: this.config.clientEmail,
|
|
279
|
+
sub: this.config.clientEmail,
|
|
280
|
+
aud: GOOGLE_TOKEN_URL,
|
|
281
|
+
iat: now,
|
|
282
|
+
exp,
|
|
283
|
+
scope: "https://www.googleapis.com/auth/firebase.messaging"
|
|
284
|
+
};
|
|
285
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
286
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
287
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
288
|
+
const sign = createSign2("SHA256");
|
|
289
|
+
sign.update(signatureInput);
|
|
290
|
+
const signature = sign.sign(this.config.privateKey, "base64url");
|
|
291
|
+
return `${signatureInput}.${signature}`;
|
|
292
|
+
}
|
|
293
|
+
async getAccessToken() {
|
|
294
|
+
if (this.accessToken && Date.now() < this.tokenExpiresAt) {
|
|
295
|
+
return this.accessToken;
|
|
296
|
+
}
|
|
297
|
+
const jwt = this.generateJWT();
|
|
298
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
302
|
+
},
|
|
303
|
+
body: new URLSearchParams({
|
|
304
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
305
|
+
assertion: jwt
|
|
306
|
+
}).toString()
|
|
307
|
+
});
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
const errorText = await response.text();
|
|
310
|
+
throw new Error(`Failed to get access token: ${errorText}`);
|
|
311
|
+
}
|
|
312
|
+
const data = await response.json();
|
|
313
|
+
this.accessToken = data.access_token;
|
|
314
|
+
this.tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS2;
|
|
315
|
+
return this.accessToken;
|
|
316
|
+
}
|
|
317
|
+
buildMessage(notification) {
|
|
318
|
+
const message = {};
|
|
319
|
+
if (notification.token) {
|
|
320
|
+
message.token = notification.token;
|
|
321
|
+
} else if (notification.topic) {
|
|
322
|
+
message.topic = notification.topic;
|
|
323
|
+
} else if (notification.condition) {
|
|
324
|
+
message.condition = notification.condition;
|
|
325
|
+
}
|
|
326
|
+
if (notification.title || notification.body || notification.imageUrl) {
|
|
327
|
+
message.notification = {};
|
|
328
|
+
if (notification.title)
|
|
329
|
+
message.notification.title = notification.title;
|
|
330
|
+
if (notification.body)
|
|
331
|
+
message.notification.body = notification.body;
|
|
332
|
+
if (notification.imageUrl)
|
|
333
|
+
message.notification.image = notification.imageUrl;
|
|
334
|
+
}
|
|
335
|
+
if (notification.data && Object.keys(notification.data).length > 0) {
|
|
336
|
+
message.data = notification.data;
|
|
337
|
+
}
|
|
338
|
+
if (notification.android) {
|
|
339
|
+
message.android = {
|
|
340
|
+
priority: notification.android.priority || "high"
|
|
341
|
+
};
|
|
342
|
+
if (notification.android.ttl) {
|
|
343
|
+
message.android.ttl = `${notification.android.ttl}s`;
|
|
344
|
+
}
|
|
345
|
+
if (notification.android.collapseKey) {
|
|
346
|
+
message.android.collapse_key = notification.android.collapseKey;
|
|
347
|
+
}
|
|
348
|
+
if (notification.android.directBootOk) {
|
|
349
|
+
message.android.direct_boot_ok = notification.android.directBootOk;
|
|
350
|
+
}
|
|
351
|
+
const androidNotification = {};
|
|
352
|
+
if (notification.android.channelId)
|
|
353
|
+
androidNotification.channel_id = notification.android.channelId;
|
|
354
|
+
if (notification.android.icon)
|
|
355
|
+
androidNotification.icon = notification.android.icon;
|
|
356
|
+
if (notification.android.color)
|
|
357
|
+
androidNotification.color = notification.android.color;
|
|
358
|
+
if (notification.android.sound)
|
|
359
|
+
androidNotification.sound = notification.android.sound;
|
|
360
|
+
if (notification.android.clickAction)
|
|
361
|
+
androidNotification.click_action = notification.android.clickAction;
|
|
362
|
+
if (notification.android.tag)
|
|
363
|
+
androidNotification.tag = notification.android.tag;
|
|
364
|
+
if (notification.android.visibility)
|
|
365
|
+
androidNotification.visibility = notification.android.visibility;
|
|
366
|
+
if (notification.android.notificationCount !== undefined) {
|
|
367
|
+
androidNotification.notification_count = notification.android.notificationCount;
|
|
368
|
+
}
|
|
369
|
+
if (Object.keys(androidNotification).length > 0) {
|
|
370
|
+
message.android.notification = androidNotification;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (notification.webpush) {
|
|
374
|
+
message.webpush = notification.webpush;
|
|
375
|
+
}
|
|
376
|
+
if (notification.apns) {
|
|
377
|
+
message.apns = notification.apns;
|
|
378
|
+
}
|
|
379
|
+
if (notification.fcmOptions) {
|
|
380
|
+
message.fcm_options = notification.fcmOptions;
|
|
381
|
+
}
|
|
382
|
+
return { message };
|
|
383
|
+
}
|
|
384
|
+
async send(notification) {
|
|
385
|
+
try {
|
|
386
|
+
const accessToken = await this.getAccessToken();
|
|
387
|
+
const payload = this.buildMessage(notification);
|
|
388
|
+
const response = await fetch(`${FCM_API_URL}/${this.config.projectId}/messages:send`, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: {
|
|
391
|
+
Authorization: `Bearer ${accessToken}`,
|
|
392
|
+
"Content-Type": "application/json"
|
|
393
|
+
},
|
|
394
|
+
body: JSON.stringify(payload)
|
|
395
|
+
});
|
|
396
|
+
const data = await response.json();
|
|
397
|
+
if (response.ok) {
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
messageId: data.name
|
|
401
|
+
};
|
|
402
|
+
} else {
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
error: data.error?.message || "Unknown error",
|
|
406
|
+
errorCode: data.error?.status
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: error.message
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async sendBatch(tokens, notification, options) {
|
|
417
|
+
const concurrency = options?.concurrency || 10;
|
|
418
|
+
const results = [];
|
|
419
|
+
for (let i = 0;i < tokens.length; i += concurrency) {
|
|
420
|
+
const batch = tokens.slice(i, i + concurrency);
|
|
421
|
+
const batchPromises = batch.map(async (token) => {
|
|
422
|
+
const result = await this.send({ ...notification, token });
|
|
423
|
+
return { ...result, token };
|
|
424
|
+
});
|
|
425
|
+
const batchResults = await Promise.all(batchPromises);
|
|
426
|
+
results.push(...batchResults);
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
sent: results.filter((r) => r.success).length,
|
|
430
|
+
failed: results.filter((r) => !r.success).length,
|
|
431
|
+
results
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async sendToTopic(topic, notification) {
|
|
435
|
+
return this.send({ ...notification, topic });
|
|
436
|
+
}
|
|
437
|
+
async sendToCondition(condition, notification) {
|
|
438
|
+
return this.send({ ...notification, condition });
|
|
439
|
+
}
|
|
440
|
+
async sendSimple(token, title, body, data) {
|
|
441
|
+
return this.send({
|
|
442
|
+
token,
|
|
443
|
+
title,
|
|
444
|
+
body,
|
|
445
|
+
data
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
async sendSilent(token, data) {
|
|
449
|
+
return this.send({
|
|
450
|
+
token,
|
|
451
|
+
data,
|
|
452
|
+
android: {
|
|
453
|
+
priority: "high"
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
async subscribeToTopic(tokens, topic) {
|
|
458
|
+
try {
|
|
459
|
+
const accessToken = await this.getAccessToken();
|
|
460
|
+
const response = await fetch(`https://iid.googleapis.com/iid/v1:batchAdd`, {
|
|
461
|
+
method: "POST",
|
|
462
|
+
headers: {
|
|
463
|
+
Authorization: `Bearer ${accessToken}`,
|
|
464
|
+
"Content-Type": "application/json"
|
|
465
|
+
},
|
|
466
|
+
body: JSON.stringify({
|
|
467
|
+
to: `/topics/${topic}`,
|
|
468
|
+
registration_tokens: tokens
|
|
469
|
+
})
|
|
470
|
+
});
|
|
471
|
+
if (response.ok) {
|
|
472
|
+
return { success: true };
|
|
473
|
+
} else {
|
|
474
|
+
const data = await response.json();
|
|
475
|
+
return { success: false, error: data.error?.message || "Failed to subscribe" };
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return { success: false, error: error.message };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async unsubscribeFromTopic(tokens, topic) {
|
|
482
|
+
try {
|
|
483
|
+
const accessToken = await this.getAccessToken();
|
|
484
|
+
const response = await fetch(`https://iid.googleapis.com/iid/v1:batchRemove`, {
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: {
|
|
487
|
+
Authorization: `Bearer ${accessToken}`,
|
|
488
|
+
"Content-Type": "application/json"
|
|
489
|
+
},
|
|
490
|
+
body: JSON.stringify({
|
|
491
|
+
to: `/topics/${topic}`,
|
|
492
|
+
registration_tokens: tokens
|
|
493
|
+
})
|
|
494
|
+
});
|
|
495
|
+
if (response.ok) {
|
|
496
|
+
return { success: true };
|
|
497
|
+
} else {
|
|
498
|
+
const data = await response.json();
|
|
499
|
+
return { success: false, error: data.error?.message || "Failed to unsubscribe" };
|
|
500
|
+
}
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return { success: false, error: error.message };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export {
|
|
507
|
+
FCMClient,
|
|
508
|
+
APNsClient
|
|
509
|
+
};
|